[TOC]

写作不易,Star是最大鼓励,感觉写的不错的可以给个Star⭐,请多多指教。Github地址

首先说明下:深拷贝和浅拷贝都是针对引用数据类型的

js数据类型

  1. 数据分为基本数据类型(string,number,boolean,null,undefined,Symbol,BigInt)和引用数据类型。
  2. 基本数据类型特点:存储的是实际数据,存储在栈内存中。
  3. 引用数据类型特点:存储的是对象在栈内存中引用(存储的是对象的地址值),真实的数据存放在堆内存中。

对基本数据类型进行拷贝,会对值进行一份拷贝,而对引用类型拷贝,则会进行地址的拷贝,最终两个对象指向的堆内存中的同一块内存地址。

堆内存用于存放由new创建的对象,栈内存存放一些基本类型的变量和对象的引用变量。

基本数据类型拷贝

// 基本数据类型存放的就是实际的数据,可以直接拷贝
let a = 10;
let b = a; // 将变量a的值赋值给变量b
a = 100; // 变量a的改变后,不会影响到变量b
console.log(a); // 100
console.log(b); // 10
1
2
3
4
5
6

引用数据类型拷贝

let obj1 = {
	name: 'lisi',
	age: '22'
};
let obj2 = obj1; // obj2复制的是obj1在栈内存中的引用(即obj1的地址值)
obj2.age = 23;
console.log(obj1.age); // 23
console.log(obj2.age); // 23
1
2
3
4
5
6
7
8

上述代码只是将obj1对象的在栈内存中的地址值赋值给了obj2对象。即两者指向了同一块堆内存空间(同一个对象),所以其中一个发生变化也会导致另外一个变化。

浅拷贝和深拷贝区别

  • 浅拷贝:创建一个新的对象,这个新对象中存放着原对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的,如果属性是引用类型,拷贝的是引用类型数据的内存地址,所以如果其中一个对象发生了变化,就会影响到另一个对象。
  • 深拷贝:将一个对象从内存中完整的拷贝一份出来,会在堆内存中开辟一个新的内存空间来存储新对象,将原对象的各个属性逐个复制出去,且修改新对象不会影响原对象。

判断依据:拷贝是否产生了新的数据(即是否在堆内存中重新开辟了一块内存空间)还是拷贝的仅仅是数据的引用(对象数据存放的是对象在栈内存的引用)。

深拷贝和浅拷贝最根本的区别在于:是否是真正获取了一个对象的复制实体,而不是引用(即对象地址)。浅拷贝就是只进行一层拷贝,深拷贝就是无限层级拷贝。

常用拷贝方法

  1. arr.concat():数组浅拷贝
  2. arr.slice():数组浅拷贝
  3. ES6中Array.from:数组浅拷贝
  4. Object.assign(target, source):对象浅拷贝
  5. ES6扩展运算符:数组和对象的浅拷贝
  6. JSON.parse(JSON.stringify(arr/obj)):数组或对象深拷贝,但不能处理函数数据、日期以及正则等
  7. 循环拷贝

concat和slice

/**
 * concat和slice可以实现数据的浅拷贝
 * 数组的元素都是基本数据类型
 */
const arr = ['test', 1, true, null, undefined];
const arr_new = arr.concat(); // 实现数组的浅拷贝
const arr_slice = arr.slice();

arr_new[1] = 2;
arr_slice[1] = 3;
// [ 'test', 1, true, null, undefined ] [ 'test', 2, true, null, undefined ] [ 'test', 3, true, null, undefined ]
console.log(arr, arr_new, arr_slice);
1
2
3
4
5
6
7
8
9
10
11
12

数组中的元素是对象或者数组

const arr = [{name: 'lisi'}, [1, 2, 3]];
const arr_new = arr.concat(); // 实现数组的浅拷贝
// const arr_new = Array.from(arr);
arr[0].name = 'lisi-from';
arr[1][0] = 222;
// [ { name: 'lisi-from' }, [ 222, 2, 3 ] ] [ { name: 'lisi-from' }, [ 222, 2, 3 ] ]
console.log(arr, arr_new);
// 无论是新数组还是旧数组都发生了变化,也就是说使用concat方法实现的是浅拷贝
1
2
3
4
5
6
7
8

