这个话题应该是说大也大,说不大也不大。我就简单给自己总结整理一下。记得16届春招网易校招的最后一个笔试题,实现一个深拷贝。
说这个话题之前,先扯一下JS的基本数据类型。
大家都知道JS中的数据类型分为
-
基本类型:string, number, boolean, null, undefined
-
引用类型:Object,特殊的有Array, Function, Date, Math, RegExp, Error等
那什么是引用类型呢?这又得扯上JS的内存机制了。
JS的内存跟其他的内存差不多,分为堆(heap)和栈(stack)。
-
栈:由系统自动分配,自动回收,效率高,但容量小。
-
堆:由程序员手动分配内存,并且手动销毁(高级语言如JS中有垃圾自动回收机制),效率不如栈,但容量大。
请注意区分数据结构中所说的堆栈和内存中的堆栈是两回事。关于JS的垃圾自动回收机制,我会另写一篇小总结。
JS的基本类型分配在栈中,而因为引用类型大小的不固定,系统将存储该引用类型的地址存在栈中,并赋值给变量本身,而具体的内容存在堆中。所以当访问一个对象的时候,先访问栈中它的地址,然后按照这个地址去堆中找到它的实际内容。
所以当复制的时候,对于基本类型的变量,系统会为新的变量在栈中开辟一个新的空间,赋予相同的值,然后这两个变量就各自独立,毫无牵连。而对于引用类型的变量,新的变量复制的是那个对象在堆中的地址,这两个变量指向的是同一个对象。
var obj = {name: 'Yecao'};
var obj2 = obj;
console.log(obj2.name); // 'Yecao'
obj2.name = 'Claire';
console.log(obj.name); //'Claire'
以上是个很简单的例子,obj2复制了obj之后,两个其实指向的是同一个对象。
好了,终于要说到我们的主题了,浅拷贝和深拷贝。最最简单的拷贝,就是将一个变量复制给另个变量,比如上文中的var obj2 = obj。 JS的基本类型不存在浅拷贝还是深拷贝的问题,主要是针对于对象,比如一般的对象,和特殊的数组对象。
浅拷贝是指复制对象的时候,指对第一层键值对进行独立的复制。一个简单的实现如下:
// 浅拷贝实现
function shallowCopy(target, source){
if( !source || typeof source !== 'object'){
return;
}
// 这个方法有点小trick,target一定得事先定义好,不然就不能改变实参了。
// 具体原因解释可以看参考资料中 JS是值传递还是引用传递
if( !target || typeof target !== 'object'){
return;
}
// 这边最好区别一下对象和数组的复制
for(var key in source){
if(source.hasOwnProperty(key)){
target[key] = source[key];
}
}
}
我们分别对对象和数组进行测试,发现成功地复制了。
//测试例子
var arr = [1,2,3];
var arr2 = [];
shallowCopy(arr2, arr);
console.log(arr2);
//[1,2,3]
var today = {
weather: 'Sunny',
date: {
week: 'Wed'
}
}
var tomorrow = {};
shallowCopy(tomorrow, today);
console.log(tomorrow);
// Object {weather: "Sunny", date: Object}
我们来看看浅拷贝会带来什么问题?对于date这个属性,tomorrow变量复制的是它在堆中的地址,这也导致了today和tomorrow的date其实是指向堆中的同一个对象。
tomorrow.weather = 'Cloudy';
tomorrow.date.week = 'Thurs';
console.log(today.weather); // 'Sunny'
console.log(today.date.week); //'Thurs'
那如何完全独立地拷贝出一份呢?这也就是所谓的深拷贝。想想,其实只要递归下去,把那些属性的值仍然是对象的再次进入对象内部一一进行复制即可。
//深拷贝实现
function deepCopy(target, source){
if( !source || typeof source !== 'object'){
return;
}
// 这个方法有点小trick,target一定得事先定义好,不然就不能改变实参了。
// 具体原因解释可以看参考资料中 JS是值传递还是引用传递
if( !target || typeof source !== 'object'){
return;
}
for(var key in source){
if(source.hasOwnProperty(key)){
if( source[key] && typeof source[key] == 'object'){
target[key] = Array.isArray(source[key])?[]:{};
deepCopy(target[key], source[key]);
}
else{
target[key] = source[key];
}
}
}
}
注意:Array方法的slice,concat其实是一种浅复制。
深复制还可以利用JSON的方式。 JSON.parse(JSON.stringify(obj));
对于一般的需求是可以满足的,但是它有缺点。下例中,可以看到JSON复制会忽略掉值为undefined以及函数表达式。
var obj = {
a: 1,
b: 2,
c: undefined,
sum: function() { return a + b; }
};
var obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2);
//Object {a: 1, b: 2}
还有说,JSON不能复制正则表达式,其实我想知道一般正则表达式是怎么复制的。知道的可以回复一下。
参考资料:
-
http://www.cnblogs.com/ys-ys/p/5300189.htmlJavaScript 中对内存的一些了解
-
https://www.zhihu.com/question/27114726javascript传递参数如果是object的话,是按值传递还是按引用传递?
-
http://bosn.me/js/js-call-by-sharing/ JS是按值传递还是按引用传递?
-
http://www.cleey.com/blog/single/id/776.html堆和栈的区别之数据结构和内存
-
http://www.imooc.com/article/11253 也来谈一谈js的浅复制和深复制
-
https://github.com/sofish/everyday/blob/master/writings/javascript-type.md JavaScript 类型