对象扩展vs.对象赋值

Object spread vs. Object.assign

本文关键字:对象 赋值 扩展 vs      更新时间:2023-09-26

假设我有一个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 = {...}上,我们清楚地将parentObjectchildObject键的值分配给全新的对象文字,而它由旧的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

  • NodeJS8.7(通过Kangax)开始支持。当我在本地测试时确认了9.8。
  • TypeScript2.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({})之上的语法糖。

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

  • 对象。分配作品在大多数浏览器(不编译)
  • ...对象未标准化
  • ...保护您不意外地改变对象
  • ...将填充对象。
  • ...需要更少的代码来表达相同的想法

太多错误的答案…

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

总结
  1. ...spreadObject.assign都没有更快。视情况而定。
  2. 如果不考虑副作用/对象突变,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,我们知道会发生什么