对于数组拷贝,ES6中提供了两种新的方法(都是浅拷贝):

Array.from

var arr = [2, 3, 4];
var arr2 = Array.from(arr);
arr.push(5);
console.log(arr); // [2, 3, 4, 5]
console.log(arr2); // [2, 3, 4]
arr2.push(6);
console.log(arr); // [2, 3, 4, 5]
console.log(arr2); // [2, 3, 4, 6]
1
2
3
4
5
6
7
8

扩展运算符(...)

var arr = [2, 3, 4];
var arr2 = [...arr];
arr.push(5);
console.log(arr); // [2, 3, 4, 5]
console.log(arr2); // [2, 3, 4]

arr2.push(6);
console.log(arr); // [2, 3, 4, 5]
console.log(arr2); // [2, 3, 4, 6]
1
2
3
4
5
6
7
8
9

需要注意:Array.from和扩展运算符实现的都是浅拷贝,无法处理数组元素是引用类型的情况。

const arr = [1, 2, [3, 4]];
const arr2 = [...arr];

arr2[2][1] = 666;
console.log(arr2); // [1, 2, [3, 666]]
console.log(arr); // [1, 2, [3, 666]]
arr2[1] = 123;
console.log(arr2); // [1, 123, [3, 666]]
console.log(arr); // [1, 2, [3, 666]]
1
2
3
4
5
6
7
8
9

从上述代码结果来看,浅拷贝只能断开一层的引用,如果数据结构是多层引用类型的话,浅拷贝就不能解决问题了,这时候我们需要用到深拷贝。

数组循环拷贝

var arr = [2, 3, 4];
var arr2 = [];
for(let i of arr) {
	arr2.push(i);
}
arr.push(5);
arr2.push(6);
console.log(arr); // [2, 3, 4, 5]
console.log(arr2); // [2, 3, 4, 6]
1
2
3
4
5
6
7
8
9

对象循环拷贝

var obj1 = {
	name: 'lisi',
	age: 22
};
var obj2 = {};
for(let key in obj1) {
    // 仅拷贝对象自身属性,过滤掉原型链上的属性
    if (obj1.hasOwnProperty(key)) {
	    obj2[key] = obj1[key];
    }
}
console.log(JSON.stringify(obj1));
// {"name":"lisi","age":22}
console.log(JSON.stringify(obj2));
// {"name":"lisi","age":22}
obj1.job = 'worker';
obj2.job = 'teacher';
console.log(JSON.stringify(obj1));
// {"name":"lisi","age":22,"job":"worker"}
console.log(JSON.stringify(obj2));
// {"name":"lisi","age":22,"job":"teacher"}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

对象多层嵌套循环拷贝

var obj1 = {
	name: 'lisi',
	arr: [1, 2, 3]
}
function copy(obj1) {
	var obj2 = {};
	for(let key in obj1) {
		if (obj1.hasOwnProperty(key)) {
    	    obj2[key] = obj1[key];
        }
	}
	return obj2;
}
var obj2 = copy(obj1);
obj2.arr.push(4);
console.log(obj1.arr); // [1, 2, 3, 4]
console.log(obj2.arr); // [1, 2, 3, 4]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

从上述例子结果来看,浅拷贝仅仅拷贝了一层,而对于引用类型的数据(即多层数据),拷贝的只是引用类型数据在堆内存中的地址。

浅拷贝完整实现

  1. 检验参数,对于非对象类型的数据不进行拷贝,直接返回即可
  2. 考虑兼容数组
// Object.prototype.toString.call(null) => "[object Null]"
// Object.prototype.toString.call([]) => "[object Array]"
// function isObject(obj) {
//     return Object.prototype.toString.call(obj) === '[object Object]';
// }

// 判断是否是引用类型数据
function isObject(obj) {
    return typeof obj === 'object' || obj !== null;
}
const shallowClone = source => {
    // 只拷贝对象,如果当前拷贝的不是对象类型,则直接return
    if (!isObject(source)) return source;
    // 根据obj的类型判断是新建一个数组还是对象
    const new_obj = source instanceof Array ? [] : {};
    for (let key in source) {
        console.log(key);
        // 遍历source,并且判断是source的属性才拷贝,原型上的属性不进行拷贝
        if (source.hasOwnProperty(key)) {
            new_obj[key] = source[key];
        }
    }
    return new_obj;
};

