Rest/Spread 属性 • 探索 ES2018 和 ES2019
Rest/Spread 属性
Sebastian Markbåge的ECMAScript提案『Rest/Spread属性』可以:
rest操作符(…)在对象解构中的使用。目前,该操作符仅适用于数组解构和参数定义。
spread操作符(…)在对象字面量中的使用。目前,这个操作符只能在数组字面量和函数以及方法调用中使用。
在对象解构中使用rest操作符(...)
在对象解构模式中,rest操作符(…)将解构源的所有可枚举的属性复制到其操作数中,但对象自面量中已经提及的那些属性除外。
const obj = {foo:1,bar:2,baz:3}; const {foo,...rest} = obj;// Same as:// const foo = 1;// const reset = {bar: 2,baz: 3};
如果你正在使用对象解构来处理命名参数,rest操作符(…)可以收集其余所有参数。
function func(param1, param2, ...rest){ // rest 操作符 console.log('All parameters:', {param1,param2,...rest}); // spread 操作符 return param1 + param2; }
语法限制
在每个对象字面量的顶层,最多可以使用一次rest操作符,并且必须出现在对象字面量的末尾:
const {...rest,foo} = obj; // SyntaxErrorconst {foo,...rest1,...rest2} = obj; // SyntaxError
但是,如果对象字面量是嵌套的,就可以多次使用rest操作符:
const obj = { foo: { a:1, b:2, c:3, }, bar: 4, baz: 5, };const {foo:{a,...rest1},...rest2} = obj;// Same as:// const a = 1;// const rest1 = {b:2,c:3};// const rest2 = {bar:4,baz:5};
###在对象字面量中使用spread操作符(...)
通过对象字面量创建对象时,spread操作符(…)将其操作数的所有可枚举属性插入到创建的对象中:
> const obj = {foo:1,bar:2,baz:3}; > {...obj,qux:4} {foo:1,bar:2,baz:3,qux:4}
请注意,即使属性不冲突,顺序也很重要,因为对象会记录插入的顺序:
> {qux:4,...obj} {qux:4,foo:1,bar:2,baz:3}
如果属性发生冲突,顺序排在后面的属性值会覆盖前面的属性值:
> const obj = {foo:1,bar:2,baz:3}; > {...obj,foo:true} {foo:true,bar:2,baz:3} > {foo:true,...obj} {foo:1,bar:2,baz:3}
spread操作符的常见用例
在本节中,我们将介绍spread操作符可以在哪些场景中使用。在这些场景中我们还会用到Object.assign()方法,这个方法和spread操作符类似(我们将在后面详细介绍)。
克隆对象
克隆对象Obj的可枚举属性:
const clone1 = {...obj};const clone2 = Object.assign({},...obj);
克隆对象的原型总是Object.prototype,通过对象字面量创建的对象的原型默认也是Object.prototype:
> Object.getPrototypeOf(clone1) === Object.prototypetrue> Object.getPrototypeOf(clone2) === Object.prototypetrue> Object.getPrototypeOf({}) === Object.prototypetrue
克隆一个对象Obj,包括它的原型:
const clone1 = {__proto__: Object.getPrototypeOf(obj),...obj};const clone2 = Object.assign( Object.create(Object.getPrototypeOf(obj)),obj );
请注意,对象字面量中的Proto只是Web浏览器中实现的属性,一般来说,在javascript引擎中没有实现。(译者注:当Object.prototype.__proto__
已被大多数浏览器厂商所支持的今天,其存在和确切行为仅在ECMAScript 2015规范中被标准化为传统功能,以确保Web浏览器的兼容性。为了更好的支持,建议只使用 Object.getPrototypeOf()
)
真正的克隆对象
有时我们需要忠实地复制一个对象Obj的所有属性,包括(writable,enumerable,…)getter和setter。这时Object.assign()和spread操作符就不在起作用,我们需要使用属性描述符:
const clone1 = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj) );
Object.getOwnPropertyDescriptors()在『探索ES2016和ES2017』中有解释。
陷阱:克隆总是浅拷贝
请记住,通过之前讲过几种克隆方法,我们只能得到浅拷贝:如果其中的一个原始属性值是对象,则克隆将引用同一对象,但不会(递归地,深入地)克隆自己:
const original = {prop:{}};const clone = Object.assign({},original); console.log(original.prop === clone.prop);// trueoriginal.prop.foo = 'abc'; console.log(clone.prop.foo); // abc
各种其他用例
合并两个对象obj1和obj2:
const merged = {...obj1,...obj2};const merged = Object.assign({},obj1,obj2);
填写用户数据的默认值:
const DEFAULTS = {foo:'a',bar:'b'};const userData = {foo:1};const data = {...DEFAULTS,...userData};const data = Object.assigin({},DEFAULTS,userData);// {foo:1,bar:'b'}
非破坏性地更新foo属性:
const obj = {foo: 'a', bar: 'b'}; const obj2 = {...obj, foo: 1}; const obj2 = Object.assign({}, obj, {foo: 1}); // {foo: 1, bar: 'b'}
为内联属性foo和bar指定默认值:
const userData = {foo:1}; const data = {foo:'a',bar:'b',...userData}; const data = Object.assign({},{foo:'a',bar:'b'},userData);// {foo:1,bar:'b}
Spread与Object.assign()
spread操作符和Object.assign()非常相似,两者的主要区别是spread定义新的属性,但Object.assign()设置它们。我们会在后面解释到底是什么意思。
使用Object.assign()的两种方式
使用Object.assign()这里有两种方式:
第一种方式:破坏性地(现有的对象会被改变)。
Object.assign(target, source1, source2);
上面的代码中,target会被改变;source1和source2被复制到target中。
第二种方式:非破坏性地(现有的对象不会被改变)。
const result = Object.assign({}, source1, source2);
上面的代码中,通过对象字面量创建了一个空对象,并且source1和source2被复制到其中。
spread操作符与使用Object.assign()的第二种方式非常相似。接下来,我们就来看看两者的相似之处以及它们的不同之处。
spread和Object.assign()都是通过"get "取值
两个操作都是通过「get」从源对象读取属性,然后再把取到的属性写入目标对象。结果,在这个过程中,getters变成了普通的数据属性。
下面来看个例子:
const original = { get foo() { return 123; } };
original的getter为foo(它的属性描述符有get和set属性)
> Object.getOwnPropertyDescriptor(original, 'foo') { get: [Function: foo], set: undefined, enumerable: true, configurable: true }
但它的克隆clone1和clone2,foo是一个普通的数据属性(它的属性描述符具有属性值并且是可写的)
> const clone1 = {...original}; > Object.getOwnPropertyDescriptor(clone1, 'foo') { value: 123, writable: true, enumerable: true, configurable: true }> const clone2 = Object.assign({}, original); > Object.getOwnPropertyDescriptor(clone2, 'foo') { value: 123, writable: true, enumerable: true, configurable: true }
Spread定义属性,Object.assign()设置属性
spread操作符在目标对象中定义新属性,Object.assign()通过『set』来创建属性,这有两个后果。
使用setter的目标对象
首先,Object.assign()会触发setters,但spread不会触发:
Object.defineProperty(Object.prototype, 'foo', { set(value) { console.log('SET', value); }, });const obj = {foo: 123};
上面这段代码插入了一个能被所有普通对象继承的setter foo。
如果我们通过Object.assign()克隆obj,则会触发这个继承的setter:
> Object.assign({}, obj)SET 123 {}
使用spread操作符,则不会:
> { ...obj }{ foo: 123 }
Object.assign()也会在复制期间触发自己的setter,它不会覆盖它们。
具有只读属性的目标对象
另外,通过继承只读属性Object.assign()可以停止创建自己的属性,但spread操作符不能。
Object.defineProperty(Object.prototype, 'bar', { writable: false, value: 'abc', });
上面这段代码插入了一个能被所有普通对象继承的只读属性bar。
这样的话,就不能再通过赋值来创建自己的属性bar(只会在严格模式下得到一个异常;在非严格模式下,设置失败不会有异常提示)
> const tmp = {}; > tmp.bar = 123; TypeError: Cannot assign to read only property 'bar'
在下面的代码中,我们通过字面量成功创建了属性bar。这是有效的,因为字面量不设置属性,它们定义属性:
const obj = {bar: 123};
但是,Object.assgin()通过赋值来创建属性,这就是为什么我们无法克隆obj的原因:
> Object.assign({}, obj) TypeError: Cannot assign to read only property 'bar'
使用spread操作符是可以克隆的:
> { ...obj }{ bar: 123 }
spread和Object.assign()都只考虑自己的枚举属性
两个操作都忽略所有继承的属性和所有不可枚举的属性。
下面的obj对象继承了proto中的一个(可枚举)属性,并且有两个自己的属性:
const proto = { inheritedEnumerable: 1,};const obj = Object.create(proto, { ownEnumerable: { value: 2, enumerable: true, }, ownNonEnumerable: { value: 3, enumerable: false, },});
如果你克隆obj,结果只有属性ownEnumerable。不会复制inheritedEnumerable和ownNonEnumerable属性:
> {...obj}{ ownEnumerable: 2 }> Object.assign({}, obj){ ownEnumerable: 2 }
共同学习,写下你的评论
评论加载中...
作者其他优质文章