开发中我们经常需要复制一个对象。如果直接用赋值会有下面问题。
const person = {
name: 'wjr',
age: 18
}
const person2 = person
console.log(person2); // {name: 'wjr', age: 18}
person2.name = 'WJR'
// 修改了person2,person也被改变了
console.log(person2); // {name: 'WJR', age: 18}
console.log(person); // {name: 'WJR', age: 18}
当改变新对象时,原对象也跟着发生变化,以上情况被称为赋值(Copy),当我们修改新对象时,不希望原对象也发生改变,此时我们就需要了解一下深拷贝与浅拷贝,以及其中的区别。
概念:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,那么拷贝的就是基本类型的值;如果属性是引用类型(数组、对象),拷贝的就是内存地址。因为基本类型以及引用类型的地址都存放在内存的栈中,因此浅拷贝可以认为是拷贝栈中的值,即在内存栈中开辟新的空间存储基本类型和引用类型的地址。
例如:
let obj = {
a: 1,
b: {
c: 2
}
}
那么obj的浅拷贝就可以看作是在内存栈中开辟新空间存放a的值和b的地址,b的地址指向内存堆空间的c的值。
Object.assign()
Object.assign()
用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
const target = {}
const source = { a: 1, b: { c: 1 } }
console.log(Object.assign(target, source)); // { a: 1, b: { c: 1 } }
console.log(target); // { a: 1, b: { c: 1 } }
console.log(source); // { a: 1, b: { c: 1 } }
接着当修改对象source中的对象b的属性c时,可以看到复制结果target也会随之改变,如下代码,可见Object.assign()
实现的是浅拷贝。
const target = {}
const source = { a: 1, b: { c: 1 } }
console.log(Object.assign(target, source)); // { a: 1, b: { c: 5 } }
console.log(target); // { a: 1, b: { c: 5 } }
console.log(source); // { a: 1, b: { c: 5 } }
source.b.c = 5
扩展运算符...Object
扩展运算符的实际效果和Object.assign()
一样,如下代码
const source = { a: 1, b: { c: 1 } }
const target = { ...source }
console.log(source); // { a: 1, b: { c: 1 } }
console.log(target); // { a: 1, b: { c: 1 } }
当修改对象source中的对象b的属性c时,也可以看到复制结果target也会随之改变。如下
const source = { a: 1, b: { c: 1 } }
const target = { ...source }
console.log(source); // { a: 1, b: { c: 5 } }
console.log(target); // { a: 1, b: { c: 5 } }
source.b.c = 5
Array.prototype.slice()
slice()
方法返回一个新的数组对象,这一对象是一个由begin
和end
决定的原数组的浅拷贝,原始数组不会被改变。
const a = [0, 1, [2, 3]]
const b = a.slice() // 不填参数,默认全部
console.log(a); // [0, 1, [2, 3]]
console.log(b); // [0, 1, [2, 3]]
当修改数组b内的数组时,a也随之发生变化,如下代码,可见Array.prototype.slice()
实现的是浅拷贝。
const a = [0, 1, [2, 3]]
const b = a.slice() // 不填参数,默认全部
console.log(a); // [0, 1, [5, 3]]
console.log(b); // [0, 1, [5, 3]]
b[2][0] = 5
Array.prototype.concat()
将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。
深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。
// 原对象
const obj = {
uName: 'wjr',
age: 18,
movieArr: ['Scent of a Woman', 'Gone with the Wind'],
minObj: {
one: 1,
two: 2
}
}
const newObj = {}
// 定义实现深拷贝的函数
function deepClone(newObj, oldObj) {
for (let k in oldObj) {
if (oldObj[k] instanceof Array) {
newObj[k] = []
deepClone(newObj[k], oldObj[k])
} else if (oldObj[k] instanceof Object) {
newObj[k] = {}
deepClone(newObj[k], oldObj[k])
}
else {
newObj[k] = oldObj[k]
}
}
}
// 调用深拷贝函数
deepClone(newObj, obj)
// 修改拷贝后的对象
newObj.age = 19
newObj.movieArr[1] = 'Titanic'
newObj.minObj.one = 3
// 输出新旧对象以对比修改新对象旧对象是否变化
console.log('原对象:', obj);
console.log('拷贝后再经修改的对象', newObj);
该方法的原理是:JSON.stringify()
将JSON对象转换为JSON字符串,JSON字符串作为简单数据类型将在内存栈中开辟新空间并存放,接着,JSON.parse()
将已在内存栈开辟新空间的JSON字符串转换为JSON对象,并且将地址存放在原先在内存栈中开辟的新空间,地址所指向的值存放在内存堆中新开辟的空间。拷贝后的对象不和原对象共用同一内存栈和内存堆空间,因此实现了原对象与新对象互不影响的深拷贝。下面我们通过代码更深刻的体会这行代码的巧妙之处。
另外,该方法对数组实现的拷贝效果和对对象相同。
// 原对象
const obj = {
uName: 'wjr',
age: 18,
movieArr: ['Scent of a Woman', 'Gone with the Wind'],
minObj: {
one: 1,
two: 2
}
}
// 新对象
const newObj = JSON.parse(JSON.stringify(obj))
console.log('原对象:', obj);
console.log('新对象:', newObj);
拷贝结果
当修改拷贝的对象内的对象,不改变原对象,如下代码:
// 原对象
const obj = {
uName: 'wjr',
age: 18,
movieArr: ['Scent of a Woman', 'Gone with the Wind'],
minObj: {
one: 1,
two: 2
}
}
// 新对象
const newObj = JSON.parse(JSON.stringify(obj))
// 修改拷贝后的对象
newObj.age = 19
newObj.movieArr[1] = 'Titanic'
newObj.minObj.one = 3
// 输出新旧对象以对比修改新对象旧对象是否变化
console.log('原对象:', obj);
console.log('拷贝后再经修改的对象', newObj);
如上实现了和递归实现深拷贝
相同的效果,但是该方法有以下几个问题:
会忽略undefined
会忽略symbol
会忽略函数
// undefined、symbol和函数这三种情况会直接忽略
const obj = {
name: 'wjr',
a: undefined,
b: Symbol('wjr'),
c: function () {
}
}
const newObj = JSON.parse(JSON.stringify(obj))
console.log('原对象:', obj);
console.log('新对象:', newObj);
循环引用会报错
let obj = {
a: 1,
b: {
c: 2,
d: 3
}
}
obj.a = obj.b;
obj.b.c = obj.a;
let b = JSON.parse(JSON.stringify(obj));
// Uncaught TypeError: Converting circular structure to JSON
不能正确处理new Date()
const nowTime = new Date()
const newTime = JSON.parse(JSON.stringify(nowTime))
console.log('原对象:', nowTime);
console.log('新对象:', newTime);
解决方法:转成时间戳
const nowTime = (new Date()).valueOf()
const newTime = JSON.parse(JSON.stringify(nowTime))
console.log('原对象:', nowTime);
console.log('新对象:', newTime);
不能处理正则
const obj = {
name: 'wjr',
a: /'123'/
}
const newObj = JSON.parse(JSON.stringify(obj))
console.log('原对象:', obj);
console.log('新对象:', newObj);
参考文档: JavaScript 实用工具库——lodash中文文档
– | 和原数据是否指向同一对象 | 第一层数据为基本数据类型 | 原数据中包含子对象 |
---|---|---|---|
赋值 | 是 | 改变会使原数据一同改变 | 改变会使原数据一同改变 |
浅拷贝 | 否 | 改变不会使原数据一同改变 | 改变会使原数据一同改变 |
深拷贝 | 否 | 改变不会使原数据一同改变 | 改变不会使原数据一同改变 |
参考链接:详细解析赋值、浅拷贝、深拷贝