在 JavaScript 引用数据类型中,变量保存的是一个指向堆内存的指针,当需要访问引用类型(如对象,数组等)的值时,首先从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据。
代码语言:javascript复制let obj1 = { x: 1, y: 2 }
let obj2 = obj1
obj2.x = 2
console.log(obj1) // { x: 2, y: 2 }
console.log(obj2) // { x: 2, y: 2 }
以上的拷贝方式就是浅拷贝,当 obj2 的值改变时,obj1 的值也随之发生改变。
浅拷贝
代码语言:javascript复制let arr1 = [0, 1, ['a', 'b']]
let arr2 = arr1.concat()
let arr3 = arr1.slice()
let arr4 = Array.from(arr3)
arr2 === arr1 // false 看起来像深拷贝
arr3 === arr1 // false 看起来像深拷贝
arr4 === arr3 // false 看起来像深拷贝
// 然鹅
let arr5 = [{name: 'Leo'}]
let arr6 = arr4.slice()
let arr7 = arr4.concat()
let arr8 = Array.from(arr4)
arr5[0].name = 'Jack'
arr6[0].name === 'Jack' // 其实还是浅拷贝
arr7[0].name === 'Jack' // 其实还是浅拷贝
arr8[0].name === 'Jack' // 其实还是浅拷贝
Array.prototype.concat(), Array.prototype.slice(), Array.from() 只能实现对一维数组的深拷贝。
Object.assign()
代码语言:javascript复制let obj1 = { x: 1, y: 2 }
let obj2 = Object.assign({}, obj1)
obj1 === obj2 // false
obj1.x = 2
console.log(obj1) // { x: 2, y: 2 }
console.log(obj2) // { x: 1, y: 2 } // 一维对象可以进行深拷贝
// 然鹅
let obj3 = { x: {name: 'Leo'} }
let obj4 = Object.assign({}, obj3)
obj3 === obj4 // false
obj3.x.name = 'Jack'
obj4.x.name === 'Jack' // true // 其实还是浅拷贝
深拷贝
使用 JSON.parse() JSON.stringify() 实现深拷贝
代码语言:javascript复制let obj1 = {
x: 1,
y: {
name: 'Leo',
friends: ['Lily', 'Elsa']
}
}
let obj2 = JSON.parse(JSON.stringify(obj1))
obj1 === obj2 // false
obj1.y.name = 'Jack'
obj1.y.friends.push('Tim')
obj2.y.name === 'Leo' // 深拷贝
console.log(obj2.y.friends) // ["Lily", "Elsa"] // 深拷贝
JSON.parse 和 JSON.stringify 看起来不错,不过存在一些问题:
- undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。
- 所有以 symbol 为属性键的属性都会被完全忽略掉。
- 不可枚举的属性会被忽略。
JSON.stringify({a: function add (){}}) // '{}'
JSON.stringify({x: undefined, y: Object, z: Symbol("")}) // '{}'
JSON.stringify([undefined, Object, Symbol("")]) // '[null,null,null]'
JSON.stringify({[Symbol("foo")]: "foo"}) // '{}'
使用递归
代码语言:javascript复制function deepClone(o) {
// if o is not an object
if (!o || (typeof o) != 'object') return o
let res = Array.isArray(o) ? [] : {}
let keys = Object.keys(o)
for (let i = 0; i< keys.length; i ) {
let key = keys[i]
if (typeof key === 'object') {
res[key] = deepClone(o[key])
} else {
res[key] = o[key]
}
}
return res
}
测试代码:
代码语言:javascript复制let obj1 = {
x: {name: 'Leo'},
y: undefined,
z: function add () {},
t: Symbol('tt'),
m: [1, 2, 3, 4, 5],
n: [[1, 2]]
}
let obj2 = deepClone(obj1)
obj1.n[0].push(3)
console.log(obj2.n[0]) // [1, 2]
注意:由于使用 for in
循环,所以只能深度拷贝对象自身属性(非原型链上的属性),并且属性为 enumerable。
使用递归拷贝对象的方法,在目标非常大,层级关系非常深的时候会出现性能问题,具体解决方案可以参考我之前写的 JavaScript递归优化 使用栈代替递归的方式解决。
lodash
lodash 中提供 4 个对象拷贝相关的方法:
代码语言:javascript复制_.clone() // 提供浅拷贝
_.cloneDeep() // 提供深拷贝
_.cloneDeepWith() // 提供递归拷贝,并且可以自定义拷贝内容
_.cloneWith() // 提供浅拷贝,并且可以自定义拷贝内容
demo
代码语言:javascript复制function customizer(value) {
if (_.isElement(value)) {
return value.cloneNode(true)
}
}
let el = _.cloneDeepWith(document.body, customizer)
console.log(el === document.body) // => false
console.log(el.nodeName) // => 'BODY'
console.log(el.childNodes.length) // => 20
相信上述几种方法已经能够满足我们平时大部分的需求了,如果有额外的需求,只能自己定义实现深/浅拷贝的方式了。