对象传播与Object.assign

Oli*_*ari 343 javascript ecmascript-6

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

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

使用对象传播

options = {...optionsDefault, ...options};
Run Code Online (Sandbox Code Playgroud)

或者使用Object.assign

options = Object.assign({}, optionsDefault, options);
Run Code Online (Sandbox Code Playgroud)

这是让我惊讶的提交.

JMM*_*JMM 300

这不一定是详尽无遗的.

传播语法

options = {...optionsDefault, ...options};
Run Code Online (Sandbox Code Playgroud)

好处:

  • 如果创作代码在没有本机支持的环境中执行,您可能只需编译此语法(而不是使用polyfill).(以Babel为例.)

  • 不那么冗长.

缺点:

  • 一个建议,不规范.(考虑一下如果你现在写它会做什么,它没有标准化.)

  • 文字,而非动态.


Object.assign()

options = Object.assign({}, optionsDefault, options);
Run Code Online (Sandbox Code Playgroud)

好处:

  • 标准化.

  • 动态.例:

    var sources = [{a: "A"}, {b: "B"}, {c: "C"}];
    options = Object.assign.apply(Object, [{}].concat(sources));
    // or
    options = Object.assign({}, ...sources);
    
    Run Code Online (Sandbox Code Playgroud)
  • 更冗长.

缺点:

  • 如果创作代码在没有本机支持的环境中执行,则需要进行填充.

这是让我惊讶的提交.

这与你提出的问题没有直接关系.该代码没有使用Object.assign(),它正在使用用户代码(object-assign)执行相同的操作.他们似乎正在使用Babel编译该代码(并将其与Webpack捆绑在一起),这就是我所说的:您可以编译的语法.他们显然倾向于将其object-assign作为依赖包含在他们的构建中.

  • 可能值得注意的是,对象休息差价已经转移到第3阶段,因此将来很可能会标准化https://twitter.com/sebmarkbage/status/781564713750573056 (15认同)
  • @JMM我不确定我看到"更详细." 作为一个缺点. (11认同)
  • 正如@JMM所提到的,现在它已经在ES2018规范https://node.green/#ES2018-features-object-rest-spread-properties (7认同)
  • @Omar我猜你刚才证明这是一个意见:)如果有人不承认这个优势那么是的,其他条件相同他们只能使用`Object.assign()`.或者您可以手动迭代一组对象和他们自己的道具,手动将它们分配给目标并使其更加冗长:P (6认同)
  • “每个字节都很重要”这就是@yzorg 的最小化器/丑化器 (2认同)

tom*_*hes 150

对于参考对象,休息/传播在ECMAScript 2018中作为第4阶段最终确定.该提议可在此处找到.

在大多数情况下,对象重置和传播的工作方式相同,关键的区别在于spread定义属性,而Object.assign()设置它们.这意味着Object.assign()会触发setter.

值得记住的是,除此之外,对象rest/spread 1:1映射到Object.assign()并且对数组(可迭代)传播的行为不同.例如,在传播数组时,传播空值.但是,使用对象传播空值会无声地传播到空.

数组(可迭代)传播示例

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
Run Code Online (Sandbox Code Playgroud)

对象传播示例

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

console.log(z); //{a: 1, b: 2}
Run Code Online (Sandbox Code Playgroud)

这与Object.assign()的工作方式一致,都默默地排除null值而没有错误.

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

