深拷贝与浅拷贝

473 阅读7分钟

其实都是老生常谈的问题了,经常会看见关于深拷贝与浅拷贝的话题,自己也一直是只了解一点,并未系统深入的研究过,理解也始终浅薄,想自己再整理一遍,作为自己的一次学习上的补充。

主要会从以下几个方向来编写:

  1. 什么是浅拷贝和深拷贝?
  2. 浅拷贝和深拷贝的应用场景?
  3. 如何实现?

1. 什么是浅拷贝和深拷贝?

为什么会有深拷贝和浅拷贝,这跟JS中的数据类型有关系。

在JavaScript中,数据分为基本类型和引用类型。

  1. 基本类型包括string,number,boolean,null和undefined,(ES6中还包括Symbol)。这几种基本类型的数据都是直接复制到变量中,它们按照值进行赋值、复制、传递函数参数以及返回函数结果。

  2. JS的其余部分则依赖于引用。引用时指向对象所在内存位置的指针。两个或多个变量不需要各自拥有某个对象的副本,它们只需要指向同一个对象即可。通过引用对指称目标作出的修改会反映到其他引用中。

所以,其实浅拷贝和深拷贝这两个概念是针对引用类型来说的。

而区分浅拷贝和深拷贝的前提是,对象里嵌套对象的情况,如var obj = {name: 'armor', arr: [1, 2, 3]} 如果只是纯对象的话,例如:var obj = {name: 'armor'}这样的情况,就没必要区分浅拷贝和深拷贝了。

现在再来看看浅拷贝和深拷贝,顺便可以加上赋值来对比一下:

  1. 赋值:一个对象被赋值后,两个对象指向的是同一个存储空间,无论哪个对象发生改变,都会影响另一个对象。
  2. 浅拷贝:浅拷贝只复制指向某个对象的指针,而不复制对象本身,也就是拷贝的结果是两个对象指向同一个地址,修改其中一个对象的属性,则另一个对象的属性也会改变。
  3. 深拷贝:开辟新的栈,两个对象对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。

用两个个例子来看一下浅拷贝和深拷贝的效果, 先来看一个浅拷贝的例子:

var obj1 = {
    name: 'armor',
    age: 14,
    friends: ['ann', 'yang', 'Amy']
};

// 浅拷贝
var obj2 = Object.assign({}, obj1);

// 改变obj2的基本类型
obj2.name = 'fox';

// 改变obj2中嵌套的对象
obj2.friends[1] = 'liu';

console.log('obj1: ', obj1);
console.log('obj2: ', obj2);


<!--输出结果:影响到了原数据-->
obj1:  { name: 'armor', age: 14, friends: [ 'ann', 'liu', 'Amy' ] }
obj2:  { name: 'fox', age: 14, friends: [ 'ann', 'liu', 'Amy' ] }

下面这个是深拷贝:

var obj1 = {
    name: 'armor',
    age: 14,
    friends: ['ann', 'yang', 'Amy']
};

// 深拷贝
var obj2 = JSON.parse(JSON.stringify(obj1));

// 改变obj2的基本类型
obj2.name = 'fox';

// 改变obj2中嵌套的对象
obj2.friends[1] = 'liu';

console.log('obj1: ', obj1);
console.log('obj2: ', obj2);


<!--输出结果:没有影响原数据-->
obj1:  { name: 'armor', age: 14, friends: [ 'ann', 'yang', 'Amy' ] }
obj2:  { name: 'fox', age: 14, friends: [ 'ann', 'liu', 'Amy' ] }

可以看见浅拷贝后的对象,修改对象中第一层为基本数据类型的值对原数据没有影响;修改原对象中的子对象,会使原数据一同改变。

而深拷贝则完全不会影响到原数据。

2. 浅拷贝和深拷贝的应用场景?

  1. 浅拷贝一般用在涉及到状态变化时使用,比如 React/Redux 和 Preact/unistore 之流。
  2. 例如一个表格中的信息,点击修改或编辑,当弹出框中的内容变化时,我们希望不会影响到表格中的数据,,而是确定修改完之后再改变表格数据。这时需要两个数据互不影响,就可以使用深拷贝。
  3. 从服务器fetch到数据之后我将其存放在store中,通过props传递给界面,然后我需要对这堆数据进行修改,那涉及到修改就一定有保存和取消,所以我们需要将这堆数据拷贝到其他地方

3. 浅拷贝的实现

3.1 用于对象:Object.assign()

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。

var obj1 = {
    people: {
        name: 'armor'
    }
};
var obj2 = Object.assign({}, obj1);

obj2.people.name = 'fox';

console.log(obj1.people.name);
// 输出: fox

3.2 用于数组:Array.prototype.concat()

var obj1 = [{
    arr: [1, 2, 3]
}];
var obj2 = obj1.concat();

obj2[0].arr[1] = 4;

console.log(obj1[0]);
// 输出: { arr: [ 1, 4, 3 ] }

3.3 用于数组:Array.prototype.slice()

var obj1 = [{
    arr: [1, 2, 3]
}, 2, 3];
var obj2 = obj1.slice();

obj2[0].arr[1] = 4;
obj2[1] = 5; // 修改第一层基础类型不影响原数据

console.log(obj1);
// 输出: [ { arr: [ 1, 4, 3 ] }, 2, 3 ]

3.4 手动遍历复制实现

