深复制与浅复制

1,147 阅读4分钟

这是我参与8月更文挑战的第6天,活动详情查看: 8月更文挑战

前言

深复制也叫深拷贝,是指源对象与复制对象彼此完全独立,任何一个对象改动都不会影响另一个,彼此拥有不同的引用地址。

浅复制也叫浅拷贝,复制对象只是复制了源对象的引用地址,地址所指向的数据是同一份。当源对象中没有引用类型时,深复制和浅复制时一样的。

这篇文章用来总结一些复制的常用方法,想到别的还会来更新。

深复制

方法1:JSON.parse(JSON.stringify(obj))

JSON.stringify有一些弊端:

  1. 无法对函数,RegExp等特殊对象进行克隆(函数会变成null,正则会变成{})
  2. 抛弃对象的constructor,所有的构造函数都指向Object
  3. 对象中有循环引用时报错
  4. 被转化的值中有NaN和Infinity时,值会被转化成null
  5. 被转化的值中有undefined,任意的函数以及Symbol时,这些值都会转化为null,对象中这些值对应的key也会被忽略。
  6. 当含有不可枚举的属性值时,该属性也会被忽略。

值有NaN和Infinity

let obj = {
    name: 'xxx',
    a: NaN,
    b: Infinity
}

console.log(JSON.stringify(obj))

//结果:
{"name":"xxx","a":null,"b":null}

转换的值/对象的值有undefined,函数,Symbol

let name = 'xxx';
let sss = Symbol('sss');
let arr = [
    name,
    undefined,
    sss,
    function fun() {},
    
    {
        a: undefined,
        t: () => {},
        c: sss
    }
]
console.log(JSON.stringify(arr))

//结果:
["xxx",null,null,null,{}]

循环引用

let bar = {
  a: {
    c: foo
  }
};
let foo = {
  b: bar
};
console.log(JSON.stringify(foo));

//结果:
VM291:3 Uncaught ReferenceError: foo is not defined

含有不可枚举属性

let obj = Object.create(null, {
  a: { value: 1, enumerable: false },
  b: { value: 2, enumerable: true },
});
console.log(JSON.stringify(obj));

//结果:
{"b":2}

方法2:递归实现

function deepClone(obj) {

    if(typeof obj === 'object') {
        let objStr = Object.prototype.toString.call(obj);
        let res;
        if(objStr === '[object Array]') {  //数组
            res = [];
        }else if(objStr === '[object Object]') {  //对象
            res = {}
        }else {  //null, RegExp, Date等
            return obj;
        }

        Object.keys(obj).forEach(item => {
            res[item] = deepClone(obj[item])
        })
        return res;

    }else {
        return obj;
    }
}

let arr = [1, 'str', /\d/, {}, [2]];

let b = deepClone(arr);

这里判断数据类型主要用到了typeof和Object.prototype.toString两种。typeof将基本类型筛出来,然后再用Object.prototype.toString.call去判断数组和对象

typeof

typeof的返回值有八种:undefined,string,number,boolean,function,object,symbol,bigint

对于基本类型和函数,typeof可以轻松判断,但是null和引用类型(包括通过String,RegExp,Date构造函数生成的对象),typeof则都返回object

image.png

Object.prototype.toString

这是一个超好用的方法,它可以判断所有的类型

Object.prototype.toString.call([]);        //"[object Array]"
Object.prototype.toString.call(1);         //"[object Number]"
Object.prototype.toString.call(Symbol('sy'));    //"[object Symbol]"
Object.prototype.toString.call({});      //"[object Object]"
Object.prototype.toString.call(new String('aaa'));   //"[object String]"
Object.prototype.toString.call(/\d+/);   //"[object RegExp]"
Object.prototype.toString.call(function(){});   //"[object Function]"

除了typeof,Object.prototype.toString,还可以用constructor, instanceof等来判断对象类型。

方法3:Map/weakMap

虽然方法2的递归实现已经可以解决大部分问题了,但是还是无法解决循环引用的问题,同时也无法解决相同引用的问题。

循环引用

let a = {};
let b = {};

a.b = b;
b.a = a;

相同引用

let arr = [1, 2, 3];

let obj = {};
obj.a1 = arr;
obj.a2 = arr;
  • 可以看出通过我们方法2的深拷贝方式,循环引用的对象的会导致栈溢出。
  • 方法2对数组和对象的特殊处理也会让本来指向同一个引用类型的两个对象不再相等。

可以考虑用WeakMap/Map来做一个记录,如果已经在map中添加过,则直接引用。

function cloneDeep(obj) {
    let visitedMap = new WeakMap();

    function baseClone(target) {
        if(typeof target === 'object' && target) {
            if(visitedMap.get(target)) {   //已经记录过
                return visitedMap.get(target);
            }

            let result = Array.isArray(target) ? [] : {};
            visitedMap.set(target, result);

            const keys = Object.keys(target);
            for(let i=0; i<keys.length; i++){
                result[keys[i]] = baseClone(target[keys[i]]);
            }

            return result;

        }else {
            return target;
        }
    }

    return baseClone(obj);
}

let arr = [1];
let obj = { 1:arr, 2:arr };

let newArr = cloneDeep(obj);
console.log(newArr[1] === newArr[2]);

let a = {}, b= {};
a.b = b;
b.a = a;

let circle = {};
circle.circle = circle;

let newArr1 = cloneDeep(circle);
console.log(newArr1.circle === newArr1);

浅复制

方法1:Object.assign

Objcet.assign方法接收一个目标对象和一个或多个源对象作为参数,然后将每个源对象中的可枚举的自有属性复制到目标对象。

let a1 = {
    a: 2,
    b: 3,
    c: {
        s:'引用类型'
    },
    2: 's',
};
Object.defineProperty(a1, 'b', {
    enumerable: false,
});


let obj = Object.assign({}, a1);
console.log(obj);  //{2: "s", a: 2, c: {s: "引用类型"}}

obj.c.s = '修改引用类型';
console.log(a1);  //{2: "s", a: 2, c: {s: "修改引用类型"}}

方法2:concat

concat方法用于连接两个或多个数组,返回一个新数组。但是它不会递归去查找数组中嵌套的引用类型,所以返回一个浅拷贝的数组。

let arr1 = [{s:1}, 'str', [1]];
arr2 = [].concat(arr1)
console.log(arr2);   //[{s: 1},"str", [1]]

arr2[1] = '修改基本类型'
arr2[0].s = '修改引用类型';
console.log(arr1);  //[{s: ""},"str", [1]]
console.log(arr2);  //[{s: "修改引用类型"}, "修改基本类型", [1]]

方法3:扩展运算符

扩展运算符主要用来将对象或数组展开。

let arr1 = [{s:1}, 'str', [1]];
let arr2 = [...arr1]
console.log(arr2);   //[{s: 1},"str", [1]]

arr2[1] = '修改基本类型'
arr2[0].s = '修改引用类型';
console.log(arr1);  //[{s: "修改引用类型"},"str", [1]]
console.log(arr2);  //[{s: "修改引用类型"}, "修改基本类型", [1]]