ES6之对象的扩展

date
Jun 13, 2018
slug
kkikonoo
status
Published
tags
JavaScript
summary
type
Post
 
[toc]

简洁表示法

const foo = 'bar';const baz = {foo};// 等同于const baz = {foo: foo};const o = {  name: "张三",  method() {    return "Hello!" + this.name;  }};// 等同于const o = {  method: function() {    return "Hello!" + this.name;  }};// 不等同于。因为对象不单独构成作用域,this指向全局const o = {  method: () => {    return "Hello!" + this.name;  }};
this 关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字 super,指向当前对象的原型对象。
const proto = {  foo: 'hello'};const obj = {  foo: 'world',  find() {    return super.foo;  }};Object.setPrototypeOf(obj, proto);obj.find() // "hello"
注意,super 关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。JavaScript 引擎内部,super.foo 等同于 Object.getPrototypeOf(this).foo(属性)或 Object.getPrototypeOf(this).foo.call(this)(方法)。下面代码中,super.foo 指向原型对象 proto 的 foo 方法,但是绑定的 this 却还是当前对象 obj,因此输出的就是 world。
const proto = {  x: 'hello',  foo() {    console.log(this.x);  },};const obj = {  x: 'world',  foo() {    super.foo();  }}Object.setPrototypeOf(obj, proto);obj.foo() // "world"
属性的赋值器(setter)和取值器(getter),事实上也是采用这种写法。注意,简写的对象方法不能用作构造函数,会报错。
const cart = {  _wheels: 4,  f() {    this.foo = 'bar';  }  get wheels () {    return this._wheels;  },  set wheels (value) {    if (value < this._wheels) {      throw new Error('数值太小了!');    }    this._wheels = value;  }}new cart.f() // 报错
JavaScript 定义对象的属性,有两种方法。方法一是直接用标识符作为属性名,方法二是用表达式作为属性名,这时要将表达式放在方括号之内。注意,属性名表达式与简洁表示法,不能同时使用。

属性的可枚举性和遍历

可枚举性

对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象。
let obj = { foo: 123 };Object.getOwnPropertyDescriptor(obj, 'foo')//  {//    value: 123,//    writable: true,//    enumerable: true,//    configurable: true//  }
描述对象的enumerable属性,称为“可枚举性”,如果该属性为 false,就表示某些操作会忽略当前属性。目前,有四个操作会忽略 enumerable 为 false 的属性。
  • for…in 循环:只遍历对象自身的和继承的可枚举的属性。
  • Object.keys():返回对象自身的所有可枚举的属性的键名。
  • JSON.stringify():只串行化对象自身的可枚举的属性。
  • Object.assign(): 忽略 enumerable 为 false 的属性,只拷贝对象自身的可枚举的属性。
前三个是 ES5 就有的,其中只有 for…in 会返回继承的属性,其他三个方法都会忽略继承的属性,只处理对象自身的属性。
实际上,引入“可枚举”(enumerable)这个概念的最初目的,就是让某些属性可以规避掉 for…in 操作,不然所有内部属性和方法都会被遍历到。比如,对象原型的 toString 方法,以及数组的 length 属性,就通过“可枚举性”,从而避免被 for…in 遍历到。
另外 ES6 规定所有 Class 的原型的方法都是不可枚举的。总的来说,引入继承的属性会让问题复杂化,大多数时候我们只关心对象自身的属性,所以尽量不要用 for…in 循环,而用 Object.keys()代替

属性的遍历

ES6 一共有 5 种方法可以遍历对象的属性。 (1)for…in:for…in 循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。 (2)Object.keys(obj):返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。 (3)Object.getOwnPropertyNames(obj):返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。 (4)Object.getOwnPropertySymbols(obj):返回一个数组,包含对象自身的所有 Symbol 属性的键名。 (5)Reflect.ownKeys(obj):返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。
以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。
  • 首先遍历所有数值键,按照数值升序排列。
  • 其次遍历所有字符串键,按照加入时间升序排列。
  • 最后遍历所有 Symbol 键,按照加入时间升序排列。
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })// ['2', '10', 'b', 'a', Symbol()]
上面代码中,Reflect.ownKeys 方法返回一个数组,包含了参数对象的所有属性。这个数组的属性次序是这样的,首先是数值属性 2 和 10,其次是字符串属性 b 和 a,最后是 Symbol 属性。