console.log(shallowClone([{name: 'lisi', age: 22}]));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

如果数组元素是基本类型,就会拷贝一份,互不影响,而如果是对象或者数组,就会只拷贝对象和数组的引用(即地址值),这样我们无论对新旧对象进行了修改,两者都会发生变化。

深拷贝剖析

深拷贝的实现一般有两种:

  • JSON.parse(JSON.stringify(obj))
  • for...in循环递归浅拷贝

数组深拷贝

JSON.parse结合JSON.stringify。

const arr = [{name: 'lisi'}, [1, 2, 3]];
const arr2 = JSON.parse(JSON.stringify(arr));

arr[0].name = 'wangwu';
arr2[1][1] = 666;
console.log(arr); // [ { name: 'wangwu' }, [ 1, 2, 3 ] ]
console.log(arr2); // [ { name: 'lisi' }, [ 1, 666, 3 ] ]
1
2
3
4
5
6
7
const arr = [{name: 'lisi'}, [1, 2, 3], function fn() {}, {test: function() {console.log('123')}}];
const arr2 = JSON.parse(JSON.stringify(arr));

arr[0].name = 'wangwu';
arr2[1][1] = 666;
console.log(arr); // [ { name: 'wangwu' }, [ 1, 2, 3 ], [Function: fn], { test: [Function: test] } ]
console.log(arr2); // [ { name: 'lisi' }, [ 1, 666, 3 ], null, {} ]
1
2
3
4
5
6
7

JSON.parse(JSON.stringify(obj))的缺点

普通对象和数组都能拷贝,但是有如下缺点:

  • 正则属性会变为空对象
  • 函数属性会丢失
  • 日期对象属性会变为字符串
let obj = {
    a: 100,
    b: [10, 20, 30],
    c: {
        x: 10
    },
    d: /^\d+$/,
    e: new Date(),
    f: function(a) { return a + 2;}
};
let obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj);
console.log(obj2);
1
2
3
4
5
6
7
8
9
10
11
12
13

递归浅拷贝

let obj = {
   a: 100,
   b: [10, 20, 30],
   c: {
       x: 10
   },
   d: /^\d+$/,
   e: new Date(),
   f: function(a) { return a + 2;},
   g: null,
   h: undefined
};
function deepClone(obj) {
   // 确保既可以拷贝普通对象,也可以拷贝实例对象
   let newObj = new obj.constructor();
   for (let key in obj) {
       if (obj.hasOwnProperty(key)) {
           newObj[key] = obj[key];
       }
   }
   return newObj;
}
const newObj = deepClone(obj);
console.log(obj);
console.log(newObj);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

循环递归拷贝--深拷贝

const arr = [4, 5, 6];
// for...in遍历数组的时候遍历的是元素下标
for (let key in arr) {
    console.log(key); // 0 1 2
}
1
2
3
4
5

1. 基本实现

关键实现点:

  1. 递归
  2. 多种数据类型拷贝:
    1. 属性为函数
    2. 属性为null或者undefined
    3. 属性为Date
    4. 属性为正则

实现思路:先针对特殊情况进行处理。

let obj = {
   a: 100,
   b: [10, 20, 30],
   c: {
       x: 10
   },
   d: /^\d+$/,
   e: new Date(),
   f: function(a) { return a + 2;},
   g: null,
   h: undefined
};
function deepClone(obj) {
   // 先对特殊情况进行处理
   // 针对基本数据类型直接拷贝即可
   if (typeof obj !== 'object') {
       return obj;
   }
   if (obj == null) {
       return obj;
   }
   if (obj instanceof RegExp) { // 针对正则做处理
       return new RegExp(obj);
   }
   if (obj instanceof Date) { // 针对日期做处理
       return new Date(obj);
   }

   // 确保既可以拷贝普通对象,也可以拷贝实例对象
   let newObj = new obj.constructor();
   for (let key in obj) {
       if (obj.hasOwnProperty(key)) {
           newObj[key] = deepClone(obj[key]);
       }
   }
   return newObj;
}
const newObj = deepClone(obj);
console.log(obj);
console.log(newObj);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

