对象扩展vs. Object.assign

假设我有一个options变量,我想设置一些默认值。

这两种选择的优点/缺点是什么?

使用对象扩展

options = {...optionsDefault, ...options};

或者使用Object.assign

options = Object.assign({}, optionsDefault, options);

这就是让我疑惑的提交

287110 次浏览

这并不一定详尽无遗。

传播的语法

options = {...optionsDefault, ...options};

优点:

  • 如果在没有本机支持的环境中编写代码以执行,则可以只编译此语法(而不是使用polyfill)。(比如巴别塔。)

  • 可以减少复杂性。

缺点:

  • 当这个答案最初写的时候,这是一个提案,没有标准化。在使用提案时,请考虑如果您现在使用提案编写代码,而它没有得到标准化或在向标准化发展的过程中发生变化,该怎么办。这已经在ES2018中标准化了。

  • 字面的,不是动态的。


# EYZ0

options = Object.assign({}, optionsDefault, options);

优点:

  • 标准化。

  • < p >动态。例子:

    var sources = [{a: "A"}, {b: "B"}, {c: "C"}];
    options = Object.assign.apply(Object, [{}].concat(sources));
    // or
    options = Object.assign({}, ...sources);
    

Disadvantages:

  • More verbose.
  • If authoring code for execution in environments without native support you need to polyfill.

This is the commit that made me wonder.

That's not directly related to what you're asking. That code wasn't using Object.assign(), it was using user code (object-assign) that does the same thing. They appear to be compiling that code with Babel (and bundling it with Webpack), which is what I was talking about: the syntax you can just compile. They apparently preferred that to having to include object-assign as a dependency that would go into their build.

对象扩展操作符(…)在浏览器中不起作用,因为它还不是任何ES规范的一部分,只是一个建议。唯一的选择是用Babel(或类似的东西)编译它。

正如您所看到的,它只是Object.assign({})上的语法糖。

在我看来,这些是重要的区别。

  • 对象。赋值在大多数浏览器中都有效(不需要编译)
  • 对象的...没有标准化
  • ...可以防止意外地改变对象
  • ...将填充对象。在没有它的浏览器中赋值
  • ...需要更少的代码来表达相同的思想
这现在是ES6的一部分,因此是标准化的,并且在MDN上也有文档: # EYZ0 < / p >

它使用起来非常方便,与对象解构一起使用也很有意义。

上面列出的另一个优势是object. assign()的动态功能,不过这就像在文字对象中展开数组一样简单。在编译后的babel输出中,它完全使用了Object.assign()所演示的内容

因此,正确的答案是使用object spread,因为它现在是标准化的,被广泛使用(参见react, redux等),易于使用,并且具有object .assign()的所有特性。

正如其他人所提到的,在写这篇文章的时候,Object.assign()需要一个polyfill,而...需要一些transpiling(可能也需要一个polyfill)才能工作。

考虑下面的代码:

// Babel wont touch this really, it will simply fail if Object.assign() is not supported in browser.
const objAss = { message: 'Hello you!' };
const newObjAss = Object.assign(objAss, { dev: true });
console.log(newObjAss);


// Babel will transpile with use to a helper function that first attempts to use Object.assign() and then falls back.
const objSpread = { message: 'Hello you!' };
const newObjSpread = {...objSpread, dev: true };
console.log(newObjSpread);

它们都产生相同的输出。

下面是从Babel到ES5的输出:

var objAss = { message: 'Hello you!' };
var newObjAss = Object.assign(objAss, { dev: true });
console.log(newObjAss);


var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };


var objSpread = { message: 'Hello you!' };
var newObjSpread = _extends({}, objSpread, { dev: true });
console.log(newObjSpread);

这是我目前的理解。Object.assign()实际上是标准化的,而作为对象传播的...还没有。唯一的问题是浏览器对前者以及未来对后者的支持。

