在 JavaScript 中,深拷贝和浅拷贝是处理对象和数组复制时的两种重要概念,它们的主要区别在于复制对象内部嵌套对象或数组时的行为。
浅拷贝
-
定义:浅拷贝会创建一个新的对象或数组,这个新的对象或数组会复制原对象或数组的基本类型值和引用类型的引用地址。也就是说,对于引用类型(如对象、数组),新对象和原对象会共享这些引用,对其中一个对象中引用类型值的修改会反映到另一个对象上。
-
实现方式:
- 对象的浅拷贝 :可以使用
Object.assign()
方法。它将所有可枚举属性的值从一个或多个源对象复制到目标对象。例如:
js
let originalObject = { a: 1, b: { c: 2 } };
let shallowCopiedObject = Object.assign({}, originalObject);
console.log(shallowCopiedObject);
// { a: 1, b: { c: 2 } }
// 修改浅拷贝对象中的嵌套对象
shallowCopiedObject.b.c = 3;
console.log(originalObject.b.c);
// 3,原对象也被改变,因为共享引用
- 数组的浅拷贝 :可以使用数组的
slice()
方法或concat()
方法。slice()
方法会返回一个新的数组对象,这一对象是一个由begin
和end
决定的原数组的浅拷贝(包括begin
,不包括end
)。concat()
方法用于合并两个或多个数组,返回一个新数组,也是浅拷贝。例如:
js
let originalArray = [1, { value: 2 }];
let slicedArray = originalArray.slice();
let concatenatedArray = [].concat(originalArray);
// 修改浅拷贝数组中的嵌套对象
slicedArray[1].value = 3;
console.log(originalArray[1].value);
// 3,原数组也被改变,因为共享引用
深拷贝
-
定义:深拷贝会递归地复制对象及其所有嵌套的对象和数组,创建一个与原对象完全独立的新对象。对新对象的任何修改都不会影响原对象,反之亦然。
-
实现方式:
- 使用
JSON.parse(JSON.stringify())
:这是一种简单但有局限性的深拷贝方法。它先将对象转换为 JSON 字符串,再将其解析回对象,从而实现深拷贝。例如:
js
let original = { a: 1, b: { c: 2 } };
let deepCopied = JSON.parse(JSON.stringify(original));
// 修改深拷贝对象中的嵌套对象
deepCopied.b.c = 3;
console.log(original.b.c);
// 2,原对象不受影响
- 局限性 :这种方法不能处理函数、
undefined
、Symbol
等特殊类型。例如:
js
let originalWithFunction = { a: 1, func: function() { return 'Hello'; } };
let copiedWithFunction = JSON.parse(JSON.stringify(originalWithFunction));
console.log(copiedWithFunction.func);
// undefined,函数丢失
- 手动实现深拷贝:可以通过递归的方式手动实现深拷贝,以处理各种复杂情况。以下是一个简单的深拷贝函数示例:
js
function deepCopy(obj) {
if (typeof obj!== 'object' || obj === null) {
return obj;
}
let copy = Array.isArray(obj)? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepCopy(obj[key]);
}
}
return copy;
}
let complexObject = { a: 1, b: [2, 3], c: { d: 4 } };
let deeplyCopied = deepCopy(complexObject);
// 修改深拷贝对象中的嵌套对象
deeplyCopied.c.d = 5;
console.log(complexObject.c.d);
// 4,原对象不受影响
在这个函数中,首先判断传入的obj
是否为对象或null
,如果不是则直接返回。然后根据obj
是数组还是对象创建相应的空容器copy
。接着通过for...in
循环遍历obj
的所有属性,并使用递归调用deepCopy
来复制属性值,确保所有嵌套的对象和数组都被深拷贝。
- 多方面考虑深拷贝 :考虑不同的数据类型,比如基本类型(
number
,string
,boolean
,null
,undefined
)、对象、数组、日期、正则表达式、Map
、Set
等等。不同的数据类型可能需要不同的处理方式。比如,处理日期对象时,应该创建一个新的Date
实例,而不是直接复制引用。然后,如何处理循环引用的问题。比如,如果一个对象的属性引用了自身,或者对象之间相互引用,普通的深拷贝方法可能会进入无限递归,导致栈溢出。这时候需要一种机制来记录已经拷贝过的对象,避免重复拷贝,可以使用WeakMap
来保存已拷贝的对象,键是原对象,值是拷贝后的对象。接下来,我需要思考如何递归地处理对象的属性。对于普通对象和数组,需要遍历它们的属性或元素,并对每个属性值进行深拷贝。对于其他特殊对象类型,比如RegExp
、Date
、Map
、Set
,需要分别处理。例如,正则表达式可以直接复制,但可能需要保留flags
和source
;Date
对象需要创建新的实例;Map
和Set
需要遍历其元素并进行深拷贝。以下是一个完整的深拷贝过程代码示例:
js
function deepClone(target, map = new WeakMap()) {
// 处理非对象类型(基本类型、函数)
if (typeof target !== 'object' || target === null) return target
// 处理循环引用
if (map.has(target)) return map.get(target)
// 处理特殊对象类型
switch (true) {
case target instanceof Date:
return new Date(target)
case target instanceof RegExp:
return new RegExp(target.source, target.flags)
case target instanceof Map:
return cloneMap(target, map)
case target instanceof Set:
return cloneSet(target, map)
case ArrayBuffer.isView(target): // 处理 TypedArray
return target.slice()
case target instanceof ArrayBuffer: // 处理 ArrayBuffer
return cloneArrayBuffer(target)
}
// 处理普通对象/数组
const clone = Array.isArray(target)
? []
: Object.create(Object.getPrototypeOf(target))
map.set(target, clone) // 记录已克隆对象
// 处理 Symbol 属性
Reflect.ownKeys(target).forEach(key => {
clone[key] = deepClone(target[key], map)
})
return clone
}
深拷贝和浅拷贝各有其适用场景。浅拷贝适用于只需要复制对象的顶层结构,且不担心共享引用带来的问题时;而深拷贝则适用于需要完全独立的对象副本,避免对新对象的修改影响原对象的情况。