对象的扩展
属性的简洁表示法
ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法
const foo = 'bar';
const baz = {foo}; // ! 简写
baz // {foo: "bar"}
// 等同于
const baz = {foo: foo};
function f(x, y) {
return {x, y};
}
// 等同于
function f(x, y) {
return {x: x, y: y};
}
f(1, 2) // Object {x: 1, y: 2}
不仅是属性名,方法也可以简写
const o = {
method() {
return "Hello!";
}
};
// 等同于
const o = {
method: function() {
return "Hello!";
}
};
// 这种写法用于函数的返回值,将会非常方便。
注意,简写的对象方法不能用作构造函数,会报错。
属性名表达式
js 定义对象的属性,有两种方法
// 方法一 ES5 仅支持这一种
obj.foo = true;
// 方法二
obj['a' + 'bc'] = 123;
方法二还可以定义方法名
注意,属性名表达式与简洁表示法,不能同时使用,会报错。
// 报错
const foo = 'bar';
const bar = 'abc';
const baz = { [foo] };
// 正确
const foo = 'bar';
const baz = { [foo]: 'abc'};
注意,属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串
[object Object]
,这一点要特别小心。
const keyA = {a: 1};
const keyB = {b: 2};
const myObject = {
[keyA]: 'valueA', // 这里 以一个对象作为属性名
[keyB]: 'valueB'
};
myObject // Object {[object Object]: "valueB"}
方法的 name 属性
函数的 name 属性,返回函数名
如果对象的方法使用了取值函数,和存值函数,则 name 属性不是在该方法上面,而是在 该方法的属性的描述对象的 get 和 set 属性上面
const obj = {
get foo() {},
set foo(x) {}
};
obj.foo.name
// TypeError: Cannot read property 'name' of undefined
const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');
descriptor.get.name // "get foo"
descriptor.set.name // "set foo"
特殊情况
-
bind方法创造的函数,name属性返回 bound 加上原函数名字
-
Function 构造函数创造的函数,name 属性返回 anonymous
(new Function()).name // "anonymous" var doSomething = function() { // ... }; doSomething.bind().name // "bound doSomething"
如果对象的方法是一个 Symbol 值,那么name
属性返回的是这个 Symbol 值的描述。
const key1 = Symbol('description');
const key2 = Symbol();
let obj = {
[key1]() {},
[key2]() {},
};
obj[key1].name // "[description]"
obj[key2].name // ""
属性的可枚举性和遍历
可枚举性
-
对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。
Object.getOwnPropertyDescriptor
方法可以获取该属性的描述对象。let obj = { foo: 123 }; Object.getOwnPropertyDescriptor(obj, 'foo') // { // value: 123, // writable: true, // enumerable: true, // 可枚举属性 // configurable: true // }
描述对象的
enumerable
属性,称为“可枚举性”,如果该属性为false
,就表示某些操作会忽略当前属性。四个操作会忽略不可枚举的 即该属性位false
-
for...in
循环:只遍历对象自身的和继承的可枚举的属性。 -
Object.keys()
:返回对象自身的所有可枚举的属性的键名。 -
JSON.stringify()
:只串行化对象自身的可枚举的属性。 -
Object.assign()
: 忽略enumerable
为false
的属性,只拷贝对象自身的可枚举的属性。 (ES6新增)
实际上,引入“可枚举”(
enumerable
)这个概念的最初目的,就是让某些属性可以规避掉for...in
操作,不然所有内部属性和方法都会被遍历到另外,ES6 规定,所有 Class 的原型的方法都是不可枚举的。
总的来说,操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以,尽量不要用
for...in
循环,而用Object.keys()
代替。 -
属性的遍历
-
ES6 一共5中方法遍历对象属性
(1)for…in
for...in
循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。(2)Object.keys(obj)
Object.keys
返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。(3)Object.getOwnPropertyNames(obj)
Object.getOwnPropertyNames
返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。(4)Object.getOwnPropertySymbols(obj)
Object.getOwnPropertySymbols
返回一个数组,包含对象自身的所有 Symbol 属性的键名。(5)Reflect.ownKeys(obj)
Reflect.ownKeys
返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。以上 5 种方法遍历对象键名,都遵守同样的顺序规则
- 首先遍历所有数值键,按照数值升序排列
- 其次遍历所有字符串键,按照加入时间升序排列
- 最后遍历所有 Symbol 键,按照加入时间升序排列
super 关键字
我们知道 this 关键字总是指向函数所在的当前对象,ES6 新增一个类似的关键字 super,指向当前对象的原型对象
const proto = {
foo: 'hello'
};
const obj = {
foo: 'world',
find() {
return super.foo; // 调用原型对象的foo属性
}
};
Object.setPrototypeOf(obj, proto);
obj.find() // "hello"
注意,
super
关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。
// 报错
const obj = {
foo: super.foo
}
// 报错
const obj = {
foo: () => super.foo
}
// 报错
const obj = {
foo: function () {
return super.foo
}
}
// 上面 2 3 种方法,其实都是 super用在一个函数里面,然后赋值给 foo 属性!
目前,只有对象方法的简写法可以让 JavaScript 引擎确认,定义的是对象的方法。
const proto = {
x: 'hello',
foo() {
console.log(this.x);
},
};
const obj = {
x: 'world',
foo() {
super.foo();
}
}
Object.setPrototypeOf(obj, proto);
obj.foo() // "world" // 因为是在obj中 调用的!!!!!! 所以此时 this 指向 obj
对象的扩展运算符
解构赋值
-
对象的解构赋值用来从一个对象取值,相当于将目标对象 自身的所有可遍历的、但尚未被读取的属性,分配到指定的对象上面,所有的键和它们的值,都会拷贝到新对象上面
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 }; x // 1 y // 2 z // { a: 3, b: 4 }
由于解构赋值要求等号右边是一个对象,所以如果等号右边是
undefined
或null
,就会报错,因为它们无法转为对象。解构赋值必须是最后一个参数,否则会报错。
注意,解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本。
let obj = { a: { b: 1 } }; let { ...x } = obj; obj.a.b = 2; x.a.b // 2 因为是浅拷贝的缘故
扩展运算符的解构取值,不能复制继承自原型对象的属性
let o1 = { a: 1 }; let o2 = { b: 2 }; o2.__proto__ = o1; let { ...o3 } = o2; o3 // { b: 2 } o3.a // undefined
const o = Object.create({ x: 1, y: 2 }); o.z = 3; let { x, ...newObj } = o; let { y, z } = newObj; x // 1 因为是单纯的解构赋值,所以可以读到继承来的属性 y // undefined z // 3
上面因为,使用扩展运算符只能读取到属于 o 自身的属性,所以无法读取 y,只能读到 z。
ES6 规定,变量声明语句之中,如果使用解构赋值,扩展运算符后面必须是一个变量名,而不能是一个解构赋值表达式,所以上面代码引入了中间变量
newObj
,如果写成下面这样会报错。let { x, ...{ y, z } } = o; // SyntaxError: ... must be followed by an identifier in declaration contexts
-
解构赋值的一个用处,是扩展某个函数的参数,引入其他操作。
扩展运算符
-
对象的扩展运算符(
...
)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中let z = { a: 3, b: 4 }; let n = { ...z }; n // { a: 3, b: 4 } // 数组也可以 let foo = { ...['a', 'b', 'c'] }; foo // {0: "a", 1: "b", 2: "c"}
空对象,没有效果
不是对象,会自己转成对象
// 等同于 {...Object(true)} {...true} // {} // 等同于 {...Object(undefined)} {...undefined} // {} // 等同于 {...Object(null)} {...null} // {}
但是,如果扩展运算符后面是字符串,它会自动转成一个类似数组的对象,因此返回的不是空对象。
{...'hello'} // {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}
对象的扩展运算符等同于使用 Object.assign() 方法
let aClone = { ...a }; // 等同于 let aClone = Object.assign({}, a);
上面只是拷贝了对象实例的属性,完整克隆一个对象,还要拷贝对象原型的睡醒
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) )
-
扩展运算符可以用来合并两个对象
let ab = { ...a, ...b }; // 等同于 let ab = Object.assign({}, a, b);
如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉。
let aWithOverrides = { ...a, x: 1, y: 2 }; // 等同于 let aWithOverrides = { ...a, ...{ x: 1, y: 2 } }; // 等同于 let x = 1, y = 2, aWithOverrides = { ...a, x, y }; // 等同于 let aWithOverrides = Object.assign({}, a, { x: 1, y: 2 }); // 上面代码中,a对象的x属性和y属性,拷贝到新对象后会被覆盖掉。
可以用来修改某些属性的值
与数组的扩展运算符一样,对象的扩展运算符后面可以跟表达式。
扩展运算符的参数对象之中,如果有取值函数
get
,这个函数是会执行的。let a = { get x() { throw new Error('not throw yet'); } } let aWithXGetter = { ...a }; // 报错
AggregateError 错误对象
ES2021 标准之中,为了配合新增的Promise.any()
方法(详见《Promise 对象》一章),还引入一个新的错误对象AggregateError
AggregateError 在一个错误对象里面,封装了多个错误。如果某个单一操作,同时引发了多个错误,需要同时抛出这些错误,那么就可以抛出一个 AggregateError 错误对象,把各种错误都放在这个对象里面
AggregateError 本身是一个构造函数,用来生成 AggregateError 实例
AggregateError(errors[, message])
- errors 数组,每个成员都是一个错误对象,必须参数
- message,提示信息,可选参数
const error = new AggregateError([
new Error('ERROR_11112'),
new TypeError('First name must be a string'),
new RangeError('Transaction value must be at least 1'),
new URIError('User profile link must be https'),
], 'Transaction cannot be processed')
AggregateError
的实例对象有三个属性。
- name:错误名称,默认为“AggregateError”。
- message:错误的提示信息。
- errors:数组,每个成员都是一个错误对象。
try {
throw new AggregateError([
new Error("some error"),
], 'Hello');
} catch (e) {
console.log(e instanceof AggregateError); // true
console.log(e.message); // "Hello"
console.log(e.name); // "AggregateError"
console.log(e.errors); // [ Error: "some error" ]
}
总结
本文主要介绍了ES6中关于对象的扩展,包括属性的间接表示法,属性名表达式([]),name属性,属性的可枚举性和遍历,super关键字,对象的扩展运算符,以及AggregateError对象。