对象的扩展与新增方法、运算符的扩展
一、对象的扩展
1. 属性的简洁表示法
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!";
}
};
2. 属性名表达式
ES6
允许字面量定义对象时,用表达式作为对象的属性名,即把表达式放在方括号内。
let propKey = 'foo';
let obj = {
[propKey]: true,
['a' + 'bc']: 123
};
3. 属性的可枚举性和遍历
对象的每个属性都有一个描述对象(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
的属性,只拷贝对象自身的可枚举的属性。
总的来说,操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以,尽量不要用for...in
循环,而用Object.keys()
代替。
ES6
一共有 5
种方法可以遍历对象的属性。
for...in
循环遍历对象自身的和继承的可枚举属性(不含 Symbol
属性)。
Object.keys
返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol
属性)的键名。
Object.getOwnPropertyNames
返回一个数组,包含对象自身的所有属性(不含 Symbol
属性,但是包括不可枚举属性)的键名。
Object.getOwnPropertySymbols
返回一个数组,包含对象自身的所有 Symbol
属性的键名。
Reflect.ownKeys
返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol
或字符串,也不管是否可枚举。
4. 对象的扩展运算符
解构赋值
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }
由于解构赋值要求等号右边是一个对象,所以如果等号右边是undefined
或null
,就会报错,因为它们无法转为对象。
另外,解构赋值必须是最后一个参数,否则同样会报错。
let { ...z } = null; // 运行时错误
let { ...z } = undefined; // 运行时错误
let { ...x, y, z } = someObject; // 句法错误
let { x, ...y, ...z } = someObject; // 句法错误
注意,解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本。
另外,扩展运算符的解构赋值,不能复制继承自原型对象的属性。
let o1 = { a: 1 };
let o2 = { b: 2 };
o2.__proto__ = o1;
let { ...o3 } = o2;
o3 // { b: 2 }
o3.a // undefined
如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉。
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
属性,拷贝到新对象后会被覆盖掉。
如果把自定义属性放在扩展运算符前面,就变成了设置新对象的默认属性值。
let aWithDefaults = { x: 1, y: 2, ...a };
// 等同于
let aWithDefaults = Object.assign({}, { x: 1, y: 2 }, a);
// 等同于
let aWithDefaults = Object.assign({ x: 1, y: 2 }, a);
二、对象的新增方法
1、Object.assign()
Object.assign()
方法用于对象的合并,将源对象(source
)的所有可枚举属性,复制到目标对象(target
)。
const target = { a: 1, b: 1 };
const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
Object.assign()
方法的第一个参数是目标对象,后面的参数都是源对象。
注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
如果该参数不是对象,则会先转成对象,然后返回。
由于undefined
和null
无法转成对象,所以如果它们作为参数,就会报错。
如果非对象参数出现在源对象的位置(即非首参数),那么处理规则有所不同。首先,这些参数都会转成对象,如果无法转成对象,就会跳过。
typeof Object.assign(2) // "object"
Object.assign(undefined) // 报错
Object.assign(null) // 报错
let obj = {a: 1};
Object.assign(obj, undefined) === obj // true
Object.assign(obj, null) === obj // true
其他类型的值(即数值、字符串和布尔值)不在首参数,也不会报错。但是,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果。如下所示:
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()
拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),不会拷贝不可枚举的属性(enumerable: false
)。
但属性名为 Symbol
值的属性,也会被Object.assign()
拷贝。
注意点
Object.assign()
方法实行的是浅拷贝,而不是深拷贝。如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。
const obj1 = {a: {b: 1}, c: 3};
const obj2 = Object.assign({}, obj1);
obj1.a.b = 2;
obj1.a.c = 4;
obj2.a.b // 2
obj2.a.c // 3,非引用类数据不会相互影响
- 遇到同名属性,
Object.assign()
的处理方法是替换,而不是添加。
const target = { a: { b: 'c', d: 'e' } }
const source = { a: { b: 'hello' } }
Object.assign(target, source)
// { a: { b: 'hello' } }
Object.assign()
可以用来处理数组,但是会把数组视为对象。
Object.assign([1, 2, 3], [4, 5])
// [4, 5, 3]
上面代码中,Object.assign()
把数组视为属性名为 0
、1
、2
的对象,因此源数组的 0
号属性4
覆盖了目标数组的 0
号属性1
。
Object.assign()
只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制。
const source = {
get foo() { return 1 }
};
const target = {};
Object.assign(target, source)
// { foo: 1 }
2、__proto__属性,Object.setPrototypeOf(),Object.getPrototypeOf()
__proto__
属性(前后各两个下划线),用来读取或设置当前对象的原型对象(prototype
),,不推荐使用,非标准API
。
Object.setPrototypeOf
方法的作用与__proto__
相同,用来设置一个对象的原型对象(prototype
),返回参数对象本身
// 格式,第一个参数不能是undefined或者null
Object.setPrototypeOf(object, prototype)
// 用法
const o = Object.setPrototypeOf({}, null);
// 等同于
function setPrototypeOf(obj, proto) {
obj.__proto__ = proto;
return obj;
}
// 实例
let proto = {};
let obj = { x: 10 };
Object.setPrototypeOf(obj, proto);
proto.y = 20;
proto.z = 40;
obj.x // 10
obj.y // 20
obj.z // 40
上面代码将proto
对象设为obj
对象的原型,所以从obj
对象可以读取proto
对象的属性。
Object.getPrototypeOf()
方法与Object.setPrototypeOf
方法配套,用于读取一个对象的原型对象。
function Rectangle() {
// ...
}
const rec = new Rectangle();
Object.getPrototypeOf(rec) === Rectangle.prototype
// true
Object.setPrototypeOf(rec, Object.prototype);
Object.getPrototypeOf(rec) === Rectangle.prototype
// false
3、Object.keys(),Object.values(),Object.entries()、Object.fromEntries()
Object.keys
方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable
)属性的键名。
Object.values
方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable
)属性的键值。
Object.entries()
方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable
)属性的键值对数组。
const obj = { foo: 'bar', baz: 42 };
Object.entries(obj)
// [ ["foo", "bar"], ["baz", 42] ]
Object.fromEntries()
方法是Object.entries()
的逆操作,用于将一个键值对数组转为对象。
Object.fromEntries([
['foo', 'bar'],
['baz', 42]
])
// { foo: "bar", baz: 42 }
三、运算符的扩展
1、链判断运算符
编程实务中,如果读取对象内部的某个属性,往往需要判断一下,属性的上层对象是否存在。比如,读取message.body.user.firstName
这个属性,安全的写法是写成下面这样。
// 错误的写法
const firstName = message.body.user.firstName || 'default';
// 正确的写法
const firstName = (message
&& message.body
&& message.body.user
&& message.body.user.firstName) || 'default';
这样的层层判断非常麻烦,因此 ES2020
引入了“链判断运算符”(optional chaining operator
)?.
,简化上面的写法。
const firstName = message?.body?.user?.firstName || 'default';
const fooValue = myForm.querySelector('input[name=foo]')?.value
上面代码使用了?.
运算符,直接在链式调用的时候判断,左侧的对象是否为null
或undefined
。如果是的,就不再往下运算,而是返回undefined
。
该运算符也可以用于判断对象方法是否存在,如果存在就立即执行。如下所示。
iterator.return?.()
注意,如果iterator.return
不是null
或undefined
,但也不是函数,那么iterator.return?.()
会报错。
2、Null 判断运算符
const headerText = response.settings.headerText || 'Hello, world!';
const animationDuration = response.settings.animationDuration || 300;
const showSplashScreen = response.settings.showSplashScreen || true;
如上所示,读取对象属性的时候,如果某个属性的值是null
或undefined
,有时候需要为它们指定默认值。常见做法是通过||运算符指定默认值。但是这样存在问题,属性的值为null
或undefined
,默认值就会生效,但是属性的值如果为空字符串或false
或0
,默认值也会生效。
为了避免这种情况,ES2020
引入了一个新的 Null
判断运算符??
。它的行为类似||
,但是只有运算符左侧的值为null
或undefined
时,才会返回右侧的值。
const headerText = response.settings.headerText ?? 'Hello, world!';
const animationDuration = response.settings.animationDuration ?? 300;
const showSplashScreen = response.settings.showSplashScreen ?? true;
??
本质上是逻辑运算,它与其他两个逻辑运算符&&
和||
有一个优先级问题,它们之间的优先级到底孰高孰低。优先级的不同,往往会导致逻辑运算的结果不同。
现在的规则是,如果多个逻辑运算符一起使用,必须用括号表明优先级,否则会报错。
// 报错
lhs && middle ?? rhs
lhs ?? middle && rhs
// 正确用法
(lhs && middle) ?? rhs;
lhs && (middle ?? rhs);