浅拷贝和深拷贝涉及到了一些基本概念,例如:可变性。如果你不了解它,可以先看看变量赋值与原始对象可变性。
拷贝出来的目标对象有着与原对象相同的属性值。如果我们的对象中嵌套了对象,那么像
object.assign
和扩展运算符(...
)这样的浅层复制机制将只创建根级对象的副本,但深层对象仍将被共享。
最直接的方式是新定义对象,在对其赋值:
let md = { name: 'lio', url: 'lio-zero.github.io' }
let obj = {
name: md.name
}
obj.name = 'lion'
console.log(md) // { name: "lio", url: "lio-zero.github.io" }
操作对象,上面方法显然不太灵活,这时使用循环,复制过程需要改变一些数据,可以使用这种方法:
let md = { name: 'lio', url: 'lio-zero.github.io' }
let obj = {}
for (const key in md) {
obj[key] = md[key]
}
obj.name = 'lion'
console.log(md) // { name: "lio", url: "lio-zero.github.io" }
concat
方法用于连接两个或多个数组。该方法不会改变现有的数组,而仅仅会返回被连接数组的一个副本。
let arr = [1, 2, 3]
const arr2 = arr.concat()
arr2[1] = 23
console.log(arr) // [1, 2, 3]
slice
和 concat
的用法类似。
let arr = [1, 2, 3]
const arr2 = arr.concat()
arr2[1] = 23
console.log(arr) // [1, 2, 3]
使用 Array.from
创建数组的不可变副本:
let arr = [1, 2, 3]
const arr2 = Array.from(arr)
arr2[1] = 23
console.log(arr) // [1, 2, 3]
let md = { name: 'lio', url: 'lio-zero.github.io' }
let obj = Object.assign({}, md)
obj.name = 'lion'
console.log(md) // { name: "lio", url: "lio-zero.github.io" }
更加简便的方式是使用 ES6 的扩展运算符(...
)
// 对象
let md = { name: 'lio', url: 'lio-zero.github.io' }
let obj = { ...md }
obj.name = 'lion'
console.log(md) // { name: "lio", url: "lio-zero.github.io" }
// 数组
let arr = [1, 2, 3]
let arr2 = [...arr]
arr2[1] = 23
console.log(arr) // [1, 2, 3]
我们在上文总结了多种浅拷贝的方法,数组可以使用 Array.slice()
、Array.from()
和 Array.concat()
方法,对象可以使用 Object.assign()
方法。另外,可以使用老套的新对象赋值的方式或 ES6 的扩展运算符适用于对象和数组进行浅拷贝。
就向刚一开始说的,上面都存在一个问题,假如对象中存在更深层级的对象,这些较深的对象一旦发生了改变,则原对象和引用原数据的对象都将改变。
const arr = [1, 2, { name: 'K.O' }]
const arr2 = arr.concat()
arr2[2].name = 'O.K'
console.log(arr) // [1, 2, { name: 'O.K'}]
console.log(arr2) // [1, 2, { name: 'O.K'}]
可以看到,我们访问了 arr
和 arr2
都改变了。
这时,我们就需要深度创建一个对象的副本。
深拷贝:拷贝出来的目标对象有着与原始对象相同的属性值,且嵌套的对象更改也不会影响到原始对象的改变,也就是它们之间有着相同的数据,但数据存储在不同的内存地址中。
如果对象足够简单,我们可以使用 JSON.stringify
将其转换为字符串,然后使用 JSON.parse
将其转换回 JavaScript 对象。
let obj = {
name: 'MSN',
user: {
name: 'BBC'
},
arr: [1, 2, 3]
}
let newObj = JSON.parse(JSON.stringify(obj))
newObj.user.name = 'K.O'
console.log(JSON.stringify(obj, null, 2))
/*
{
"name": "MSN",
"user": {
"name": "BBC"
},
"arr": [
1,
2,
3
]
}
*/
console.log(JSON.stringify(newObj, null, 2))
/*
{
"name": "MSN",
"user": {
"name": "K.O"
},
"arr": [
1,
2,
3
]
}
*/
可以看到,我们成功了,但这是有局限的。它只能应用于 number
、string
、boolean
、object
、array
这几种类型。
局限包含:
- 如果对象有任何不能用 JSON 字符串表示的数据(例如:函数、RegExp、Date、Set 和 Map 等特殊对象)
- 会忽略
undefined
和symbol
原始类型 - 无法解决循环引用的问题
这些数据将丢失!
更好的做法是递归调用浅拷贝,把所有属于对象的属性类型都遍历赋给另一个对象。
考虑:对象多层次的赋值方式,考虑其中存在数组情况
function deepCopy(obj) {
let res = obj instanceof Array ? [] : {}
for (const [k, v] of Object.entries(obj)) {
res[k] = typeof v == 'object' ? deepCopy(v) : v
}
return res
}
let newObj = deepCopy(obj)
newObj.user.name = 'K.O'
newObj.arr.push(4)
console.log(JSON.stringify(obj, null, 2))
/*
{
"name": "MSN",
"user": {
"name": "BBC"
},
"arr": [
1,
2,
3
]
}
*/
console.log(JSON.stringify(newObj, null, 2))
/*
{
"name": "MSN",
"user": {
"name": "K.O"
},
"arr": [
1,
2,
3,
4
]
}
*/
现在,它已经很不错了,对于大部分的用例。但它还是存在一些问题,一些特殊对象我们没有进行处理。
由于本文过长,完美的深拷贝这里不再展示,您可以下文提供的资料找到答案。
我们在开发时,肯定不会手动去实现这些方法。我们会使用一些库来辅助我们开发,例如:
- 使用函数库 lodash 的
_.cloneDeep
方法 - jQuery 的
jQuery.extend()
方法 - etc
但理解并熟知如何实现它们对于我们玩转 JS 有一定的帮助。