扩展运算符

扩展运算符(…)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。数组是特殊的对象,如果扩展运算符后面不是对象,则会自动将其转为对象。
// 等同于 {...Object(1)}{...1} // {}// 等同于 {...Object(true)}{...true} // {}// 等同于 {...Object(undefined)}{...undefined} // {}// 等同于 {...Object(null)}{...null} // {}{...'hello'}// {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}
上面第一个扩展运算符后面是整数 1,会自动转为数值的包装对象 Number{1}。由于该对象没有自身属性,所以返回一个空对象。
对象的扩展运算符等同于使用Object.assign()方法,只是拷贝了对象实例的属性值,假如源对象的属性值是一个对象的引用,那么它也只指向那个引用。如果想完整克隆一个对象,还拷贝对象原型的属性,可以采用下面的写法。
// 写法一const clone1 = {  __proto__: Object.getPrototypeOf(obj),  ...obj};// 写法二,推荐const clone2 = Object.assign(  Object.create(Object.getPrototypeOf(obj)),  obj);// 写法三,推荐const clone3 = Object.create(  Object.getPrototypeOf(obj),  Object.getOwnPropertyDescriptors(obj))
扩展运算符的参数对象之中,如果有取值函数 get,这个函数是会执行的。
// 并不会抛出错误,因为 x 属性只是被定义,但没执行let aWithXGetter = {  ...a,  get x() {    throw new Error('not throw yet');  }};// 会抛出错误,因为 x 属性被执行了let runtimeError = {  ...a,  ...{    get x() {      throw new Error('throw now');    }  }};

链判断运算符

ES2020 引入了“链判断运算符”(optional chaining operator)?.,调用的时候判断,左侧的对象是否为 null 或 undefined。如果是的,就不再往下运算,而是返回 undefined。
  • obj?.prop // 对象属性
  • obj?.[expr] // 同上
  • func?.(…args) // 函数或对象方法的调用。不是返回 false!!!
const firstName = (message  && message.body  && message.body.user  && message.body.user.firstName) || 'default';// 等同于const firstName = message?.body?.user?.firstName || 'default';
注意点: (1)短路机制:a?.[++x],如果 a 是 undefined 或 null,那么 x 不会进行递增运算。链判断运算符一旦为真,右侧的表达式就不再求值。 (2)delete 运算符:delete a?.b,如果 a 是 undefined 或 null,会直接返回 undefined,而不会进行 delete 运算。 (3)括号的影响:(a?.b).c,.c 总是会执行。一般来说,使用?.运算符的场合,不应该使用圆括号。 (4)报错场合:以下写法是禁止的,会报错。
// 构造函数new a?.()new a?.b()// 链判断运算符的右侧有模板字符串a?.`{b}`a?.b`{c}`// 链判断运算符的左侧是 supersuper?.()super?.foo// 链运算符用于赋值运算符左侧a?.b = c
(5)右侧不得为十进制数值:为保证兼容以前的代码,允许foo?.3:0被解析成foo ? .3 : 0,因此规定如果?.后面紧跟一个十进制数字,那么?.不再被看成是一个完整的运算符,而会按照三元运算符进行处理。

Null 判断运算符

对象属性的值是 null 或 undefined,有时候需要为它们指定默认值。常见做法是通过||运算符指定默认值,但属性的值如果为空字符串或 false 或 0,默认值也会生效。 ES2020 引入 Null 判断运算符??,只有运算符左侧的值为 null 或 undefined 时,才会返回右侧的值。 ??与&&和||的优先级孰高孰低?现在的规则是,如果多个逻辑运算符一起使用,必须用括号表明优先级,否则会报错。
const animationDuration = response.settings?.animationDuration ?? 300; // 报错lhs && middle ?? rhslhs ?? middle && rhs// ok(lhs && middle) ?? rhs;lhs && (middle ?? rhs);

新增的方法

Object.is()

相等运算符(==)和严格相等运算符(===)比较两个值时,前者会自动转换数据类型,后者的 NaN 不等于自身,以及+0 等于-0。其实应该只要两个值是一样的,它们就应该相等。ES6 提出“Same-value equality”(同值相等)算法,Object.is 就是用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致,不同之处只有两个:一是+0 不等于-0,二是 NaN 等于自身。
Object.is('foo', 'foo')// trueObject.is({}, {})// false+0 === -0 //trueNaN === NaN // falseObject.is(+0, -0) // falseObject.is(NaN, NaN) // true

