资讯专栏INFORMATION COLUMN

JavaScript 深拷贝

zhangwang / 2451人阅读

摘要:深拷贝是一件看起来很简单的事情,但其实一点儿也不简单。我们也可以利用这个实现对象的深拷贝。而是利用之前已经拷贝好的值。深拷贝的详细的源码可以在这里查看。大功告成我们虽然的确解决了深拷贝的大部分问题。

js深拷贝是一件看起来很简单的事情,但其实一点儿也不简单。对于循环引用的问题还有一些内置数据类型的拷贝,如Map, Set, RegExp, Date, ArrayBuffer 和其他内置类型。处理起来并非像想象的那么简单。下面终结一下在实际的项目中,使用的一些深拷贝的方法的优缺点。大家也可以关注我的GitHub,互相交流学习进步。
JSON.parse

先将一个对象转为json对象。然后再解析这个json对象。

let obj = {a:{b:22}};
let copy = JSON.parse(JSON.stringify(obj));

这种方法的优点就是代码写起来比较简单。但是缺点也是显而易见的。你先是创建一个临时的,可能很大的字符串,只是为了把它重新放回解析器。另一个缺点是这种方法不能处理循环对象。

如下面的循环对象用这种方法的时候会抛出异常

let a = {};
let b = {a};
a.b = b;
let copy = JSON.parse(JSON.stringify(a));

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

let a = {};
let b = new Set();
b.add(11);
a.test = b;
let copy = JSON.parse(JSON.stringify(a));

a 的值打印如下

copy的值打印如下

对比发现,Set已丢失。

Structured Clone 结构化克隆算法 MessageChannel

建立两个端,一个端发送消息,另一个端接收消息。

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

这种方法的优点就是能解决循环引用的问题,还支持大量的内置数据类型。缺点就是这个方法是异步的。

History API

利用history.replaceState。这个api在做单页面应用的路由时可以做无刷新的改变url。这个对象使用结构化克隆,而且是同步的。但是我们需要注意,在单页面中不要把原有的路由逻辑搞乱了。所以我们在克隆完一个对象的时候,要恢复路由的原状。

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

var obj = {};
var b = {obj};
obj.b = b
var copy = structuralClone(obj); 
console.log(copy);

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

Notification API

这个api主要是用于桌面通知的。如果你使用Facebook的时候,你肯定会发现时常在浏览器的右下角有一个弹窗,对就是这家伙。我们也可以利用这个api实现js对象的深拷贝。

function structuralClone(obj) {
  return new Notification("", {data: obj, silent: true}).data;
}

var obj = {};
var b = {obj};
obj.b = b
var copy = structuralClone(obj);
console.log(copy)

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

lodash的_.cloneDeep()

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

我们这里再次关注一下lodash是如何解决循环应用这个问题的?

从相关的代码中。我们可以发现。lodash是用一个栈记录了。所有被拷贝的引用值。如果再次碰到同样的引用值的时候,不会再去拷贝一遍。而是利用之前已经拷贝好的值。

lodash深拷贝的详细的源码可以在这里查看。
https://github.com/lodash/lod...

实现一个简易点的深拷贝,以解决循环引用的问题为目标

我们仅仅实现一个简易点的深拷贝。能优雅的处理循环引用的即可。在实现深拷贝之前,我们首先温习回顾一下js中的遍历对象的属性的方法和各种方法的优缺点。

js中遍历一个对象的属性的方法

Object.keys() 仅仅返回自身的可枚举属性,不包括继承来的,更不包括Symbol属性

Object.getOwnPropertyNames() 返回自身的可枚举和不可枚举属性。但是不包括Symbol属性

Object.getOwnPropertySymbols() 返回自身的Symol属性

for...in 可以遍历对象的自身的和继承的可枚举属性,不包含Symbol属性

Reflect.ownkeys() 返回对象自身的所有属性,不管是否可枚举,也不管是否是Symbol。注意不包括继承的属性

实现深拷贝,解决循环引用问题
/**
 * 判断是否是基本数据类型
 * @param value 
 */
function isPrimitive(value){
  return (typeof value === "string" || 
  typeof value === "number" || 
  typeof value === "symbol" ||
  typeof value === "boolean")
}

/**
 * 判断是否是一个js对象
 * @param value 
 */
function isObject(value){
  return Object.prototype.toString.call(value) === "[object Object]"
}

/**
 * 深拷贝一个值
 * @param value 
 */
