聊聊Javascript里的深浅拷贝

466 阅读5分钟

1. 从数据类型讲起

我们知道,JS里的数据类型分为原始类型引用类型两种

  • 对于原始类型来说:

    数据保存在栈内存中,当使用赋值表达式拷贝一个原始类型数据时,会在栈内存中添加一个相等的数据,然后将这条新数据赋给新的变量。说白了,原始类型的拷贝是按值拷贝的。

    	let a1 = 'sally'
        let a2 = a1
        console.log(a2) // sally
        a2 = 'cathy'
        console.log(a1,a2) // sally,cahty
    
  • 对于引用类型来说:

    数据本身保存在堆内存中,但数据的引用保存在栈内存中。当我们将一个引用类型赋给变量时,变量中保存的是栈内存中的引用,每次用到变量时,会根据这个引用,去堆内存中寻找真正的数据。而当我们试图用变量复制一个引用类型时,我们只是在栈内存中复制了一份引用,保存到新变量里。进而可知,旧变量和新变量里保存的引用的值是一样的,指向的是堆内存中同样的地址(同样的数据)。因此,当我们试图改变新变量里的数据时,旧变量里的数据也会发生改变,因为他们俩实质上指向的是同一个对象。

    	let obj1 = {a:'sally'}
        let obj2 = obj1 // 按引用拷贝
        obj2.a = 'cathy'
        console.log(obj1.a,obj2.a) // cathy,cathy
    

2.深浅拷贝的概念

有了以上对原始类型和引用类型的区分,以及他们在拷贝时的行为的区分,我们就可以正式开始聊聊深拷贝和浅拷贝了。

首先明确一点,不论深拷贝或浅拷贝,都是基于JavaScript里的对象(包括数组)来讨论的。有时我们在复制一个对象时,不想仅仅复制这个对象的引用,而是重新建立一个对象(引用地址不同),将原对象中的属性拷贝到新对象里。在拷贝对象的过程中,就有了深拷贝和浅拷贝的区分

浅拷贝

在拷贝对象的过程中,只对第一层键值进行独立的复制,如果属性仍然是个对象,则直接复制对象的引用

深拷贝

在拷贝对象的过程中,对每一层键值都进行独立复制,如果遇到属性仍然是个对象,则进入这个对象内部,将每个属性都一一复制出来。

3.实现浅拷贝的若干方法

相对于深拷贝需要我们自己手写代码实现,浅拷贝可以借由JavaScript提供的一些API巧妙实现

  1. 数组的浅拷贝1—concat

    concat方法的本意,是将两个数组合并,

    	let arr1 = [1,2]
        let arr2 = [3,4]
    	let arr = arr1.concat(arr2)
        console.log(arr) // [1,2,3,4]
    

    对数组1使用concat方法,传入数组2,得到数组1和数组2的合并数组。此方法不会更改现有数组,而是返回一个新数组。

    如果我们不给concat传arr2,将返回一个arr1的拷贝数组,并且这是一个浅拷贝

    	var arr = ['a', 'b', 'c'];
    		var arrCopy = arr.concat();
    		arrCopy[0] = 'test'
    		console.log(arr); // ["a", "b", "c"]
    		console.log(arrCopy); // ["test", "b", "c"]
    
    		var arrObj = [{a:10},{b:20}]
        var arrObjCopy = arrObj.concat()
        arrObjCopy[0].a = 0
        console.log(arrObj[0].a) // 0
        console.log(arrObjCopy[0].a) // 0
    
  2. 数组的浅拷贝2——slice

与concat同理,slice也可以浅拷贝一个数组


var arr1 = [{"name":"weifeng"},{"name":"boy"}];//原数组
   var arr2 = arr1.slice(0);//拷贝数组
   arr1[1].name="girl";
   console.log(arr1);// [{"name":"weifeng"},{"name":"girl"}]
   console.log(arr2);//[{"name":"weifeng"},{"name":"girl"}

  1. 数组的浅拷贝3——扩展运算符

ES6新增的扩展运算符...可以用于数组的浅拷贝

	var arr = [1,2,{a:3}]
   var arrCopy =[...arr]
   console.log(arrCopy) // [1,2,{a:3}]
   arrCopy[2].a=0
   console.log(arr[2].a) // 0
  1. 数组的浅拷贝4——Array.from() ES6给Array新增了一个静态方法from,可以用于将类数组和实现了iterator接口的数据类型转换为数组。如果向该方法传入一个数组,则将返回该数组的一个浅拷贝
	var arr = [1,2,{a:3}]
   var arrCopy = Array.from(arr)
   console.log(arrCopy) // [1,2,{a:3}]
   arrCopy[2].a=0
   console.log(arr[2].a) // 0
	
  1. 对象的浅拷贝——Object.assign()

该方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。

4. 浅拷贝的代码实现

我们可以自己实现一个shallowCopy方法,用来对数组和对象进行统一的浅拷贝


	function shallowCopy(obj){
    	if(typeof obj !== 'object') return
        let newObj = obj instanceof Array?[]:{}
        for(let k in obj){
        	if(obj.hasOwnProperty(k)){
            	newObj[k] = obj[k]
            }
        }
        return newObj
    }
    
    let arr = [1,2,3]
    let obj = {a:1,b:2}
    
    let arrNew = shallowCopy(arr)
    let objNew = shallowCopy(obj)

5. 深拷贝的代码实现

有了上面对浅拷贝的实现思路,我们可以进一步实现深拷贝。只要在每次对属性进行拷贝时,判断一下属性是否为引用类型typeof k ===object',如果是,就递归调用这个拷贝方法。


	function deepCopy(obj){
    	if(typeof obj !== 'object') return
        let newObj = obj instanceof Array?[]:{}
        for(let k in obj){
        	if(obj.hasOwnProperty(k)){
            	newObj[k] = typeof obj[k] === 'object'?deepCopy(obj[k]):obj[k]
            }
        }
        return newObj
    }
    
    let arr = [1,2,3]
    let obj = {a:1,b:2}
    
    let arrNew = deepCopy(arr)
    let objNew = deepCopy(obj)

6. 实现深拷贝的另一种简易方法

先对数据执行JSON.stringfy,再对结果执行JSON.parse,就可以得到原数据的一个深拷贝。美中不足,这种方法不能拷贝函数,所以项目中需要用到深拷贝,还是手写一个公共方法吧。

let obj = {a:{b:2}}

let objNew = JSON.parse(JSON,stringfy(obj))