数据类型函数实现:

function getType(target) {
    return Object.prototype.toString.call(target);
}
1
2
3
函数调用 结果
Object.prototype.toString.call(123) "[object Number]"
Object.prototype.toString.call('a') "[object String]"
Object.prototype.toString.call(true) "[object Boolean]"
Object.prototype.toString.call(null) "[object Null]"
Object.prototype.toString.call(undefined) "[object Undefined]"
Object.prototype.toString.call(Symbol()) "[object Symbol]"
Object.prototype.toString.call(BigInt(1)) "[object BigInt]"
Object.prototype.toString.call({}) "[object Object]"
Object.prototype.toString.call([]) "[object Array]"
Object.prototype.toString.call(function(){}) "[object Function]"
Object.prototype.toString.call(new Map) "[object Map]"
Object.prototype.toString.call(new Set) "[object Set]"
Object.prototype.toString.call(new Date) "[object Date]"
Object.prototype.toString.call(new RegExp) "[object RegExp]"
Object.prototype.toString.call(new Error) "[object Error]"
Object.prototype.toString.call(Math) "[object Math]"
Object.prototype.toString.call(JSON) "[object JSON]"
Object.prototype.toString.call(document) "[object HTMLDocument]"
Object.prototype.toString.call(window) "[object Window]"

上述结果都是在浏览器下执行的结果。 下面我们抽离出一些常用的数据类型以便后面使用:

const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';

const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const regexpTag = '[object RegExp]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
1
2
3
4
5
6
7
8
9
10
11
12

在上面的集中类型中,我们简单将他们分为两类:

  • 可以继续遍历的类型
  • 不可以继续遍历的类型

需要分别为它们做不同的拷贝。

2. 解决循环引用

关键点:理解weakmap的真正意义。

let obj = {
    a: 100,
    b: [10, 20, 30],
    c: {
        x: 10
    },
    d: /^\d+$/,
    e: new Date(),
    f: function(a) { return a + 2;},
    g: null,
    h: undefined
};
function deepClone(obj, hash = new WeakMap) {
    // 函数是不需要拷贝的
    // 排除不是对象类型,包括函数和基本数据类型
    // typeof function name(params){} // 'function'
    if (typeof obj !== 'object') {
        return obj;
    }
    if (obj == null) {
        return obj;
    }
    if (obj instanceof RegExp) { // 针对正则做处理
        return new RegExp(obj);
    }
    if (obj instanceof Date) {
        return new Date(obj);
    }
    // 确保既可以拷贝普通对象,也可以拷贝实例对象
    let newObj = new obj.constructor();
    // 代码能执行到这里,说明obj是一个对象类型
    if (hash.get(obj)) {
        return hash.get(obj);
    }
    hash.set(obj, newObj);
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObj[key] = deepClone(obj[key], hash);
        }
    }
    return newObj;
}
// const newObj = deepClone(obj);
// console.log(obj);
// console.log(newObj);
// 处理循环引用
let o = {};
o.x = o;
console.log(o);
let o1 = deepClone(o);
console.log(o1);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 确保既可以拷贝普通对象,也可以拷贝实例对象
let newObj = new obj.constructor();
1
2

这样写的好处:因为我们还使用了原对象的构造方法,所以它可以保留对象原型上的数据,如果直接使用普通的{},那么原型必然是丢失了的。

3. 克隆函数

实际上克隆函数是没有实际应用场景的,两个对象使用一个在内存中处于同一个地址的函数也是没有任何问题的,特意看了下lodash对函数的处理:

const isFunc = typeof value == 'function';
if (isFunc || !cloneableTags[tag]) {
      return object ? value : {};
}
1
2
3
4

从上述代码可以看出,如果发现是函数的话就会直接返回了,没有做特殊的处理。

深克隆完整实现

