对象扩展运算符 vs Object.assign

407 阅读4分钟

对象扩展运算符 { ...a } 基本可以取代 Object.assign, 实际中性能表现如何呢?

使用 Deno bench 进行测速

代码

Deno.bench("S0 = 变量赋值 a", () => {
  const a = { x: 1, y: 2, z: 3 };
  const b = { a: 1, b: 2, c: 3 };
  let r = a;
  r;
});

Deno.bench("A0 = 修改属性 a.x = b.x", () => {
  const a = { x: 1, y: 2, z: 3 };
  const b = { x: 4, y: 5, z: 6 };
  a.x = b.x
  a.y = b.y
  a.z = b.z
});

Deno.bench("A1 = 修改属性 {...a, a}", () => {
  const a = { x: 1, y: 2, z: 3 };
  const b = { ...a, x: 4, y: 5, z: 6 };
});

Deno.bench("A2 = 修改属性 assign(a, a)", () => {
  const a = { x: 1, y: 2, z: 3 };
  const b = { x: 4, y: 5, z: 6 };
  let r = Object.assign(a, b);
  r;
});

Deno.bench("B1 = 添加属性 {...a, o}", () => {
  const a = { x: 1, y: 2, z: 3 };
  const b = { ...a, a: 1, b: 2, c: 3 };
});

Deno.bench("B2 = 添加属性 { o, ...a}", () => {
  const a = { x: 1, y: 2, z: 3 };
  const b = { a: 1, b: 2, c: 3, ...a };
});

Deno.bench("B3 = 添加属性 assign(a,b)", () => {
  const a = { x: 1, y: 2, z: 3 };
  const b = { a: 1, b: 2, c: 3 };
  let r = Object.assign(a, b);
  r;
});

Deno.bench("C1 = 复制对象 {...a}", () => {
  const a = { x: 1, y: 2, z: 3 };
  const b = { a: 1, b: 2, c: 3 };
  let r = {
    ...a,
  };
  r;
});

Deno.bench("C2 = 复制对象 assign({}, a)", () => {
  const a = { x: 1, y: 2, z: 3 };
  const b = { a: 1, b: 2, c: 3 };
  let r = Object.assign({}, a);
  r;
});

Deno.bench("D1 = 合并对象 {...a, ...b}", () => {
  const a = { x: 1, y: 2, z: 3 };
  const b = { a: 1, b: 2, c: 3 };
  let r = {
    ...a,
    ...b,
  };
  r;
});

Deno.bench("D2 = 合并对象 assign({}, a, b)", () => {
  const a = { x: 1, y: 2, z: 3 };
  const b = { a: 1, b: 2, c: 3 };
  let r = Object.assign(Object.assign({}, a), b);
  r;
});

Deno.bench("D3 = 合并对象 assign({...a},b)", () => {
  const a = { x: 1, y: 2, z: 3 };
  const b = { a: 1, b: 2, c: 3 };
  let r = Object.assign({ ...a }, b);
  r;
});

结果

benchmark                               time (avg)             (min … max)       p75       p99      p995
-------------------------------------------------------------------------- -----------------------------
S0 = 变量赋值 a                         492.79 ps/iter    (462.5 ps … 11.2 ns)  487.5 ps  529.2 ps  537.5 ps
A0 = 修改属性 a.x = b.x                   4.28 ns/iter    (4.08 ns … 11.01 ns)   4.24 ns   5.03 ns   7.62 ns
A1 = 修改属性 {...a, a}                  11.84 ns/iter    (10.5 ns … 24.65 ns)  11.22 ns  18.34 ns  19.63 ns
A2 = 修改属性 assign(a, a)                  28 ns/iter   (25.96 ns … 42.86 ns)  27.68 ns  36.85 ns  37.97 ns
B1 = 添加属性 {...a, o}                 738.88 ns/iter   (648.36 ns … 1.02 µs) 818.67 ns   1.02 µs   1.02 µs
B2 = 添加属性 { o, ...a}                 45.91 ns/iter   (43.72 ns … 65.36 ns)  45.67 ns  57.33 ns  58.56 ns
B3 = 添加属性 assign(a, b)                38.56 ns/iter    (36.5 ns … 58.95 ns)   38.4 ns  51.28 ns  53.09 ns
C1 = 复制对象 {...a}                     11.05 ns/iter   (10.14 ns … 33.34 ns)   10.8 ns  22.01 ns   23.3 ns
C2 = 复制对象 assign({}, a)              40.53 ns/iter   (38.59 ns … 61.55 ns)  40.52 ns  51.92 ns  53.32 ns
D1 = 合并对象 {...a, ...b}              683.04 ns/iter (578.99 ns … 977.66 ns) 760.65 ns 977.66 ns 977.66 ns
D2 = 合并对象 assign({}, a, b)           75.04 ns/iter  (70.82 ns … 185.35 ns)  75.49 ns  90.53 ns  93.51 ns
D3 = 合并对象 assign({...a},b)          740.79 ns/iter   (632.12 ns … 1.01 µs) 813.14 ns   1.01 µs   1.01 µs

