banner
他山之石

他山之石

深拷贝探秘

本文总结了一些解决深拷贝的常用方法,没有银弹,每种方法都有其优劣,使用时要区分其场景。

image

在 JavaScript 中,深拷贝已经是一个老生常谈的问题,也无数次被面试官用来考核一个前端的 JS 水平,同时,在开发中,如果对深拷贝理解不够深刻,也会出现很难发现的 BUG。此前,我已经写过一篇文章《深入理解 JavaScript 之克隆》,其中分析了深拷贝的概念,但是其解决方法还很粗浅,这篇文章,记录了一些解决深拷贝的常用方法,没有银弹,每种方法都有其优劣,使用时要区分其场景。

递归拷贝#

递归拷贝应该是最开始了解深拷贝时遇到的解决办法,其原理是对引用类型进行遍历,判断其值是否为引用类型,如果为引用类型则递归调用该函数并传入该值,否则为简单数据类型则执行常规赋值拷贝。

function deepClone(obj) {
  var result;
  if (!(obj instanceof Object)) {
    return obj;
  } else if (obj instanceof Function) {
    result = eval(obj.toString());
  } else {
    result = Array.isArray(obj) ? [] : {};
  }
  for (let key in obj) {
    result[key] = deepClone(obj[key]);
  }
  return result;
}

这是一个简单的深拷贝方法,处理了对象、数组和函数,但是 Date、Regexp、Map 等引用类型都没有处理,而且无法解决对象循环引用的场景。

JSON.parse(JSON.stringify(object))#

深拷贝一般用 JSON.parse (JSON.stringify (object)) 就可以解决了,

这种方法的局限性:

  • 会忽略 undefined
  • 不能序列化函数
  • 不能解决循环引用的对象
    undefined 和函数会被忽略,而尝试拷贝循环引用的对象则会报错:
Uncaught TypeError:Converting circular structure to JSON

另外,诸如 Map, Set, RegExp, Date, ArrayBuffer 和其他内置类型在进行序列化时会丢失。

但是在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决大部分问题,并且该函数是内置函数中处理深拷贝性能最快的。

Structured Clone 结构化克隆算法#

Structured cloning 是一种现有的算法,用于将值从一个地方转移到另一地方。例如,每当您调用 postMessage 将消息发送到另一个窗口或 WebWorker 时,都会使用它。关于结构化克隆的好处在于它处理循环对象并 支持大量的内置类型。结构化克隆包含使用 MessageChannel 和 History API。

MessageChannel#

MessageChannel API 允许我们创建一个新的消息通道,并通过它的两个 MessagePort 属性发送数据。
我们可以创建一个 MessageChannel 并发送消息。在接收端,消息包含我们原始数据对象的结构化克隆。
MessageChannel 的 postMessage 传递的数据也是深拷贝的,这和 web worker 的 postMessage 一样。而且还可以拷贝 undefined 和循环引用的对象。

// 有undefined + 循环引用
let obj = {
  a: 1,
  b: {
    c: 2,
    d: 3
  },
  f: undefined
};
obj.c = obj.b;
obj.e = obj.a;
obj.b.c = obj.c;
obj.b.d = obj.b;
obj.b.e = obj.b.c;

function deepCopy(obj) {
  return new Promise(resolve => {
    const { port1, port2 } = new MessageChannel();
    port2.onmessage = ev => resolve(ev.data);
    port1.postMessage(obj);
  });
}

// MessageChannel是异步的
deepCopy(obj).then(copy => {
  let copyObj = copy;
  console.log(copyObj, obj);
  console.log(copyObj == obj);
});

但拷贝有函数的对象时,还是会报错:

Uncaught (in promise) DOMException:Failed to execute 'postMessage' on 'MessagePort': function() {} could not be cloned.

这种方法的缺点是它是异步的,但是你可以使用 await 来解决。

const clone = await structuralClone(obj);

History#

如果你曾经使用 history.pushState () 写过 SPA,你可以提供一个状态对象来保存 URL。事实证明,这个状态对象使用结构化克隆,而且是同步的。我们必须小心使用,不要把原有的路由状态搞乱了,所以我们需要在完成克隆之后恢复原始状态。为了防止发生任何意外,请使用 history.replaceState () 而不是 history.pushState ()。

const structuralClone = obj => {
  const oldState = history.state;
  history.replaceState(obj, document.title);
  const copy = history.state;
  history.replaceState(oldState, document.title);
  return copy;
};

此方法的优点是:能解决循环对象的问题,并且支持许多内置类型的克隆,也是同步的。缺点是有的浏览器对调用频率有限制。比如 Safari 30 秒内只允许调用 100 次

Notification#

Notification 是用于桌面通知的。也可以用来实现 JS 对象的深拷贝。

function structuralClone(obj) {
    return new Notification('', {data: obj, silent: true}).data;
}
const obj = /* ... */;
const clone = structuralClone(obj);

此方法的优点就是可以解决循环对象问题,也支持许多内置类型的克隆,并且是同步的。缺点就是这个 api 的使用需要向用户请求权限,但是用在这里克隆数据的时候,不经用户授权也可以使用。在 http 协议的情况下会提示你再 https 的场景下使用。
由于某种原因,Safari 总是返回 undefined。

lodash 的深拷贝函数#

lodash 的_.cloneDeep () 支持循环对象,和大量的内置类型,对很多细节都处理的比较不错,推荐使用。

解决循环引用#

通过分析上述的方法,发现只有结构化克隆算法才能解决循环引用的问题,那么使用原生 JS 如何解决呢?这里尝试使用闭包来解决循环问题。

function deepClone(object) {
  const memo = {};
  function clone(obj) {
    var result;
    if (!(obj instanceof Object)) {
      return obj;
    } else if (obj instanceof Function) {
      result = eval(obj.toString());
    } else {
      result = Array.isArray(obj) ? [] : {};
    }
    for (let key in obj) {
      if (memo[obj[key]]) {
        result[key] = memo[obj[key]];
      } else {
        memo[obj[key]] = obj[key];
        result[key] = clone(obj[key]);
      }
    }
    return result;
  }
  return clone(object);
}

var obj = {};
var b = { obj };
obj.b = b;

var obj2 = deepClone(obj);

在上述代码中,对递归拷贝进行了优化,使用闭包来解决循环拷贝的问题,使用了 memo 这个变量来记录被拷贝的引用地址,在每次递归前,记录递归的参数值,这样在递归内部就可以跳出递归,解决循环引用问题。

总结#

  • 如果您没有循环对象,并且不需要保留内置类型,则可以使用跨浏览器的 JSON.parse (JSON.stringify ()) 获得最快的克隆性能。
  • 如果你想要一个适当的结构化克隆,MessageChannel 是你唯一可靠的跨浏览器的选择。
加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。