< a href = " http://babeljs.io/repl/ ? babili = false&评估= true& lineWrap = false&预设= es2015 % 2 creact % 2 cstage-2&代码= % 0 aconst % 20 objSpread % 20% 3 d % 20% 7 b % 20消息% 3 a % 20% 27你好% 20你! % 27% 20% 7 d % 3 b % 0 aconst % 20 newObjSpread % 20% 3 d % 20% 7 b…objSpread % 2 c % 20 dev % % 20 3真的% 20% 7 d % 3 b % 0 aconsole.log (newObjSpread) % 3 b % 0 a % 0 aconst % 20 objAss % 20% 3 d % 20% 7 b % 20消息% 3 a % 20% 27你好% 20你! % 27% 20% 7 d % 3 b % 0 aconst % 20 newObjAss % 20% 3 d % 20 object.assign (objAss % 2 c % 20% 7 b % 20 dev % 3 7 d) % 20真正% 20% % 3 b % 0 aconsole.log (newObjAss) % 3 b”rel="noreferrer">在这里玩代码

希望这能有所帮助。

作为参考,对象rest/spread在ECMAScript 2018中作为第4阶段完成。该提案可以在在这里中找到。

在大多数情况下,对象赋值和展开的工作方式是相同的,关键的区别是spread定义属性,而Object.assign()设置属性. #。这意味着Object.assign()触发设置器。

值得记住的是,除此之外,对象rest/spread 1:1映射到object .assign(),与数组(可迭代)spread的作用不同。例如,当展开一个数组时,将展开空值。然而,使用对象传播时,空值会被无声地传播到空值。

数组(可迭代)扩展示例

const x = [1, 2, null , 3];
const y = [...x, 4, 5];
const z = null;


console.log(y); // [1, 2, null, 3, 4, 5];
console.log([...z]); // TypeError

对象扩展示例

const x = null;
const y = {a: 1, b: 2};
const z = {...x, ...y};


console.log(z); //{a: 1, b: 2}

这与Object.assign()的工作方式是一致的,两者都无声地排除空值而没有错误。

const x = null;
const y = {a: 1, b: 2};
const z = Object.assign({}, x, y);


console.log(z); //{a: 1, b: 2}

我想总结一下“扩展对象合并”ES特性在浏览器和工具生态系统中的状态。

规范

浏览器:Chrome, SF, Firefox(60版本,IIUC)

  • 浏览器支持“传播属性”在Chrome 60中发布,包括这个场景。
  • 在当前的Firefox(59)中不支持此场景,但在我的Firefox开发者版中可以。所以我相信它会在Firefox 60中发布。
  • Safari:未测试,但Kangax说它可以在桌面Safari 11.1中运行,但不适用于SF 11
  • iOS Safari:未测试,但Kangax说它可以在iOS 11.3运行,但不能在iOS 11运行
  • 还没有在Edge中

工具:Node 8.7, TS 2.1

链接

  • < a href = " http://kangax.github。io/compat-table/es2016plus/#test-object_rest/spread_properties_object_spread_properties" rel="noreferrer">Kangax "属性扩展" . io/compat-table/es2016plus/#test-object_rest/spread_properties_object_spread_properties" rel="noreferrer">Kangax "属性扩展
  • # EYZ0

代码示例(兼作兼容性测试)

var x = { a: 1, b: 2 };
var y = { c: 3, d: 4, a: 5 };
var z = {...x, ...y};
console.log(z); // { a: 5, b: 2, c: 3, d: 4 }

再次强调:在编写此示例时,无需编译即可在Chrome(60+)、Firefox Developer Edition (Firefox 60预览版)和Node(8.7+)中工作。

为什么答案?

我把这个2.5 写在原来的问题之后。但我也有同样的问题,谷歌让我来这里。我是SO改善长尾理论使命的奴隶。

由于这是“数组扩散”语法的扩展,我发现它很难谷歌,而且很难在兼容性表中找到。我能找到的最接近的是Kangax“财产蔓延”,但该测试在同一个表达式中没有两个扩展(不是merge)。此外,提案/草案/浏览器状态页面中的名称都使用了“属性扩展”,但在我看来,这是社区在建议使用扩展语法进行“对象合并”后达成的“第一个原则”。(这也许可以解释为什么谷歌这么难。)所以我在这里记录了我的发现,这样其他人就可以查看、更新和编译关于这个特定特性的链接。我希望它能流行起来。请帮助传播它登陆规范和浏览器的消息。

