在编程中,深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是两个重要的概念,特别是在处理对象或数组时。它们的主要区别在于如何处理对象或数组中的引用类型(如对象、数组等)。
浅拷贝(Shallow Copy)
浅拷贝会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是引用类型,拷贝的就是内存地址,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
在JavaScript中,我们可以使用Object.assign()
方法或展开运算符(...
)来进行浅拷贝。
使用Object.assign()
进行浅拷贝:
let obj1 = {a: 1,b: { c: 2 }
};let obj2 = Object.assign({}, obj1);obj2.b.c = 3;console.log(obj1); // { a: 1, b: { c: 3 } }
console.log(obj2); // { a: 1, b: { c: 3 } }
深拷贝(Deep Copy)
深拷贝会创建一个新的对象,并递归地复制对象的所有属性及其子对象,直到它们都是基本类型为止。这样,新对象与原始对象没有任何关联。
在JavaScript中,没有内置的函数可以直接进行深拷贝,但我们可以使用JSON方法(但这种方法有局限性,比如不能处理函数和循环引用)或者手动实现一个深拷贝函数。
使用JSON方法进行深拷贝(有局限性):
let obj1 = {a: 1,b: { c: 2 }
};let obj2 = JSON.parse(JSON.stringify(obj1));obj2.b.c = 3;console.log(obj1); // { a: 1, b: { c: 2 } }
console.log(obj2); // { a: 1, b: { c: 3 } }
在 JSON.stringify()
和 JSON.parse()
的上下文中,不能处理函数和循环引用指的是:
- 函数(Functions):
当您尝试使用JSON.stringify()
将一个包含函数的 JavaScript 对象转换为 JSON 字符串时,该函数将不会被转换为字符串的一部分。在 JSON 规范中,函数不是有效的数据类型,因此它们会被忽略。如果您在对象中有一个函数属性,并且您尝试将其转换为 JSON 字符串,那么这个函数属性将不会出现在生成的字符串中。
例如:
const obj = {name: "John",greet: function() {console.log("Hello, " + this.name);}
};const jsonString = JSON.stringify(obj);
console.log(jsonString); // 输出: {"name":"John"}
如上所示,greet
函数没有出现在输出的 JSON 字符串中。
- 循环引用(Circular References):
在 JavaScript 中,对象可以通过属性相互引用,形成循环引用。当您尝试使用JSON.stringify()
转换包含循环引用的对象时,它会抛出一个错误,因为 JSON 格式不支持循环引用。
例如:
const obj1 = {};
const obj2 = { ref: obj1 };
obj1.alsoRef = obj2;try {const jsonString = JSON.stringify(obj1);
} catch (error) {console.error(error); // 抛出错误,因为存在循环引用
}
在这个例子中,obj1
和 obj2
通过 ref
和 alsoRef
属性相互引用,形成了一个循环。当您尝试使用 JSON.stringify()
转换这个对象时,它会抛出一个错误。
为了避免循环引用的问题,您可以使用一个 replacer
函数来排除或处理循环引用。但是,请注意,即使您使用 replacer
函数,您也无法在 JSON 字符串中保留循环引用的结构,因为 JSON 格式本身不支持这种结构。
对于 JSON.parse()
来说,它不会遇到循环引用的问题,因为它只是将有效的 JSON 字符串转换回 JavaScript 对象。但是,如果您从某个源(如服务器)接收到包含无效循环引用的 JSON 字符串,并且尝试使用 JSON.parse()
解析它,那么它将抛出一个 SyntaxError
。在正常的 JSON 字符串中,您不会遇到循环引用,因为 JSON 格式不支持它们。
手动实现深拷贝(递归方法):
function deepCopy(obj, hash = new WeakMap()) {if (typeof obj !== 'object' || obj === null) {return obj;}if (hash.has(obj)) {return hash.get(obj);}let copy = Array.isArray(obj) ? [] : {};hash.set(obj, copy);for (let key in obj) {if (obj.hasOwnProperty(key)) {copy[key] = deepCopy(obj[key], hash);}}return copy;
}let obj1 = {a: 1,b: { c: 2 }
};let obj2 = deepCopy(obj1);obj2.b.c = 3;console.log(obj1); // { a: 1, b: { c: 2 } }
console.log(obj2); // { a: 1, b: { c: 3 } }
这个手动实现的深拷贝函数可以处理对象和数组,并且可以处理循环引用。它使用了一个WeakMap
来存储已经拷贝过的对象,以便在遇到循环引用时能够返回正确的拷贝。
手写深拷贝
deepClone
函数是一个实现了深拷贝功能的函数,它递归地遍历对象并复制其属性,包括数组和嵌套对象。同时,添加了递归深度的打印以及属性的克隆过程,这有助于理解函数是如何工作的。
简要分析:
deepClone
函数接受两个参数:obj
(要克隆的对象)和depth
(当前递归的深度,默认为0)。- 使用
console.log
打印当前递归的深度和被克隆的对象,这对于调试和理解函数执行过程非常有用。 - 检查
obj
是否不是对象或是否为null
,如果是,则直接返回obj
本身(基本数据类型和null
的值传递)。 - 初始化
result
变量,根据obj
的类型(数组或对象)来创建一个新的空数组或空对象。 - 使用
for...in
循环遍历obj
的所有可枚举属性。 - 使用
hasOwnProperty
方法检查属性是否是obj
自身的属性(而不是继承自原型链的属性)。 - 对于每个属性,递归调用
deepClone
函数来克隆属性的值,并将结果存储在新的result
对象中。 - 返回
result
,即克隆后的对象。
在示例使用部分,您创建了一个包含各种类型属性的对象 original
,并使用 deepClone
函数克隆了它。然后,您打印了原始对象和克隆后的对象,以验证深拷贝是否成功。
执行这段代码,您应该会在控制台看到递归深度和对象被克隆的过程,以及原始对象和克隆后的对象的内容。由于 deepClone
函数实现了深拷贝,所以原始对象和克隆后的对象在内存中是独立的,修改其中一个不会影响另一个。
基础版本
会写基础版本的就够了,对于有工作经验的人还要考虑Map,Sety以及循环引用
function deepClone(obj, depth = 0) {console.log('depth:', depth, 'value:', obj);if (typeof obj !== 'object' || obj === null) {return obj;}let result;if (Array.isArray(obj)) {result = [];} else {result = {};}for (let key in obj) {if (obj.hasOwnProperty(key)) {const value = obj[key];console.log(`Cloning property ${key}`)result[key] = deepClone(value, depth + 1)}}return result;
}
// 示例使用
const original = {number: 1,bool: true,str: 'string',array: [1, 2, 3],obj: { child: 'child', father: { child_1: 'father_1' } }
};const cloned = deepClone(original);// console.log('Original:', original);
console.log('Cloned:', cloned);
PS D:\练\js\手写\13-深拷贝> node .\lian.js\
depth: 0 value: {number: 1,bool: true,str: 'string',array: [ 1, 2, 3 ],obj: { child: 'child', father: { child_1: 'father_1' } }
}
Cloning property number
depth: 1 value: 1
Cloning property bool
depth: 1 value: true
Cloning property str
depth: 1 value: string
Cloning property array
depth: 1 value: [ 1, 2, 3 ]
Cloning property 0
depth: 2 value: 1
Cloning property 1
depth: 2 value: 2
Cloning property 2
depth: 2 value: 3
Cloning property obj
depth: 1 value: { child: 'child', father: { child_1: 'father_1' } }
Cloning property child
depth: 2 value: child
Cloning property father
depth: 2 value: { child_1: 'father_1' }
Cloning property child_1
depth: 3 value: father_1
Cloned: {number: 1,bool: true,str: 'string',array: [ 1, 2, 3 ],obj: { child: 'child', father: { child_1: 'father_1' } }
}
提供的 deepClone 函数虽然能够处理大部分基本的深拷贝场景,但它确实有一些潜在的缺陷和限制:
- 循环引用:该函数没有处理循环引用的逻辑。如果对象中存在循环引用(即对象属性直接或间接地引用了自己),函数会陷入无限递归,导致栈溢出错误。
- 特殊类型:该函数没有处理如 Date、RegExp、Function、Error、Map、Set、BigInt、Symbol 等特殊类型的对象。这些类型的对象在直接复制时可能无法保持其原始状态或行为。
- 性能:对于非常大的对象或深度嵌套的对象,递归可能会导致性能问题。虽然现代JavaScript引擎对递归做了优化,但在某些情况下,使用循环而非递归可能会更有效。
- 不可枚举属性:for...in 循环只遍历对象自身的可枚举属性。如果对象有不可枚举的属性(例如通过 Object.defineProperty 定义的属性),这些属性将不会被复制到新对象中。
- getter/setter:如果对象的属性是通过 getter/setter 方法定义的,那么简单地复制属性值可能不是您想要的行为。您可能希望在新对象上也保持这些 getter/setter 方法。
- Buffer 和其他类型化数组:如果对象包含 Node.js 中的 Buffer 或其他类型化数组(如 Uint8Array),则简单的复制可能不会按预期工作。
- 原型链:该函数只复制了对象自身的属性,而没有复制原型链上的属性。在某些情况下,您可能希望保持原型链的完整性。
- 未考虑 null 和 undefined 作为对象属性的值:虽然函数处理了 null 和 undefined 作为整体输入的情况,但如果它们作为对象的属性值出现(例如 { prop: null } 或 { prop: undefined }),则会被正常复制。但在某些情况下,您可能希望对这些值进行特殊处理。
为了解决这些问题,您可能需要扩展 deepClone 函数以包含额外的逻辑来处理上述特殊情况。另外,也有一些现成的库(如 lodash 的 _.cloneDeep 方法)提供了更强大和灵活的深拷贝功能。
手写浅拷贝
手写浅拷贝(Shallow Copy)通常指的是复制对象的顶层属性,而不是递归地复制对象的所有子属性。在 JavaScript 中,浅拷贝可以通过多种方式实现,包括使用扩展运算符(...)、Object.assign() 方法,或者通过循环遍历对象的属性。
以下是几种实现浅拷贝的方法:
1. 使用扩展运算符(Spread Operator)
function shallowCopy1(obj) { return {...obj};
}
2. 使用 Object.assign() 方法
function shallowCopy2(obj) { return Object.assign({}, obj);
}
3. 使用循环遍历属性
function shallowCopy3(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] = obj[key]; } } return copy;
}
请注意,这些方法在处理数组、对象以及基本类型时表现良好,但它们都是浅拷贝,这意味着如果对象的属性值是另一个对象或数组,那么新对象和原对象将引用相同的子对象或数组。
示例
const original = { a: 1, b: { c: 2 }, d: [3, 4]
}; const copy1 = shallowCopy1(original);
const copy2 = shallowCopy2(original);
const copy3 = shallowCopy3(original); // 修改原始对象的子对象或数组
original.b.c = 3;
original.d.push(5); // 浅拷贝的对象也会受到影响,因为它们引用了相同的子对象或数组
console.log(copy1.b.c); // 输出 3
console.log(copy2.d); // 输出 [3, 4, 5]
console.log(copy3.b.c); // 输出 3
在这个示例中,由于浅拷贝,copy1、copy2 和 copy3 的 b 属性和 d 属性分别引用了与 original 相同的对象和数组。因此,当修改 original 的这些属性时,浅拷贝的对象也会受到影响。