// 类型获取
Object.prototype.toString.call(/\d/).slice(8, -1); => "RegExp"
1
2
// 递归拷贝
// hash = new WeakMap 哈希表解决循环引用问题
// WeakMap 弱引用,不能用Map,会导致内存泄露
function deepClone(value, hash = new WeakMap) {
    // 先把特殊情况全部过滤掉
    // null == undefined // true
    // 排除null和undefined
    if (value == null) { // null和undefined不需要拷贝的,直接返回
        return value;
    }
    if (value instanceof RegExp) { // 处理正则
        return new RegExp(value);
    }
    if (value instanceof Date) { // 处理日期
        return new Date(value);
    }
    // 函数是不需要拷贝的
    // 排除不是对象类型,包括函数和基本数据类型
    if (typeof value !== 'object') {
        return value;
    }

    // 根据constructor来区分对象和数组
    // 还可以确保既可以复制普通对象,也可以复制实例对象
    let newObj = new obj.constructor();
    // 说明obj是一个对象类型
    if (hash.get(obj)) {
        // 有拷贝的就直接返回
        return hash.get(obj);
    }
    hash.set(obj, newObj); // 制作一个映射表,解决循环拷贝问题
    // 克隆Set
    if (obj instanceof Set) {
        console.log(newObj);
        obj.forEach(item => {
            newObj.add(deepClone(item, hash));
        });
        return newObj;
    }
    // 克隆Map
    if (obj instanceof Map) {
        obj.forEach((item, key) => {
            newObj.set(key, deepClone(item, hash));
        });
        return newObj;
    }
    // 区克隆对象和数组
    for (let key in value) {
        // 不拷贝原型链上的属性
        if (value.hasOwnProperty(key)) {
            // 递归拷贝
            obj[key] = deepClone(value[key], hash);
        }
    }
    return obj;
}
// let obj = {name: 'lisi', age: {num: 10}};
let obj = [[1, 2, 3]];
let obj1 = deepClone(obj);
// obj.age.num = 100;
console.log(obj); // { name: 'lisi', age: { num: 100 } }
// obj1.age.num = 1000;
console.log(obj1); // { name: 'lisi', age: { num: 1000 } }

let o = {};
o.x = o; // 循环引用,死循环了
let o1 = deepClone(o); // 如果这个对象拷贝过了,就返回那个拷贝的结果就可以了
console.log(o1); // RangeError: Maximum call stack size exceeded
// { x: [Circular] }

// 判断类型 typeof instanceof constructor
// Object.prototype.toString.call()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// 根据constructor来区分对象和数组
// let obj = new value.constructor();

[1, 2].constructor
ƒ Array() { [native code] }

let obj = {name: 'lisi'}
obj.constructor
ƒ Object() { [native code] }
1
2
3
4
5
6
7
8
9

总结

所谓的深拷贝和浅拷贝,都是针对Object和Array这样的引用数据类型。

  • 浅拷贝:只拷贝第一层的原始类型值,和第一层的引用类型地址。
  • 深拷贝:拷贝所有的属性值,以及属性地址指向的值的内存空间。

通过递归拷贝或者JSON.parse(JSON.stringify())来做深拷贝,都会有一些问题。

需要注意:以下方法都属于浅拷贝。

  • 对象的Object.assign()
  • 数组的Array.prototype.slice()
  • 数组的Array.prototype.concat()
  • 数组的Array.from()
  • ES6的扩展运算符

参考文档

  1. 如何写出一个惊艳面试官的深拷贝?
  2. 头条面试官:你知道如何实现高性能版本的深拷贝嘛?
  3. 终于弄清楚JS的深拷贝和浅拷贝了前端面试题系列9」浅拷贝与深拷贝的含义、区别及实现(文末有岗位内推哦~)
  4. 深拷贝的终极探索(90%的人都不知道)
  5. JavaScript专题之深浅拷贝
  6. js中对象的复制,浅复制(浅拷贝)和深复制(深拷贝)
  7. 浅谈js中的浅拷贝和深拷贝
  8. javascript中的深拷贝和浅拷贝?
  9. React 数据为什么要使用immutable方式?浅复制与深复制思考
  10. 面试官:请你实现一个深克隆
  11. js浅拷贝与深拷贝方法