对象传播与 Object.assign
Posted
技术标签:
【中文标题】对象传播与 Object.assign【英文标题】:Object spread vs. Object.assign 【发布时间】:2015-12-31 17:50:54 【问题描述】:假设我有一个options
变量,我想设置一些默认值。
这两种选择的优点/缺点是什么?
使用对象传播
options = ...optionsDefault, ...options;
或者使用 Object.assign
options = Object.assign(, optionsDefault, options);
这就是让我好奇的commit。
【问题讨论】:
嗯,第一个是提议的新语法,不是 ES6 的一部分,所以这取决于你要遵守什么标准。 定义“best”(小心,不要以基于意见的问题结束:-) 如果在没有本机支持的环境中运行,也可能取决于您希望如何支持它。您可能只能编译的语法。您可能需要 polyfill 的对象或方法。 除了兼容性问题之外,Object.assign 可以改变原始对象,这很有用。不能传播。 澄清@pstanton 的评论 - object.assign 可以修改现有的 target 对象(从源覆盖属性,同时保持其他属性不变);它不会触及 source 对象。我首先将他的“原始对象”读为“源对象”,因此为其他类似误读的人写下这篇笔记。 :) 【参考方案1】:这不一定是详尽的。
扩展语法
options = ...optionsDefault, ...options;
优点:
如果编写代码以在没有本机支持的环境中执行,您可以只编译此语法(而不是使用 polyfill)。 (以 Babel 为例。)
不那么冗长。
缺点:
最初编写此答案时,这是proposal,未标准化。在使用提案时,请考虑如果您现在使用它编写代码并且它没有标准化或随着它走向标准化而发生变化,您会做什么。此后,这已在 ES2018 中标准化。
文字,非动态。
Object.assign()
options = Object.assign(, optionsDefault, options);
优点:
标准化。
动态。示例:
var sources = [a: "A", b: "B", c: "C"];
options = Object.assign.apply(Object, [].concat(sources));
// or
options = Object.assign(, ...sources);
缺点:
更详细。 如果编写代码以在没有本机支持的环境中执行,则需要 polyfill。这是让我好奇的提交。
这与您的要求没有直接关系。该代码没有使用Object.assign()
,而是使用了执行相同操作的用户代码(object-assign
)。他们似乎正在使用 Babel 编译该代码(并将其与 Webpack 捆绑),这就是我所说的:您可以编译的语法。他们显然更愿意将 object-assign
作为依赖项包含在他们的构建中。
【讨论】:
可能值得注意的是,object rest spread 已移至第 3 阶段,因此将来可能会标准化twitter.com/sebmarkbage/status/781564713750573056 @JMM 我不确定我是否看到“更详细”。作为一个缺点。 @Omar 好吧,我想您只是证明了这是一种观点 :) 如果有人不承认这种优势,那么是的,在其他条件相同的情况下,他们可以使用Object.assign()
。或者您可以手动迭代对象数组和它们自己的道具,手动将它们分配给目标并使其更加冗长:P
正如@JMM 提到的,它现在在 ES2018 规范中 node.green/#ES2018-features-object-rest-spread-properties
"每个字节都很重要" 这就是最小化/丑化对@yzorg 的作用【参考方案2】:
作为参考,对象休息/传播在 ECMAScript 2018 中作为第 4 阶段最终确定。该提案可以在 here 找到。
在大多数情况下,对象重置和传播的工作方式相同,关键区别在于spread defines properties, whilst Object.assign() sets them。这意味着 Object.assign() 会触发 setter。
值得记住的是,除此之外,object 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
对象传播示例
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
【讨论】:
这应该是答案......现在就是未来。 这是正确的答案。主要的不同是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
“未来就是现在!” -George Allen明天太晚了。
null 处理方式不同的演示是“苹果和橙子”——没有意义的比较。在数组的情况下,null 是数组的一个元素。在展开的情况下,null 是整个对象。正确的比较是 x 具有 null 属性:const x = c: null;
。在这种情况下,AFAIK,我们会看到类似数组的行为://a: 1, b: 2, c: null
。
谢谢——有了这个改变,我明白你的意思了。 object spread 不会抱怨 null 对象,它只是跳过它,但是如果尝试传播 null 对象,array spread 会给出 TypeError。【参考方案3】:
我认为扩展运算符和Object.assign
之间的一大区别似乎在当前答案中没有提到,即扩展运算符不会将源对象的原型复制到目标对象。如果您想向对象添加属性并且不想更改它的实例,则必须使用Object.assign
。
编辑:我实际上已经意识到我的示例具有误导性。扩展运算符对Object.assign
进行脱糖,第一个参数设置为空对象。在下面的代码示例中,我将错误作为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
【讨论】:
“不会保持原型完整” - 它肯定会,因为它不会修改任何东西。在您的示例中,errorExtendedUsingAssign === error
,但errorExtendedUsingSpread
是一个新对象(原型未被复制)。
@maaartinus 你说得对,我的措辞可能很糟糕。我的意思是原型不在复制的对象上。我可能会更清楚地编辑它。
以下是“浅克隆”对象及其类的方法吗? let target = Object.create(source); Object.assign(target, source);
@ToolmakerSteve 是的,它会复制所有对象的“自己的属性”,实际上是一个浅层克隆。见:***.com/questions/33692912/…
@wortwart 我想区别实际上是扩展运算符不会影响源对象(在这种情况下为error
),而Object.assign
将修改源对象并返回它。扩展运算符实际上只是 Object.assign 的糖,第一个参数设置为空对象 (github.com/tc39/proposal-object-rest-spread/blob/master/…)【参考方案4】:
注意:Spread 不仅仅是 Object.assign 周围的语法糖。它们在幕后的运作方式大不相同。
Object.assign 将 setter 应用于新对象,Spread 不会。此外,对象必须是可迭代的。
复制 如果您现在需要对象的值,并且不希望该值反映对象的其他所有者所做的任何更改,请使用此选项。
用它来创建对象的浅拷贝 始终将不可变属性设置为复制的好习惯 - 因为可变版本可以传递到不可变属性中,所以复制将确保您始终处理不可变对象
分配 分配与复制有些相反。 分配将生成一个直接将值分配给实例变量的设置器,而不是复制或保留它。 调用 assign 属性的 getter 时,它返回对实际数据的引用。
【讨论】:
为什么说“复制”?这些粗体标题是什么。当我读到这篇文章时,我觉得我错过了一些背景...... 我认为您说“复制”时的意思是“传播”,如果是,我理解您的回答:) 只有这个答案解决了使用扩展运算符时参考相等性丢失的事实,这是第二个最不赞成的答案,这太疯狂了。【参考方案5】:我想通过工具总结一下“扩展对象合并”ES功能在浏览器和生态系统中的状态。
规格
https://github.com/tc39/proposal-object-rest-spread 此语言功能 is a stage 4 proposal,这意味着它已合并到 ES 语言规范中,但尚未广泛实施。浏览器:在 Chrome、SF、Firefox 中(版本 60,IIUC)
浏览器支持“传播属性”shipped in Chrome 60,包括这种情况。 目前的 Firefox (59) 不支持这种情况,但在我的 Firefox 开发者版中可以。所以我相信它会在 Firefox 60 中发布。 Safari:未经测试,但 Kangax 表示它适用于 Desktop Safari 11.1,但不适用于 SF 11 ios Safari:未测试,但 Kangax 表示它适用于 iOS 11.3,但不适用于 iOS 11 尚未在 Edge 中工具:节点 8.7、TS 2.1
NodeJS 从 8.7 开始支持(通过 Kangax)。我在本地测试时于 9.8 确认。 TypeScript has suported it since 2.1,当前为 2.8链接
Kangax "property spread" https://davidwalsh.name/merge-objects代码示例(兼作兼容性测试)
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 开发者版(Firefox 60 预览版)和 Node(8.7+)中运行。
为什么要回答?
我写这篇文章是在最初的问题之后的 2.5 年。但我也有同样的问题,这就是谷歌发给我的地方。我是 SO 改善长尾任务的奴隶。
由于这是“数组扩展”语法的扩展,我发现很难用谷歌搜索,也很难在兼容性表中找到。我能找到的最接近的是Kangax "property spread",但该测试在同一个表达式中没有两个展开(不是合并)。此外,提案/草稿/浏览器状态页面中的名称都使用“property spread”,但在我看来,这是社区在提议使用传播语法进行“对象合并”之后得出的“第一原则”。 (这可以解释为什么用 google 很难。)所以我在这里记录我的发现,以便其他人可以查看、更新和编译有关此特定功能的链接。我希望它能流行起来。请帮助传播它在规范和浏览器中登陆的消息。
最后,我会将此信息添加为评论,但我无法在不破坏作者原意的情况下对其进行编辑。具体来说,我无法编辑@ChillyPenguin 的评论,而不会失去纠正@RichardSchulte 的意图。但多年后,理查德证明是对的(在我看来)。所以我改写了这个答案,希望它最终会在旧答案上获得吸引力(可能需要数年时间,但这就是 long tail 效应的全部意义所在)。
【讨论】:
您的“为什么回答”部分可能不需要 @LocustHorde 也许我可以将第二段(为什么这个主题很难用谷歌搜索)移到它自己的部分。然后其余的可能适合评论。 请注意,对于console.log(z)
,Safari 会有以下内容:b: 2, c: 3, d: 4, a: 5
(顺序不同)【参考方案6】:
正如其他人所提到的,在撰写本文时,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);
它们都产生相同的输出。
这是从 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()
实际上是标准化的,而对象传播 ...
还没有。唯一的问题是浏览器对前者的支持,未来也支持后者。
Play with the code here
希望这会有所帮助。
【讨论】:
谢谢!您的代码示例使我的上下文非常容易做出决定。转译器(babel 或 typescript)通过在内联代码中包含 pollyfill 使扩展运算符与浏览器更兼容。只是出于兴趣,TypeScript 转译版本实际上与 Babel 相同:typescriptlang.org/play/… hmm...你的两个案例not不会产生相同的结果吗?在第一种情况下,您将属性从一个对象复制到另一个对象,而在另一种情况下,您正在创建一个新对象。 Object.assign 返回目标,因此在第一种情况下 objAss 和 newObjAss 是相同的。 添加新的第一个参数
应该可以解决不一致问题。【参考方案7】:
对象扩展运算符 (...) 在浏览器中不起作用,因为它还不是任何 ES 规范的一部分,只是一个提议。唯一的选择是用 Babel(或类似的东西)编译它。
如您所见,它只是 Object.assign() 之上的语法糖。
据我所知,这些是重要的区别。
Object.assign 适用于大多数浏览器(无需编译)...
对象未标准化
...
可防止您意外改变对象
...
将在没有它的浏览器中填充 Object.assign
...
需要更少的代码来表达相同的想法
【讨论】:
Object.assign
的 not 语法糖,因为扩展运算符总是会给你一个新对象。
实际上,我很惊讶其他人没有更多地强调可变性差异。想想开发人员在使用 Object.assign 调试意外突变时浪费的所有时间
现在大多数现代浏览器都支持这一点(与其他 ES6 一样):developer.mozilla.org/en-US/docs/Web/javascript/Reference/…【参考方案8】:
其他答案太老了,找不到好的答案。 下面的示例是针对对象文字的,有助于两者如何相互补充,以及它如何不能相互补充(因此存在差异):
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
这里还有几个小例子,也适用于数组和对象:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
【讨论】:
【参考方案9】:这现在是 ES6 的一部分,因此是标准化的,并且在 MDN 上也有文档: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator
使用起来非常方便,与对象解构一起使用也很有意义。
上面列出的另一个优势是 Object.assign() 的动态功能,但是这就像在字面量对象中展开数组一样简单。在编译的 babel 输出中,它使用了 Object.assign() 所展示的内容
所以正确的答案是使用 object spread,因为它现在已经标准化、广泛使用(参见 react、redux 等),易于使用,并且具有 Object.assign() 的所有功能
【讨论】:
不,它不是 ES6 的一部分。您提供的链接仅指展开运算符在 arrays 上的使用。正如 JMM 的回答中所解释的,在 objects 上使用扩展运算符目前是第 2 阶段(即草稿)提案。 我认为我从未见过如此完全基于虚假信息的答案。即使在一年之后,这也不是 ES 规范的一部分,并且在大多数环境中不受支持。 Chilly 和 3o 原来是错的,Richard 是对的。浏览器支持和工具支持都在落地,但在理查德给出答案后花了 1.5 年。有关截至 2018 年 3 月的支持摘要,请参阅我的新答案。 @yzorg 谢谢你的辩护,我,但是我的回答实际上是错误的 2 年,所以他们在评论时都是正确的。很高兴您提供更新的答案! ChillyPenguin 你是对的。 3ocene同意了,这可能是我见过的最错误的答案哈哈!它在您发表评论一年后登陆 ES2018。太糟糕了,我不能对自己的答案投反对票。我会删除它,但从技术上讲,如果您现在阅读它,它仍然有效。老实说?我仍然喜欢有时使用 Object.assign() :shrug: @RikkiSchulte 当时我很沮丧,这个页面上的大多数信息都是过时的“历史”信息,给人的印象是该功能仍然不可用。但是,话又说回来,我的回答中仍然有一些警告,这些警告已经过时了,应该被删除,或者整个更新。【参考方案10】:当您必须使用 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
【讨论】:
【参考方案11】:从 2014 年到 2018 年,(1) 创建对象的浅表副本和 (2) 将多个对象合并为一个对象的方法发生了很大变化。
下面概述的方法在不同时期变得可用并被广泛使用。这个答案提供了一些历史视角,并不详尽。
如果没有库或现代语法的任何帮助,您将使用 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
Underscore.js 1.0 has_.extend()
options = _.extend(, defaultOptions, options)
⋮
2014
2ality published an article about Object.assign()
coming to ES2015
object-assign
发布到 npm。
var objectAssign = require('object-assign')
options = objectAssign(, defaultOptions, options)
The Object Rest/Spread Properties syntax 建议用于 ES2016。
2015
Object.assign
受 Chrome (45), Firefox (34) and Node.js (4) 支持。不过,较旧的运行时需要 Polyfill。
options = Object.assign(, defaultOptions, options)
Object Rest/Spread Properties 提案进入第 2 阶段。
2016
Object Rest/Spread Properties 语法未包含在 ES2016 中,但提案已进入第 3 阶段。2017
Object Rest/Spread Properties 语法未包含在 ES2017 中,但可在 Chrome (60), Firefox (55), and Node.js (8.3) 中使用。不过,较旧的运行时需要进行一些转译。
options = ...defaultOptions, ...options
2018
Object Rest/Spread Properties 提案已进入第 4 阶段,语法已包含在 ES2018 标准中。【讨论】:
【参考方案12】: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。
【讨论】:
【参考方案13】:展开运算符将数组展开到函数的单独参数中。
let iterableObjB = [1,2,3,4]
function (...iterableObjB) //turned into
function (1,2,3,4)
【讨论】:
这个问题是关于应用于对象的扩展语法与Object.assign
的使用,与数组或函数调用无关。【参考方案14】:
我们将创建一个名为 identity 的函数,它只返回我们给它的任何参数。
identity = (arg) => arg
还有一个简单的数组。
arr = [1, 2, 3]
如果你用 arr 调用 identity,我们就知道会发生什么
【讨论】:
以上是关于对象传播与 Object.assign的主要内容,如果未能解决你的问题,请参考以下文章