什么是浅拷贝?
let shallow = ['list', {items: ['item-01', 'item-02', 'item-03']}]
let shallowCopy = Array.from(shallow)
shallowCopy[1].items = []
console.log(shallow) // ['list', {items: ['any']}]
shallow[1].items = ['any']
console.log(shallowCopy) // ['list', {items: ['any']}]
这就是浅拷贝:拷贝的新对象发生改变,源对象也会发生改变;源对象发生改变,新对象也会发生改变。引用MDN的描述:
对象的浅拷贝是其属性与拷贝源对象的属性共享相同引用(指向相同的底层值)的副本。因此,当你更改源或副本时,也可能导致其他对象也发生更改——也就是说,你可能会无意中对源或副本造成意料之外的更改。这种行为与深拷贝的行为形成对比,在深拷贝中,源和副本是完全独立的。
什么是深拷贝?
let deep = ['list', {items: ['item-01', 'item-02', 'item-03']}]
let deepCopy = JSON.parse(JSON.stringify(deep))
deepCopy[1].items = []
console.log(deep) // ['list', {items: ['any']}]
deep[1].items = ['any']
console.log(deepCopy) // ['list', {items: []}]
这就是深拷贝:拷贝的新对象发生改变,源对象不会发生改变;源对象发生改变,新对象也不会发生改变。引用MDN的描述:
对象的深拷贝是指其属性与其拷贝的源对象的属性不共享相同的引用(指向相同的底层值)的副本。因此,当你更改源或副本时,可以确保不会导致其他对象也发生更改;也就是说,你不会无意中对源或副本造成意料之外的更改。这种行为与浅拷贝的行为形成对比,在浅拷贝中,对源或副本的更改可能也会导致其他对象的更改(因为两个对象共享相同的引用)。
数组的浅拷贝与深拷贝
// 数据
let primitives = [undefined, null, 'string', 1, true, BigInt(2), Symbol()]
let objects = [{[Symbol()]: 'object'}, ['array']]
- 数组的内置方法
// Array.from -> 浅拷贝
let arrayFromCopyPrimitve = Array.from(primitives)
let arrayFromCopyObject = Array.from(objects)
arrayFromCopyPrimitve[0] = 'void'
console.log(primitives) // [undefined, null, 'string', 1000, true, 2000n, Symbol()]
arrayFromCopyObject[1][0] = 'any'
console.log(objects) // [{key: 'unknown'}, ['any']]
objects[0].key = 'unknown'
console.log(arrayFromCopyPrimitve) // ['void', null, 'string', 1000, true, 2000n, Symbol()]
console.log(arrayFromCopyObject) // [{key: 'unknown'}, ['any']]
// Array.prototype.concat -> 浅拷贝
let concatArray = objects.concat(primitives)
concatArray[0] = 'void'
console.log(primitives) // [undefined, null, 'string', 1000, true, 2000n, Symbol()]
concatArray[1][0] = 'any'
console.log(objects) // [{key: 'unknown'}, ['any']]
objects[0].key = 'unknown'
console.log(concatArray) // ['void', ['any'], undefined, null, 'string', 1000, ...]
// Array.prototype.slice -> 浅拷贝
let sliceArray = objects.slice()
sliceArray[1][0] = 'any'
console.log(objects) // [{key: 'unknown'}, ['any']]
objects[0].key = 'unknown'
console.log(sliceArray) // [{key: 'unknown'}, ['any']]
// Array.prototype.filter -> 浅拷贝
let filterArray = objects.filter(() => true)
filterArray[1][0] = 'any'
console.log(objects) // [{key: 'unknown'}, ['any']]
objects[0].key = 'unknown'
console.log(filterArray) // [{key: 'unknown'}, ['any']]
- 遍历
// 浅拷贝
let shallowArray = objects.map(v => v)
shallowArray[1][0] = 'any'
console.log(objects) // [{key: 'unknown'}, ['any']]
objects[0].key = 'unknown'
console.log(shallowArray) // [{key: 'unknown'}, ['any']]
// 深拷贝
let deepArray = objects.map(v => Object.assign({}, v))
shallowArray[1][0] = 'any'
console.log(objects) // [{key: 'unknown'}, ['array']]
objects[0].key = 'unknown'
console.log(shallowArray) // [{key: 'object'}, ['any']]
// 浅拷贝
let shallowArray = []
for (let i = 0; i < objects.length; i++) {
shallowArray[i] = objects[i]
}
shallowArray[1][0] = 'any'
console.log(objects) // [{key: 'unknown'}, ['any']]
objects[0].key = 'unknown'
console.log(shallowArray) // [{key: 'unknown'}, ['any']]
// 深拷贝
let deepArray = []
for (let i = 0; i < objects.length; i++) {
deepArray[i] = Object.assign({}, objects[i])
}
deepArray[1][0] = 'any'
console.log(objects) // [{key: 'unknown'}, ['array']]
objects[0].key = 'unknown'
console.log(deepArray) // [{key: 'object'}, ['any']]
- JSON转换
const jsonArray = JSON.parse(JSON.stringify(objects))
jsonArray[1][0] = 'any'
console.log(objects) // [{key: 'unknown'}, ['array']]
objects[0].key = 'unknown'
console.log(jsonArray) // [{key: 'object'}, ['any']]
- 扩展运算符
// [...] -> 浅拷贝
const spreadArray = [...objects]
spreadArray[1][0] = 'any'
console.log(objects) // [{key: 'unknown'}, ['any']]
objects[0].key = 'unknown'
console.log(spreadArray) // [{key: 'unknown'}, ['any']]
从上面可以得出,数组的内置方法(包括扩展运算符)都是浅拷贝,而遍历和JSON转换使用新的内存区域深拷贝才得以实现。
对象的浅拷贝与深拷贝
// 数据
let first = {a: {b: {c: 'd'}}, e: 'f'}
let second = {h: {i: 'j'}, k: 'l'}
- 对象的内置方法
// Object.assign -> 浅拷贝
let assignObj = Object.assign({}, first, second)
assignObj.h.i = 'x'
console.log(first) // { a: { b: { c: 'd' } }, e: 'f' }
console.log(second) // { h: { i: 'x' }, k: 'l' }
second.k = {}
console.log(assignObj) // { a: { b: { c: 'd' } }, e: 'f', h: { i: 'x' }, k: 'l' }
- 遍历
function type(data) {
return Reflect.toString.call(data).replace(/.+[ ](.+)]$/g, '$1').toLowerCase()
}
function copy(target, source = {}) {
let iterator = []
if (Array.isArray(target)) {
iterator = target.entries()
} else {
if (typeof (target) === 'object') {
iterator = Object.entries(target)
}
}
for (const [k, v] of iterator) {
if (v && typeof v === 'object') {
source[k] = copy(v, source[k])
} else {
source[k] = v
}
}
return source
}
const traversalObj = copy(second)
traversalObj.h.i = 'x'
console.log(second) // { h: { i: 'j' }, k: {} }
second.k = {}
console.log(traversalObj) // { h: { i: 'x' }, k: 'l' }
- JSON转换
const jsonObj = JSON.parse(JSON.stringify(second))
jsonObj.h.i = 'x'
console.log(second) // { h: { i: 'j' }, k: {} }
second.k = {}
console.log(jsonObj) // { h: { i: 'x' }, k: 'l' }
- 扩展运算符
// {...} -> 浅拷贝
const spreadObj = { ...second }
spreadObj.h.i = 'x'
console.log(second) // { h: { i: 'x' }, k: {} }
second.k = {}
console.log(spreadObj) // { h: { i: 'x' }, k: 'l' }
从上面可以得出,对象的内置方法(包括扩展运算符)也都是浅拷贝,而遍历和JSON转换使用新的内存区域深拷贝才得以实现。
总结
1、对象(数组)的内置方法都是浅拷贝,引用MDN的描述:
在 JavaScript 中,所有标准的内置对象复制操作(展开语法、Array.prototype.concat()、Array.prototype.slice()、Array.from()、Object.assign() 和 Object.create())创建的是浅拷贝而不是深拷贝。
2、通过测试可以看出,基本类型的数据不会相互影响。换句话说,如果对象(数组)的第一层全是基本类型,那么使用其中任何一种方法都是深拷贝。
3、如果需要实现深拷贝就需要使用新的变量去接收数据,得到新的内存区域分配。
4、JSON转换是将对象转为字符串然后再解析出来,也就是说它存在局限性,比如对象中存在函数。另外,大量的数据转换在性能上也存在一定隐患。
5、什么时候深拷贝取决于要不要影响对应的数据,使用哪种手段进行深拷贝取决于对象的复杂度。
注:文章中的深拷贝方法并不完善,仅为此处测试用。