浅析 JavaScript 中的深拷贝

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zwkkkk1/article/details/82107900

  js 中的深浅拷贝是一个非常重要的知识点,因为在开发过程经常会遇到问题,特别是深拷贝。

  至于什么是浅拷贝,什么是深拷贝这里不做解释,此外数组的浅拷贝可以使用 slice()concat()函数,对象可以使用 Object.assign(),这里也不过多介绍,再开发真正会遇到的问题在于深拷贝,下面我会列出几个方法以及各自的缺陷吧。

深拷贝就是指完全的拷贝一个对象,即使嵌套了对象,两者也相互分离,修改一个对象的属性,也不会影响另一个。

JSON.parse() 和 JSON.stringfy() 的运用

举个例子

  var obj1 = {
    member: ["Jack", "Rose", "Lucy"]
  }
  var obj2 = JSON.parse(JSON.stringify(obj1));
  obj2.member[0] = "Paul";

  console.log(obj1.member); // ['Jack', 'Rose', 'Lucy']
  console.log(obj2.member); //  ['Paul', 'Rose', 'Lucy']

方法很方便,JSON.parse(JSON.stringify(obj1)) 将对象转为 json string,再转为对象,如果不在意下面提到的问题,也可以用这种方法,效率还是不错的。

问题1 丢失constructor

  假如 obj1 由构造函数 Foo() 创建,那它的 constructor 也指向 Foo(),同时 obj1 instanceof Foo 也为 true。

  然后通过 var obj2 = JSON.parse(JSON.stringify(obj1)); 拷贝出来的 obj2 的 constructor 指向 Object(),当然 obj2 instanceof Foo 为 false。

问题2 内容丢失\改变

看个例子:

// 一个比较全的测试用例,下面的方法也会用到这个例子
  function Foo() {
    this.arr1 = [1, 2, 3];
    this.arr2 = [1, undefined, null, function() {}];
    this.obj1 = {a: 1, b: 2, c: 3};
    this.obj2 = {a: undefined, b: null, c: function() {}};
    this.unf = undefined;
    this.nul = null;
    this.func = function() {console.log("hello deepCopy")};
    this.date = new Date(0);
    this.reg = new RegExp('/^\w$/g');
    this.err = new Error('ERROR!!!');
  }

这是 new Foo() 的打印结果

这里写图片描述

这是 JSON.parse(JSON.stringify(new Foo() )) 的打印结果

这里写图片描述

直观点用个表格:

属性 new Foo() JSON.parse(JSON.stringify(new Foo())
arr1 [1, 2, 3] [1, 2, 3]
arr2 [1, undefined, null, f()] [1, null, null, null]
date Thu Jan 01 1970 08:00:00 GMT+0800 (中国标准时间) {} “1970-01-01T00:00:00.000Z”
err Error: ERROR! {}
func f() 被忽略
obj1 {a: 1, b: 2, c: 3} {a: 1, b: 2, c: 3}
obj2 {a: undefined, b: null, c: f()} {b: null}
reg /\/^w$\/g/ {}
proto.constructor Foo() Object()

我们可以发现只有 arr1、obj1 的结果是正确的,其他数据均有不同程度的失真,其中尤其是 arr2、obj2 的改变,与 JSON.stringify() 的特性有关,此外我们也可以看到上一个说的点 __proto__.constructor 变为了 Object()

关于JSON.stringify()在《你不知道的JavaScript》中卷中有提到:

  所有安全的 JSON 值(JSON-safe)都可以使用 JSON.stringify(..) 字符串化。安全的 JSON 值是指能够呈现为有效 JSON 格式的值。

  为了简单起见,我们来看看什么是不安全的 JSON 值。undefinedfunctionsymbol (ES6+)包含循环引用(对象之间相互引用,形成一个无限循环)的对象都不符合 JSON 结构标准,支持 JSON 的语言无法处理它们。

  JSON.stringify(..) 在对象中遇到 undefined、function 和 symbol 时会自动将其忽略,在数组中则会返回 null(以保证单元位置不变)。

  这种方法比较适合平常开发中使用,因为通常不需要考虑对象和数组之外的类型。

递归实现深拷贝

直接列函数:

  function deepCopy(obj) {
    if (typeof obj === 'object') {
      let newObj = obj instanceof Array ? [] : Object.create();
      for(let key in obj) {
        if(obj.hasOwnProperty(key)) {
          newObj[key] = deepCopy(obj[key]);
        }
      }
      return newObj;
    }
    return obj;
  }

下面是递归实现对上面的测试用例的结果:
这里写图片描述

继续表格

属性 new Foo() deepCopy(new Foo())
arr1 [1, 2, 3] [1, 2, 3]
arr2 [1, undefined, null, f()] [1, undefined, {}, f()]
date Thu Jan 01 1970 08:00:00 GMT+0800 (中国标准时间) Thu Jan 01 1970 08:00:00 GMT+0800 (中国标准时间)
err Error: ERROR! Error: ERROR!
func f() f()
obj1 {a: 1, b: 2, c: 3} {a: 1, b: 2, c: 3}
obj2 {a: undefined, b: null, c: f()} {a: undefined, b: {}, c: f()}
reg /\/^w$\/g/ /\/^w$\/g/
proto.constructor Foo() Foo()

  我们可以看到在 deepCopy() 中,已经对大部分的属性都能进行复制了,不过要看到在 arr2、obj2null 的转换仍有问题,会把 null 复制成 {} 一个空对象。

  并且我们在 deepCopy() 中用 Object.create() 取代 {} 来创建对象,使 __proto__.constructor 不丢失。

总结

  一个完善的深拷贝是不好实现的,还有许多可完善的点:比如能拷贝自身可枚举、自身不可枚举、自身 Symbol 类型、原型上可枚举、原型上不可枚举、原型上 Symbol、循环引用等等,不过上面的方法在日常使用应该还是足够的。

  有兴趣的可以看看下面几篇文章,当然也可以看看 JQuery、lodash 等库中对深拷贝的实现。

深入深入再深入 js 深拷贝对象
从零实现jQuery的extend

猜你喜欢

转载自blog.csdn.net/zwkkkk1/article/details/82107900