深度解析:从 Object.assign() 到手写深拷贝 —— 面试官视角下的“深浅拷贝”必考题
在 JavaScript 面试中,深拷贝与浅拷贝是几乎必问的核心知识点。它不仅是对语言特性的考察,更是对候选人内存模型理解、边界情况处理、递归思维和代码实现能力的综合检验。
面试官往往以 Object.assign() 为切入点,逐步引导你深入,最终考察你是否能手写一个健壮的深拷贝函数。本文将从面试官的视角出发,带你完整走一遍这场“表演”。
一、从 Object.assign() 开场:浅拷贝的起点
面试官:
Object.assign()是做什么的?它是深拷贝吗?
✅ 标准回答(展示你的基本功)
Object.assign(target, ...sources) 方法用于将一个或多个源对象的所有可枚举属性复制到目标对象,并返回目标对象本身(不是新对象)。
const target = { a: 1 };
const source = { b: 2 };
const result = Object.assign(target, source);
console.log(result === target); // true,返回的是 target 本身
因此:
- ❌
Object.assign()不是深拷贝。 - ✅ 它实现的是浅拷贝:只复制第一层属性,嵌套对象仍然是引用传递。
const obj1 = { a: 1, nested: { b: 2 } };
const obj2 = Object.assign({}, obj1);
obj2.nested.b = 3;
console.log(obj1.nested.b); // 3!源对象也被修改了
关键点:
Object.assign()返回的是目标对象的引用,而不是一个全新的对象。若想返回新对象,应传入空对象{}作为目标。
二、浅拷贝 vs 深拷贝:赋值与引用的本质
面试官:为什么
obj2.nested.b = 3会修改obj1?这背后的原理是什么?
✅ 深入底层:内存模型与数据类型
JavaScript 中的数据类型分为两类:
| 类型 | 存储位置 | 赋值行为 |
|---|---|---|
基本类型number, string, boolean, null, undefined, symbol, bigint | 栈内存(Stack) | 值传递:复制实际值,互不影响 |
引用类型object, array, function, Date | 堆内存(Heap) | 引用传递:复制指针地址,共享同一内存 |
let a = 10;
let b = a; // 值复制
b = 20;
console.log(a); // 10
let obj1 = { name: 'Alice' };
let obj2 = obj1; // 引用复制
obj2.name = 'Bob';
console.log(obj1.name); // 'Bob'!因为 obj1 和 obj2 指向同一个对象
浅拷贝:只复制第一层的值或引用。
深拷贝:递归复制所有层级,彻底断开引用关系。
三、简单深拷贝:JSON.parse(JSON.stringify())
面试官:有没有简单的深拷贝方法?
✅ 回答(并主动暴露问题)
最简单的方法是:
const deepCopy = JSON.parse(JSON.stringify(obj));
✅ 优点:代码简洁,能处理多层嵌套对象。
❌ 缺点:存在多个边界问题,不能用于生产环境:
-
会丢失
undefined、function、Symbolconst obj = { a: undefined, b: function() {}, c: Symbol('c') }; const copy = JSON.parse(JSON.stringify(obj)); console.log(copy); // { a: null, b: undefined, c: undefined } ❌ -
不能处理循环引用(Circular Reference)
const obj = { name: 'Alice' }; obj.self = obj; // 循环引用 JSON.stringify(obj); // TypeError: Converting circular structure to JSON -
Date对象会变成字符串const obj = { date: new Date() }; const copy = JSON.parse(JSON.stringify(obj)); console.log(copy.date instanceof Date); // false -
RegExp、Error等特殊对象也会出问题
结论:
JSON.parse(JSON.stringify())仅适用于纯数据对象(POJO),且不含函数、undefined、循环引用等特殊情况。
四、高级深拷贝:手写实现(面试官的“钩子”)
面试官:那你能手写一个深拷贝函数吗?
这是面试的高潮部分。你需要展示递归思维、类型判断、边界处理能力。
✅ 手写深拷贝(健壮版本)
function deepClone(target, map = new WeakMap()) {
// 处理 null 和 基本类型
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 clone = Array.isArray(target) ? [] : Object.create(Object.getPrototypeOf(target));
map.set(target, clone); // 缓存当前对象,防止循环引用
// 递归拷贝所有可枚举属性(包括 Symbol)
const symbols = Object.getOwnPropertySymbols(target);
[...Object.keys(target), ...symbols].forEach(key => {
clone[key] = deepClone(target[key], map);
});
return clone;
}
✅ 关键点解析
| 技术点 | 说明 |
|---|---|
typeof target !== 'object' | 排除 null 和基本类型 |
instanceof | 识别 Date、RegExp 等内置对象 |
WeakMap | 缓存已拷贝对象,解决循环引用(WeakMap 不阻止垃圾回收) |
Object.create(proto) | 保留原型链,比 {} 更准确 |
Object.getOwnPropertySymbols() | 拷贝 Symbol 属性 |
Object.keys() + getOwnPropertySymbols() | 遍历所有可枚举属性 |
✅ 测试用例
const obj = {
a: 1,
b: { c: 2 },
d: undefined,
e: function() {},
f: Symbol('f'),
g: new Date(),
h: /abc/
};
obj.self = obj; // 循环引用
const copy = deepClone(obj);
console.log(copy.b === obj.b); // false
console.log(copy.self === copy); // true
console.log(copy.g instanceof Date); // true
五、面试官的考察逻辑(“表演时间”)
面试官不是在背书,而是在看你的思维过程:
- API 细节 → 你是否真正理解
Object.assign()的返回值? - 业务场景 → 你在项目中是否遇到过浅拷贝导致的 bug?
- 底层原理 → 你是否理解栈、堆、引用传递?
- 边界处理 → 你是否考虑
undefined、function、循环引用? - 代码实现 → 你能否写出健壮、可维护的递归函数?
六、总结:一张表看懂所有区别
| 方法 | 是否深拷贝 | 支持函数 | 支持 undefined | 支持 Symbol | 支持循环引用 | 保留原型 |
|---|---|---|---|---|---|---|
Object.assign() | ❌ 浅拷贝 | ✅ | ✅ | ❌ | ✅ | ✅ |
JSON.parse(JSON.stringify()) | ✅(有限) | ❌ | ❌ | ❌ | ❌ | ❌ |
| 手写深拷贝 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
面试加分回答
“在实际开发中,我优先使用
const和不可变数据结构来避免副作用。对于深拷贝需求,我会根据场景选择:简单数据用JSON方法,复杂对象使用 Lodash 的cloneDeep,或在关键路径手写健壮版本。同时,我会通过WeakMap缓存和类型判断来确保性能与正确性。”
掌握这些,你不仅能通过面试,更能写出更安全、可维护的代码。