前言
拷贝是在编程中常见的操作,它用于创建一个对象或数组的副本,以便在不影响原始对象的情况下进行操作和修改。在 JavaScript 中,拷贝通常针对引用类型进行操作,包括对象和数组。
一、浅拷贝
浅拷贝是指创建一个新对象或数组,并将原始对象或数组的引用复制给新对象或数组。这意味着新对象和原始对象仍然共享相同的内部数据。当对原始对象进行修改时,新对象也会受到影响。
以下是几种常见的浅拷贝方法:
Object.create(obj)
:使用Object.create()
方法可以创建一个新对象,新对象的原型是指定的对象obj
。Object.assign({}, obj)
:通过Object.assign()
方法可以将一个或多个源对象的属性复制到目标对象中,并返回目标对象。[].concat(arr)
:使用数组的concat()
方法可以将一个或多个数组合并为一个新数组。- 数组解构:通过解构赋值语法可以快速创建一个数组的浅拷贝。
arr.slice()
:数组的slice()
方法返回一个新数组,包含原数组的浅拷贝。
需要注意的是,浅拷贝只能处理一层的数据,如果原始对象或数组中包含嵌套的对象或数组,则浅拷贝只会复制它们的引用。这意味着对于嵌套对象或数组的修改会影响到新对象或数组。
浅拷贝示例:
1. 使用 Object.create(obj)
进行浅拷贝:
javascript
const obj = { a: [1, 2], b: { c: 3 } };
const newObj = Object.create(obj);
console.log(newObj); // 输出:{}
console.log(newObj.a); // 输出:[1, 2]
console.log(newObj.b); // 输出:{ c: 3 }
// 修改原始对象
obj.a.push(3);
obj.b.c = 4;
console.log(obj); // 输出:{ a: [ 1, 2, 3 ], b: { c: 4 } }
console.log(newObj.a); // 输出:[1, 2, 3],新对象也受到影响
console.log(newObj.b); // 输出:{ c: 4 },新对象也受到影响
在上面的示例中,我们使用 Object.create(obj)
方法创建了一个新对象 newObj
,并将原始对象 obj
的引用复制给了它。当我们修改原始对象 obj
的值时,浅拷贝对象 newObj
的值也随之发生了变化。这是因为浅拷贝只是复制了原始对象的引用,而没有创建新的内存空间来存储数据。
2. 使用 Object.assign({}, obj)
进行浅拷贝:
javascript
const obj = { a: [1, 2], b: { c: 3 } };
const newObj = Object.assign({}, obj);
console.log(newObj); // 输出:{ a: [1, 2], b: { c: 3 } }
// 修改原始对象
obj.a.push(3);
obj.b.c = 4;
console.log(obj); // 输出:{ a: [ 1, 2, 3 ], b: { c: 4 } }
console.log(newObj); // 输出:{ a: [ 1, 2, 3 ], b: { c: 4 } }
console.log(newObj.a); // 输出:[1, 2, 3],新对象也受到影响
console.log(newObj.b); // 输出:{ c: 4 },新对象也受到影响
在这个示例中,我们使用 Object.assign()
方法将一个空对象和原始对象 obj
的属性进行合并,并返回一个新的对象 newObj
。当我们修改原始对象 obj
的值时,浅拷贝对象 newObj
的值并会发生变化。这是因为浅拷贝复制了原始对象的一层属性,也复制嵌套对象的引用。
3. 使用数组的 concat()
方法进行浅拷贝:
javascript
const arr = [[1, 2], { a: 3 }];
const newArr = [].concat(arr);
console.log(newArr); // 输出:[[1, 2], { a: 3 }]
// 修改原始数组
arr[0].push(3);
arr[1].a = 4;
console.log(arr); // 输出:[[1, 2, 3], { a: 4 } ]
console.log(newArr); // 输出:[[1, 2, 3], { a: 4 }],新数组也受到影响
在这个示例中,我们使用数组的 concat()
方法创建了一个新数组 newArr
,其中包含原始数组 arr
的所有元素。当我们修改原始数组 arr
的值时,浅拷贝数组 newArr
的值并会发生变化。这是因为浅拷贝复制数组内部引用类型的引用。
4. 使用数组解构进行浅拷贝:
javascript
const arr = [[1, 2], { a: 3 }];
const [...newArr] = arr;
console.log(newArr); // 输出:[[1, 2], { a: 3 }]
// 修改原始数组
arr[0].push(3);
arr[1].a = 4;
console.log(arr); // 输出:[[1, 2, 3], { a: 4 }]
console.log(newArr); // 输出:[[1, 2, 3], { a: 4 }],新数组也受到影响
在这个示例中,我们使用数组解构语法 [...arr]
创建了一个新数组 newArr
,其中包含原始数组 arr
的所有元素。当我们修改原始数组 arr
的值时,浅拷贝数组 newArr
的值并不会发生变化。这是因为浅拷贝复制数组内部引用类型的引用。
5. 使用 arr.toReversed().reverse() 进行浅拷贝:
javascript
const arr = [[1, 2], { a: 3 }];
const newArr = arr.toReversed().reverse();
console.log(newArr); // 输出:[{ a: 3 }, [1, 2]]
// 修改原始数组
arr[0].push(3);
arr[1].a = 4;
console.log(newArr); // 输出:[{ a: 4 }, [1, 2, 3]],新数组也受到影响
在这个示例中,toReversed()是新增的方法,需要更高版本的node才能使用,所以使用时可能会报错,
二、深拷贝
深拷贝是指创建一个与原始对象完全独立的新对象或数组,新对象拥有与原始对象相同的值,但内部数据不共享。这样,在对原始对象进行修改时,不会影响到新对象。
1. JSON.parse(JSON.stringify(obj))
在 JavaScript 中,常用的深拷贝方法是使用 JSON.parse(JSON.stringify(obj))
。该方法通过将对象序列化为 JSON 字符串,然后再解析成新的对象来实现深拷贝。但是需要注意的是,这种方法也存在一些缺陷:
- 无法处理特殊的数据类型:
undefined
、function
和Symbol
会在序列化过程中丢失。 - 无法处理循环引用:如果原始对象中存在循环引用(即对象内部存在相互引用的情况),那么深拷贝会陷入无限循环,在处理大型对象时可能导致堆栈溢出。
2. cloneDeep()
为了解决深拷贝的问题,我们可以借助第三方库如 Lodash 的 cloneDeep()
方法,它能够处理上述的特殊数据类型和循环引用。
javascript
const cloneObj = _.cloneDeep(obj);
使用第三方库可以更安全、可靠地进行深拷贝操作。
深拷贝示例:
- 使用
JSON.parse(JSON.stringify(obj))
进行深拷贝:
javascript
const obj = { name: 'Charlie', age: 35 };
const newObj = JSON.parse(JSON.stringify(obj));
console.log(newObj); // 输出: { name: 'Charlie', age: 35 }
// 修改原始对象
obj.age = 40;
console.log(obj); // 输出: { name: 'Charlie', age: 40 }
console.log(newObj); // 输出: { name: 'Charlie', age: 35 }
在这个示例中,我们使用 JSON.parse(JSON.stringify(obj))
方法创建了一个新的对象 newObj
,并将原始对象 obj
的所有属性值复制给了它。当我们修改原始对象 obj
的值时,深拷贝对象 newObj
的值并不会发生变化。这是因为深拷贝创建了新的内存空间来存储数据,并且递归地复制了所有嵌套的对象和数组。
需要注意的是,JSON.parse(JSON.stringify(obj))
方法存在一些限制。例如,它无法处理特殊的数据类型(如 undefined
、function
和 Symbol
),并且在处理循环引用时可能会导致无限循环的问题。
- 使用第三方库 Lodash 的
cloneDeep()
进行深拷贝(需要先安装 Lodash):
javascript
const _ = require('lodash');
const obj = { name: 'David', age: 40 };
const newObj = _.cloneDeep(obj);
console.log(newObj); // 输出: { name: 'David', age: 40 }
// 修改原始对象
obj.age = 45;
console.log(obj); // 输出: { name: 'David', age: 45 }
console.log(newObj); // 输出: { name: 'David', age: 40 }
在这个示例中,我们使用第三方库 Lodash 的 cloneDeep()
方法创建了一个新的对象 newObj
,并将原始对象 obj
的所有属性值复制给了它。当我们修改原始对象 obj
的值时,深拷贝对象 newObj
的值并不会发生变化。这是因为 cloneDeep()
方法能够处理特殊的数据类型和循环引用,并且递归地复制了所有嵌套的对象和数组。
三、总结
浅拷贝和深拷贝都是在编程中常见的操作,用于创建对象或数组的副本。浅拷贝只复制引用,而深拷贝创建独立的副本。对于浅拷贝,我们可以使用 Object.assign()
、concat()
或解构赋值等方法。对于深拷贝,可以使用 JSON.parse(JSON.stringify(obj))
或第三方库提供的深拷贝方法。在实际应用中,我们需要根据具体情况选择适合的拷贝方式,以确保数据的完整性和安全性。