浅拷贝
浅拷贝是创建一个新对象,这个新对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是引用类型,拷贝的就是内存地址。
常见的浅拷贝方法
1. Object.assign()
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = Object.assign({}, obj1);
console.log(obj2.b === obj1.b); // true (引用的是同一个对象)
obj2.a = 10;
obj2.b.c = 20;
console.log(obj1.a); // 1 (基本类型不受影响)
console.log(obj1.b.c); // 20 (引用类型被修改了)
2. 展开语法 (...)
const arr1 = [1, 2, { a: 3 }];
const arr2 = [...arr1];
console.log(arr2[2] === arr1[2]); // true
arr2[0] = 10;
arr2[2].a = 30;
console.log(arr1[0]); // 1
console.log(arr1[2].a); // 30
3. Array.prototype.slice() / Array.prototype.concat()
这些数组方法返回的都是一个新的浅拷贝数组。
深拷贝
深拷贝就是将一个对象在内存中在拷贝一份,两个对象相互不独立,不影响
1. JSON.parse(JSON.stringify(obj)) - 简单但有缺陷
这是最简单、最常用的“抖机灵”式深拷贝方法。
const obj1 = {
a: 1,
b: { c: 2 },
d: new Date(),
e: function() {},
f: undefined,
g: Symbol('g')
};
const obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj2.b === obj1.b); // false (已经是不同的对象了)
obj2.b.c = 20;
console.log(obj1.b.c); // 2 (未受影响)
优点:
- 代码极其简单,一行搞定。
- 能处理绝大多数只包含 JSON 安全数据(字符串、数字、布尔、数组、普通对象)的场景。
致命缺陷:
- 会忽略 undefined、Symbol 和函数。
- 无法处理循环引用(一个对象的属性直接或间接引用了自身),会抛出 TypeError。
- 日期对象 (Date) 会被转换为字符串。
- 正则表达式 (RegExp) 会被转换为空对象。
2. structuredClone() - 现代浏览器的“官方答案”
这是一个新的、内置在浏览器和 Node.js 中的全局函数,专门用于深拷贝。
const obj1 = {
a: 1,
b: { c: 2 },
d: new Date(),
e: /abc/g
};
const obj2 = structuredClone(obj1);
console.log(obj2.b === obj1.b); // false
console.log(obj2.d); // Date 对象被正确拷贝
console.log(obj2.e); // RegExp 对象被正确拷贝
优点:
- 官方标准,语义清晰。
- 功能强大:支持循环引用、支持多种内置类型(Date, RegExp, Map, Set, Blob, File 等)。
- 性能优秀:底层由浏览器 C++ 实现,通常比手写的 JS 递归拷贝快得多。
缺点:
- 不能拷贝函数:会抛出 DataCloneError。
- 不能拷贝原型链。
- 兼容性:是一个较新的 API,不支持一些老旧浏览器。
3. 使用第三方库,如 lodash.cloneDeep
这是生产环境中最可靠、最常用的选择。
import { cloneDeep } from 'lodash-es';
const obj1 = { a: 1, b: { c: 2 }, e: function() {} };
const obj2 = cloneDeep(obj1);
console.log(obj2.b === obj1.b); // false
console.log(typeof obj2.e); // "function" (函数也被正确处理)
优点:
- 功能最全面:处理了几乎所有的边缘情况,包括循环引用、各种内置类型、函数、原型链等。
- 久经考验:在无数项目中被验证过,非常稳定。 面试官让你手写,是想考察你的递归思想、类型判断能力和对边缘情况的处理。
下面是一个逐步完善的手写版本,你可以根据面试的时间和要求,展示不同层次的实现。
版本一:基础递归版 (只考虑普通对象和数组)
codeJavaScript
function deepClone(target) {
// 1. 处理基本类型和 null
if (target === null || typeof target !== 'object') {
return target;
}
// 2. 判断是数组还是对象
const newObj = Array.isArray(target) ? [] : {};
// 3. 遍历 target 的所有属性
for (const key in target) {
// 4. 只拷贝自有属性,避免拷贝原型链上的属性
if (Object.prototype.hasOwnProperty.call(target, key)) {
// 5. 递归调用 deepClone
newObj[key] = deepClone(target[key]);
}
}
return newObj;
}
讲解要点:
- 递归出口:首先处理非对象的情况,直接返回值。
- 类型判断:根据 Array.isArray 创建新的空数组或空对象。
- 遍历:使用 for...in 遍历所有可枚举属性。
- hasOwnProperty:关键一步!确保我们只拷贝对象自己的属性,而不是继承来的。
- 递归:对每个属性的值,再次调用 deepClone,实现深层拷贝。
版本二:处理循环引用 (进阶版)
上面的版本如果遇到循环引用,会陷入无限递归导致栈溢出。我们需要一个方法来“记住”已经拷贝过的对象。
codeJavaScript
function deepClone(target, map = new WeakMap()) {
if (target === null || typeof target !== 'object') {
return target;
}
// 1. 解决循环引用
// 检查 map 中是否已经有当前对象的拷贝记录
if (map.has(target)) {
return map.get(target);
}
const newObj = Array.isArray(target) ? [] : {};
// 2. 将新创建的对象存入 map,key 是原始对象,value 是新对象
map.set(target, newObj);
for (const key in target) {
if (Object.prototype.hasOwnProperty.call(target, key)) {
newObj[key] = deepClone(target[key], map); // 3. 递归时传递 map
}
}
return newObj;
}
讲解要点:
- 使用 WeakMap:WeakMap 的键是弱引用,当原始对象没有其他引用时,GC 可以回收它,避免了因拷贝而产生的内存泄漏。这是处理循环引用的最佳实践。
- 存取记录:在拷贝一个对象之前,先检查 map 里有没有。如果有,直接返回之前创建的拷贝。如果没有,就创建一个新的,并立即存入 map,然后再进行递归。
版本三:处理更多类型 (完整版)
一个更完整的实现,还需要考虑 Date, RegExp 等内置类型。
codeJavaScript
function deepClone(target, map = new WeakMap()) {
if (target === null || typeof target !== 'object') {
return target;
}
// 处理 Date
if (target instanceof Date) {
return new Date(target);
}
// 处理 RegExp
if (target instanceof RegExp) {
return new RegExp(target);
}
if (map.has(target)) {
return map.get(target);
}
const newObj = Array.isArray(target) ? [] : {};
map.set(target, newObj);
// 使用 Reflect.ownKeys 可以拷贝 Symbol 类型的 key
for (const key of Reflect.ownKeys(target)) {
newObj[key] = deepClone(target[key], map);
}
return newObj;
}
讲解要点:
- 类型检查:在递归前,增加对 Date, RegExp 等特殊对象的 instanceof 判断,并调用它们各自的构造函数来创建新实例。
- 遍历优化:使用 Reflect.ownKeys 代替 for...in + hasOwnProperty,可以拷贝包括 Symbol 类型在内的所有自有属性。