繁体   English   中英

Object 传播与 Object.assign

[英]Object spread vs. Object.assign

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

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

使用 object 传播

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);

缺点:

  • 比较啰嗦。
  • 如果编写在没有本机支持的环境中执行的代码,则需要 polyfill。

这是让我感到疑惑的提交。

这与您要问的内容没有直接关系。 该代码没有使用Object.assign() ,而是使用了执行相同操作的用户代码 ( object-assign )。 他们似乎在用 Babel 编译代码(并用 Webpack 捆绑它),这就是我所说的:你可以编译的语法。 他们显然更愿意将object-assign作为依赖项包含在他们的构建中。

对于参考对象 rest/spread 在 ECMAScript 2018 中最终确定为第 4 阶段。该提案可以在这里找到。

大多数情况下,对象重置和扩展的工作方式相同,关键区别在于扩展定义了属性,而 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

对象传播示例

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

我想总结一下“扩展对象合并”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 }

再次重申:在编写此示例时,该示例无需在 Chrome (60+)、Firefox Developer Edition(Firefox 60 预览版)和 Node (8.7+) 中进行转换。

为什么要回答?

我是在原始问题提出 2.5后写这篇文章的。 但我有同样的问题,这是谷歌发给我的。 我是 SO 改善长尾任务的奴隶。

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

最后,我会添加此信息作为评论,但我无法在不破坏作者原始意图的情况下对其进行编辑。 具体来说,我无法编辑@ChillyPenguin 的评论,而不会失去纠正@RichardSchulte 的意图。 但几年后,事实证明理查德是对的(在我看来)。 所以我写了这个答案,希望它最终能在旧答案上获得吸引力(可能需要数年时间,但这毕竟是长尾效应的全部内容)。

正如其他人所提到的,在撰写本文时, Object.assign()需要一个 polyfill 和 object spread ...需要一些转换(可能也需要一个 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()实际上是标准化的,而 as object spread ...还没有。 唯一的问题是浏览器支持前者,将来也会支持后者。

在这里玩代码

希望这可以帮助。

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

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

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

使用它来创建对象的浅拷贝 良好的做​​法是始终将不可变属性设置为复制 - 因为可变版本可以传递到不可变属性中,复制将确保您将始终处理不可变对象

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

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

如您所见,它只是 Object.assign({}) 的语法糖。

据我所知,这些是重要的区别。

  • Object.assign 适用于大多数浏览器(无需编译)
  • ...对于对象没有标准化
  • ...防止您意外地改变对象
  • ...将在没有它的浏览器中 polyfill Object.assign
  • ...需要更少的代码来表达相同的想法

两者之间存在巨大差异,后果非常严重。 最受支持的问题甚至都没有涉及到这一点,关于 object 传播作为提案的信息在 2022 年不再相关。

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.

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

首先,让我们使用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地将 parentObject 中childObject键的值分配给全新的 object 文字,并且它是由旧的childObject组成的内容无关。 这是一个新的 object。

如果您认为这在实践中无关紧要,让我展示一个现实世界的场景,说明理解这一点有多重要。

在一个非常大的 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;
}

所以,瞧,当客户名称更改时; 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 === object 相等测试),Vue 决定是时候重新运行计算的 function。

修复? 使用 Object.assign 不会丢弃以前的 object,而是将其更改到位...

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

顺便说一句,如果您在 Vue2 中,则不应使用 Object.assign ,因为 Vue 2 无法直接跟踪那些 object 更改,但同样的逻辑适用,只需使用 Vue.set 而不是 Z497031794414A53B524BZ.9414A53B524B43AC

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

其他答案很旧,无法得到好的答案。
下面的示例用于对象文字,有助于两者如何相互补充,以及如何相互补充(因此有所不同):

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

这现在是 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当目标 object 是一个常数并且您想一次设置多个属性时是必需的。

例如:

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。

错误答案太多...

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

概括

  1. ...spreadObject.assign是更快。 这取决于。
  2. 大多数时候Object.assign更快。 如果不需要考虑副作用,它可以使性能翻倍。
  3. 抛开性能不谈,使用任何一种方法通常都没有好处/坏处,直到遇到边缘情况,例如复制包含 getter/setter 的对象或只读属性。 在这里阅读更多。

表现

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

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

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的 arguments ,或者仅使用{}作为第一个参数来创建新的 object:

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()

当您必须使用 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

(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年

2014年

2015年

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

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

2016年

  • ES2016 中未包含 Object Rest/Spread Properties 语法,但提案已进入第 3 阶段。

2017年

2018年

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

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

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

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

identity = (arg) => arg

和一个简单的数组。

arr = [1, 2, 3]

如果你用 arr 调用身份,我们知道会发生什么

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM