低门坎完全明白JavaScript中的深拷贝和浅拷贝

在说深拷贝与浅拷贝前,我们先看两个简朴的案例:

//案例1
var num1 = 1, num2 = num1;
console.log(num1) //1
console.log(num2) //1

num2 = 2; //修正num2
console.log(num1) //1
console.log(num2) //2

//案例2
var obj1 = {x: 1, y: 2}, obj2 = obj1;
console.log(obj1) //{x: 1, y: 2}
console.log(obj2) //{x: 1, y: 2}

obj2.x = 2; //修正obj2.x
console.log(obj1) //{x: 2, y: 2}
console.log(obj2) //{x: 2, y: 2}

根据通例头脑,obj1应当和num1一样,不会由于别的一个值的转变而转变,而这里的obj1 却跟着obj2的转变而转变了。一样是变量,为何表现不一样呢?这就要引入JS中基础范例援用范例的观点了。

基础范例和援用范例

ECMAScript变量能够包括两种差别数据范例的值:基础范例值和援用范例值。基础范例值指的是那些保留在栈内存中的简朴数据段,即这类值完整保留在内存中的一个位置。而援用范例值是指那些保留堆内存中的对象,意义是变量中保留的实际上只是一个指针,这个指针指向内存中的另一个位置,该位置保留对象。

打个比如,基础范例和援用范例在赋值上的区分能够按“连锁店”和“单店”来明白:基础范例赋值等于在一个新的处所装置连锁店的范例规范新开一个分店,新开的店与其他旧店互不相干,各自运营;而援用范例赋值相当于一个店有两把钥匙,交给两个老板同时治理,两个老板的行动都有能够对一间店的运营形成影响。

上面清晰清晰明了的引见了基础范例和援用范例的定义和区分。现在基础范例有:Boolean、Null、Undefined、Number、String、Symbol,援用范例有:Object、Array、Function。之所以说“现在”,由于Symbol就是ES6才出来的,以后也能够会有新的范例出来。

再回到前面的案例,案例1中的值为基础范例,案例2中的值为援用范例。案例2中的赋值就是典范的浅拷贝,而且深拷贝与浅拷贝的观点只存在于援用范例

深拷贝与浅拷贝

既然已知道了深拷贝与浅拷贝的因由,那末该怎样完成深拷贝?我们先离别看看Array和Object自有要领是不是支撑:

Array

var arr1 = [1, 2], arr2 = arr1.slice();
console.log(arr1); //[1, 2]
console.log(arr2); //[1, 2]

arr2[0] = 3; //修正arr2
console.log(arr1); //[1, 2]
console.log(arr2); //[3, 2]

此时,arr2的修正并没有影响到arr1,看来深拷贝的完成并没有那末难嘛。我们把arr1改成二维数组再来看看:

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

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

咦,arr2又转变了arr1,看来slice()只能完成一维数组的深拷贝

具有一致特征的另有:concatArray.from()

Object

Object.assign()

var obj1 = {x: 1, y: 2}, obj2 = Object.assign({}, obj1);
console.log(obj1) //{x: 1, y: 2}
console.log(obj2) //{x: 1, y: 2}

obj2.x = 2; //修正obj2.x
console.log(obj1) //{x: 1, y: 2}
console.log(obj2) //{x: 2, y: 2}
var obj1 = {
    x: 1, 
    y: {
        m: 1
    }
};
var obj2 = Object.assign({}, obj1);
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 1, y: {m: 1}}

obj2.y.m = 2; //修正obj2.y.m
console.log(obj1) //{x: 1, y: {m: 2}}
console.log(obj2) //{x: 2, y: {m: 2}}

经测试,Object.assign()也只能完成一维对象的深拷贝

JSON.parse(JSON.stringify(obj))

var obj1 = {
    x: 1, 
    y: {
        m: 1
    }
};
var obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 1, y: {m: 1}}

obj2.y.m = 2; //修正obj2.y.m
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 2, y: {m: 2}}

JSON.parse(JSON.stringify(obj)) 看起来很不错,不过MDN文档 的形貌有句话写的很清晰:

undefined、恣意的函数以及 symbol 值,在序列化历程当中会被疏忽(出现在非数组对象的属性值中时)或许被转换成
null(出现在数组中时)。

我们再来把obj1革新下:

var obj1 = {
    x: 1,
    y: undefined,
    z: function add(z1, z2) {
        return z1 + z2
    },
    a: Symbol("foo")
};
var obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1) //{x: 1, y: undefined, z: ƒ, a: Symbol(foo)}
console.log(JSON.stringify(obj1)); //{"x":1}
console.log(obj2) //{x: 1}

发明,在将obj1举行JSON.stringify()序列化的历程当中,y、z、a都被疏忽了,也就考证了MDN文档的形貌。既然如许,那JSON.parse(JSON.stringify(obj))的运用也是有局限性的,不能深拷贝含有undefined、function、symbol值的对象,不过JSON.parse(JSON.stringify(obj))简朴粗犷,已满足90%的运用场景了。

经由考证,我们发明JS 供应的自有要领并不能彻底处理Array、Object的深拷贝题目。只能祭出大杀器:递归

function deepCopy(obj) {
    // 建立一个新对象
    let result = {}
    let keys = Object.keys(obj),
        key = null,
        temp = null;

    for (let i = 0; i < keys.length; i++) {
        key = keys[i];    
        temp = obj[key];
        // 假如字段的值也是一个对象则递归操纵
        if (temp && typeof temp === 'object') {
            result[key] = deepCopy(temp);
        } else {
        // 不然直接赋值给新对象
            result[key] = temp;
        }
    }
    return result;
}

var obj1 = {
    x: {
        m: 1
    },
    y: undefined,
    z: function add(z1, z2) {
        return z1 + z2
    },
    a: Symbol("foo")
};

var obj2 = deepCopy(obj1);
obj2.x.m = 2;

console.log(obj1); //{x: {m: 1}, y: undefined, z: ƒ, a: Symbol(foo)}
console.log(obj2); //{x: {m: 2}, y: undefined, z: ƒ, a: Symbol(foo)}

能够看到,递归圆满的处理了前面遗留的一切题目,我们也能够用第三方库:jquery的$.extend和lodash的_.cloneDeep来处理深拷贝。上面虽然是用Object考证,但关于Array也一样实用,由于Array也是特别的Object。

到这里,深拷贝题目基础能够告一段落了。然则,另有一个异常特别的场景:

轮回援用拷贝

var obj1 = {
    x: 1, 
    y: 2
};
obj1.z = obj1;

var obj2 = deepCopy(obj1);

此时假如挪用适才的deepCopy函数的话,会堕入一个轮回的递归历程,从而致使爆栈。jquery的$.extend也没有处理。处理这个题目也异常简朴,只须要推断一个对象的字段是不是援用了这个对象或这个对象的恣意父级即可,修正一下代码:

function deepCopy(obj, parent = null) {
    // 建立一个新对象
    let result = {};
    let keys = Object.keys(obj),
        key = null,
        temp= null,
        _parent = parent;
    // 该字段有父级则须要追溯该字段的父级
    while (_parent) {
        // 假如该字段援用了它的父级则为轮回援用
        if (_parent.originalParent === obj) {
            // 轮回援用直接返回同级的新对象
            return _parent.currentParent;
        }
        _parent = _parent.parent;
    }
    for (let i = 0; i < keys.length; i++) {
        key = keys[i];
        temp= obj[key];
        // 假如字段的值也是一个对象
        if (temp && typeof temp=== 'object') {
            // 递归实行深拷贝 将同级的待拷贝对象与新对象传递给 parent 轻易追溯轮回援用
            result[key] = DeepCopy(temp, {
                originalParent: obj,
                currentParent: result,
                parent: parent
            });

        } else {
            result[key] = temp;
        }
    }
    return result;
}

var obj1 = {
    x: 1, 
    y: 2
};
obj1.z = obj1;

var obj2 = deepCopy(obj1);
console.log(obj1); //太长了去浏览器试一下吧~ 
console.log(obj2); //太长了去浏览器试一下吧~ 

至此,已完成一个支撑轮回援用的深拷贝函数。固然,也能够运用lodash的_.cloneDeep噢~。

迎接议论,点个赞再走吧~

文章同步于以下社区,能够选一个关注我噢 。◕‿◕。

simbawu | github | segmentfault | 知乎 | 简书 | 掘金

    原文作者:simbawu
    原文地址: https://segmentfault.com/a/1190000014191218
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