一文搞懂浅拷贝、深拷贝

138 阅读4分钟

通俗的讲,浅拷贝就是只拷贝一层,改变拷贝后的内容会影响到拷贝前的,而深拷贝是层层拷贝,拷贝前后的内容互不影响,在内存中有各自的地址

先看浅拷贝

浅拷贝

let obj = {
	name: '小明',
    address: {
    	x: 100,
        y: 90
    }
}
let newObj = {...obj}
obj.address.x = 200
console.log(obj) // {name: '小明', adress: {x: 200,y:90}}
console.log(newObj) // {name: '小明', adress: {x: 200,y:90}}

...扩展运算符只能拷贝一层

let a = [1,2,3,4,5]
let arr = [a]
let newArr = arr.slice()
newArr[0][0] = 100
console.log(arr) // [100, 2, 3, 4, 5]
console.log(newArr) // [100, 2, 3, 4, 5]

slice也是浅拷贝

我们来实现一个自己的浅拷贝

function shallowClone(obj) {
	let newObj = {}
    for (let key in obj) {
    	// 判断是否是实例上的属性
    	if (obj.hasOwnProperty(key)) {
        	newObj[key] = obj[key]
        }
    }
    return newObj
}

深拷贝

let obj = {
	name: '小明',
    address: {
    	x: 100,
        y: 90
    }
}
let o = JSON.parse(JSON.stringify(obj))
obj.address.x = 200
console.log(obj) // {name: '小明', adress: {x: 200,y:90}}
console.log(o) // {name: '小明', adress: {x: 100,y:90}}

这种方式可以实现常见的深拷贝,但是一些特殊情况就不行了 比如:

let obj = {
	name: '小明',
    address: {
    	x: 100,
        y: 90
    },
    fn:function(){},
    un: undefined
}
let o = JSON.parse(JSON.stringify(obj))
obj.address.x = 200
console.log(obj) // {name: '小明', adress: {x: 200,y:90}, fn:function(){}, un: undefined}
console.log(o) // {name: '小明', adress: {x: 100,y:90}}

可以看到,函数和undefined在拷贝过程中都丢失了

接下来实现一个自己的深拷贝,根据上面实现的浅拷贝,很容易就想到深拷贝需要使用递归来实现,但是需要考虑一些特殊情况:

  • 如果是null 或者 undefined ,就直接返回
if (obj == nullreturn obj
  • 如果是普通类型,也直接返回
if (typeof obj !== object) return obj
  • 如果是日期
if (obj instanceof Date) return new Date(obj)
  • 如果是正则
if (obj instanceof RegExp) return new RegExp(obj)

剩下的则是引用类型的的 对象或者数组

接下来来实现第一版

function deepClone(obj) {
	if (obj == null) return obj
    if (typeof obj !== "object") return obj
    if (obj instanceof Date) return new Date(obj)
    if (obj instanceof RegExp) return new RegExp(obj)
    // 剩下的只可能是数组或对象
    let cloneObj = Object.prototype.toString.call(obj) == "[object Array]" ? [] : {}
    for (let key in obj) {
    	if (obj.hasOwnProperty(key)) {
        	// 因为属性也可能是引用类型,所以需要递归
        	cloneObj[key] = deepClone(obj[key])
        }
    }
    return cloneObj
}
let obj = {
	name: '小明',
    address: {
    	x: 100,
        y: 90
    },
    fn:function(){},
    un: undefined
}
let newObj = deepClone(obj)
obj.address.x = 200
console.log(obj) // {name: '小明', adress: {x: 200,y:90}, fn:function(){}, un: undefined}
console.log(newObj) // {name: '小明', adress: {x: 100,y:90}, fn:function(){}, un: undefined}

到这里实现了我们想要的深拷贝功能,

还可以做一点小优化,上面我们判断类型是对象还是数组的时候用了 Object.prototype.toString,

下面我们换一种方式实现: 任何实例上都有一个constructor属性,也就是它的构造函数

console.log([1,2,3].constructor) // ƒ Array() { [native code] }
console.log({a:1}.constructor) // ƒ Object() { [native code] 

可以直接通过:

let cloneObj = new obj.constructor // 如果obj为对象,则cloneObj = {}; 如果obj为数组,则cloneObj = []

第二版优化完整版

function deepClone(obj) {
	if (obj == null) return obj
    if (typeof obj !== "object") return obj
    if (obj instanceof Date) return new Date(obj)
    if (obj instanceof RegExp) return new RegExp(obj)
    // 剩下的只可能是数组或对象
    let cloneObj = new obj.constructor
    for (let key in obj) {
    	if (obj.hasOwnProperty(key)) {
        	// 因为属性也可能是引用类型,所以需要递归
        	cloneObj[key] = deepClone(obj[key])
        }
    }
    return cloneObj
}
let obj = {
	name: '小明',
    address: {
    	x: 100,
        y: 90
    },
    fn:function(){},
    un: undefined
}
let newObj = deepClone(obj)
obj.address.x = 200
console.log(obj) // {name: '小明', adress: {x: 200,y:90}, fn:function(){}, un: undefined}
console.log(newObj) // {name: '小明', adress: {x: 100,y:90}, fn:function(){}, un: undefined}

还有一种特殊情况需要考虑进去,就是对象可能存在循环引用的情况,比如:

let obj = {
	name: '小明',
    address: {
    	x: 100,
        y: 90
    },
    fn:function(){},
    un: undefined
}
obj.o = obj

循环引用的对象如果采用递归克隆的话会出现爆栈,因此需要过滤掉已经遍历过的对象 采用weakMap数据结构,weakMap 的键只能是对象

第三版优化完整版

function deepClone(obj, hash = new WeakMap()) {
	if (obj == null) return obj
    if (typeof obj !== "object") return obj
    if (obj instanceof Date) return new Date(obj)
    if (obj instanceof RegExp) return new RegExp(obj)
    // 剩下的只可能是数组或对象
    // 如果已经存入了hash中,说明已经遍历过,直接return
    if (hash.get(obj)) return hash.get(obj)
    let cloneObj = new obj.constructor
    // 已经遍历过的对象存入weakMap
    hash.set(obj, cloneObj)
    for (let key in obj) {
    	if (obj.hasOwnProperty(key)) {
        	// 因为属性也可能是引用类型,所以需要递归
        	cloneObj[key] = deepClone(obj[key], hash)
        }
    }
    return cloneObj
}
let obj = {
	name: '小明',
    address: {
    	x: 100,
        y: 90
    },
    fn:function(){},
    un: undefined
}
let newObj = deepClone(obj)
obj.address.x = 200
console.log(obj) // {name: '小明', adress: {x: 200,y:90}, fn:function(){}, un: undefined}
console.log(newObj) // {name: '小明', adress: {x: 100,y:90}, fn:function(){}, un: undefined}

总结:

  • 浅拷贝只遍历一层,多层引用类型的数据,改变拷贝前的值会影响到拷贝后的值
  • 深拷贝是层层遍历,拷贝前后的数据互不影响
  • ...扩展运算符和slice都属于浅拷贝,如果要实现自定义的浅拷贝,只需做一层遍历即可,但是要通过hasOwnProperty过滤掉原型上的属性
  • 实现深拷贝需要在遍历的基础上加上递归,需要考虑日期、正则等特殊类型以及循环引用的特殊情况

最后推荐一篇超详细的文章:如何写出一个惊艳面试官的深拷贝?