作者:Valeri Karpov @code_barbarian 2019/02/19 原文链接
对象的剩余/延展操作提案到达了stage4在2018年,这就意味着在未来它将会被纳入到ECMAScript的规范中。自Node.js8起,它也被包含在Node.js的LTS中,所以今天你可以安全的使用它。
$ node -v
v8.9.4
$ node
> const obj = { foo: 1, bar: 1 };
undefined
> {...obj, baz: 1 };
{ foo:1, bar: 1, baz: 1 }
对象延展操作符{...obj}
和Object.assign()
相似,所以,你应该用哪个?事实证明,答案要比您想象的微妙的多。
Object Spread 速览
对象延展操作符的核心思想是利用已存在的对象的属性创建一个新的普通对象。因此{...obj}
创建了一个有着和obj相同的属性和值的新对象。对于普通的旧JS对象你其实就是创建了一个obj的拷贝。
const obj = { foo:'bar' };
const clone = { ...obj }; // `{ foo: 'bar' }`
obj.foo = 'baz';
clone.foo; // 'bar'
如同Object.assign()
,对象延展操作符不对继承属性和类信息进行拷贝。它却拷贝ES6 symbols。
class BaseClass {
foo(){
return 1;
}
}
class MyClass extends BaseClass {
bar(){
return 2;
}
}
const obj = new MyClass();
obj.baz = function(){
return 3;
}
obj[Symbol.for('test')] = 4;
//此操作不从MyClass或者BaseClass拷贝任何属性
const clone = { ...obj };
console.log(clone);// {baz:[Function],[Symbol(test)]:4}
console.log(clone.constructor.name); // Object
console.log(clone instance MyClass); // false
你还可以借助对象延展操作符混入其他属性。顺序有影响:对象延展操作符会重写先前定义的属性,而不会覆盖后加入的。
const obj = { a: 'a', b: 'b', c: 'c' };
{ a: 1, b: null, c: void 0, ...obj }; // { a: 'a', b: 'b', c: 'c' }
{ a: 1, b: null, ...obj, c: void 0 }; // { a: 'a', b: 'b', c: undefined }
{ a: 1, ...obj, b: null, c: void 0 }; // { a: 'a', b: null, c: undefined }
{ ...obj, a: 1, b: null, c: void 0 }; // { a: 1, b: null, c: undefined }
与Object.assign()的区别
对于上述示例,本质上说对象延展操作符有是可以用Object.assign()替换的。事实上对象延展规则明确表明{...obj}
和Object.assign({},obj)
是相等的。
const obj = { a: 'a', b: 'b', c: 'c' };
Object.assign({ a: 1, b: null, c: void 0 }, obj); // { a: 'a', b: 'b', c: 'c' }
Object.assign({ a: 1, b: null }, obj, { c: void 0 }); // { a: 'a', b: 'b', c: undefined }
Object.assign({ a: 1 }, obj, { b: null, c: void 0 }); // { a: 'a', b: null, c: undefined }
Object.assign({}, obj, { a: 1, b: null, c: void 0 }); // { a: 1, b: null, c: undefined }
所以,是什么原因让你选择其中之一呢?一个关键的区别就是对象延展操作符总是返回给你一个POJO对象。Object.assign()
函数则是更新它的第一位有效参数:
class MyClass {
set val(v) {
console.log('Setter called', v);
return v;
}
}
const obj = new MyClass();
Object.assign(obj, { val: 42 }); // Prints "Setter called 42"
换而言之,Object.assign()
更新一个合适的对象,也因此它会触发ES6 setters。如果你更喜欢使用immutable技巧,对象延展操作符明显更胜一筹。使用Object.assign()
,你不得不每次都要确保你传递的第一位参数是一个空对象{}
。
关于性能呢?这里有一些简单的衡量基准。如果是拿Object.assign({},obj)
和对象延展操作符对比的话,那么对象延展操作符更快。如果是其他形式的调用,这两者平分秋色,可以互换。
这是一个使用benchmark对Object.assign()
进行的测试:
const Benchmark = require('benchmark');
const suite = new Benchmark.Suite;
const obj = { foo: 1, bar: 2 };
suite.
add('Object spread', function() {
({ baz: 3, ...obj });
}).
add('Object.assign()', function() {
Object.assign({ baz: 3 }, obj);
}).
on('cycle', function(event) {
console.log(String(event.target));
}).
on('complete', function() {
console.log('Fastest is ' + this.filter('fastest').map('name'));
}).
run({ 'async': true });
在这个测试案例中,两者结果很相似:
Object spread x 3,170,111 ops/sec +-1.50% (90 runs sampled)
Object.assign() x 3,290,165 ops/sec +-1.86% (88 runs sampled)
Fastest is Object.assign()
然而,一旦是将一个空对象作为第一位参数传递给Object.assign()
,对象延展操作符则稳操胜券了:
suite.
add('Object spread', function() {
({ baz: 3, ...obj });
}).
add('Object.assign()', function() {
Object.assign({}, obj, { baz: 3 });
})
输出如下:
Object spread x 3,065,831 ops/sec +-2.12% (85 runs sampled)
Object.assign() x 2,461,926 ops/sec +-1.52% (88 runs sampled)
Fastest is Object spread
ESLint配置
默认情况下,ESLint不允许在解析器级别上使用对象剩余/延展操作符。你需要在.eslintrc.yml
里设置parserOptions.ecmaVersion
选项至少是9,否则你将遇到解析报错。
parserOptions:
# Otherwise object spread causes 'Parsing error: Unexpected token ..'
ecmaVersion: 9
ESLint添加了一个新的规则prefer-object-spread
,它允许你强制使用对象延展操作符而不是Object.assign()
,开启此功能,按如下配置:
parserOptions:
ecmaVersion: 9
rules:
prefer-object-spread: error
现在如果你使用Object.assign()
而不是对象延展操作符,ESlint将会报告如下错误:
Use an object spread instead of `Object.assign` eg: `{ ...foo }` prefer-object-spread
未完待续
与对象Object.assign()
相比,对象延展操作符在语法上既整洁又提供性能优势。如果您正在运行Node.js 8或更高版本,请尝试使用这些新运算符,并使您的代码更简洁。