美文网首页JavaScript深入浅出让前端飞Web 前端开发
(译)最全的javaScript中对象深度拷贝指南

(译)最全的javaScript中对象深度拷贝指南

作者: 古朋 | 来源:发表于2018-02-13 22:05 被阅读19次

    原文地址

    我在JavaScript中如何拷贝一个对象?这是一个简单的问题,但是答案确不是很简单。

    Did you ever wanted to create a deep copy of an object in JavaScript? There is a way, but you are not gonna like it...
    I feel like we need something better 🤔 pic.twitter.com/IDazhB8BKJ
    — Surma (@dassurma) 2018年1月22日

    引用调用

    JavaScript通过引用来传递所有的值。如果你不知道这是什么意思,下面有个例子👇:

    function mutate(obj) {
      obj.a = true;
    }
    
    const obj = {a: false};
    mutate(obj)
    console.log(obj.a); // prints true
    

    mutate方法改变了作为参数传递进来的这个对象。在值调用环境中,这个函数式传递的这个值,所以相关于这个函数是执行了一个拷贝。这个函数使这个对象对外是不可见的。但是在像js的这种引用调用的环境,将会得到这个真实的对象。所以最后控制台输出的为true

    不过,你想要保持你的原始的对象,其他函数只是创建了这个对象的拷贝。

    在下面就介绍几种深度拷贝的方式

    JSON.parse

    第一种最古老的方式就是通过将对象转换为JSON字符串格式,然后将其转换为对象。

    let obj = { name : "huyue" };
    let copy = JSON.parse(JSON.stringify(obj));
    obj.name = 'hy';
    console.log(copy);//'huyue'
    

    但是这种方式有些问题

    问题一:当对象中出现循环引用的时候会报错。尽管你可能认为你不会如此使用,但是那些还是会很容易发生。比如当你构建了树状类型的数据机构的时候,其中一个节点引用了父级的某个节点,这样就出现了这种场景。

    const x = {};
    const y = {x};
    x.y = y; // Cycle: x.y.x.y.x.y.x.y.x...
    const copy = JSON.parse(JSON.stringify(x)); // throws!
    

    问题二:这种方式只支持基础类型,像MapSetRegExpDateArrayBuffer,函数对象等都会在序列化的时候弄丢

    var source = { name:function(){console.log(1);}, child:{ name:"child" } } 
    var target = JSON.parse(JSON.stringify(source));
    console.log(target.name); //undefined
    

    注:JSON对象是ES5中引入的新的类型(支持的浏览器为IE8+),浏览器支持情况

    结构化克隆

    结构化克隆是一个现有算法,它是被用来把一个领域的值传递到另一个。比如,你调用postMessage去发送一个消息给另一个窗口或WebWorker。结构化很好的地方就是他能处理循环对象,并且支持多种内置类型

    MessageChannel

    我们通过MessageChannel创建一个新的消息通道,并通过它的两个MessagePort属性来发送数据和获取数据。我们接受到的这条信息就是会包含原始数据的结构化克隆对象。但是这种方式是异步情况,所以下面例子使用的async awit实现了的,也可参见在线地址

    function structuralClone(obj) {
      return new Promise(resolve => {
        const {port1, port2} = new MessageChannel();
        port2.onmessage = ev => resolve(ev.data);
        port1.postMessage(obj);
      });
    }
    
    const obj = /* ... */;
    const clone = await structuralClone(obj);
    

    注:浏览器支持IE10+,浏览器支持力度情况

    History API

    如果你曾经使用过history.pushState()去构建一个SPA(单页应用),你应该会知道能提供一个状态对象去保存这URL。这个状态对象就是结构化克隆,并且还是同步的。我们一定要小心,避免在使用这个状态对象的时候去混淆任何程序逻辑,所以我们需要在我们克隆了之后去恢复这个原始的状态对象。为了防止发生任何事件,请使用history.replaceState()而不是history.pushState()。replaceState和pushState区别详情

    function structuralClone(obj) {
      const oldState = history.state;
      history.replaceState(obj, document.title);
      const copy = history.state;
      history.replaceState(oldState, document.title);//就是为了恢复原始状态对象,避免干扰
      return copy;
    }
    
    const obj = /* ... */;
    const clone = structuralClone(obj);
    

    为了复制一个对象,使用浏览器的引擎感觉有些笨拙。不过你还是可以这么做,有些事情还是得注意,因为Safari浏览器会限制30秒内调用relaceState的次数上限为100次

    注:浏览器支持IE10+,浏览器支持力度情况

    Notification API

    这种方式由Jeremy Banks建议,通知接口用于向用户配置和显示桌面通知,这个消息通知的api有一个与它们相关的数据对象被克隆。看到这,可能有的人表示有点不是很明白,那么可以点击在线示例

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

    它基本触犯了浏览器内的权限机制,所以怀疑这个可能会非常慢。出于某种原因,Safari浏览器总是返回undefined。可以使用在线示例

    注:浏览器不支持IE,浏览器支持力度情况

    性能测试

    对上面几种方式进行性能测试看哪种方式性能最高。刚开始尝试时,我拿一个小JSON对象,并通过这些克隆对象一千次的不同方式来进行测试。幸运的是, Mathias Bynens告诉我在给一个对象增加属性的时候V8是有缓存。为了确保不走缓存,所以我写了一个[函数](a function that generates objects of given depth and width using random key names),使用随机键名称生成给定深度和宽度的对象,并重新运行测试示例

    图表统计

    image
    image
    image

    总结

    • 如果你不会使用循环对象并且不会使用内置类型,那么还是推荐使用JSON.parse。并且浏览器兼容性还更好(ie8+)
    • 如果在考虑性能和浏览器兼容,MessageChannel是最好的选择。(ie10+)

    相关文章

      网友评论

        本文标题:(译)最全的javaScript中对象深度拷贝指南

        本文链接:https://www.haomeiwen.com/subject/hecgvxtx.html