背景 在日常开发中,偶尔会遇到需要复制对象的情况,需要进行对象的复制。
由于现在流行标题党,所以,一文带你了解js数据储存及深复制(深拷贝)与浅复制(浅拷贝)
理解 首先就需要理解 js 中的数据类型了 js 数据类型包含
基础类型
:String
、Number
、 null
、undefined
、Boolean
以及ES6
引入的Symbol
、es10
中的BigInt
引用类型
:Object
由于 js 对变量的储存是栈内存
、堆内存
完成的。
基础类型
将数据保存在栈内存
中
引用类型
将数据保存在堆内存
中
由于 js 在数据读取和写入的时候,对基础类型
是直接读写栈内存
中的数据,引用类型
是将一个内存地址保存在栈内存中,读写都是修改栈内存中指向堆内存的地址
以如下代码为例
1 2 3 4 5 6 7 8 9 let obj = { a:1, arr:[1,3,5,7,9], b:2, c:{ num:100 } } let num = 10
在内存中的表现为
我们声明个obj1
1 2 let obj1 = obj; console.log(obj1 == obj);//true
因为这个赋值,把内存变成了这样
然后,内存中只是给js栈内存新增了一个指向堆内存
的地址而已,这种就叫做浅复制
。因为如图可以看到,如果我们修改obj.a
的话,实际修改的是堆内存0x88888888
中的变量a
,由于obj1
也指向这个地址,所以obj1.a
也被修改了
深复制
是指,不单单复制引用地址,连堆内存都复制一遍,使obj
和obj1
不指向同一个地址。
代码 分开来看深复制
与浅复制
浅复制 由上述图可知,浅复制只是复制第一层,也就是,基本类型
复制新值,引用类型
复制引用地址
浅复制
可以使用的方案有循环赋值
、扩展运算符
、object.assign()
,
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 let obj = { a:1, arr:[1,3,5,7,9], b:2, c:{ num:100 } } function clone1(obj){ // 使用循环赋值 let b = {}; for(let key in obj){ b[key] = obj[key] } return b } function clone2(obj){ // 使用扩展运算符 let b = { ...obj }; return b } function clone3(obj){ // 使用object.assign() let b = {}; Object.assign(b,obj) return b } let obj1 = clone1(obj); let obj2 = clone2(obj); let obj3 = clone3(obj); console.log(obj1 === obj); //false 代表复制成功了 console.log(obj2 === obj); //false 代表复制成功了 console.log(obj3 === obj); //false 代表复制成功了 console.log('obj0.c.num修改前',obj.c.num); //100 console.log('obj1.c.num修改前',obj1.c.num); //100 console.log('obj2.c.num修改前',obj2.c.num); //100 console.log('obj3.c.num修改前',obj3.c.num); //100 obj0.c.num = 555; console.log('obj0.c.num修改后',obj.c.num); //555 console.log('obj1.c.num修改后',obj1.c.num); //555 console.log('obj2.c.num修改后',obj2.c.num); //555 console.log('obj3.c.num修改后',obj3.c.num); //555
由于是浅复制,所以引用类型只是复制了内存地址,修改其中一个对象的子属性后,引用这个地址的值都会被修改。
浅克隆图解如下
深复制 由于浅复制只是复制第一层,为了解决引用类型的复制,需要使用深复制来完成对象的复制,基本类型
复制新值,引用类型
开辟新的堆内存
。
深复制
可以使用的方案有JSON.parse(JSON.stringify(obj))
、循环赋值
。
JSON.parse(JSON.stringify(obj)) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 let obj = { a:1, arr:[1,3,5,7,9], c:{ num:100 }, fn:function(){ console.log(1) }, date:new Date(), reg:/\.*/g } function clone1(obj){ // 使用JSON.parse(JSON.stringify(obj)) return JSON.parse(JSON.stringify(obj)) } let obj1 = clone1(obj); console.log(obj === obj1); //false 代表复制成功了 obj.c.num = 555; console.log(obj.c.num,obj1.c.num) // 555,100
看起来是复制成功了!!~地址也变了,修改obj
,obj1
的引用地址不会跟着变化。
但是我们来console
一下obj
以及obj1
1 2 console.log(obj) console.log(obj1)
似乎发现了离奇的事情,只有obj.a
以及obj.c
正确的复制了,日期类型
、方法
、正则表达式
均没有复制成功,发生了一些奇怪的事情
循环赋值 deepClone 那么为了解决这种事情,就需要写一个deepClone
方法来完成深复制了,参考了许多开源库的写法,将所有的复制项单独拆出,方便未来对特殊类型进行扩展,也防止不同功能间的变量互相干扰
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 //既然是深复制,一定要传入一个object,再return 一个新的 Object function deepClone(obj){ let newObj; if(obj instanceof Array){ // 数组的话,要new一个数组 newObj = [] }else if(obj instanceof Object){ // 对象的话,要new一个对象 newObj = {} } if(obj === null) { return cloneNull(obj) } if(typeof obj=='function'){ return cloneFunction(obj) } if(typeof obj!='object') { return cloneOther(obj) } if(obj instanceof RegExp) { return cloneRegExp(obj) } if(obj instanceof Date){ return cloneDate(obj) } if(obj instanceof Array){ for(let index in obj){ newObj[index] = deepClone(obj[index]); // 对数组子项进行复制 } } if(obj instanceof Object){ for(let key in obj){ newObj[key] = deepClone(obj[key]); // 对对象子项进行复制 } } return newObj; } function cloneNull(obj){ // 复制NULL return obj } function cloneFunction(obj){ // 复制方法, //这个方法待完善,暂时未找到能够完美复制function的方案,如果有方案,望指出 return obj } function cloneOther(obj){ // 复制非对象的数据 return obj } function cloneRegExp(obj){ // 复制正则对象 return new RegExp(obj) } function cloneDate(obj){ // 复制日期对象 return new Date(obj) }
这样一个基本上满足功能的深复制就完成了。先测试一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 let obj = { a:1, arr:[1,3,5,7,9], c:{ num:100 }, fn:function(){ console.log(1) }, date:new Date(), reg:/\.*/g } let obj1 = deepClone(obj); console.log(obj.c === obj1.c); // false 代表复制成功 console.log(obj.fn === obj1.fn);// true 由于方法单纯修改了引用的地址,所以这里是浅复制 console.log(obj.date === obj1.date);// false 代表复制成功 console.log(obj.reg === obj1.reg);// false 代表复制成功
再console
一下
1 2 console.log(obj) console.log(obj1)
这样,就完成了deepClone
深复制方法
经过深复制后,图解如下
优化 deepClone 上述代码还有优化空间,参考了lodash
库,在进行 new 对象时,可以使用 constructor
构造函数 来进行创建新的实例,这样
可以不用判断递归中,是数组还是对象
如果深复制的某一项是某个原型的实例,深复制完成后,依然是该原型的实例
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 function deepClone(obj){ let newObj = new obj.constructor; if(obj === null) { return cloneNull(obj) } if(typeof obj=='function'){ return cloneFunction(obj) } if(typeof obj!='object') { return cloneOther(obj) } if(obj instanceof RegExp) { return cloneRegExp(obj) } if(obj instanceof Date){ return cloneDate(obj) } if(obj instanceof Array){ for(let index in obj){ newObj[index] = deepClone(obj[index]); // 对数组子项进行复制 } } if(obj instanceof Object){ for(let key in obj){ newObj[key] = deepClone(obj[key]); // 对对象子项进行复制 } } return newObj; } function cloneNull(obj){ // 复制NULL return obj } function cloneFunction(obj){ // 复制方法, //这个方法待完善,暂时未找到能够完美复制function的方案,如果有方案,望指出 return obj } function cloneOther(obj){ // 复制非对象的数据 return obj } function cloneRegExp(obj){ // 复制正则对象 return new RegExp(obj) } function cloneDate(obj){ // 复制日期对象 return new Date(obj) }
最终版本 deepClone 然后可以有一个合并版本的,比较节省代码,将下方区分开的复制方法,合并到deepClone
中,可以极大地减少代码体积
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function deepClone(obj){ // let newObj = new obj.constructor; if(obj === null) return obj // if(typeof obj=='function') return obj // 由于typeof obj=='function'也符合下方的typeof obj!='object',所以此条可以省略 if(typeof obj!='object') return obj if(obj instanceof RegExp) return new RegExp(obj) if(obj instanceof Date) return new Date(obj) // 运行到这里,基本上只存在数组和对象两种类型了 for(let index in obj){ newObj[index] = deepClone(obj[index]); // 对子项进行递归复制 } return newObj; }