javascript中的对象类型拷贝问题

在这篇文章中,我们将讨论 对象类型(objects)、或者说引用类型在 javascript 中的拷贝问题,会分别对 浅拷贝深拷贝 进行说明。

在开始之前,我们需要了解一些基础知识:Objectsjavascript 中只是一块内存地址的简单引用,任何变量都可以指向这块地址,以至于有的时候,这使其变得难以琢磨,比如,将一个对象的引用复制给另一个变量,此时,这块地址就会有2处引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var foo = {
a : "abc"
}
console.log(foo.a); // abc

var bar = foo;
console.log(bar.a); // abc

foo.a = "yo foo";
console.log(foo.a); // yo foo
console.log(bar.a); // yo foo

bar.a = "whatup bar?";
console.log(foo.a); // whatup bar?
console.log(bar.a); // whatup bar?

如上面代码,我们声明了一个 foo 变量,该变量指向 { a: "abc"} 对象所在的内存地址,此时,可以使用 foo.a 查看其中的属性 a 的值为 abc。之后,我们又将 foo 赋值给了 bar 变量,即将 { a: "abc"} 对象的地址赋值给了 bar 变量,这时候,该对象就被2个变量所引用,随后,我们执行 foo.a = "yo foo" 改变了该对象中 a 属性的值,再次打印 foo.abar.a 后显示的结果都是 yo foo。同理,修改 bar.a 得到的也是一样的结果,因为他们改变的都是同一个对象的属性值。

浅拷贝(shallow copy)

如果你的对象里所有的属性值都是 值类型,那么你可以使用 es6 中新的对象API Object.assign() 或者使用扩展运算符 ... 进行拷贝操作,俗称 浅拷贝

1
2
3
var obj = { foo: "foo", bar: "bar" };

var copy = { ...obj }; // Object { foo: "foo", bar: "bar" }
1
2
3
var obj = { foo: "foo", bar: "bar" };

var copy = Object.assign({}, obj); // Object { foo: "foo", bar: "bar" }

上面的两种方式都可以将多个源对象中的属性拷贝到目标对象上。

1
2
3
4
5
var obj1 = { foo: "foo" };
var obj2 = { bar: "bar" };

var copySpread = { ...obj1, ...obj2 }; // Object { foo: "foo", bar: "bar" }
var copyAssign = Object.assign({}, obj1, obj2); // Object { foo:

但问题是,对于上面的两种方式,如果被拷贝的对象中的属性值也是一个对象类型,那么,对于该属性的拷贝只是拷贝一个对象的引用,这跟第一个例子是一样的情况 var bar = foo;

1
2
3
4
5
6
7
8
var foo = { a: 0 , b: { c: 0 } };
var copy = { ...foo };

copy.a = 1;
copy.b.c = 2;

console.dir(foo); // { a: 0, b: { c: 2 } }
console.dir(copy); // { a: 1, b: { c: 2 } }

也就是说,你并没有拷贝出一个副本出来,不管对哪个变量进行了修改,那个属性值为对象类型的属性都会被改变。

深拷贝(deep copy)

为了能够 深拷贝 一个对象类型的值,一种解决方法是,首先将该对象序列化成一个 JSON 字符串,然后再将其解析回来。

1
2
var obj = { a: 0, b: { c: 0 } };
var copy = JSON.parse(JSON.stringify(obj));

不幸的是,这种方式只对那些可以序列化为 JSON 格式的数据有用,并且要求这些属性值不存在任何的循环引用。比如,Date 对象,当你对其进行 字符串化 后,再解析回来的时候,该数据就只是一个时间字符串了,而不是原先的 Date 对象。

1
2
3
var d = new Date()  // Wed Apr 10 2019 16:49:10 GMT+0800 

console.log(JSON.parse( JSON.stringify(d) )) // "2019-04-10T08:49:10.865Z"

最终解决

综上所述,我们需要对不同的数据类型进行处理,所以有如下方法:

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
function deepClone(obj) {
var copy;

// Handle the 3 simple types, and null or undefined
if (null == obj || "object" != typeof obj) return obj;

// Handle Date
if (obj instanceof Date) {
copy = new Date();
copy.setTime(obj.getTime());
return copy;
}

// Handle Array
if (obj instanceof Array) {
copy = [];
for (var i = 0, len = obj.length; i < len; i++) {
copy[i] = deepClone(obj[i]);
}
return copy;
}

// Handle Function
if (obj instanceof Function) {
copy = function() {
return obj.apply(this, arguments);
}
return copy;
}

// Handle Object
if (obj instanceof Object) {
copy = {};
for (var attr in obj) {
if (obj.hasOwnProperty(attr)) copy[attr] = deepClone(obj[attr]);
}
return copy;
}

throw new Error("Unable to copy obj as type isn't supported " + obj.constructor.name);
}

具体思路,就是针对不同类型的数据做不同的拷贝处理,对于有循环引用的数据进行递归操作。上面的函数可以直接应用在实际项目中。