function shallowCopy(source) {
    // 如果不是对象,返回自身
    if(!(typeof source === 'object' && source != null)) return source;
    var target = Array.isArray(source) ? [] : {}; // 判断对象是不是数组
    for(var key in source) {
        if(source.hasOwnProperty(key)) {
            target[key] = source[key];
        }
    }
    return target;
}

上面几种方式都是浅拷贝的实现,当然,如果没有嵌套的子对象存在时,其实也可以把这几个方法当作深拷贝来使用。

4. 深拷贝的实现

4.1 用于对象或数组:JSON.parse(JSON.stringify())

注意:不能用于处理函数和undefined

var obj1 = [{
    arr: [1, 2, 3]
}, 2, 3, function(){console.log(1);}, undefined];
var obj2 = JSON.parse(JSON.stringify(obj1));

obj2[0].arr[1] = 4;
obj2[1] = 5; 

console.log(obj1);
console.log(obj2);
// 输出:
// [ { arr: [ 1, 2, 3 ] }, 2, 3, [Function], undefined ]
// [ { arr: [ 1, 4, 3 ] }, 5, 3, null, null ]

可以看见,深拷贝不影响原对象,但是这个方法不能对函数及undefined进行拷贝处理,有缺陷。

4.2 递归遍历

实现方式:

  • 遍历对象或数组
  • 判断对象里每一项的数据类型
  • 如果是基本类型,直接赋值,否则递归调用自身,继续判断

其实这个深拷贝只不过是在浅拷贝的基础上加上递归实现的。可以说浅拷贝就是只拷贝了一层,而深拷贝就是无限层级的拷贝。

function deepCopy(source) {
    if(!(typeof source === 'object' && source != null)) return source;  // 如果不是对象,返回自身
    var target = Array.isArray(source) ? [] : {}; // 判断对象是不是数组
    for(var key in source) { 
        if(typeof source[key] === 'object' && source[key] != null) {
            target[key] = deepCopy(source[key]);
        } else {
            target[key] = source[key];
        }
    }
    return target;
}

循环引用的问题

正常的对象用上面的解决方式已经够了,但是如果遇到一种情况,被复制的对象中具有指向它自己的对象,如果用上面的方法,这时候我们来看一下结果:

function deepCopy(source) {
    if(!(typeof source === 'object' && source != null)) return source;  // 如果不是对象,返回自身
    var target = Array.isArray(source) ? [] : {}; // 判断对象是不是数组
    for(var key in source) { 
        if(typeof source[key] === 'object' && source[key] != null) {
            target[key] = deepCopy(source[key]);
        } else {
            target[key] = source[key];
        }
    }
    return target;
}

var obj1 = {
    name: 'armor',
    age: 14,
    friends: ['ann', 'yang', 'Amy'],
    d:1
};
obj1.c = obj1;

// 浅拷贝
var obj2 = deepCopy(obj1);

// 报错了, Uncaught RangeError: Maximum call stack size exceeded

值得讨论的是,虽然obj1存在循环引用,但直接打印obj1不会报错;调用deepCopy时才会爆栈。

下面看一下解决方案:

哈希表WeakMap

也就是循环检测,可以设置一个数组或者哈希表存储已拷贝过的对象,当检测到当前对象已存在于哈希表中时,取出该值并返回即可。

function deepCopy(source, hash=new WeakMap()) {
    if(!(typeof source === 'object' && source != null)) return source;  // 如果不是对象,返回自身
    if(hash.has(source)) {
        return hash.get(source);
    }
    var target = Array.isArray(source) ? [] : {}; // 判断对象是不是数组
    hash.set(source, target);
    for(var key in source) { 
        if(typeof source[key] === 'object' && source[key] != null) {
            target[key] = deepCopy(source[key], hash);
        } else {
            target[key] = source[key];
        }
    }
    return target;
}

用上面的例子检测一下:

var obj1 = {
    name: 'armor',
    age: 14,
    friends: ['ann', 'yang', 'Amy'],
    d:1
};
obj1.c = obj1;

// 浅拷贝
var obj2 = deepCopy(obj1);

// 改变obj2的基本类型
obj2.name = 'zhong';

// 改变obj2中嵌套的对象
obj2.friends[1] = 'liu';

console.log('obj1: ', obj1);
console.log('obj2: ', obj2);

// 正常输出:
// obj1:  {name: "armor", age: 14, friends: Array(3), d: 1, c: {…}}
// obj2:  {name: "zhong", age: 14, friends: Array(3), d: 1, c: {…}}

数组方式

如果不能使用ES6的weakMap语法,也可以使用数组来保存引用到对象自身的key值

function hasSource(arr, target) {
    for(var i = 0; i < arr.length; i++) {
        if(arr[i].source === target) {
            return arr[i];
        }
    }
    return false;
}
function deepCopy(source, list = []) {
    if(!(typeof source === 'object' && source != null)) return source;  // 如果不是对象,返回自身
    var uniData = hasSource(list, source);
    if(uniData) {
        return uniData.target;
    }
    var target = Array.isArray(source) ? [] : {}; // 判断对象是不是数组
    list.push({source: source, target: target});
    for(var key in source) { 
        if(typeof source[key] === 'object' && source[key] != null) {
            target[key] = deepCopy(source[key], list);
        } else {
            target[key] = source[key];
        }
    }
    return target;
}