最后,我本可以将这些信息作为评论添加进来,但我无法在不破坏作者原始意图的情况下编辑它们。具体来说,我无法编辑@ chillpenguin的评论,否则它就会失去纠正@RichardSchulte的意图。但多年后,理查德(在我看来)证明是对的。所以我写了这个答案,希望它最终能在旧答案的基础上获得吸引力(可能需要几年的时间,但这就是长尾效应的意义所在)。

注意:Spread不仅仅是Object.assign的语法糖。它们在幕后的运作方式大不相同。

对象。assign对新对象应用setter,而Spread则不会。此外,对象必须是可迭代的。

<强>复制 如果您需要对象当前的值,并且您不希望该值反映该对象的其他所有者所做的任何更改,则使用此选项

用于创建对象的浅拷贝 始终将不可变属性设置为copy的好做法-因为可变版本可以传递到不可变属性中,copy将确保您始终处理一个不可变对象

< >强分配 赋值在某种程度上与复制相反。 Assign将生成一个setter,它将值直接赋给实例变量,而不是复制或保留它。 当调用赋值属性的getter时,它返回一个对实际数据的引用。< / p >

其他答案都是旧的,不能得到一个好的答案 下面的例子是对象字面量,帮助如何两者可以互补,以及如何不能互补(因此有差异):

var obj1 = { a: 1,  b: { b1: 1, b2: 'b2value', b3: 'b3value' } };


// overwrite parts of b key
var obj2 = {
b: {
...obj1.b,
b1: 2
}
};
var res2 = Object.assign({}, obj1, obj2); // b2,b3 keys still exist
document.write('res2: ', JSON.stringify (res2), '<br>');
// Output:
// res2: {"a":1,"b":{"b1":2,"b2":"b2value","b3":"b3value"}}  // NOTE: b2,b3 still exists


// overwrite whole of b key
var obj3 = {
b: {
b1: 2
}
};
var res3 = Object.assign({}, obj1, obj3); // b2,b3 keys are lost
document.write('res3: ', JSON.stringify (res3), '<br>');
// Output:
// res3: {"a":1,"b":{"b1":2}}  // NOTE: b2,b3 values are lost
这里还有几个小例子,同样是数组&对象:< br > # EYZ0 < / p >

我认为扩展操作符和Object.assign之间的一个很大的区别,在当前的答案中似乎没有提到的是,扩展操作符不会将源对象的原型复制到目标对象。如果你想给一个对象添加属性,并且你不想改变它的实例,那么你将不得不使用Object.assign

编辑:实际上我已经意识到我的例子是有误导性的。展开运算符将初始化为Object.assign,第一个参数设置为空对象。在下面的代码示例中,我将error作为Object.assign调用的第一个参数,因此这两个参数是不相等的。Object.assign的第一个参数实际上被修改,然后返回,这就是它保留原型的原因。我在下面又加了一个例子:

const error = new Error();
error instanceof Error // true


const errorExtendedUsingSpread = {
...error,
...{
someValue: true
}
};
errorExtendedUsingSpread instanceof Error; // false


// What the spread operator desugars into
const errorExtendedUsingImmutableObjectAssign = Object.assign({}, error, {
someValue: true
});
errorExtendedUsingImmutableObjectAssign instanceof Error; // false


// The error object is modified and returned here so it keeps its prototypes
const errorExtendedUsingAssign = Object.assign(error, {
someValue: true
});
errorExtendedUsingAssign instanceof Error; // true

参见:https://github.com/tc39/proposal-object-rest-spread/blob/master/Spread.md

当你必须使用Object.assign时,我想添加这个简单的例子。

class SomeClass {
constructor() {
this.someValue = 'some value';
}


someMethod() {
console.log('some action');
}
}




const objectAssign = Object.assign(new SomeClass(), {});
objectAssign.someValue; // ok
objectAssign.someMethod(); // ok


const spread = {...new SomeClass()};
spread.someValue; // ok
spread.someMethod(); // there is no methods of SomeClass!

使用JavaScript时可能不清楚。但是如果你想创建一些类的实例,使用TypeScript会更容易

const spread: SomeClass = {...new SomeClass()} // Error

展开运算符将数组展开到函数的单独参数中。

let iterableObjB = [1,2,3,4]
function (...iterableObjB)  //turned into
function (1,2,3,4)

我们将创建一个名为identity的函数,它只返回我们给它的任何参数。

