对象扩展vs.对象赋值
Object spread vs. Object.assign
假设我有一个options
变量,我想设置一个默认值。
这两种选择的优缺点是什么?
使用对象扩展
options = {...optionsDefault, ...options};
或者使用Object.assign
options = Object.assign({}, optionsDefault, options);
这是让我好奇的提交。
这并不一定是详尽的
<标题>传播语法options = {...optionsDefault, ...options};
优点:
如果在没有本机支持的环境中编写执行代码,您可以只编译此语法(而不是使用polyfill)。(以Babel为例)
可以减少复杂性。
缺点:
当这个答案最初写的时候,这是一个建议,不是标准化的。在使用提案时,考虑一下如果你现在用它写代码,而它没有标准化,或者在标准化的过程中发生变化,你会怎么做。这已在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);
缺点:
- 更详细。
- 如果在没有本机支持的环境中编写执行代码,则需要填充。
这是一个让我好奇的提交。
这和你问的没有直接关系。该代码没有使用Object.assign()
,它使用的是用户代码(object-assign
),它做同样的事情。他们似乎是在用Babel编译代码(并将其与Webpack捆绑在一起),这就是我所说的:你可以编译的语法。他们显然更喜欢这样,而不是把object-assign
作为一个依赖项包含到他们的构建中。
参考对象rest/spread在ECMAScript 2018中作为第4阶段最终确定。提案可以在这里找到。
对于大多数对象分配和扩展的工作方式相同,关键的区别在于扩展定义属性,而object .assign()设置属性。这意味着Object.assign()会触发setter。
值得记住的是,除此之外,object rest/spread 1:1映射到object .assign(),与array (iterable) spread的作用不同。例如,在展开数组时,会展开空值。但是,使用对象扩展时,空值会无声地扩展为空值。
Array (Iterable) Spread Example
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
之间的一个大区别似乎没有在当前的答案中提到,即扩展操作符不会将源对象的原型复制到目标对象。如果你想给一个对象添加属性,而又不想改变它所属的实例,那么你必须使用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
两者之间存在巨大差异,并会产生非常严重的后果。被点赞最多的问题甚至都没有涉及到这一点,关于物体传播是一个提议的信息在2022年不再相关了。
不同之处在于Object.assign
改变了对象的位置,而扩展操作符(...
)创建了一个全新的对象,这将打破对象引用的等价性。首先,让我们看看效果,然后我将给出一个现实世界的例子,说明理解这个基本区别是多么重要。
首先,我们使用 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
属性来重新计算。
这是意料之外的,因为在这些计算函数中根本没有使用客户的名字。只使用其他客户数据(如年龄、性别)。发生了什么事?为什么当客户的名字改变时,我们要重新计算所有这些计算函数?
我们有一个Vuex商店是这样做的:
mutations: {
setCustomer(state, payload) {
// payload being { name: 'Bob' }
state.customer = { ...state.customer, ...payload };
}
我们的计算是这样的:
veryExpensiveComputed() {
const customerAge = this.$store.state.customer.age;
}
所以,瞧!当客户名称改变时,Vuex突变实际上是将其完全改变为一个新对象;由于computed依赖于该对象来获取客户年龄,所以Vue将这个非常特定的对象实例作为依赖项,当它被更改为新对象(未通过===
对象相等性测试)时,Vue认为是时候重新运行computed函数了。
修复吗?使用对象。赋值不丢弃前一个对象,而是在适当的位置更改它…
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])
})
}
注意:Spread不仅仅是Object.assign周围的语法糖。他们在幕后的运作方式大不相同。
对象。assign将setter应用于新对象,Spread则不会。此外,对象必须是可迭代的。
如果您需要当前对象的值,并且不希望该值反映该对象的其他所有者所做的任何更改,请使用此方法。
用于创建对象的浅拷贝将不可变属性设置为copy是一个很好的做法——因为可变版本可以传递给不可变属性,copy将确保你总是在处理一个不可变对象
Assign在某种程度上与copy相反。Assign将生成一个setter,它直接将值赋给实例变量,而不是复制或保留它。当调用assign属性的getter时,它返回对实际数据的引用。
我想总结一下"扩展对象合并"ES特性在浏览器中的现状,以及通过工具在生态系统中的现状。
规范- https://github.com/tc39/proposal-object-rest-spread
- 这个语言特性是第4阶段的提案,这意味着它已经被合并到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
- 尚未处于边缘
工具:Node 8.7, TS 2.1
- NodeJS从8.7(通过Kangax)开始支持。当我在本地测试时确认了9.8。
- TypeScript从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 Developer Edition (Firefox 60预览版)和Node(8.7+)中无需编译即可运行。
<标题>为什么的答案?我写这2.5 年原来的问题。但我也有同样的问题,这就是谷歌给我的答案。我是SO改善长尾的使命的奴隶。
由于这是"数组扩展"语法的扩展,我发现很难在谷歌上找到它,而且很难在兼容性表中找到。我能找到的最接近的是Kangax的"property spread",但这个测试在同一个表达式中没有两个spread(不是合并)。此外,提案/草案/浏览器状态页面中的名称都使用"属性扩展",但在我看来,这是社区在"对象合并"中使用扩展语法的提案之后达成的"第一原则"。(这或许可以解释为什么谷歌搜索如此困难。)因此,我在这里记录了我的发现,以便其他人可以查看、更新和编译有关此特定特性的链接。我希望它能流行起来。请帮助传播它在规范和浏览器中登陆的消息。
最后,我想把这些信息作为评论添加进去,但是我不能在不破坏作者原意的情况下编辑它们。具体来说,我不能编辑@ chillpenguin的评论,而不失去他纠正@RichardSchulte的意图。但多年后,理查德的观点被证明是正确的(在我看来)。所以我写了这个答案,希望它最终能吸引旧的答案(可能需要几年的时间,但这就是长尾效应的意义所在)。
标题>标题>正如其他人所提到的,在写作的这一刻,Object.assign()
需要一个多边形和对象扩展...
需要一些转译(也许也是一个多边形)才能工作。
考虑以下代码:
// 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()
实际上是标准化的,而作为对象传播的...
还没有。唯一的问题是浏览器对前者的支持,而在将来,后者也会支持。
使用这里的代码
希望这对你有帮助。
对象扩展操作符(…)在浏览器中不起作用,因为它还不是任何ES规范的一部分,只是一个建议。唯一的选择是用Babel(或类似的东西)编译它。
正如您所看到的,它只是Object.assign({})之上的语法糖。
在我看来,这些是重要的区别。
- 对象。分配作品在大多数浏览器(不编译)
-
...
对象未标准化 -
...
保护您不意外地改变对象 -
...
将填充对象。 -
...
需要更少的代码来表达相同的想法
太多错误的答案…
我搜索了一下,发现了很多关于这个的错误信息。
总结-
...spread
和Object.assign
都没有更快。视情况而定。 - 如果不考虑副作用/对象突变,
Object.assign
几乎总是更快。除了性能之外,通常使用这两种方法都没有什么好处或坏处,直到遇到极端情况,比如复制包含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)
}
如果担心副作用
在有副作用的情况下,例如要与另一个对象组合的对象作为实参传入。
在下面的例子中,bigPayload
将发生突变,如果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()
其他答案都是旧的,无法得到一个好的答案
下面的例子是对象字面量,帮助两者如何相互补充,以及如何不能相互补充(因此不同):
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
(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
-
Underscore.js 1.0有
_.extend()
options = _.extend({}, defaultOptions, options)
⋮
2014
-
2ality发布关于
Object.assign()
即将登陆ES2015的文章 -
object-assign
发布到npm.var objectAssign = require('object-assign') options = objectAssign({}, defaultOptions, options)
-
ES2016的Object Rest/Spread Properties语法建议。
2015
-
Object.assign
被Chrome (45), Firefox(34)和Node.js(4)支持,但旧的运行时需要Polyfill。options = Object.assign({}, defaultOptions, options)
-
Object Rest/Spread Properties提案达到第2阶段。
2016
- Object Rest/Spread Properties语法没有被包含在ES2016中,但是提案达到了第三阶段。
2017
-
Object Rest/Spread Properties语法没有包含在ES2017中,但可用于Chrome (60), Firefox(55)和Node.js(8.3)。对于旧的运行时,需要进行一些编译。
options = { ...defaultOptions, ...options }
2018
- Object 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
请记住,您正在改变目标对象j,因此尽可能使用Object.assign
和空目标或扩展来分配给新对象j。
这现在是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()
当您必须使用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
扩展操作符将Array扩展到函数的单独实参中。
let iterableObjB = [1,2,3,4]
function (...iterableObjB) //turned into
function (1,2,3,4)
我们将创建一个名为identity的函数,它只返回我们给它的任何参数。
identity = (arg) => arg
和一个简单的数组
arr = [1, 2, 3]
如果你用arr调用identity,我们知道会发生什么
- 如何通过json对象数组为嵌套对象赋值
- 了解 JavaScript 对象赋值的顺序
- Javascript GC 并将对象赋值为 null
- 如何为 JSON 对象赋值
- Jquery对象赋值给变量返回未定义
- 为什么不是't当给这个对象赋值时,它是通过引用传递的
- JavaScript TV4对象赋值,NodeJS
- 给对象赋值.变量动态形成
- 如何在Typescript中把一个空对象赋值给一个对象
- 在作用域之外将对象赋值给let关键字
- Javascript -在对象赋值时不替换对象属性
- 对象赋值上的嵌套解构
- 对象扩展vs.对象赋值
- 将一个新对象赋值给一个以前使用过的变量(TypeError: mangoFruit. JS).showName不是一个函
- JavaScript对象.赋值不能在Date对象上工作
- 扩展运算符相当于对象赋值
- 为什么在本地为全局 JavaScript 对象赋值会有所不同
- 从onclick函数给对象赋值
- 将对象赋值给临时变量
- 将对象赋值给“this”