从 Object.assign() 到手写深拷贝 —— 面试官视角下的“深浅拷贝”必考题

364 阅读5分钟

深度解析:从 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));

优点:代码简洁,能处理多层嵌套对象。

缺点:存在多个边界问题,不能用于生产环境

  1. 会丢失 undefinedfunctionSymbol

    const obj = { a: undefined, b: function() {}, c: Symbol('c') };
    const copy = JSON.parse(JSON.stringify(obj));
    console.log(copy); // { a: null, b: undefined, c: undefined } ❌
    
  2. 不能处理循环引用(Circular Reference)

    const obj = { name: 'Alice' };
    obj.self = obj; // 循环引用
    JSON.stringify(obj); // TypeError: Converting circular structure to JSON
    
  3. Date 对象会变成字符串

    const obj = { date: new Date() };
    const copy = JSON.parse(JSON.stringify(obj));
    console.log(copy.date instanceof Date); // false
    
  4. RegExpError 等特殊对象也会出问题

结论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识别 DateRegExp 等内置对象
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

五、面试官的考察逻辑(“表演时间”)

面试官不是在背书,而是在看你的思维过程

  1. API 细节 → 你是否真正理解 Object.assign() 的返回值?
  2. 业务场景 → 你在项目中是否遇到过浅拷贝导致的 bug?
  3. 底层原理 → 你是否理解栈、堆、引用传递?
  4. 边界处理 → 你是否考虑 undefinedfunction、循环引用?
  5. 代码实现 → 你能否写出健壮、可维护的递归函数?

六、总结:一张表看懂所有区别

方法是否深拷贝支持函数支持 undefined支持 Symbol支持循环引用保留原型
Object.assign()❌ 浅拷贝
JSON.parse(JSON.stringify())✅(有限)
手写深拷贝

面试加分回答

“在实际开发中,我优先使用 const 和不可变数据结构来避免副作用。对于深拷贝需求,我会根据场景选择:简单数据用 JSON 方法,复杂对象使用 Lodash 的 cloneDeep,或在关键路径手写健壮版本。同时,我会通过 WeakMap 缓存和类型判断来确保性能与正确性。”

掌握这些,你不仅能通过面试,更能写出更安全、可维护的代码。