# 来来来,深拷贝和浅拷贝了解一下
javascript中的数据类型分为
基本类型
和引用类型
.我们平时所说的深浅拷贝都是针对引用类型而言的.对于基本类型,没有深浅拷贝的说法
# 浅拷贝
当我们在对象拷贝时,如果属性(假设为key)是对象(包括数组),这时我们传递的是一个地址(该地址指向key对象),即两个对象的属性值是同一个地址,指向了同一块内存空间.当通过其中一个对象去影响了这块内存空间的内容,相同的变化也会反映在另外一个对象上
# 复制引用
let obj = {
a:1,
b:{
c:2
}
}
// 浅拷贝实现函数
function shallowCopy(obj){
if(typeof obj !== 'object'){
return false
}
let o = Array.isArray(obj) ? [] : {}
for(let p in obj){
if(obj.hasOwnProperty(p)){
o[p] = obj[p]
}
}
return o
}
// 使用浅拷贝函数生成一个新的对象
let obj2 = shallowCopy(obj)
// a的属性值是基本类型,我们先来改变它
obj2.a = 2
console.log(obj.a) // 1
console.log(obj2.a) // 2
// 发现`obj2.a`的修改并不会影响到`obj.a`
console.log(obj.b.c) // 2
console.log(obj2.b.c) // 2
// 再来修改obj2.b的内容,这是一个对象
obj2.b.c = 3
console.log(obj.b.c) // 3
console.log(obj2.b.c) // 3
// 发现`obj2.b.c`的修改同步影响到了`obj.b.c`.这是因为`obj.b`和`obj2.b`指向的是同一块内存空间.相当于一个有着两扇门的房间,不管从哪头门进去,最终进去的都是同一个房间
// 我们再次修改`obj2.b`的内容,和上次的修改不太一样,这次我们重新赋值一个新的对象给它
obj2.b = {d:1}
console.log(obj.b) // {c: 3}
console.log(obj2.b) // {d: 1}
// 从上面的结果可以看出来,此时的`obj.b`与`obj2.b`之间已经不存在关联了,这是因为 obj2.b = {d:1} 这步操作赋值使得`obj2.b`和`obj.b`不再指向同一内存地址.相当于新造了一个房子,并且将连接原先房间的其中一头门拆下来,重新安装到这个新房子上
# Object.assign()
我们还可以使用Object.assign
方法来实现函数的浅拷贝
let obj = {
a:1,
b:{
c:2
},
fn:function(){}
}
let obj2 = Object.assign({},obj)
console.log(obj) // {a: 1, b: {…}, fn: ƒ}
console.log(obj2) // {a: 1, b: {…}, fn: ƒ}
obj.b.c = 5
console.log(obj.b.c) // 5
console.log(obj2.b.c ) // 5
可以看出改变了obj.b.c
的值,obj2.b.c
的值也跟着变了
# concat()和slice()
这两个是数组的方法,都会返回一个新的数组,可以用来实现数组的浅拷贝.
let arr = [1,2,{a:1}]
let arr2 = arr.concat() // 或 let arr2 = arr.slice()
// 改变基本类型的值,不影响另外一个
arr[0] = 11
console.log(arr[0]) // 11
console.log(arr2[0]) // 1
// 改变了引用类型的值,会同步影响另外一个
arr[2].a = 2
console.log(arr[2].a) // 2
console.log(arr2[2].a) // 2
# 深拷贝(deepCopy)
深拷贝是一个与浅拷贝相对的概念,深拷贝就是要完完全全的拷贝对象了.实现深拷贝的核心是对于基本类型,直接赋值,对于引用类型,再次遍历,递归赋值
# JSON对象方法
对一个对象先用JSON.stringify
方法对其序列化,然后再用JSON.parse
方法反序列化,也能实现一定程度上的深拷贝.但是对象里面的函数会被过滤掉
let obj = {
a:1,
b:{c:2},
fn:function(){}
}
let obj2 = JSON.parse(JSON.stringify(obj))
console.log(obj2) // {a: 1, b: {c: 2}}
我们发现obj2
中并没有包含fn
,因为它在 JSON.stringify 这一步的时候就已经被过滤掉了.
我们在上面的代码后面再添加几句,如下
obj2.b.c = 3
console.log(obj.b.c) // 2
console.log(obj2.b.c) // 3
我们改变了obj2
中属性值为对象的里面的内容,并没有影响到obj
还有一点就是,经过了JSON.parse(JSON.stringify)
这么两级反转一波以后,新对象丢失了原来的constructor
,其新的constructor
都变成了Object
function Person(name){
this.name = name
}
let p = new Person('zhangsan')
let p2 = JSON.parse(JSON.stringify(p))
console.log(p.constructor === Person) // true
console.log(p2.constructor === Object) // true
# 递归实现
下面我们使用递归的方式来实现一个深拷贝函数
let obj = {
a:1,
b:{
c:2
},
fn:function(){}
}
// 深拷贝实现函数
function deepCopy(obj){
if(typeof obj !== 'object'){
return false
}
let o = Array.isArray(obj) ? [] : {}
if(obj && typeof obj === 'object'){
// for...in循环会遍历原型链上的属性
for(let p in obj){
// hasOwnProperty()用于判断一个对象自身是否具有指定的属性,并不包括原型链上的属性
if(obj.hasOwnProperty(p)){
if(obj[p] && typeof obj[p] === 'object'){
o[p] = deepCopy(obj[p])
}else{
o[p] = obj[p]
}
}
}
}
return o
}
let obj2 = deepCopy(obj)
console.log(obj) // {a: 1, b: {…}, fn: ƒ}
console.log(obj2) // {a: 1, b: {…}, fn: ƒ}
// 可以看出对象中的函数也被拷贝过来了
obj2.b.c = 3
console.log(obj2.b.c) // 3
console.log(obj.b.c) // 2
// 修改`obj2.b.c`并没有影响到`obj.b.c`
# 对象循环引用怎么办?
对象循环引用导致自己形成一个闭环,如下
let obj = {}
obj.obj = obj
let obj2 = deepCopy(obj) // Maximum call stack size exceeded
假如我们的原对象是一个循环引用的对象,那么当我们用上面那个深拷贝函数进行拷贝的时候是会报红的
我们引入WeakMap
结构来存储已经被拷贝的对象,每次拷贝前先查询该对象是否已经被拷贝,若已经被拷贝则取出对象返回,修改后的代码如下:
function deepCopy(obj, hash = new WeakMap()){
if(typeof obj !== 'object'){
return false
}
if(hash.has(obj)) return hash.get(obj)
let o = Array.isArray(obj) ? [] : {}
if(obj && typeof obj === 'object'){
for(let p in obj){
if(obj.hasOwnProperty(p)){
hash.set(obj,o)
if(obj[p] && typeof obj[p] === 'object'){
o[p] = deepCopy(obj[p], hash)
}else{
o[p] = obj[p]
}
}
}
}
return o
}
# 有特殊对象怎么办?
let obj = {
d:new Date(),
r:/\w/g
}
let obj2 = deepCopy(obj)
console.log(obj)
console.log(obj2)
发现在obj2
中,d和r两个属性值都变成了{}
空对象,说明此时的函数还无法拷贝某些特殊类型的对象,如Date
,RegExp
等.这时候就需要使用 结构化拷贝 了(constructor)
let obj = {
d:new Date(),
r:/\w/g
}
function deepCopy(obj, hash = new WeakMap()){
if(typeof obj !== 'object'){
return false
}
if(hash.has(obj)) return hash.get(obj)
let o = Array.isArray(obj) ? [] : {}
let C = obj.constructor
if(obj && typeof obj === 'object'){
switch(C){
case Date:
o = new C(obj)
break;
case RegExp:
o = new C(obj)
break;
default:
}
for(let p in obj){
if(obj.hasOwnProperty(p)){
hash.set(obj,o)
if(obj[p] && typeof obj[p] === 'object'){
o[p] = deepCopy(obj[p], hash)
}else{
o[p] = obj[p]
}
}
}
}
return o
}
let obj2 = deepCopy(obj)
console.log(obj2) // 此时d和r两个属性都保留了原来的值
上面的函数中只列举了Date
和RegExp
这两种类型,若有其他类型需求的,可以自行添加上去
总结: 关于深浅拷贝的基本情况我们差不多已经了解了,但其实在深浅拷贝这个问题上还是有蛮多坑需要我们去踩的.在日常开发中,大家可以自行选择合适的拷贝方式,并不一定要用最全的那个.
← EventTarget介绍 原型和原型链 →