Object.assign和...(展开运算符/扩展运算符)的区别

4,121 阅读4分钟

1、基础

"..."叫展开运算符(扩展运算符)的时候,大部分情况下:

  1. {...obj} 同 Object.assign({}, obj)
  2. {...obj, a: 1} 同 Object.assign(obj, { a: 1});

基本使用形式如下:

const obj = { a: 'a', b: 'b', c: 'c' };
let a = {...obj, a: 233}; // { a: 233, b: 'b', c: 'c' }
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
let b = [...arr1, ...arr2] // [1,2,3,4,5,6]

[...arr, a: 1]的性能比[a: 1, ...arr]的高、用在对象上的时候也是放在前面比放在后面性能高。放在后面的时候,还出现测试执行时间不稳定的情况。

2、性能测试:

1、对象合并测试
    const count = 10000;

    function foo1() {
      console.time('Object.assign对象合并使用耗时');
      let obj = {};
      for (let i = 0; i < count; i++) {
        obj = Object.assign(obj, { ['key' + i]: i });
      }
      console.timeEnd('Object.assign对象合并使用耗时');
    }

    function foo2() {
      console.time('展开运算符对象合并使用耗时');
      let obj = {};
      for (let i = 0; i < count; i++) {
        obj = { ...obj, ['key' + i]: i };
      }
      console.timeEnd('展开运算符对象合并使用耗时');
    }
2、数组合并测试代码:

多次循环合并的测试:

    const count = 10000;
    function foo3() {
      console.time('展开运算符数组合并使用耗时');
      let arr = [];
      for (let i = 0; i < count; i++) {
        arr = [...arr, i];
      }
      console.timeEnd('展开运算符数组合并使用耗时');
    }
    function foo4() {
      console.time('使用push方法使用耗时');
      let arr = [];
      for (let i = 0; i < count; i++) {
        arr.push(i);
      }
      console.timeEnd('使用push方法使用耗时');
    }
    function foo5() {
      console.time('使用concat方法使用耗时');
      let arr = [];
      for (let i = 0; i < count; i++) {
        arr.concat([i]);
      }
      console.timeEnd('使用concat方法使用耗时');
    }

两个大数组合并

    const count = 10000;
    function foo6() {
      let testArr1 = [];
      let testArr2 = [];
      for (let i = 0; i < count * 100; i++) {
        testArr1.push(i);
        testArr2.push(count * 100 - i);
      }
      console.time('展开运算符数组合并使用耗时');
      let arr = [];
      arr = [...testArr1, ...testArr2];
      console.timeEnd('展开运算符数组合并使用耗时');
    }

    function foo7() {
      let testArr1 = [];
      let testArr2 = [];
      for (let i = 0; i < count * 100; i++) {
        testArr1.push(i);
        testArr2.push(count * 100 - i);
      }
      console.time('使用concat方法使用耗时');
      testArr2.concat(testArr1);
      console.timeEnd('使用concat方法使用耗时');
    }

提示:chrome console.time打印的时间比Performance里的更精确、Firefox的console.time打印没有性能分析里的精确。firefox查看更加精确的时间 打开F12开发web开发者工具面板、点击性能记录好后,从瀑布图找到DOM事件、点击对应的DOM事件选项字样,右边就有时间了。

火狐截图 image.png

谷歌浏览器截图 image.png

3、测试结果列表:
测试条件Chrome版本94.0.4606.81Firefox版本93.0 (64 位)
Object.assign对象合并14.07080078125 ms12.11 ms
展开运算符对象合并18201.126953125 ms7424.69 ms
展开运算符数组加单个42.848876953125 ms877.14 ms
push方法数组加单个0.489013671875 ms0.89 ms
concat方法数组加单个0.6640625 ms2.40 ms
展开运算符两个大数组合并38.1337890625 ms43.64 ms
concat方法两个大数组合并4.3740234375 ms31.28 ms
4、性能测试结论

在循环比较多或者操作的数组长度比较大的情况下:

  • 1、Object.assign多数情况下性能比展开运算符(...)的性能高
  • 2、数组自带的方法connat、push性能比展开运算符(...)的性能高

那么问题来了,是不是有上面的结果,我们就不能使用...运算符了呢?我觉得不一定:

  • 理由1:一般情况我们没那么多超级大的对象,超级大的数组这样给你搞。
  • 理由2:...的写法比Object.assign更加优雅;
  • 理由3:比如我们想数组去重 let arr = [1, 1, 2]; 用的 [...new Set(arr)]呢;
  • 理由4:说不定后面浏览器会优化这个玩意儿呢?

内心:编不下去了。好吧,偷偷把之前写的...换了算了。不过在没有真的遇见性能瓶颈前,是没必要过早优化的。

可能有人会想,使用babel这些来进行编译,下面是es6转es5的示例,这里只是一部分,各个版本的babel转换可能还不太一样。但是有一点就是增加了一大堆代码。明显没有我们手动优化来的完美。

如下es6源代码:

let arr1 = [1, 2, 3]
let arr2 = [4, 5, 6]
let arr3 = [...arr1, ...arr2]
let arr4 = [7, ...arr1]
let arr5 = [...arr1, 8]

let obj1 = { a: 1, b: 2 }
let obj2 = {
  ...obj1, 
  c: 3
}
let obj3 = {
  d: 4, 
  ...obj1 
}

使用typescript的tsc编译(tsc版本是4.6.4)

var __assign = (this && this.__assign) || function () {
    __assign = Object.assign || function(t) {
        for (var s, i = 1, n = arguments.length; i < n; i++) {
            s = arguments[i];
            for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
                t[p] = s[p];
        }
        return t;
    };
    return __assign.apply(this, arguments);
};
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
    if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
        if (ar || !(i in from)) {
            if (!ar) ar = Array.prototype.slice.call(from, 0, i);
            ar[i] = from[i];
        }
    }
    return to.concat(ar || Array.prototype.slice.call(from));
};
var arr1 = [1, 2, 3];
var arr2 = [4, 5, 6];
var arr3 = __spreadArray(__spreadArray([], arr1, true), arr2, true);
var arr4 = __spreadArray([7], arr1, true);
var arr5 = __spreadArray(__spreadArray([], arr1, true), [8], false);
var obj1 = { a: 1, b: 2 };
var obj2 = __assign(__assign({}, obj1), { c: 3 });
var obj3 = __assign({ d: 4 }, obj1);

使用@babel/preset-env编译(版本是7.17.10)

"use strict";

function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }

function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

var arr1 = [1, 2, 3];
var arr2 = [4, 5, 6];
var arr3 = [].concat(arr1, arr2);
var arr4 = [7].concat(arr1);
var arr5 = [].concat(arr1, [8]);
var obj1 = {
  a: 1,
  b: 2
};

var obj2 = _objectSpread(_objectSpread({}, obj1), {}, {
  c: 3
});

var obj3 = _objectSpread({
  d: 4
}, obj1);

3、Object.definedProperty对比

展开运算符
    let obj = {
      a: 1,
    }
    Object.keys(obj).forEach((key) => {
      let internalValue = obj[key];
      Object.defineProperty(obj, key, {
        get() {
          console.log(`getting key "${key}": ${JSON.stringify(internalValue)}`)
          return internalValue;
        },
        set(newValue) {
          console.log(`setting key "${key}": ${JSON.stringify(newValue)}`)
          internalValue = newValue
        }
      })
    })
    let cc = {...obj, a: '555' }
    cc.a = 666

控制台结果:

getting key "a": 1 只是触发get 1次,并且打印的值为 1

Object.assign
   let obj = {
     a: 1,
   }
   Object.keys(obj).forEach((key) => {
     let internalValue = obj[key];
     Object.defineProperty(obj, key, {
       get() {
         console.log(`getting key "${key}": ${JSON.stringify(internalValue)}`)
         return internalValue;
       },
       set(newValue) {
         console.log(`setting key "${key}": ${JSON.stringify(newValue)}`)
         internalValue = newValue
       }
     })
   })
   let cc = Object.assign(obj, { a: '555' })
   cc.a = 666

控制台结果:

setting key "a": "555"
setting key "a": 666 触发了两次set

4、Proxy的结果和上面的definedProperty结果一样

   let obj = {
     a: 1,
   }
   obj = new Proxy(obj, {
       get: function (o, sKey) {
         console.log(`getting key "${sKey}": ${JSON.stringify(o[sKey])}`)
         return o[sKey];
       },
       set: function (o, sKey, vValue) {
         console.log(`setting key "${sKey}": ${JSON.stringify(vValue)}`)
         o[sKey] = vValue;
         return true;
       }}
   )

   let cc = {...obj, a: '555' }
   // let cc = Object.assign(obj, { a: '555' })
   cc.a = 666

5、不定参数“...

   // 不定参数限制:每个函数最多只能申明一个不定参数,而且一定要放到参数末尾
   // 错误:function foo(object, ...keys, last) { } // Uncaught SyntaxError: parameter after rest parameter
   function foo(object, ...keys) {
     console.log(object, keys);
   }
   let arr = [1, 2, 3, 4]
   foo({}, ...arr) // {}, [1, 2, 3, 4]
   foo(...arr) // 1, [2, 3, 4]
   
   let obj = {a: 1, b: 2, c: 3}; 
   let { a, ...o} = obj; 
   // a = 1
   // o = { b: 2, c: 3 }
   let arr = [1, 2, 3, 4];
   let [a, ...o] = arr;
   // a = 1
   // o = [2, 3, 4]

6、小结

在Chrome版本94.0.4606.81 、 Firefox版本93.0 (64 位)的浏览器下

  • 对象合并,数组合并,Object.assign、connat的性能会比展开运算符“...”的性能高
  • Object.assign会触发Proxy/Object.definedProperty的set方法,展开运算符“...”不会触发
  • 它两都是浅拷贝
  • 合并对象、数组的时候,展开运算符放在前面的性能比放在后面的性能高
  • 不定参数的时候,有自己的使用方式