console.log(z); //{a: 1, b: 2}
Run Code Online (Sandbox Code Playgroud)

  • 这应该是选定的答案. (7认同)
  • 这应该是答案......现在是未来. (4认同)
  • 这是正确的答案。主要的不同是`Object.assign`将使用setter。`Object.assign({set a(v){this.b=v}, b:2}, {a:4}); // {b: 4}` 与 `{...{set a(v){this.b=v}, b:2}, ...{a:4}}; // {a: 4, b: 2}` (3认同)
  • “未来是现在!” -[乔治·艾伦](https://images-na.ssl-images-amazon.com/images/I/51QsOyT3SWL._SX307_BO1,204,203,200_.jpg) 明天就太晚了。 (2认同)
  • 以不同方式处理 null 的演示是“苹果和橘子”——不是有意义的比较。在数组情况下,null 是数组的一个元素。在展开的情况下,null 是整个对象。正确的比较是 x 具有 *null 属性*:`const x = {c: null};`。在这种情况下,据我所知,我们会看到类似于数组的行为:`//{a: 1, b: 2, c: null}`。 (2认同)
  • 谢谢 - 通过这一改变,我明白了你的观点。对象传播不会抱怨空对象,它只是跳过它,但如果尝试传播空对象,则数组传播会给出 TypeError 。 (2认同)

san*_*e89 34

There\'s a huge difference between the two, with very serious consequences. The most upvoted questions do not even touch this, and the information about object spread being a proposal is not relevant in 2022 anymore.

\n

The difference is that Object.assign changes the object in-place, while the spread operator (...) creates a brand new object, and this will break object reference equality.

\n

First, let\'s see the effect, and then I\'ll give a real-world example of how important it is to understand this fundamental difference.

\n

First, let\'s use Object.assign:

\n
// Let\'s create a new object, that contains a child object;\nconst parentObject = { childObject: { hello: \'world \'} };\n\n// Let\'s get a reference to the child object;\nconst childObject = parentObject.childObject;\n\n// Let\'s change the child object using Object.assign, adding a new `foo` key with `bar` value;\nObject.assign(parentObject.childObject, { foo: \'bar\' });\n\n// childObject is still the same object in memory, it was changed IN PLACE.\nparentObject.childObject === childObject\n// true\n
Run Code Online (Sandbox Code Playgroud)\n

Now the same exercise with the spread operator:

\n
// Let\'s create a new object, that contains a child object;\nconst parentObject = { childObject: { hello: \'world \'} };\n\n// Let\'s get a reference to the child object;\nconst childObject = parentObject.childObject;\n\n// Let\'s change the child object using the spread operator;\nparentObject.childObject = {\n  ...parentObject.childObject,\n  foo: \'bar\',\n}\n\n// They are not the same object in memory anymore!\nparentObject.childObject === childObject;\n// false\n
Run Code Online (Sandbox Code Playgroud)\n

It\'s easy to see what is going on, because on the parentObject.childObject = {...} we are cleary assigning the value of the childObject key in parentObject to a brand new object literal, and the fact it\'s being composed by the old childObject content\'s is irrelevant. It\'s a new object.

\n

And if you assume this is irrelevant in practice, let me show a real world scenario of how important it is to understand this.

\n

In a very large Vue.js application, we started noticing a lot of sluggishness when typing the name of the customer in an input field.

\n

After a lot of debugging, we found out that each char typed in that input triggered a hole bunch of computed properties to re-evaluate.

\n

This wasn\'t anticipated, since the customer\'s name wasn\'t used at all in those computeds functions. Only other customer data (like age, sex) was being used. What was goin on? Why was vue re-evaluating all those computed functions when the customer\'s name changed?

\n

Well, we had a Vuex store that did this:

\n
mutations: {\n  setCustomer(state, payload) {\n    // payload being { name: \'Bob\' }\n    state.customer = { ...state.customer, ...payload };\n  }\n
Run Code Online (Sandbox Code Playgroud)\n

And our computed were like this:

\n
veryExpensiveComputed() {\n   const customerAge = this.$store.state.customer.age;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

So, voil\xc3\xa1! When the customer name changed, the Vuex mutation was actually changing it to a new object entirely; and since the computed relied on that object to get the customer age, Vue counted on that very specific object instance as a dependency, and when it was changed to a new object (failing the === object equality test), Vue decided it was time to re-run the computed function.

\n

The fix? Use Object.assign to not discard the previous object, but to change it in place ...

\n
mutations: {\n  setCustomer(state, payload) {\n    // payload being same as above: { name: \'Bob\' }\n    Object.assign(state.customer, payload);\n  }\n
Run Code Online (Sandbox Code Playgroud)\n

BTW, if you are in Vue2, you shouldn\'t use Object.assign because Vue 2 can\'t track those object changes directly, but the same logic applies, just using Vue.set instead of Object.assign:

\n
mutations: {\n  setCustomer(state, payload) {\n    Object.keys(payload).forEach(key => {\n      Vue.set(state.customer, key, payload[key])\n    })\n  }\n
Run Code Online (Sandbox Code Playgroud)\n

  • 这是一个非常重要的区别,但问题是询问 `Object.assign({}, obj1, obj2)` 和 `{...obj1, ...obj2}` 之间的区别。将“{}”作为第一个参数可以减轻对“Object.assign”突变的任何担忧 (2认同)
  • 你在帖子中的 VueX 评论确实让我明白了两者之间的区别。多么美味的帖子啊。 (2认同)

Sea*_*son 27

我认为扩展运算符与Object.assign当前答案中似乎没有提到的差别在于扩展运算符不会保持原型完整.如果要向对象添加属性,并且不想更改它的实例,则必须使用它Object.assign.下面的例子应该证明这一点:

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

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

const errorExtendedUsingAssign = Object.assign(error, {
  someValue: true
});
errorExtendedUsingAssign instanceof Error; // true
Run Code Online (Sandbox Code Playgroud)

  • @maaartinus你是对的,我可能措辞很不好。我的意思是原型不在复制的对象上。我可能会对其进行更清晰的编辑。 (2认同)

Kar*_*mar 12

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

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

据我所知,这些是重要的差异.

  • Object.assign适用于大多数浏览器(无需编译)
  • ... 对象不标准化
  • ... 保护您免受意外改变对象
  • ... 如果没有它,将在没有它的浏览器中填充Object.assign
  • ... 需要更少的代码来表达相同的想法

  • 对于`Object.assign`来说,它不是*语法糖,因为扩展运算符总是会给你一个新对象. (29认同)
  • 实际上,我很惊讶其他人并没有更多地强调可变性差异。想想所有的开发人员时间都浪费在使用 Object.assign 调试意外突变上 (4认同)

Mic*_*umo 12

正如其他人所提到的那样,在写作的这个时刻,Object.assign()需要一个...polyfill 和对象传播需要一些转换(也可能是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);
Run Code Online (Sandbox Code Playgroud)

这两者都产生相同的输出.

这是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);
Run Code Online (Sandbox Code Playgroud)

这是我迄今为止的理解.Object.assign()实际上是标准化的,而目标传播...还没有.唯一的问题是前者和未来的浏览器支持,后者也是如此.

在这里玩代码

希望这可以帮助.

  • 嗯...你的两个案例*不会产生相同的结果吗?在第一种情况下,您将属性从一个对象复制到另一个对象,而在另一个对象中,您正在创建一个新对象.Object.assign返回目标,因此在第一种情况下objAss和newObjAss是相同的. (2认同)

yzo*_*org 11

我想通过工具总结浏览器和生态系统中"传播对象合并"ES功能的状态.

规格

浏览器:在Chrome,SF,Firefox中很快(版本60,IIUC)

  • 浏览器支持Chrome 60中提供的"传播属性" ,包括此方案.
  • 对这种情况的支持在当前的Firefox(59)中不起作用,但是在我的Firefox Developer Edition中可以使用.所以我相信它将在Firefox 60中发布.
  • Safari:未经过测试,但Kangax表示它适用于Desktop Safari 11.1,但不适用于SF 11
  • iOS Safari:没有尝试过,但Kangax表示它适用于iOS 11.3,但不适用于iOS 11
  • 还没在Edge

工具:节点8.7,TS 2.1

链接

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

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 }
Run Code Online (Sandbox Code Playgroud)

再次:在撰写本文时,此示例在Chrome(60+),Firefox Developer Edition(Firefox 60的预览版)和Node(8.7+)中无需转换.

为何回答?

我在原始问题之后的2.5 写这篇文章.但我有同样的问题,这就是谷歌发给我的地方.我是改善长尾的使命的奴隶.

由于这是"数组传播"语法的扩展,我发现谷歌很难找到,而且在兼容性表中很难找到.我能找到的最接近的是Kangax"属性传播",但该测试在同一个表达式中没有两个点差(不是合并).此外,提案/草稿/浏览器状态页面中的名称都使用"属性传播",但在我看来,这是社区在使用"对象合并"的扩展语法后提出的"第一主体".(这可能解释了为什么谷歌这么难.)所以我在这里记录我的发现,以便其他人可以查看,更新和编译有关此特定功能的链接.我希望它能够抓住.请帮助传播它在规范和浏览器中登陆的消息.

最后,我会将此信息添加为评论,但我无法在不破坏作者原始意图的情况下编辑它们.具体来说,我不能编辑@ChillyPenguin的评论而不会失去纠正@RichardSchulte的意图.但多年后理查德证明是对的(在我看来).所以我写了这个答案,希望它最终可以获得旧答案的牵引力(可能需要数年,但这就是长尾效应毕竟是什么).

  • 您可能不需要“为什么回答”部分 (3认同)

Cha*_*wen 8

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

Object.assign将setter应用于新对象,Spread不应用.此外,该对象必须是可迭代的.

复制 如果您此时需要对象的值,请使用此选项,并且您不希望该值反映对象的其他所有者所做的任何更改.

使用它来创建对象良好实践的浅表副本,以便始终将不可变属性设置为复制 - 因为可变版本可以传递给不可变属性,复制将确保您始终处理不可变对象

分配 分配与复制有些相反.Assign将生成一个setter,它直接将值赋给实例变量,而不是复制或保留它.调用assign属性的getter时,它返回对实际数据的引用.

  • 疯狂的是,只有这个答案解决了使用扩展运算符时引用相等性丢失的事实,并且这是这里第二少投票的答案。 (5认同)
  • 为什么说“复制”?这些大胆的标题是什么。读这篇文章时,我感觉好像错过了一些背景信息... (3认同)

Cal*_*twr 7

错误答案太多了...

我进行了搜索,发现了很多关于此的错误信息。

概括

  1. 两者...spread都不Object.assign是更快。这取决于。
  2. Object.assign如果不考虑副作用/对象突变,几乎总是更快。
  3. 除了性能之外,使用这两种方法通常没有优点/缺点,直到遇到边缘情况,例如复制包含 getter/setter 或只读属性的对象。在这里阅读更多内容。

表现

是否Object.assign更快...spread,这取决于您想要组合的内容以及您正在使用的运行时(实现和优化不时发生)。对于小物体来说,这并不重要。

对于较大的物体,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)
}
Run Code Online (Sandbox Code Playgroud)

如果担心副作用

在副作用很重要的情况下,例如将与另一个对象组合的对象作为参数传入。

在下面的示例中,将被突变,如果外部的其他函数/对象依赖于bigPayload,这是一个坏主意。在这种情况下,您可以交换传递给 的参数,或者仅用作第一个参数来创建新对象:retrieveAndCombinebigPayloadObject.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)
}
Run Code Online (Sandbox Code Playgroud)

测试

请尝试下面的代码片段,亲自看看是否可行。注意:运行需要一段时间。

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()
Run Code Online (Sandbox Code Playgroud)