Rest/Spread 属性
Sebastian Markbåge 的 ECMAScript 提案Rest/Spread 属性 https://github.com/sebmarkbage/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; // SyntaxError
- const {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() http://exploringjs.com/es6/ch_oop-besides-classes.html#Object_assign 方法, 这个方法和 spread 操作符类似(我们将在后面详细介绍).
克隆对象
克隆对象 Obj 的可枚举属性:
- const clone1 = {...obj};
- const clone2 = Object.assign({},...obj);
克隆对象的原型总是 Object.prototype, 通过对象字面量创建的对象的原型默认也是 Object.prototype:
- > Object.getPrototypeOf(clone1) === Object.prototype
- true
- > Object.getPrototypeOf(clone2) === Object.prototype
- true
- > Object.getPrototypeOf({}) === Object.prototype
- true
克隆一个对象 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()
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/getPrototypeOf )
真正的克隆对象
有时我们需要忠实地复制一个对象 Obj 的所有属性, 包括 (writable,enumerable,...)getter 和 setter. 这时 Object.assign() 和 spread 操作符就不在起作用, 我们需要使用属性描述符 http://speakingjs.com/es5/ch17.html#property_attributes :
- const clone1 = Object.defineProperties({},
- Object.getOwnPropertyDescriptors(obj)
- );
Object.getOwnPropertyDescriptors() http://exploringjs.com/es2016-es2017/ch_object-getownpropertydescriptors.html 在探索 ES2016 和 ES2017中有解释.
陷阱: 克隆总是浅拷贝
请记住, 通过之前讲过几种克隆方法, 我们只能得到浅拷贝: 如果其中的一个原始属性值是对象, 则克隆将引用同一对象, 但不会 (递归地, 深入地) 克隆自己:
- const original = {prop:{}};
- const clone = Object.assign({},original);
- console.log(original.prop === clone.prop);// true
- original.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(它的属性描述符 http://speakingjs.com/es5/ch17.html#property_attributes 有 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 }
来源: https://juejin.im/entry/5b20c98851882513fd20033f