关于对象的深拷贝那点事

204 阅读5分钟

深拷贝和浅拷贝

最开始接触浅拷贝,可能是通过一些数组或者是js对象合并的实现,可能并没有对其中的原理和实现做一些比较深入的整理和学习,今天终于有时间可以系统的去整理一下这部分的知识点

让我们进入正题,可能我们对于浅拷贝会有一个初步的理解:

    需要我们自己创建一个对象, 用来接受你要重新复制或引用的对象值。如果对象属性是基本
    的数据类型, 复制的就是基本类型的值给新对象;但是如果属性是 引用数据类型,复制的就
    是内存中的地址, 如果是其中的一种对象改变了这个内存中的地址的话,那么肯定是会影响
    另一个对象。

实际开发中我们可能会用以下的几种方式

  • object.assign(target, ...sources)

    • 但是使用中需要注意几点

      1. 它不会拷贝对象的继承属性
      2. 它不会拷贝对象的不可枚举的属性
      3. 可以拷贝 Symbol 类型的属性
  • 拓展运算符 let cloneObj = {...obj}

  • concat 拷贝数组

  • slice 拷贝数组

如何实现一个浅拷贝

基于上面一些关于浅拷贝的理解, 如果让你去实现一个浅拷贝,大致的思路会分为两部分:

1. 对于基本类型做一个最基本的一个拷贝

2. 对于引用类型开辟一个新的存储, 并且拷贝一层对象属性

那么我们围绕这两个思路,尝试去实现一下吧:

const shallowClone = (target) => {
  if (typeof target === 'object' && target !== null) {
    const cloneTarget = Array.isArray(target) ? [] : {};
    
    for (let item in target) {
       if (target.hasOwnProperty(item)) {
          cloneTarget[item] = target[item];
       }
    }
    return cloneTarget;
  } else {
     return target;
  }
}

这段代码的实现思路就是, 通过利用类型判断, 针对于引用类型的对象进行 for 循环遍历对象属性赋值给目标对象的属性.

小tip

Object.prototype.hasOwnProperty() 方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是是否会有指定的键)

深拷贝的原理和实现

从上面可以看出浅拷贝只是创建了一个新对象, 复制了原有对象的基本类型的值, 而引用类型只拷贝了一层属性, 在深层是无法拷贝的

那么 对于深拷贝而言, 它对于复杂引用数据类型, 其在内存中完全开辟了一块内存地址, 并将原有的对象完全复制过来存在。

那么我们来看一下深拷贝的原理:

将一个对象从内存中完整地拷贝出来一份给目标对象,并从堆内存中开辟出一个全新的空间存在新对象, 且新对象的修改并不会改变原对象, 二者实现真正的分离

既然我们知道了深拷贝的实现原理, 那我们是不是可以去想一想它的实现的方式呢?

方式1 (JSON.stringify)

这种方式的话,其实就是把一个对象序列化成为一个JSON的字符串,并将对象里面的内容转换成字符串, 最后再用 JSON.parse() 方法将JSON 字符串生成一个新的对象。

let obj1 = {a:1, b: [1,2,3]}
let str = JSON.stringify(obj1)
let obj2 = JSON.parse(str)

console.log(obj2);  // {a:1, b : [1,2,3]}

其实基于 JSON.stringify 实现深拷贝需要注意一些地方,这里我做了一些总结:

1. 拷贝的对象的值中如果有函数, undefined、symbol这几种类型, 经过JSON.stringify序列化之后的字符串中这个键值对会消失

2. 拷贝Date 引用类型会变成字符串

3. 无法拷贝不可枚举的属性

4. 无法拷贝对象的原型链

5. 拷贝 RegExp 引用类型会变成空对象

6. 对象中含有 NaN、Infinity、 -Infinity, JSON 序列化的结果会变成 null

7. 无法拷贝对象的循环引用 既对象成环 (obj[key] = obj)

下面我会给出一段代码供大家去测试学习,希望可以交流一下

function testObj() {
    this.func = function () { console.log('test-func')};
    this.obj = { test: 1};
    this.arr = [11,22,33];
    this.unde = undefined;
    this.reg = /123456/;
    this.date = new Date(0);
    this.NaN = NaN;
    this.infinity = Infinity;
    this.sym = Symbol(11);
}

let obj1 = new testObj();
Object.defineProperty(obj1, "innumerable", {
   enumrable: false,
   value: 'innumerable',
})

console.log('obj1', obj1)

let testStr = JSON.stringify(obj1);
let testObj2 = JSON.parse(testStr);
console.log('testObj2', testObj2)

json.stringify.jpg

那么我们先来尝试实现一下吧

如何实现一个深拷贝

const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)

const deepClone = function (obj, hash = new WeakMap()) {

    // 日期对象直接返回一个新的日期对象
    if (obj.constructor === Date) return new Date(obj) 
    //正则对象直接返回一个新的正则对象
    if (obj.constructor === RegExp)  return new RegExp(obj) 
    
    //如果循环引用了就用 weakMap 来解决
    if (hash.has(obj)) return hash.get(obj)

    let allDesc = Object.getOwnPropertyDescriptors(obj)
    //遍历传入参数所有键的特性
    let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)

    //继承原型链
    hash.set(obj, cloneObj)
    for (let key of Reflect.ownKeys(obj)) {
    cloneObj[key] = (isComplexDataType(obj[key]) 
        && typeof obj[key] !== 'function') ? deepClone(obj[key], hash)         : obj[key]
    }
    return cloneObj
}

其中我们可以上面提到的几个问题做几个具体的解析

1. 针对于能够遍历对象的不可枚举属性以及 Symbol 类型, 我们可以使使用Reflect.ownKeys方法

2. 当参数为 Date、RegExp 类型则直接生成一个新的实例返回

3. 利用 Object的 getOwnPropertyDescriptors方法可以获取对象的所有属性,以及对应的特性,顺便结合 object.create() 创建一个新的对象,并继承传入原对象的原型链

4. 利用WeakMap 类型作为hash, 因为 WeakMap 是弱引用类型, 可以有效防止内存泄漏,作为检测循环引用很有帮助, 如果存在循环,则引用直接返回 WeakMap存储的值

test 代码:

let obj = {
    num: 0,
    str: '',
    boolean: true,
    unf: undefined,
    nul: null,
    obj: { name: '我是一个对象', id: 1 },
    arr: [0, 1, 2],
    func: function () { console.log('我是一个函数') },
    date: new Date(0),
    reg: new RegExp('/我是一个正则/ig'),
    [Symbol('1')]: 1,
};

Object.defineProperty(obj, 'innumerable', {
   enumerable: false, value: '不可枚举属性' }
);

obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))
obj.loop = obj // 设置loop成循环引用的属性
let cloneObj = deepClone(obj)
cloneObj.arr.push(4)
console.log('obj', obj)
console.log('cloneObj', cloneObj)

好了,今天的深拷贝的探索先到这里即将结束, 我们可以一起交流探索 一些相关的实现方式。

告辞