Object.assign()

用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。如果只有一个参数,会直接返回该参数。 如果该参数不是对象,则会先转成对象(undefined 和 null 无法转成对象),然后返回。但如果非对象参数出现在源对象的位置(即非首参数),这时无法转成对象,就会跳过,即就不会报错。
const obj = {a: 1};Object.assign(obj) === obj // truetypeof Object.assign(2) // "object"Object.assign(undefined) // 报错Object.assign(null) // 报错let obj = {a: 1};Object.assign(obj, undefined) === obj // trueObject.assign(obj, null) === obj // true
其他类型的值(即数值、字符串和布尔值)不在首参数,也不会报错。但除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果。因为只有字符串的包装对象,会产生可枚举属性。属性名为 Symbol 值的属性,也会被 Object.assign 拷贝。
const v1 = 'abc';const v2 = true;const v3 = 10;const obj = Object.assign({}, v1, v2, v3);console.log(obj); // { "0": "a", "1": "b", "2": "c" }Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' })// { a: 'b', Symbol(c): 'd' }

注意点

(1)浅拷贝

Object.assign 方法实行的是浅拷贝,就是说如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。
const obj1 = {a: {b: 1}};const obj2 = Object.assign({}, obj1);obj1.a.b = 2;obj2.a.b // 2

(2)同名属性的替换

对于同名属性的嵌套对象,Object.assign 的处理方法是替换,而不是添加。一些函数库提供 Object.assign 的定制版本(比如 Lodash 的_.defaultsDeep 方法),可以得到深拷贝的合并。
const target = { a: { b: 'c', d: 'e' } }const source = { a: { b: 'hello' } }Object.assign(target, source)// { a: { b: 'hello' } }

(3)数组的处理

Object.assign 可以用来处理数组,但是会把数组视为对象。因为属性名是序号!
Object.assign([1, 2, 3], [4, 5])// [4, 5, 3]

(4)取值函数的处理

Object.assign 只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制,而不会复制这个取值函数。
const source = {  get foo() { return 1 }};const target = {};Object.assign(target, source)// { foo: 1 }

常见用途

