深入理解 JavaScript 之 浅拷贝和深拷贝

1、数据类型

数据类型分为两种, 基本类型引用类型

基本类型: String, Number, Boolean, Null, Undefined,Symbol

引用类型: Object,Array,Date,Function,regexp…

1.1 区别

基本类型是按值访问的,不会影响到其他数据,例如:

var a = '前端'
var b = a
a = '前端工程师'
b // 前端

引用类型的值是按地址访问的,简单的赋值,实际上只是把地址复制了一遍,修改任意一个值会影响到另外一个,例如:

var a = [1,2,3,4]
var b = a
a[1] = '已修改'
b // [1, "已修改", 3, 4]

2、什么是深/浅拷贝?

  • 深拷贝和浅拷贝是只针对引用数据类型的。

  • 浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存

  • 深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
    深入理解 JavaScript 之 浅拷贝和深拷贝_第1张图片
    深入理解 JavaScript 之 浅拷贝和深拷贝_第2张图片

2.1 为什么要使用深拷贝?

我们希望在改变新的数组(对象)的时候,不改变原数组(对象)

3、浅拷贝的实现方式

3.1 Object.assign()

Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。

注意:当object只有一层的时候,是深拷贝

let obj1 = { person: {name: "kobe", age: 41},sports:'basketball' };
let obj2 = Object.assign({}, obj1);
obj2.person.name = "wade";
obj2.sports = 'football'
console.log(obj1); // { person: { name: 'wade', age: 41 }, sports: 'basketball' }

3.2 展开运算符…

展开运算符是一个 es6 / es2015特性,它提供了一种非常方便的方式来执行浅拷贝,这与 Object.assign ()的功能相同。

let obj1 = { name: 'Kobe', address:{x:100,y:100}}
let obj2= {... obj1}
obj1.address.x = 200;
obj1.name = 'wade'
console.log('obj2',obj2) // obj2 { name: 'Kobe', address: { x: 200, y: 100 } }

3.3 Array.prototype.concat()

let arr = [1, 3, {
    username: 'kobe'
    }];
let arr2 = arr.concat();    
arr2[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]

3.4 Array.prototype.slice()

let arr = [1, 3, {
    username: ' kobe'
    }];
let arr3 = arr.slice();
arr3[2].username = 'wade'
console.log(arr); // [ 1, 3, { username: 'wade' } ]

4、深拷贝的实现方式

4.1 JSON.parse(JSON.stringify())

let arr = [1, 3, {
    username: ' kobe'
}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'duncan'; 
console.log(arr, arr4)

深入理解 JavaScript 之 浅拷贝和深拷贝_第3张图片

这种实现深拷贝的方法有局限性,它只适用于一般数据的拷贝(对象、数组),有以下情况需要注意:

 let obj = {
        age: 18,
		hh: NaN,
        isInfinite: 1.7976931348623157E+10308,
        minusInfinity: -1.7976931348623157E+10308,
        date: new Date(),
		reg: new RegExp('\\w+'),
        err: new Error('error message'),
		fn: function () {
            console.log('fn');
        },
        hh: undefined
    };
  let objCopy = JSON.parse(JSON.stringify(obj));
  console.log('obj', obj);
  console.log('objCopy', objCopy);

深入理解 JavaScript 之 浅拷贝和深拷贝_第4张图片
总结:

  • 不支持函数
  • 不支持undefined(支持null)
  • 不支持循环引用,比如 a = {name: ‘a’}; a.self = a; a2 = JSON.parse(JSON.stringify(a))
  • 不支持Date,会变成 ISO8601 格式的字符串
  • 不支持正则表达式
  • 不支持Symbol

以上,如果拷贝的对象不涉及上面的情况,可以使用 JSON.parse(JSON.stringify(obj)) 实现深拷贝。

参考资料:关于 JSON.parse(JSON.stringify(obj)) 实现深拷贝的一些坑

4.2 手写递归方法

递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝

还有一个终极问题,对象有环怎么办?递归不就永远出不来了? 别慌,用缓存解决。我们把原对象和克隆过的对象都放进缓存列表,如果有环,返回对应的新对象即可。

class DeepClone {
  constructor() {
    this.cacheList = [];
  }
  clone(source) {
    if (source instanceof Object) {
      const cache = this.findCache(source); // 如果找到缓存,直接返回
      if (cache) return cache;
      else {
        let target;
        if (target instanceof Array) {
          target = new Array();
        } else if (target instanceof Function) {
          target = function () {
            return source.apply(this, arguments);
          };
        } else if (target instanceof Date) {
          target = new Date(source);
        } else if (target instanceof RegExp) {
          target = new RegExp(source.source, source.flags);
        } else {
          target = new Object(); // 不要忘记普通对象
        }

        this.cacheList.push([source, target]); // 把原对象和新对象放进缓存列表
        for (let key in source) {
          if (source.hasOwnProperty(key)) {
            // 不拷贝原型上的属性,浪费内存
            target[key] = this.clone(source[key]); // 递归
          }
        }
        return target;
      }
    } else {
      return source;
    }
  }
  findCache(source) {
    for (let i = 0; i < this.cacheList.length; ++i) {
      if (this.cacheList[i][0] === source) {
        return this.cacheList[i][1];
      }
    }
  }
}

总结:

对于递归克隆的深拷贝,核心有三点:

  • 对象分类
  • 递归
  • 用缓存对付环

递归克隆看起来很强大,但是完美无缺吗?其实还是有不小的距离:

  • 对象类型支持不够多(Buffer,Map,Set等都不支持
  • 存在递归爆栈的风险

如果要解决这些问题,实现一个”完美“的深拷贝,只能求教上百行代码的 Lodash.cloneDeep() 了 。

参考资料

[1] 前端手写系列01-深拷贝的两种实现与局限

你可能感兴趣的:(JavaScript,javascript,前端,开发语言)