简析 javascript 中的深拷贝和浅拷贝

1 深拷贝和浅拷贝的区别

1.1 JavaScript 的变量类型

JavaScript 中的变量类型可以分为两类:

  • 基本类型
  • 引用类型

基本类型有5种:

Undefined、Null、Boolean、Number 和 String

引用类型即我们所说的对象,存放在堆内存。

实际上引用类型保存的是一个指针,指向引用类型的值。当需要访问引用类型的值时,首先从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据。

1.2 JavaScript 的深拷贝和浅拷贝

深拷贝和浅拷贝都是对于应用类型而言的,简单的说,浅拷贝只复制了引用类型的子级的属性,而浅拷贝不止复制子级的属性,还递归复制了所有层级的属性。

所以可以知道,如果复制的对象中有饮用对象,那么在只复制了引用的情况下,复制产生的新对象和原对象引用的是同一个栈里的值。

diff_copy

废话不多说,直接上代码:

1
2
3
4
5
6
7
8
9
10
11
var zhang = {
name: "san",
age: 25,
gender: "male"
};
var zhang1 = zhang;
console.log(zhang); //Object {name: "san", age: 25, gender: "male"}
console.log(zhang1); //Object {name: "san", age: 25, gender: "male"}
zhang1.name = "si";
console.log(zhang); //Object {name: "si", age: 25, gender: "male"}
console.log(zhang1); //Object {name: "si", age: 25, gender: "male"}

明明是修改的 zhang1 的 name ,连 zhang 的 name 也改变了厚,这就是浅拷贝。
根据定义我们来实现深拷贝:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var zhang = {
name: "san",
age: 25,
gender: "male"
};
var zhang1 = {
name: zhang.name,
age: zhang.age,
gender: zhang.gender
};
console.log(zhang); //Object {name: "san", age: 25, gender: "male"}
console.log(zhang1); //Object {name: "san", age: 25, gender: "male"}
zhang1.name = "si";
console.log(zhang); //Object {name: "san", age: 25, gender: "male"}
console.log(zhang1); //Object {name: "si", age: 25, gender: "male"}

2 浅拷贝的实现方法

2.1 引用复制

浅拷贝只复制第一层的子级属性,所以只要遍历对象的子级属性进行复制就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function shallowClone(copyObj) {
var obj = {};
for ( var i in copyObj) {
obj[i] = copyObj[i];
}
return obj;
}
var x = {
a: 1,
b: { f: { g: 1 } },
c: [ 1, 2, 3 ]
};
var y = shallowClone(x);
console.log(y.b.f === x.b.f); // true

2.2 Object.assign() 函数

ES2015中提供了 Object.assign() 函数,用于将指定对象和目标对象合并。 MDN 上对于该函数的说明是:

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

因为 Object.assign() 拷贝的是属性值,加入源对象的属性值是一个指向对象的引用,它也只拷贝那个引用值。

1
2
3
4
5
6
7
let a = { b: {c:4} , d: { e: {f:1}} }
let g = Object.assign({},a)
console.log(g.d) //Object { e: { f: 1 } }
g.d.e = 32
console.log(g) //Object { b: { c: 4 }, d: { e: 32 } }
console.log(a) //Object { b: { c: 4 }, d: { e: 32 } }
console.log(h) //Object { b: { c: 4 }, d: { e: { f: 1 } } }

3 深拷贝的实现方法

3.1 JSON.parse() 和 JSON.stringify()

通过 JSON.stringify() 获得对象的 json 字符串,然后再通过 JSON.parse()将该字符串转化成实际对象。

1
let copya = JSON.parse(JSON.stringify(a));

不过这个方法的缺点也很明显:

  • json 不支持 NaN ,Infinity 和精确的浮点数
  • 不支持 function

所以使用这个方法并不好,还考虑到这个方法的效率,虽然简单,但是一般情况下都不会用。

3.2 通过递归解析的拷贝

这个方法其实就是递归地去解析对象的属性,将解析到的属性一条一条赋值给新的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function deepCopy(o, c) {
var c = c || {}
for (var i in o) {
if (typeof o[i] === 'object') {
if (o[i].constructor === Array) { //数组
c[i] = []
} else { //对象
c[i] = {}
}
deepCopy(o[i], c[i])
} else {
c[i] = o[i]
}
}
return c
}
var a = { b: {c:4} , d: { e: {f:1}} }
var g = deepCopy(a, {});
console.dir(a.b.c); //4
console.dir(g.b.c); //4
g.b.c = 5;
console.dir(a.b.c); //4
console.dir(g.b.c); //5

