js 中的深拷贝与浅拷贝

在面试中经常会问到js的深拷贝和浅拷贝,也常常让我们手写,下面我们彻底搞懂js的深拷贝与浅拷贝。

在js中 Array 和 Object  这种引用类型的值,当把一个变量赋值给另一个变量时,这个值得副本其实是一个指针,这是两个变量的指针指向的是同一片推内存,当我们改变其中一个值的时候,另一个值也会受到影响。

那么这就分为两种情况,浅拷贝和深拷贝

浅拷贝:拷贝对象的引用

// 对象
var obj1 = {
    a: 1,
    b: 2
};
var obj2 = obj1;

obj2.a = 3;

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

// 数组
var arr1 = [1, 2, 3];
var arr2 = arr1;

arr2.push(4);

console.log(arr1); // [1, 2, 3, 4]
console.log(arr2); // [1, 2, 3 ,4]

可以看出,拷贝的是一个对象的引用,常用的方法有 jQuery 的extend({}, obj) 或者 Array.prototype.slice() 和 Array.prototype.concat() 都会返回一个数组或者对象的浅拷贝

var arr1 = ['a', {a: 1}];
var arr2 = arr1.slice();

console.log(arr1 === arr2); // false

arr2[0] = 'b';
console.log(arr1); // ['a', {a: 1}] 基本类型的值不会相互影响

arr2[1].a = 2;
console.log(arr1); // ['a', {a: 2}] 引用类型的值还是无法拷贝

为了加深印象,我们可以手动实现一个slice 或者jq 的extend({}, obj)

function shallowClone(source) {
    if (!source || typeof source !== 'object') {
        throw new Error('error');
    }
    var resultObj = source.constructor === Array ? [] : {};
    for (var keys in source) {
        if (source.hasOwnProperty(keys)) {
            resultObj[keys] = source[keys];
        }
    }
    return resultObj;
}

var obj1 = {a: 1, b: [1, 2]}
var obj2 = shallowClone(obj1);

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

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

深拷贝:拷贝对象的实例

深拷贝就是拷贝出一个新的实例,新的实例与原来的实例互不影响,实现深拷贝的方法有几种:

1、使用jq 的第三个参数来递归调用 $.extend(true, obj, ...),或者是lodash等第三方库函数实现

2、自己实现一个深拷贝,自己实现也有两种方法,一种是使用递归的方式拷贝,另一种是JSON.parse和JSON.stringfy

// 递归实现深拷贝
function deepClone(source) {
    if (!source || typeof source !== 'object') {
        throw new Error('error');
    }
    var resultObj = source.constructor === Array ? [] : {};
    for (var keys in source) {
        if (source.hasOwnProperty(keys)) {
            if (source[keys] && typeof source[keys] === 'object') {
                resultObj[keys] = source[keys].constructor === Array ? [] : {};
                resultObj[keys] = deepClone(source[keys]);
            }
            else {
                resultObj[keys] = source[keys];
            }
        }
    }
    return resultObj;
}

var a1 = {
    a: 1,
    b: [1, 2],
    c: {
        a: 1,
        b: 2
    }
};

var a2 = deepClone(a1);
a2.b.push(3)

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

 循环引用问题

上面的问题看似解决了所有的问题,但是有一种情况会有问题,就是循环引用。

1. 父级引用

父级引用是指,当某个属性的值正好是这个对象本身,如果我们用上面的方法进行深拷贝,就会在子元素 -> 父元素 -> 子元素 ...之间死循环,最后导致栈溢出。比如下面的代码:

var a1 = {
    a: 1,
    b: 2
};
a1.c = a1;
var result = deepClone(a1); // Uncaught RangeError: Maximum call stack size exceeded

解决办法:就是判断一个对象的字段是否引入了这个对象或者这个对象的任意父级,那么就需要修改上面的函数:

function deepClone2(source, parent = null) {
    if (!source || typeof source !== 'object') {
        throw new Error('error');
    }
    var resultObj = source.constructor === Array ? [] : {};
    var _parent = parent;
    while (_parent) {
        if (_parent.originParent === source) {
            return _parent.currentParent;
        }
        _parent = _parent.parent;
    }
    for (var keys in source) {
        if (source.hasOwnProperty(keys)) {
            if (source[keys] && typeof source[keys] === 'object') {
                resultObj[keys] = source[keys].constructor === Array ? [] : {};
                resultObj[keys] = deepClone2(source[keys], {
                    originParent: source,
                    currentParent: resultObj,
                    parent: parent
                });
            }
            else {
                resultObj[keys] = source[keys];
            }
        }
    }
    return resultObj;
}

var a1 = {
    a: 1,
    b: 2
};
a1.c = a1;
var result = deepClone2(a1);
console.log(a1) // {a: 1, b: 2, c: {…}}

2. 同级引用

假设有如下代码

var obj = {
    a: {
        name: 'a'
    },
    b: {
        name: 'b'
    },
    c: {}
};

obj.c.d = obj.a;
console.log(obj.c.d === obj.a); // true

如果我们调用上面的deepClone2函数

var copy = deepClone2(obj);
console.log(copy.a); // {name: "a"}
console.log(copy.c.d); // {name: "a"}
console.log(copy.a === copy.c.d); // false

从上面可以看出,虽然 copy.a 与 copy.c.d是的值是相等的,但二者引用的并不是同一个对象。

这种情况是因为 obj.a 并不在obj.c.d 的对象链上,所以 deepClone2 函数就无法检测到 obj.c.d 对 obj.a 也是一种引用关系,所以 deepClone2 函数就将 obj.a 深拷贝的结果赋值给了copy.c.d。

解决方案:父级的引用是一种引用,非父级的引用也是一种引用,那么只要记录下对象A中的所有对象,并与新创建的对象一一对应即可

function deepClone3(obj) {
    // hash表,记录所有的对象的引用关系
    let map = new WeakMap();
    function dp(obj) {
        let result = null;
        let keys = Object.keys(obj);
        let key = null,
            temp = null,
            existobj = null;

        existobj = map.get(obj);
        //如果这个对象已经被记录则直接返回
        if(existobj) {
            return existobj;
        }

        result = {}
        map.set(obj, result);

        for(let i =0,len=keys.length;i<len;i++) {
            key = keys[i];
            temp = obj[key];
            if(temp && typeof temp === 'object') {
                result[key] = dp(temp);
            }else {
                result[key] = temp;
            }
        }
        return result;
    }
    return dp(obj);
}

var obj = {
    a: {
        name: 'a'
    },
    b: {
        name: 'b'
    },
    c: {}
};

// 子级引用
var copy = deepClone3(obj);
console.log(copy.a); // {name: "a"}
console.log(copy.c.d); // {name: "a"}
console.log(copy.a === copy.c.d); // true

// 父级引用
var a1 = {
    a: 1,
    b: 2
};
a1.c = a1;
var result = deepClone3(a1);
console.log(a1) // {a: 1, b: 2, c: {…}}
原文地址:https://www.cnblogs.com/shenjp/p/11252354.html