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—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——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"}
- 数组的浅拷贝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
- 数组的浅拷贝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
- 对象的浅拷贝——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))