js的对象拷贝总结

总结一下 JavaScript 中对象拷贝的问题。

先说 JavaScript 中的数据类型

JavaScript 是一种弱类型的编程语言,所谓弱类型就是在变量定义的时候不需要指定其变量的类型,变量的类型会在变量赋值时自动做判断。

JavaScript 中的数据类型分为两大类:值类型(又叫:原始数据类型)以及引用类型。

值类型

所谓值类型,简单的说,就是申请的变量(栈)内存中直接存了该变量的值,值类型包括以下几种:undefined、null、boolean、number、string、symbol。

1
2
3
4
5
6
// 值类型
var a = 10
var b = a
b = 20
console.log(a) // 10
console.log(b) // 20
引用类型

所谓引用类型,就是说,变量中存的并不是变量本身,而是一个指向某个(堆)内存的地址,该堆内存中放的是变量引用的内容。显而易见,程序中可以定义多个变量来引用该内容,他们指向的是同一个内容,如果其中一个修改了引用的内容,其他变量也会受到影响(这可能会导致一些异常)。

JavaScript 中的引用类型为 Object 类,包含 Function, Array, Date 等内置对象。

1
2
3
4
5
6
7
// 引用类型
var a = {x: 10, y: 20}
var b = a
b.x = 100
b.y = 200
console.log(a) // {x: 100, y: 200}
console.log(b) // {x: 100, y: 200}
1
2
3
4
5
6
7
8
9
10
11
var obj = {
a: 1,
b: [1,2,3]
}
var a = obj.a
var b = obj.b
a = 2
b.push(4)
console.log(obj) //{a: 1, b: Array(4)}; obj是引用类型,但是obj.a是值类型,所以 obj.a 的值不会受到 a 值的影响
console.log(a) // 2 a是值类型
console.log(b) // [1, 2, 3, 4] obj.b 和 b 是引用类型,所以 b 值的改变反应在了 obj.b 上

对象拷贝(克隆/复制)

对象的拷贝分可以简单的分为浅拷贝和深拷贝。简单的说,浅拷贝只拷贝对象的引用,对象在内存中还只是那一个;而深拷贝则拷贝了对象的实例,在内存中开辟了一个新的空间。

从上面介绍的 JavaScript 数据类型中可以知道很清楚的看到,对一个对象而言,浅拷贝只要遍历一遍对象第一层的属性,然后赋值给一个新的对象即可,或者再简单一点,直接将源对象赋值给目标对象。而,要实现深拷贝,就要将对象进行递归遍历,直到最后一层的属性值是原始数据类型。

浅拷贝实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function shallowClone(source){
if(source === null || typeof source !== "object"){
return source;
}
//判断是数组还是非数组
var target = Array.isArray(source) ? [] : {};
for(var key in source){
if(source.hasOwnProperty(key)){//自有属性,不关注原型链上的属性
target[key] = source[key];
}
}
return target;
}

//测试
var obj = {a:1,b:{c:2,d:3}};
var res = shallowClone(obj);
console.log(res); //{a:1,b:{c:2,d:3}}
res.b.c = 5;
console.log(obj);//{a:1,b:{c:5,d:3}} 浅拷贝只拷贝引用
深拷贝实现

深拷贝的实现,只要在浅拷贝的基础上加上递归判断,如果对象的属性值仍然是个对象,那就递归调用拷贝方法,知道对象的属性是一个原始数据类型。

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
function deepClone(source){
if(source === null || typeof source !== "object"){
return source;
}
//判断是数组还是非数组
var target = Array.isArray(source) ? [] : {};
for(var key in source){
if(source.hasOwnProperty(key)){//自有属性,不关注原型链上的属性
//如果对象的属性值依然是对象,则递归拷贝
if(source[key] && typeof source[key] === "object"){
target[key] = deepClone(source[key]);
}else{
//如果不是,直接赋值
target[key] = source[key];
}
}
}
return target;
}

//测试
var obj = {a:1,b:{c:2,d:3}};
var res = deepClone(obj);
console.log(res); //{a:1,b:{c:2,d:3}}
res.b.c = 5;
console.log(obj);//{a:1,b:{c:2,d:3}} 深拷贝,源和目标相互不影响
对象序列化实现深拷贝
1
var target = JSON.parse(JSON.stringify(source));

对象序列化之后得到一个字符串,而字符串本身是原始数据类型,传值的,所以,再将字符串 parse 成一个新的对象就可以实现对象的深拷贝。需要注意的是,这种方法虽然简单,但是需要保证源对象(source)是 JSON 安全的,只适用于部分情况。如果,对象中包含方法,则方法无法完成序列化,就无法将方法深拷贝。

一些题外话

判断数据类型的方法

typeof

typeof 的值有以下几种:undefined boolean number string object function、symbol。需要注意的是:

  • typeof null === “object”, 但是 null 是原始数据类型,并非对象。
  • typeof [1,2] === “object”, 引用类型中,除了 function 之外,其他的 typeof 都是 object。 typeof function(){} === “function”。
  • typeof Symbol() === “symbol”, ES6 新增。
instanceof

实例和构造函数之间的对应。例如判断一个变量是否是数组,使用typeof无法判断,但可以使用[1, 2] instanceof Array来判断。因为,[1, 2]是数组,它的构造函数就是Array。

toString

var type = Object.prototype.toString.call(obj);

obj类型 type的值
Array “[object Array]”
Function “[object Function]”
RegExp “[object RegExp]”
Date “[object Date]”
Number “[object Number]”
String “[object String]”
Boolean “[object Boolean]”
null “[object Null]”
undefined “[object Undefined]”
Object/对象字面量 “[object Object]”
Loading comments box needs to over the wall