深拷贝与浅拷贝

102 阅读6分钟

我正在参加「掘金·启航计划」。

浅拷贝的原理与实现

创建一个新的对象,来接受将要复制或者引用的对象。如果被复制的对象是基本数据类型,那么将基本类型的值赋给新对象;如果是引用数据类型,则复制的是内存中的地址,会随着内存地址的改变而发生变化。

方法一:Object.assign

Object.assign 可以用于对象的合并,也可以用于浅拷贝。语法第一个参数为拷贝的目标对象,后面的参数是拷贝的来源对象,也可以是多个来源。该方法会改变目标对象。

// 用法
Object.assign(target, ...sources)

const target = {a: 1, b: 2}
const source = {c: 3}
const result = Object.assign(target, source)
console.log(target)   // {a: 1, b: 2, c: 3}
console.log(result)   // {a: 1, b: 2, c: 3}

Object.assign 有几点需要注意的:

  • 不会拷贝对象的继承属性;
  • 不会拷贝对象不可枚举的属性;
  • 可以拷贝对象 Symbol 类型的属性。
let obj1 = { a:{ b:1 }, sym:Symbol(1)};
Object.defineProperty(obj1, 'innumerable' ,{ 
    value:'不可枚举属性', 
    enumerable:false 
});
let obj2 = {}; 
Object.assign(obj2,obj1) 
obj1.a.b = 2; 
console.log('obj1',obj1); 
console.log('obj2',obj2);

控制台打印结果,如下图所示:

image.png

方法二:扩展运算符

扩展运算符的语法为:let target = {...source}

/* 对象的拷贝 */ 
let obj = {a:1,b:{c:1}} 
let obj2 = {...obj} 
obj.a = 2 
console.log(obj) //{a:2,b:{c:1}} 
console.log(obj2); //{a:1,b:{c:1}} 
obj.b.c = 2 
console.log(obj) //{a:2,b:{c:2}} 
console.log(obj2); //{a:1,b:{c:2}} 
/* 数组的拷贝 */ 
let arr = [1, 2, 3]; 
let newArr = [...arr]; //跟arr.slice()是一样的效果

方法三:使用 concat 拷贝数组

let arr = [1, 2, 3]
let newArr = arr.concat()
console.log(newArr)  // [1, 2, 3]
newArr[0] = 10
console.log(arr)     // [1, 2, 3]
console.log(newArr)  // [10, 2, 3]

方法四:使用 slice 拷贝数组

slice 的语法为:arr.slice(start, end)

slice 由其前两个传参来决定原数组截取的起始以及结束位置,返回一个新的数组对象,不会影响原数组。

let arr = [1, 2, 3]
let newArr = arr.slice()
console.log(arr)     // [1, 2, 3]
console.log(newArr)  // [1, 2, 3]

手动实现一个浅拷贝

根据上述对浅拷贝的理解,可以得知浅拷贝针对引用类型只能进行一层对象属性的拷贝,再深层的则不会进行拷贝。那么,可以得知要实现一个浅拷贝需要做到以下两点:

  1. 针对基础类型,做一个基本的拷贝;
  2. 针对引用类型,需要开辟一个新的存储空间,拷贝一层对象属性。
const shallowClone = (target) => { 
    if (typeof target === 'object' && target !== null) { 
        const cloneTarget = Array.isArray(target) ? []: {}
        for (let prop in target) { 
            if (target.hasOwnProperty(prop)) { 
                cloneTarget[prop] = target[prop]
            } 
        } 
        return cloneTarget
    } else { 
        return target
    }
}

深拷贝的原理和实现

深拷贝对于浅拷贝来说,对于复杂的引用数据类型,在堆内存中完全开辟了一块内存地址,并将原有的对象完全复制过来进行存放。这两个对象是相互独立的,完全不受影响的。

丐版(JSON.stringify

JSON.stringify() 将一个对象序列化成 JSON 字符串,然后使用 JSON.parse() 再格式化成一个新的对象。

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]}
obj1.a = 2;
obj1.b.push(4);
console.log(obj1); //{a:2,b:[1,2,3,4]}
console.log(obj2); //{a:1,b:[1,2,3]}

从上述的代码中可以看到,使用 JSON.stringify() 可以实现一个深拷贝,在 obj1 发生变化时,obj2 不会发生变化。但是有几点需要注意:

  1. 拷贝的对象的值中如果有函数、undefinedsymbol 这几种类型,经过 JSON.stringify()之后的字符串中这个键值对会消失;
  2. 拷贝 Date 引用类型会变成字符串;
  3. 无法拷贝不可枚举的属性;
  4. 无法拷贝对象的原型链;
  5. 拷贝 RegExp 引用类型会变成空对象;
  6. 对象中含有 NaNInfinity 以及 -Infinity,序列化的结果会变成 null
  7. 无法拷贝对象的循环应用,即对象成环 (obj[key] = obj)。
function Obj() { 
    this.func = function () { 
        alert(1) 
    }; 
    this.obj = {a:1}; 
    this.arr = [1,2,3]; 
    this.und = undefined; 
    this.reg = /123/; 
    this.date = new Date(0); 
    this.NaN = NaN; 
    this.infinity = Infinity; 
    this.sym = Symbol(1);
}
let obj1 = new Obj();
Object.defineProperty(obj1,'innumerable',{ 
    enumerable:false, 
    value:'innumerable'
});
console.log('obj1', obj1);
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log('obj2', obj2);

image.png

基础版(手写递归实现)

let obj1 = { a:{ b:1 }}
function deepClone(obj) {  
    let cloneObj = {}  
    for(let key in obj) {                 
        //遍历    
        if(typeof obj[key] ==='object') {      
            cloneObj[key] = deepClone(obj[key])  //是对象就再次调用该函数递归    
        } else {      
            cloneObj[key] = obj[key]  //基本类型的话直接复制值    
        }  
    }  
    return cloneObj
}
let obj2 = deepClone(obj1);
obj1.a.b = 2;
console.log(obj2);   //  {a:{b:1}}

上述,通过 for in 遍历入参的属性,如果值是引用类型则再次递归调用该函数,若是基础类型,就直接赋值。使用递归可以实现一个深拷贝,但是还存在一些问题:

  1. 不可复制不可枚举的属性以及 Symbol 类型;
  2. 只针对普通的引用做递归复制,对于 ArrayDateFunction 等并没有做到真正的拷贝;
  3. 对象的属性里面成环,即循环引用没有解决。

改进版递归

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
}

// 下面是验证代码
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)

上述代码针对基础版做了以下改进:

  1. 针对不可枚举属性以及 Symbol 类型,使用 Reflect.ownKeys 方法;
  2. 当参数为 DateRegExp 类型,则直接生成一个新的实例返回;
  3. 利用 ObjectgetOwnPropertyDescriptors 方法可以获得对象的所有属性,以及对应的特性,顺便结合 Objectcreate 方法创建一个新对象,并继承传入原对象的原型链;
  4. 利用 WeakMap 类型作为 Hash 表,因为 WeakMap 是弱引用类型,可以有效防止内存泄漏(你可以关注一下 MapweakMap 的关键区别,这里要用 weakMap),作为检测循环引用很有帮助,如果存在循环,则引用直接返回 WeakMap 存储的值。

总结

本篇介绍了深拷贝与浅拷贝的方法以及可以自己实现的方法,全方面的了解一些基础知识。