结论分析

0. 变量赋值是 ps 级别的, 直接忽略

  1. 对于修改已知的属性, 属性赋值性能肯定还是最好的, 但我们先忽略

    1. 相比对手, 对象扩展运算符性能更好, 即使扩展运算符复制了新对象

    2. 如果 Object.assign 也要复制新对象, 参考 结论3.a

  2. 对于添加新的属性, Object.assign 性能更好

    1. 如果要使用扩展运算符将当前对象添加到新对象中

      1. 新对象特有的属性放前面性能更好

      2. 当前对象也存在的属性如果放在前面, 会有 '...' is specified more than once, so this usage will be overwritten. (这是因为属性值会被后面覆盖, 写了也白写)

  3. 在创建一个新对象的情况下,

    1. 对于复制一个对象, 对象扩展运算符性能更好

    2. 对于合并两个对象, Object.assign 的性能更好

    3. 混合使用也不能优化对象扩展运算符的性能

结合前面的结论, 可知性能卡点不在复制对象上, 而是复制后的对象上要添加新属性性能会变差

就像你看到的那样 D3(740) !== C1(11) + B3(40)

优化方案

如果平时全部使用 Object.assign() 反而不容易出现大的性能问题

对象扩展运算符写起来爽, 用法上可得多加小心了

一般情况下, 对象扩展运算符能满足大部分需求,

  • 如果添加新的属性, 那么新属性需要放在对象扩展运算符前面

  • 如果修改已有的属性, 那么已有的属性放在对象扩展运算符后面

  • 重要!!! 永远不要在创建的新对象中使用两个(或多个)对象扩展运算符

在 ts 项目中, 我们可以得到全部属性的定义, 如果一个属性为可选, 使用 undefined 初始化后, 进行属性修改性能更佳

type A = {
  x: number;
  y: number;
  z: number;
};

type B = {
  a?: number;
  b?: number;
  c?: number;
};

Deno.bench("性能较差 {...a, o}", () => {
  const a: A & B = { x: 1, y: 2, z: 3 };
  const b: A & B = { ...a, a: 1, b: 2, c: 3 };
});

Deno.bench("将不存在的属性放在前面 {o, ...a}", () => {
  const a: A = { x: 1, y: 2, z: 3 };
  const b: A & B = { a: 1, b: 2, c: 3, ...a };
});

Deno.bench("将可选属性初始化为 undefined {...a, o}", () => {
  const a: A & B = {
    x: 1,
    y: 2,
    z: 3,
    a: undefined,
    b: undefined,
    c: undefined,
  };
  const b: A & B = { ...a, a: 1, b: 2, c: 3 };
});

benchmark                                            time (avg)             (min … max)       p75       p99      p995
--------------------------------------------------------------------------------------- -----------------------------
性能较差 {...a, o}                                   743.55 ns/iter   (644.44 ns … 1.11 µs) 816.02 ns   1.11 µs   1.11 µs
将不存在的属性放在前面 {o, ...a}                             46.56 ns/iter  (43.78 ns … 116.81 ns)   46.4 ns  60.48 ns   62.6 ns
将可选属性初始化为 undefined {...a, o}                     18.43 ns/iter   (17.05 ns … 41.77 ns)  18.05 ns  30.33 ns  31.39 ns