(1)为对象添加属性 (2)为对象添加方法 (3)克隆对象:只能克隆原始对象自身的值,不能克隆它继承的值。 (4)合并多个对象 (5)为属性指定默认值:由于浅拷贝,DEFAULTS 对象和 options 对象的所有属性的值,最好都是简单类型,不要指向另一个对象。否则,DEFAULTS 对象的该属性很可能不起作用。
const DEFAULTS = {  logLevel: 0,  outputFormat: 'html'};function processContent(options) {  options = Object.assign({}, DEFAULTS, options);  console.log(options);  // ...}

Object.getOwnPropertyDescriptors()

ES2017 引入了Object.getOwnPropertyDescriptors()方法,返回指定对象所有自身属性(非继承属性)的描述对象。MDN 介绍引入目的:
  • 为了解决 Object.assign()无法正确拷贝 get 属性和 set 属性的问题。
  • 配合 Object.create()方法,将对象属性克隆到一个新对象。这属于浅拷贝。

proto属性,Object.setPrototypeOf(),Object.getPrototypeOf()

JavaScript 语言的对象继承是通过原型链实现的。ES6 提供了更多原型对象的操作方法。
proto属性(前后各两个下划线),用来读取或设置当前对象的 prototype 对象。目前,所有浏览器(包括 IE11)都部署了这个属性。无论从语义的角度,还是从兼容性的角度,都不要使用这个属性,而是 Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替。实现上,proto调用的是 Object.prototype.proto

Object.setPrototypeOf()

用来设置一个对象的 prototype 对象,返回参数对象本身。
// 格式Object.setPrototypeOf(object, prototype)// 用法const o = Object.setPrototypeOf({}, null);// 示例let proto = {};let obj = { x: 10 };Object.setPrototypeOf(obj, proto);proto.y = 20;proto.z = 40;obj.x // 10obj.y // 20obj.z // 40
上面代码将 proto 对象设为 obj 对象的原型,所以从 obj 对象可以读取 proto 对象的属性。如果第一个参数不是对象,会自动转为对象。但是由于返回的还是第一个参数,所以这个操作不会产生任何效果。由于 undefined 和 null 无法转为对象,所以如果第一个参数是 undefined 或 null,就会报错。
Object.setPrototypeOf(1, {}) === 1 // trueObject.setPrototypeOf('foo', {}) === 'foo' // trueObject.setPrototypeOf(true, {}) === true // trueObject.setPrototypeOf(undefined, {})// TypeError: Object.setPrototypeOf called on null or undefinedObject.setPrototypeOf(null, {})// TypeError: Object.setPrototypeOf called on null or undefined

Object.getPrototypeOf()

与 Object.setPrototypeOf 方法配套,用于读取一个对象的原型对象。如果参数不是对象,会被自动转为对象。如果参数是 undefined 或 null,会报错。
function Rectangle() {  // ...}const rec = new Rectangle();Object.getPrototypeOf(rec) === Rectangle.prototype// trueObject.setPrototypeOf(rec, Object.prototype);Object.getPrototypeOf(rec) === Rectangle.prototype// falseObject.getPrototypeOf(1) === Number.prototype // trueObject.getPrototypeOf('foo') === String.prototype // trueObject.getPrototypeOf(true) === Boolean.prototype // true

Object.keys(),Object.values(),Object.entries()

ES2017 引入了跟 Object.keys 配套的 Object.values 和 Object.entries,作为遍历一个对象的补充手段,供 for…of 循环使用。

Object.keys()

返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名。
var obj = { foo: 'bar', baz: 42 };Object.keys(obj)// ["foo", "baz"]let {keys, values, entries} = Object;let obj = { a: 1, b: 2, c: 3 };for (let key of keys(obj)) {  console.log(key); // 'a', 'b', 'c'}for (let value of values(obj)) {  console.log(value); // 1, 2, 3}for (let [key, value] of entries(obj)) {  console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3]}

Object.values()

返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值。返回数组的成员顺序,与本章的《属性的遍历》部分介绍的排列规则一致。
const obj = { 100: 'a', 2: 'b', 7: 'c' };Object.values(obj)// ["b", "c", "a"]
Object.create 方法的第二个参数添加的对象属性(属性 p),如果不显式声明,默认是不可遍历的,因为 p 的属性描述对象的 enumerable 默认是 false,Object.values 不会返回这个属性。只要把 enumerable 改成 true,Object.values 就会返回属性 p 的值。
const obj = Object.create({}, {p: {value: 42}});Object.values(obj) // []const obj = Object.create({}, {p:  {    value: 42,    enumerable: true  }});Object.values(obj) // [42]
会过滤属性名为 Symbol 值的属性。参数是一个字符串,会返回各个字符组成的一个数组。如果参数不是对象,Object.values 会先将其转为对象。由于数值和布尔值的包装对象,都不会为实例添加非继承的属性。所以,Object.values 会返回空数组。
Object.values({ [Symbol()]: 123, foo: 'abc' });// ['abc']Object.values('foo')// ['f', 'o', 'o']Object.values(42) // []Object.values(true) // []

Object.entries()

返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组。除了返回值不一样,该方法的行为与 Object.values 基本一致。Object.entries 的基本用途是遍历对象的属性,另一个用处是将对象转为真正的 Map 结构。
Object.entries({ [Symbol()]: 123, foo: 'abc' });// [ [ 'foo', 'abc' ] ]const obj = { foo: 'bar', baz: 42 };const map = new Map(Object.entries(obj));map // Map { foo: "bar", baz: 42 }

Object.fromEntries()

Object.fromEntries()方法是 Object.entries()的逆操作,用于将一个键值对数组转为对象。该方法的主要目的,是将键值对的数据结构还原为对象,因此特别适合将 Map 结构转为对象。配合URLSearchParams对象,将查询字符串转为对象。
Object.fromEntries([  ['foo', 'bar'],  ['baz', 42]])// { foo: "bar", baz: 42 }// 例一const entries = new Map([  ['foo', 'bar'],  ['baz', 42]]);Object.fromEntries(entries)// { foo: "bar", baz: 42 }// 例二const map = new Map().set('foo', true).set('bar', false);Object.fromEntries(map)// { foo: true, bar: false }Object.fromEntries(new URLSearchParams('foo=bar&baz=qux'))// { foo: "bar", baz: "qux" }

© 刘德华 2020 - 2023