function cloneDeep(value){

  // 记录被拷贝的值,避免循环引用的出现
  let memo = {};

  function baseClone(value){
    let res;
    // 如果是基本数据类型,则直接返回
    if(isPrimitive(value)){
      return value;
    // 如果是引用数据类型,我们浅拷贝一个新值来代替原来的值
    }else if(Array.isArray(value)){
      res = [...value];
    }else if(isObject(value)){
      res = {...value};
    }

    // 检测我们浅拷贝的这个对象的属性值有没有是引用数据类型。如果是,则递归拷贝
    Reflect.ownKeys(res).forEach(key=>{
      if(typeof res[key] === "object" && res[key]!== null){
        //此处我们用memo来记录已经被拷贝过的引用地址。以此来解决循环引用的问题
        if(memo[res[key]]){
          res[key] = memo[res[key]];
        }else{
          memo[res[key]] = res[key];
          res[key] = baseClone(res[key])
        }
      }
    })
    return res;  
  }

  return baseClone(value)
}

验证我们写的cloneDeep是否能解决循环应用的问题

var obj = {};
var b = {obj};
obj.b = b
var copy = cloneDeep(obj); 
console.log(copy);

完美。大功告成

我们虽然的确解决了深拷贝的大部分问题。不过很多细节还没有去处理。在生产环境,我们还是要使用lodash的cloneDeep。cloneDeep对每个数据类型都多带带处理的非常好。比如ArrayBuffer什么的。我们都没有处理。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/95887.html

相关文章

  • JavaScript中的浅拷贝拷贝

    摘要:所以,深拷贝是对对象以及对象的所有子对象进行拷贝实现方式就是递归调用浅拷贝对于深拷贝的对象,改变源对象不会对得到的对象有影响。 上一篇 JavaScript中的继承 前言 文章开始之前,让我们先思考一下这几个问题: 为什么会有浅拷贝与深拷贝 什么是浅拷贝与深拷贝 如何实现浅拷贝与深拷贝 好了,问题出来了,那么下面就让我们带着这几个问题去探究一下吧! 如果文章中有出现纰漏、错误之处...

    AZmake 评论0 收藏0
  • JavaScript中的浅拷贝拷贝

    摘要:所以,深拷贝是对对象以及对象的所有子对象进行拷贝实现方式就是递归调用浅拷贝对于深拷贝的对象,改变源对象不会对得到的对象有影响。 为什么会有浅拷贝与深拷贝什么是浅拷贝与深拷贝如何实现浅拷贝与深拷贝好了,问题出来了,那么下面就让我们带着这几个问题去探究一下吧! 如果文章中有出现纰漏、错误之处,还请看到的小伙伴多多指教,先行谢过 以下↓ 数据类型在开始了解 浅拷贝 与 深拷贝 之前,让我们先...

    546669204 评论0 收藏0
  • javascript拷贝(deepClone)

    摘要:实现实现一个深拷贝函数,就不得不说的数值类型。类型来看下面代码,结果会返回啥呢答案是有时候保存了元素,一不小心进行深拷贝,上面的深拷贝函数就缺少了对元素的判断。在不同的场景下,要根据业务场景,判断是否需要使用深拷贝。 javascript深拷贝是初学者甚至有经验的开发着,都会经常遇到问题,并不能很好的理解javascript的深拷贝。 深拷贝(deepClone)? 与深拷贝相对的就是...

    hatlonely 评论0 收藏0
  • JavaScript之浅、拷贝

    摘要:前言里面浅拷贝和深拷贝是非常关键的知识点,今天就来通过本文清楚的了解深浅拷贝以及该如何实现这两种拷贝方式。对象的拷贝又分为浅拷贝和深拷贝。印证了上述所说的对于所有的基本类型,简单的赋值已经是实现了深拷贝。 前言 JavaScript里面浅拷贝和深拷贝是非常关键的知识点,今天就来通过本文清楚的了解深浅拷贝以及该如何实现这两种拷贝方式。 深浅拷贝的区别 拷贝:其实就是一个对象复制给另外...

    leanxi 评论0 收藏0
  • JavaScript·随记 拷贝 vs. 浅拷贝

    摘要:而在这个运算符的相关用例中,往往会涉及到其他知识点,深拷贝和浅拷贝就是其中之一。即对象的浅拷贝会对主对象的值进行拷贝,而该值有可能是一个指针,指向内存中的同一个对象。,可以看到深拷贝和浅拷贝是对复制引用类型变量而言的。 在ES6的系列文章中,基本都会提到Spread——扩展运算符(...)。而在这个运算符的相关用例中,往往会涉及到其他知识点,深拷贝和浅拷贝就是其中之一。 背景知识 在讨...

    RyanQ 评论0 收藏0
  • JavaScript基础心法——拷贝

    摘要:原文地址基础心法深浅拷贝欢迎。上面的代码是最简单的利用赋值操作符实现了一个浅拷贝,可以很清楚的看到,随着和改变,和也随着发生了变化。展开运算符结论实现的是对象第一层的深拷贝。 原文地址:JavaScript基础心法——深浅拷贝 欢迎star。 如果有错误的地方欢迎指正。 浅拷贝和深拷贝都是对于JS中的引用类型而言的,浅拷贝就只是复制对象的引用,如果拷贝后的对象发生变化,原对象也会发生...

    keithxiaoy 评论0 收藏0

发表评论

0条评论

zhangwang

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<