在编程中,深拷贝(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 的这些属性时,浅拷贝的对象也会受到影响。