引入:理解深浅拷贝
众所周知的拷贝(copy)在 JavaScript 中,分成了 深拷贝 和 浅拷贝,单从字面不易直接区分。本期文章将为大家深入讲解 JavaScript 中的深浅拷贝。
1 - 浅拷贝
浅拷贝,在 MDN 网站上,它是这样解释的:
对象的浅拷贝是属性与拷贝的源对象属性共享相同的引用(指向相同的底层值)的副本
这句话一读,可能有点难懂,那怎么来理解这句话呢?
- 在开发中,我们会经常需要复制一个对象,创建一个新对象,这个新对象有着与原对象相同的基本类型的值,以及对原对象中引用类型的引用,这就是浅拷贝。
下面我用代码来演示一遍:
const oldObj = {
name: 'my-obj',
length: 6
}
// 使用展开运算符进行浅拷贝
const newObj = { ...oldObj }
console.log(newObj)
// 打印的结果:newObj{name: 'my-obj', length: 6}
存在的问题
需要注意的是,浅拷贝只是复制了对象的顶层结构,对于嵌套的对象或数组(引用类型),它只是复制了引用,而不是创建新的独立的对象或数组。
用代码来演示更加容易理解:
const oldObj = {
name: 'oldObj',
food: ['steak', 'burger']
}
const newObj = { ...oldObj }
newObj.name = 'newObj'
newObj.food.push('noodles')
console.log(oldObj)
// 打印的结果:oldObj{name: 'oldObj', food: ['steak', 'burger', 'noodles']}
console.log(newObj)
// 打印的结果:newObj{name: 'newObj', food: ['steak', 'burger', 'noodles']}
与基本类型(如代码中的 name 属性)“直接复制原始对象的值”而“不改变原始对象”不同的是,当你对浅拷贝得到的对象中的 嵌套对象/数组 (如代码中的 food 属性)进行修改时,原始对象中的相应嵌套对象也会被修改,因为它们实际上指向同一块内存地址。
2 - 深拷贝
在某些复杂场景中浅拷贝可能会导致意外的结果。如果需要完全独立的复制,包括嵌套的对象和数组,那这时候就需要我们的 深拷贝 登场了。
深拷贝,在 MDN 网站上,它是这样解释的:
对象的深拷贝是指其属性与其拷贝的源对象的属性不共享相同的引用(指向相同的底层值)的副本。
有了前面浅拷贝的铺垫,我们来理解这句话可能会容易很多。
同样,还是用代码来演示一遍:
const oldObj = {
food: ['steak', 'burger']
}
const newObj = JSON.parse(JSON.stringify(oldObj))
oldObj.food[0] = 'noodles'
console.log(oldObj)
// 打印的结果:oldObj{ food: ['noodles', 'burger'] }
console.log(newObj)
// 打印的结果:newObj{ food: ['steak', 'burger'] }
- 这个代码是利用 JSON 字符串来实现深拷贝的,我们可以看到,复制之后的新对象和原始对象完全独立,完全独立,对深拷贝对象的任何修改都不会影响到原始对象。
然而,利用 JSON 字符串来实现深拷贝有一个缺陷:当原始对象中有出现函数时,深拷贝没办法把函数也复制到新对象中。
如果原始对象中没有嵌套的对象或者数组,我们还可以通过 浅拷贝 的方式直接把原始对象复制给新对象。
但如果原始对象出现嵌套,就需要采用另一种实现深拷贝的方法。
const deepClone = (oldObj) => {
if (typeof deepClone === 'object' && deepClone != null) {
let copyType = Array.isArray(oldObj) ? [] : {}
for (let k in oldObj) {
if (oldObj.hasOwnProperty(k)) {
copyType[k] = deepClone(oldObj[k])
}
}
return copyType
} else {
return oldObj
}
}
- 这个封装的函数利用了递归的思想来到达深拷贝的效果,
deepClone()函数首先判断输入是否为 基本类型或 null,如果是则直接返回。再判断输入是 数组或对象,进而生成相应的空数组或者对象,之后循环遍历实参的内容。hasOwnProperty()判断对象自身是否有指定的属性,避免继承了原型链上的属性
调用的过程:
const oldObj = {
length: 6,
food: ['steak', 'burger'],
fn() {
console.log('oldObj')
}
}
const newObj = deepClone(oldObj)
newObj.food[0] = 'noodles'
newObj.fn = function () {
console.log(6)
}
console.log(oldObj)
// 打印的结果:oldObj{ length: 6 ,food: ['steak', 'burger'] ,fn:f}
console.log(newObj)
// 打印的结果:newObj{ length: 6 ,food: ['noodles', 'burger'] ,fn:f}
oldObj.fn() //打印的结果:oldObj
newObj.fn() //打印的结果:6
应用场景
- 当我们需要数据的传递,但不希望原始的对象被修改时,我们可以使用 浅拷贝 来操作。例如,在搜索框中,有一些默认的文字,当用户在搜索框里面输入其他文字时,原始默认的文字就会消失,但当搜索框里面没有输入文字时,原始默认的文字又会重新显示,我们就可以使用浅拷贝来操作,创建两个对象,一个存放默认的文字,一个存放用户输入的文字。
- 如果数据对象的结构比较简单,没有复杂的嵌套对象或数组,并且只需要在一个地方进行修改而不影响其他地方的相同数据结构,浅拷贝是一个合适的选择。例如,在前端应用中,有时候我们需要传输不同的人的姓名、年龄等这些简单数据,我们可以使用浅拷贝来操作。
- 当数据对象包含复杂的嵌套结构,如多层嵌套的对象和数组,并且需要在不同的地方对这个数据进行独立的修改而不影响其他地方的副本时,我们就应该使用深拷贝。
注意事项
- 浅拷贝
-
- 由于浅拷贝的共享引用特性,我们很容易在不经意间修改了原对象,所以在撰写代码时,需要特别注意是否会影响到原对象。
- 使用浅拷贝时需注意原对象中是否有嵌套的对象或数组,当原对象中的属性较多时,很容易遗漏一些嵌套的对象,导致错误的使用浅拷贝使数据出现错误。
- 深拷贝
-
- 深拷贝较浅拷贝消耗的资源和时间更多,同时它会创建完全独立的副本,对于内存空间要求较大,当对象结构非常复杂且包含大量的嵌套对象时,需要充分考虑深拷贝对于性能的影响,谨慎使用,以免出现错误。
- 深拷贝会使用递归的方法,所以可能会陷入无限循环,例如在A中调用了B,然后在B中又调用了A,就会陷入无限循环,导致结果的错误和内存的浪费。
思考题
const class1 = {
teacher: 'Amy',
count: 45,
advantage: ['chinese', 'math', 'science']
}
const class2 = { ...class1 }
class2.teacher = 'Bob'
class2.advantage[1] = 'english'
console.log(class1)
console.log(class2)
- 这段代码中使用了哪种拷贝方式?请解释原因。
- 分析代码执行后的输出结果,说明为什么会得到这样的结果。
- 如果要实现完全独立的副本,应该如何修改这段代码?