3 jQuery.extend() 函数的对象拷贝

3.1 jQuery.extend() 的基本用法

jQuery.extend() 可以实现深拷贝和浅拷贝,这个函数大多情况下用来扩展 jQuery / jQuery.fn 对象的方法,jQuery官网的解释是:

Description: Merge the contents of two or more objects together into the first object.

个人翻译就是将多个对象的属性合并到第一个对象中,如果有同名的属性,则将第一个对象中的属性覆盖,规则上是靠后(右)的对象属性覆盖靠前(左)的对象属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var object1 = {
apple: 0,
banana: { weight: 52, price: 100 },
cherry: 97
};
var object2 = {
banana: { price: 200 },
durian: 100
};
var object3 = {
banana: { price: 300 },
watermalon: 50
};

// Merge object2 and object3 into object1
$.extend( object1, object2, object3 );
console.log(JSON.stringify(object1)); //{"apple":0,"banana":{"price":300},"cherry":97,"durian":100,"watermalon":50}

3.2 jQuery.extend() 实现浅拷贝和深拷贝

根据 jQuery.extend() 的定义,只需要将要拷贝的对象合并到一个空对象,就可以实现拷贝了。而将 jQuery.extend() 的第一个参数设置成 true ,就可设置成深拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var obj = { b: {c:4} , d: { e: {f:1}} }
var shallowc = $.extend({}, a)
var deepc = $.extend(true, {}, a)
console.dir(obj.b.c); //4
console.dir(shallowc.b.c); //4
console.dir(deepc.b.c); //4
deepc.b.c = 5;
console.dir(obj.b.c); //4
console.dir(shallowc.b.c); //4
console.dir(deepc.b.c); //5
shallowc.b.c = 6;
console.dir(obj.b.c); //6
console.dir(shallowc.b.c); //6
console.dir(deepc.b.c); //5

可以看到 $.extend({}, a)$.extend(true, {}, a) 得到的对象分别就是浅拷贝和深拷贝得到的对象。文末附上 jQuery.extend() 的源码。

4 简单的总结

说了那么多,需要来简单的概括一下。

其实深拷贝的需求在实际开发中会出现的频次并不高,对于深拷贝,最好的方法就是抛弃需要深拷贝的代码


参考资料:
Object.assign() - JavaScript|MDN
jQuery.extend() | jQuery API Documentation
javaScript中浅拷贝和深拷贝的实现
[Javascript] 關於 JS 中的淺拷貝和深拷貝 · Larry

附上 $.extend() 源码(是不是跟楼上的递归很像)

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
jQuery.extend = jQuery.fn.extend = function() {
var options, name, src, copy, copyIsArray, clone,
target = arguments[ 0 ] || {},
i = 1,
length = arguments.length,
deep = false;

// Handle a deep copy situation
if ( typeof target === "boolean" ) {
deep = target;

// Skip the boolean and the target
target = arguments[ i ] || {};
i++;
}

// Handle case when target is a string or something (possible in deep copy)
if ( typeof target !== "object" && !jQuery.isFunction( target ) ) {
target = {};
}

// Extend jQuery itself if only one argument is passed
if ( i === length ) {
target = this;
i--;
}

for ( ; i < length; i++ ) {

// Only deal with non-null/undefined values
if ( ( options = arguments[ i ] ) != null ) {

// Extend the base object
for ( name in options ) {
src = target[ name ];
copy = options[ name ];

// Prevent never-ending loop
if ( target === copy ) {
continue;
}

// Recurse if we're merging plain objects or arrays
if ( deep && copy && ( jQuery.isPlainObject( copy ) ||
( copyIsArray = Array.isArray( copy ) ) ) ) {

if ( copyIsArray ) {
copyIsArray = false;
clone = src && Array.isArray( src ) ? src : [];

} else {
clone = src && jQuery.isPlainObject( src ) ? src : {};
}

// Never move original objects, clone them
target[ name ] = jQuery.extend( deep, clone, copy );

// Don't bring in undefined values
} else if ( copy !== undefined ) {
target[ name ] = copy;
}
}
}
}

// Return the modified object
return target;
};