本系列面试题旨在学会相关知识点,从而轻松应对面试题的各种形式,本文讲解了 JavaScript
中拷贝的相关知识,以及如何手写深浅拷贝。
感觉有帮助的小伙伴请点赞👍鼓励一下 ~
什么是拷贝
拷贝其实就是复制,很多场景需要我们复制一份数据出来,然后对复制后的数据进行操作,可能要求不影响原数据,也可能会要求和原数据产生一些联动。所以根据深拷贝和浅拷贝的功能,就可以满足上述两种要求。
值类型的拷贝
值类型其实没有深浅拷贝之分,亦可以说值类型都是深拷贝。因为值类型拷贝后的值,不会跟原数据产生任何联动,修改拷贝后的值,原数据不会产生任何变化。
let a = 1
let b = a
b = 2
console.log(a) // => 1
console.log(b) // => 2
浅拷贝
重新在堆中创建内存,拷贝前后的基本类型互不影响,拷贝前后的引用类型还是会共享同一块内存,故而会相互影响。
先写一个例子来看一下:
// 定义一个浅拷贝函数
function shallowCopy(obj) {
const cloneObj = {}
for (let i in obj) {
if (obj.hasOwnProperty(i)) {
cloneObj[i] = obj[i]
}
}
return cloneObj
}
// 声明person
let person = {
name: "张三",
hobbies: ["吃饭", "睡觉", "打豆豆"]
}
// 对person进行浅拷贝得到person1
let person1 = shallowCopy(person)
//修改值类型
person1.name = '李四'
//修改引用类型
person1.hobbies[0] = '美女'
console.log(person);
console.log(person1);
可以看到我们修改 person1
的值类型属性 name
,并没有影响到 person
的 name
,但是我们修改引用类型 hobbies
的时候,person
也随着 person1
改变了,这就是浅拷贝。
除了上面这一种,浅拷贝的实现方式还有 Object.assign()
, 展开运算符...
, array.slice()
array.concat()
。
深拷贝
从堆内存中开辟一块新的区域存放新对象,对原始对象的所有属性进行递归拷贝,对所有的引用类型的属性同样开辟新区域,修改新对象不会影响原对象。
从上面的浅拷贝的例子中可以看出,person
的 hobbies
虽然是个引用类型,但是 hobbies
的每一个元素都是一个字符串,也就是值类型,所以我们只要再次对 hobbies
进行浅拷贝,那么 hobbies
也就会互不影响了。
所以我们可以得出一个结论,只要对一个对象无限递归进行浅拷贝,最终的结果就是一个深拷贝。
递归浅拷贝
代码如下,要考虑到种种特殊情况。
// 定义一个深拷贝函数
function deepClone(obj) {
const cloneObj = new obj.constructor()
if (obj === null) return obj
if (obj instanceof Date) return new Date(obj)
if (obj instanceof RegExp) return new RegExp(obj)
if (typeof obj !== 'object') return obj
for (let i in obj) {
if (obj.hasOwnProperty(i)) {
cloneObj[i] = deepClone(obj[i])
}
}
return cloneObj
}
// 声明person
let person = {
name: "张三",
hobbies: ["吃饭", "睡觉", "打豆豆"]
}
// 对person进行浅拷贝得到person1
let person1 = deepClone(person)
//修改值类型
person1.name = '李四'
//修改引用类型
person1.hobbies[0] = '美女'
console.log(person);
console.log(person1);
结果如下,现在只有李四喜欢美女了。说明我们的深拷贝就成功了。
JSON.parse(JSON.stringify())
除了上面递归浅拷贝的方式来实现深拷贝之外,还可以使用 JSON.parse(JSON.stringify())
来达到相同的结果。
但是这种方式有它的弊端,我们来看一下
// 声明person
let person = {
name: "张三",
hobbies: ["吃饭", "睡觉", "打豆豆"],
date: new Date,
fuc: () => { },
reg: /w/
}
// 对person进行浅拷贝得到person1
const person1 = JSON.parse(JSON.stringify(person));
//修改值类型
person1.name = '李四'
//修改引用类型
person1.hobbies[0] = '美女'
console.log(person);
console.log(person1);
来看一下结果,date
属性从一个对象变成了一个字符串,fuc
属性消失了,reg
属性变成了空对象。所以说这种方式弊端是很大的,一不小心就会有意外产生。
undefined
、任意的函数以及symbol
值,在序列化过程中会被忽略;Date
日期会被当做字符串处理;NaN
和Infinity
格式的数值及null
都会被当做null
;- 其他类型的对象,包括
Map/Set/WeakMap/WeakSet
,仅会序列化可枚举的属性; - 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误;
我们再使用递归浅拷贝的方式来看一下结果,简直是一模一样。