identity = (arg) => arg

和一个简单的数组。

arr = [1, 2, 3]

如果用arr求恒等函数,我们知道会发生什么

(1)创建对象的浅副本和(2)将多个对象合并为单个对象的方法在2014年至2018年之间有了很大的发展。

下面概述的方法在不同时期变得可用并广泛使用。这个答案提供了一些历史视角,但并不详尽。

  • 如果没有任何库或现代语法的帮助,您将使用for-in循环,例如:

    var mergedOptions = {}
    for (var key in defaultOptions) {
    mergedOptions[key] = defaultOptions[key]
    }
    for (var key in options) {
    mergedOptions[key] = options[key]
    }
    
    
    options = mergedOptions
    

2006

  • jQuery 1.0有jQuery.extend():

    options = $.extend({}, defaultOptions, options)
    

2010

  • < p > # EYZ1 # EYZ2

    options = _.extend({}, defaultOptions, options)
    

2014

2015

  • Object.assignChrome (45), Firefox(34)和Node.js (4)支持。不过,旧的运行时需要Polyfill。

    options = Object.assign({}, defaultOptions, options)
    
  • 对象Rest/Spread Properties提案进入阶段2。

2016

  • 对象Rest/扩展属性语法没有被包含在ES2016中,但是提案已经到了第三阶段。

2017

2018

  • 对象Rest/Spread Properties提案达到了第4阶段,语法被包含在ES2018标准中。

当目标对象是一个常量并且您想一次设置多个属性时,Object.assign是必要的。

例如:

const target = { data: "Test", loading: true }

现在,假设你需要用一个源的所有属性来改变目标:

const source = { data: null, loading: false, ...etc }


Object.assign(target, source) // Now target is updated
target = { ...target, ...source) // Error: cant assign to constant

请记住,你正在改变目标obj,所以尽可能使用Object.assign空目标或扩散分配给一个新的obj。

两者之间有着巨大的差异,并会带来非常严重的后果。投票最多的问题甚至没有触及到这一点,关于物体传播是一项提案的信息在2022年已经无关紧要了。

不同之处在于#EYZ0就地更改对象,而展开操作符(...)创建一个全新的对象,这将破坏对象引用相等性。

首先,让我们看看效果,然后我将给出一个现实世界的例子,说明理解这种根本差异是多么重要。

首先,让我们使用Object.assign:

// Let's create a new object, that contains a child object;
const parentObject = { childObject: { hello: 'world '} };


// Let's get a reference to the child object;
const childObject = parentObject.childObject;


// Let's change the child object using Object.assign, adding a new `foo` key with `bar` value;
Object.assign(parentObject.childObject, { foo: 'bar' });


// childObject is still the same object in memory, it was changed IN PLACE.
parentObject.childObject === childObject
// true

现在对展开运算符进行同样的练习:

// Let's create a new object, that contains a child object;
const parentObject = { childObject: { hello: 'world '} };


// Let's get a reference to the child object;
const childObject = parentObject.childObject;


// Let's change the child object using the spread operator;
parentObject.childObject = {
...parentObject.childObject,
foo: 'bar',
}


// They are not the same object in memory anymore!
parentObject.childObject === childObject;
// false

很容易看出发生了什么,因为在parentObject.childObject = {...}上,我们清楚地将parentObject中的childObject键的值分配给全新的对象文字,而它由旧的childObject内容组成的事实是无关紧要的。这是一个新物件。

如果你认为这在实践中是无关紧要的,让我展示一个真实世界的场景来理解它有多重要。

在一个非常大的Vue.js应用程序中,当我们在输入字段中输入客户的名字时,我们开始注意到很多延迟。

经过大量调试后,我们发现在输入中输入的每个字符都会触发一堆需要重新计算的computed属性。

这是意料之外的,因为在那些计算函数中根本没有使用客户的名称。只使用了其他客户数据(如年龄、性别)。发生了什么事?当客户的名字改变时,vue为什么要重新计算所有这些计算函数?

我们有一家Vuex商店是这样做的:

mutations: {
setCustomer(state, payload) {
// payload being { name: 'Bob' }
state.customer = { ...state.customer, ...payload };
}

我们的计算是这样的:

veryExpensiveComputed() {
const customerAge = this.$store.state.customer.age;
}

所以,瞧!当客户名称更改时,Vuex突变实际上是将其完全更改为一个新对象;由于计算依赖于该对象来获取客户年龄,Vue依赖于那个非常具体的对象实例作为依赖项,当它被更改为一个新对象时(未通过===对象相等性测试),Vue决定是时候重新运行计算函数了。

这是固定的吗?使用对象。赋值不丢弃前一个对象,而是在适当的位置更改它…

mutations: {
setCustomer(state, payload) {
// payload being same as above: { name: 'Bob' }
Object.assign(state.customer, payload);
}

顺便说一句,如果你在ve2中,你不应该使用Object。因为Vue 2不能直接跟踪这些对象的变化,但同样的逻辑适用,只是使用Vue。set而不是Object.assign:

mutations: {
setCustomer(state, payload) {
Object.keys(payload).forEach(key => {
Vue.set(state.customer, key, payload[key])
})
}

太多的错误答案……

我搜索了一下,发现了很多关于这方面的错误信息。

总结

  1. ...spreadObject.assign都不是更快。视情况而定。
  2. 大多数情况下,Object.assign更快。如果不考虑副作用,性能可以提高一倍。
  3. 除了性能之外,使用这两种方法通常都没有优点或缺点,直到遇到极限情况,例如复制包含getter /setter的对象或只读属性。阅读更多在这里

性能

哪个更快取决于你想要组合的对象,以及你正在使用的运行时(实现和优化不时发生)。对于小物件,并不总是清楚谁是赢家。

然而,对于较大的对象,Object.assign通常更好。特别是当您不需要关心副作用时,您可以通过不复制第一个对象来节省时间。是这样的:

async function retrieveAndCombine() {
const bigPayload = await retrieveData()
const smallPayload = await retrieveData2()
    

// only the properties of smallPayload is iterated through
// whereas bigPayload is mutated.
return Object.assign(bigPayload, smallPayload)
}

如果有副作用的话

在副作用很重要的情况下,例如如果它作为参数传入,retrieveAndCombine之外的范围内的bigPayload将会发生突变。这并不理想。在这种情况下,你可以交换传递给Object.assign的参数,或者直接使用{}作为第一个参数来创建一个新对象:

async function retrieveAndCombine(bigPayload) {
const smallPayload = await retrieveData2()


// this is slightly more efficient
return Object.assign(smallPayload, bigPayload)


// this is still okay assuming `smallPayload` only has a few properties
return Object.assign({}, smallPayload, bigPayload)
}

测试

试试下面的代码片段。注意:运行需要一段时间。

const rand = () => (Math.random() + 1).toString(36).substring(7)


function combineBigObjects() {
console.log('Please wait...creating the test objects...')
const obj = {}
const obj2 = {}


for (let i = 0; i < 100000; i++) {
const key = rand()
obj[rand()] = {
[rand()]: rand(),
[rand()]: rand(),
}
obj2[rand()] = {
[rand()]: rand(),
[rand()]: rand(),
}
}
console.log('Combine big objects using spread:')
console.time()
const result1 = {
...obj,
...obj2,
}
console.timeEnd()


console.log('Combine big objects using assign:')
console.time()
Object.assign({}, obj, obj2)
console.timeEnd()


console.log('Combine big objects using assign, but mutates first obj:')
console.time()
Object.assign(obj, obj2)
console.timeEnd()
}


combineBigObjects()


function combineSmallObjects() {
const firstObject = () => ({ [rand()]: rand() })
const secondObject = () => ({ [rand()]: rand() })
console.log('Combine small objects using spread:')
console.time()
for (let i = 0; i < 500000; i++) {
const finalObject = {
...firstObject(),
...secondObject(),
}
}
console.timeEnd()


console.log('Combine small objects using assign:')
console.time()
for (let i = 0; i < 500000; i++) {
const finalObject = Object.assign({}, firstObject(), secondObject())
}
console.timeEnd()


console.log('Combine small objects using assign, but mutates first obj:')
console.time()
for (let i = 0; i < 500000; i++) {
const finalObject = Object.assign(firstObject(), secondObject())
}
console.timeEnd()
}


combineSmallObjects()