Sebastian Markbåge 提出的 Rest/Spread Properties 提案包括两部分:
在对象解构模式下,rest 操作符会将解构源的除了已经在对象字面量中指明的属性之外的,所有可枚举自有属性拷贝到它的运算对象中。
const obj = {foo: 1, bar: 2, baz: 3}; |
如果你正在使用对象解构来处理命名参数,rest 操作符让你可以收集所有剩余参数:
function func({param1, param2, ...rest}) { // rest operator |
在每个对象字面量的顶层,可以使用 rest 操作符最多一次,并且必须只能在末尾出现:
const {...rest, foo} = obj; // SyntaxError |
如果是嵌套结构,你可以多次使用 rest 操作符:
const obj = { |
对象字面量内部,spread 操作符将自身运算对象的所有可枚举的自有属性,插入到通过字面量创建的对象中:
> const obj = {foo: 1, bar: 2, baz: 3}; |
要注意的是顺序问题,即使属性 key 并不冲突,因为对象会记录插入顺序:
> {qux: 4, ...obj} |
如果 key 出现了冲突,后面的会覆盖前面的属性:
> const obj = {foo: 1, bar: 2, baz: 3}; |
这一节,我们会看看 spread 操作符的使用场景。我也会用 Object.assign() 实现一遍,它和 spread 操作符很相似(之后我们会更详细地比较它们)。
拷贝对象 obj 的可枚举自有属性:
const clone1 = {...obj}; |
clone 对象们的原型都是 Object.prototype,它是所有通过对象字面量创建的对象的默认原型:
> Object.getPrototypeOf(clone1) === Object.prototype |
拷贝一个对象 obj,包括它的原型:
const clone1 = {__proto__: Object.getPrototypeOf(obj), ...obj}; |
注意,一般来说,对象字面量内部的 proto 只是浏览器内置的特性,并非 JavaScript 引擎所有。
有时候,你需要老老实实地拷贝对象的所有自有属性(properties)和特性(writable, enumerable, …),包括 getters 和 setters。这时候 Object.assign() 和 spread 操作符就回天乏术了。你需要使用属性描述符(property descriptors):
const clone1 = Object.defineProperties({}, |
如果还希望保留 obj 的原型,可以用 Object.create()
:
const clone2 = Object.create( |
“探索 ES2016 and ES2017” 里介绍了 Object.getOwnPropertyDescriptors()
我们之前见过的所有拷贝对象的方式,都是浅拷贝:如果原始属性值是一个对象,拷贝的对象将指向同一个对象,它不会(递归的、深度的)拷贝自身:
const original = { prop: {} }; |
合并 obj1 和 obj2 两个对象:
const merged = {...obj1, ...obj2}; |
给用户数据填充默认值
const DEFAULTS = {foo: 'a', bar: 'b'}; |
安全地更新属性 foo:
const obj = {foo: 'a', bar: 'b'}; |
指定属性 foo 和 bar 的默认值:
const userData = {foo: 1}; |
spread 操作符和 Object.assign() 很相似。主要的区别在于前者定义了新属性,而后者还进行了赋值。稍后将解释这究竟意味着什么。
Object.assign() 有两种使用方式:
第一种,带有破坏性的(修改已有对象):
Object.assign(target, source1, source2); |
这里的 target 对象被修改了;source1 和 source2 被拷贝进去了。
第二种,非破坏性的(已有对象不会被修改):
const result = Object.assign({}, source1, source2); |
新对象是通过将 source1 和 source2 拷贝进一个空对象而生成的。最终,这个新对象被返回并赋值给 result。
spread 操作符类似于 Object.assign() 的第二种方式。接下来,我们来看看两者的相似和不同之处。
在写对象之前,两者都使用了 ”get“ 操作符去读取源对象的属性值。这一过程会将 getter 被转换成正常的数据属性。
来看个例子:
const original = { |
original 有一个 foo getter(它的属性描述符有 get 和 set 属性)
> Object.getOwnPropertyDescriptor(original, 'foo') |
但是在它拷贝的结果 clone1 和 clone2 里,foo 是一个正常的数据属性(属性描述符有value 和 writable 属性):
> const clone1 = {...original}; |
spread 操作符在目标对象上定义了新的属性,而Object.assign() 使用了一个 “set” 操作符来创建属性。这会导致两个结果:
首先,Object.assign() 触发 setter,而 spread 不会:
Object.defineProperty(Object.prototype, 'foo', { |
以上代码段设置了一个 foo setter,它会被所有普通对象继承。
如果我们通过 Object.assign() 拷贝 obj,继承的 setter 会被触发:
> Object.assign({}, obj) |
而 spread 就不会:
> { ...obj } |
Object.assign() 在拷贝时还会触发自有 setter,这里并没有发生重写。
第二,你可以通过继承只读属性,来阻止 Object.assign() 创建自有属性,但 spread 上这是做不到的:
Object.defineProperty(Object.prototype, 'bar', { |
以上代码设置了只读属性 bar,它会被所有普通对象继承。
这样,你就再也不能使用赋值语句去创建自有属性 bar(严格模式下会抛一个异常,宽松模式会静默失败):
> const tmp = {}; |
下列代码,我们使用对象字面量成功地创建了属性 bar。因为对象字面量没有设置属性,它只是定义了它们:
const obj = {bar: 123}; |
然而,Object.assign() 使用赋值语句创建属性,这就是不能拷贝 obj 的原因:
> Object.assign({}, obj) |
通过 spread 操作符拷贝没有问题:
> { ...obj } |
它们都会忽略所有继承的属性和不可枚举的自有属性。
对象 obj 从 proto 继承了一个可枚举属性,并且有两个自有属性:
const proto = { |
如果拷贝 obj,结果将只有属性 ownEnumerable。属性 inheritedEnumerable 和 ownNonEnumerable 没有被拷贝:
> {...obj} |
原文:http://exploringjs.com/es2018-es2019/ch_rest-spread-properties.html