编码母题 / P2 / Node 与 JavaScript / 03

支持循环引用的深拷贝

先读清题意与边界,独立尝试,再展开思路并对照可运行的参考代码。

重要程度岗位专项难度困难预计完成50–70 分钟

难度按 LeetCode 简单/中等/困难三档估计;工程题并非 LeetCode 官方题目。用时指独立编码并检查主要边界的参考时间,因经验而异。

题目是什么

深拷贝普通对象、集合和常见内置对象,保留循环与共享引用;普通对象还需保留原型、Symbol key 和属性描述符。明确函数等不可复制值的边界。

输入输出示例

同一个 ArrayBuffer 被两个视图共享时,克隆后的两个视图也必须共享同一份新缓冲区。

练习起手式与验证

复制代码后补全空方法,再运行下方三组验证;复杂题所需的辅助类型与输入准备一并放在代码块中。

/*
 * 题目:支持循环引用的深拷贝
 * 要求:深拷贝普通对象、集合和常见内置对象,保留循环与共享引用;普通对象还需保留原型、
 * Symbol key 和属性描述符。明确函数等不可复制值的边界。
 * 示例:同一个 ArrayBuffer 被两个视图共享时,
 * 克隆后的两个视图也必须共享同一份新缓冲区。
 */
function deepClone(value, seen = new WeakMap()) {}

const symbol = Symbol("s");
const shared = { n: 1 };
const source = Object.create({ inherited: true });
Object.assign(source, {
  sharedA: shared,
  sharedB: shared,
  map: new Map([[shared, new Set([shared])]]),
  date: new Date(1),
});
source.self = source;
source[symbol] = /a/gi;
const copy = deepClone(source);
console.log(copy !== source); // 预期:true
console.log(copy.self); // 预期:copy
console.log(copy.sharedA); // 预期:copy.sharedB

解题思路

展开思路、不变量与常见边界

关键切入点:WeakMap 在递归前登记源与目标;视图必须复用同一个已克隆缓冲区。

核心模型:WeakMap 必须在递归子属性前记录 source->target,否则自引用无限递归。 使用 Reflect.ownKeys 与 descriptor,数据属性递归复制 value,访问器保留 getter/ setter。时间和空间 O(n),n 为可达属性及集合元素数。

参考代码与验证

“关键机制”注释用于解释实现,下载链接提供未经页面注释修改的原始 JS 文件。

// 关键机制:WeakMap 在递归前登记源与目标;视图必须复用同一个已克隆缓冲区。
function deepClone(value, seen = new WeakMap()) {
  if (value === null || typeof value !== "object") return value;
  if (seen.has(value)) return seen.get(value);
  if (value instanceof Date) {
    const copy = new Date(value.getTime());
    seen.set(value, copy);
    return copy;
  }
  if (value instanceof RegExp) {
    const copy = new RegExp(value.source, value.flags);
    copy.lastIndex = value.lastIndex;
    seen.set(value, copy);
    return copy;
  }
  if (value instanceof ArrayBuffer) {
    const copy = value.slice(0);
    seen.set(value, copy);
    return copy;
  }
  if (ArrayBuffer.isView(value)) {
    const buffer = deepClone(value.buffer, seen);
    const copy = value instanceof DataView
      ? new DataView(buffer, value.byteOffset, value.byteLength)
      : new value.constructor(buffer, value.byteOffset, value.length);
    seen.set(value, copy);
    return copy;
  }
  if (
    value instanceof WeakMap ||
    value instanceof WeakSet ||
    value instanceof Promise
  )
    return value;
  if (value instanceof Map) {
    const copy = new Map();
    seen.set(value, copy);
    for (const [k, v] of value)
      copy.set(deepClone(k, seen), deepClone(v, seen));
    return copy;
  }
  if (value instanceof Set) {
    const copy = new Set();
    seen.set(value, copy);
    for (const item of value) copy.add(deepClone(item, seen));
    return copy;
  }
  const copy = Array.isArray(value)
    ? []
    : Object.create(Object.getPrototypeOf(value));
  seen.set(value, copy);
  for (const key of Reflect.ownKeys(value)) {
    const descriptor = Object.getOwnPropertyDescriptor(value, key);
    if (Object.hasOwn(descriptor, "value"))
      descriptor.value = deepClone(descriptor.value, seen);
    Object.defineProperty(copy, key, descriptor);
  }
  return copy;
}

// 三组以上可直接运行的验证。
const symbol = Symbol("s");
const shared = { n: 1 };
const source = Object.create({ inherited: true });
Object.assign(source, {
  sharedA: shared,
  sharedB: shared,
  map: new Map([[shared, new Set([shared])]]),
  date: new Date(1),
});
source.self = source;
source[symbol] = /a/gi;
const copy = deepClone(source);
console.log(copy !== source); // 预期:true
console.log(copy.self); // 预期:copy
console.log(copy.sharedA); // 预期:copy.sharedB
console.log(copy.sharedA !== shared); // 预期:true
console.log(copy.map.get(copy.sharedA).has(copy.sharedA)); // 预期:true
console.log(copy[symbol].flags); // 预期:"gi"
console.log(Object.getPrototypeOf(copy)); // 预期:Object.getPrototypeOf(source)
const buffer = new ArrayBuffer(8);
const bytes = new Uint8Array(buffer, 1, 3);
const data = new DataView(buffer, 2, 2);
const views = deepClone({ buffer, bytes, data, again: bytes });
console.log(views.buffer !== buffer); // 预期:true
console.log(views.bytes.buffer); // 预期:views.buffer
console.log(views.data.buffer); // 预期:views.buffer
console.log(views.again); // 预期:views.bytes
console.log(views.bytes.byteOffset); // 预期:1
console.log(views.data.byteLength); // 预期:2
const date = new Date(1);
const dates = deepClone([date, date]);
console.log(dates[0]); // 预期:dates[1]

复杂度分析

时间复杂度:O(n)。每个可达对象、属性及集合元素只克隆有限次。

空间复杂度:O(n)。WeakMap 和新对象图均与可达结构规模成正比。

下载原始 JS 文件