JS浅拷贝和深拷贝的小整理

这个话题应该是说大也大,说不大也不大。我就简单给自己总结整理一下。记得16届春招网易校招的最后一个笔试题,实现一个深拷贝。

说这个话题之前,先扯一下JS的基本数据类型。

大家都知道JS中的数据类型分为

  • 基本类型:string, number, boolean, null, undefined

  • 引用类型:Object,特殊的有Array, Function, Date, Math, RegExp, Error等

那什么是引用类型呢?这又得扯上JS的内存机制了。
JS的内存跟其他的内存差不多,分为堆(heap)和栈(stack)。

  • 栈:由系统自动分配,自动回收,效率高,但容量小。

  • 堆:由程序员手动分配内存,并且手动销毁(高级语言如JS中有垃圾自动回收机制),效率不如栈,但容量大。

请注意区分数据结构中所说的堆栈和内存中的堆栈是两回事。关于JS的垃圾自动回收机制,我会另写一篇小总结。

JS的基本类型分配在栈中,而因为引用类型大小的不固定,系统将存储该引用类型的地址存在栈中,并赋值给变量本身,而具体的内容存在堆中。所以当访问一个对象的时候,先访问栈中它的地址,然后按照这个地址去堆中找到它的实际内容。

所以当复制的时候,对于基本类型的变量,系统会为新的变量在栈中开辟一个新的空间,赋予相同的值,然后这两个变量就各自独立,毫无牵连。而对于引用类型的变量,新的变量复制的是那个对象在堆中的地址,这两个变量指向的是同一个对象。

1
2
3
4
5
6
7
8
9
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不能复制正则表达式,其实我想知道一般正则表达式是怎么复制的。知道的可以回复一下。

参考资料: