← 中文资源

简体中文版本

旧运行时的 structuredClone 回退

优先使用原生 structuredClone,缺失时回退到支持循环引用、日期、正则、Map、Set 与类型化数组的深拷贝。

阅读英文原文

功能说明

该示例为需要深拷贝但不能假定运行时支持 structuredClone 的场景提供统一入口。运行环境有原生实现时直接委托,保留其完整语义与性能;没有时使用手写回退,支持普通对象、数组、日期、正则、错误、Map、Set、ArrayBuffer、DataView 与类型化数组,并用 WeakMap 处理循环引用。

关键代码

nativeClone 通过 globalThis 的可选类型断言探测函数,避免依赖特定 TypeScript lib 声明。回退函数先处理原始值与函数、Symbol,再查 WeakMap 判断是否已复制。容器在递归子元素前先登记到 seen,因此自引用结构能正确指回副本。类型化数组按 byteOffset 与 byteLength 切片,避免把整个底层 buffer 带入;Map、Set 与普通对象都先创建空副本再填充。

使用注意

原生 structuredClone 与回退在边缘类型上不完全一致,例如 Error 的 cause、访问器属性、不可枚举属性与 SharedArrayBuffer 的处理。回退遇到访问器或类实例会抛 CloneError,而不是静默丢失数据;无法克隆函数与 Symbol 与原生行为一致。跨 realm 对象可能因原型不同被拒绝。不要把该函数用于含大量数组的结构而不评估内存,两倍复制仍然存在。

验证方式

在强制走回退的测试中克隆嵌套对象与数组,断言深度相等且引用不同;构造自引用对象验证循环被保留;克隆 Date、RegExp、Map、Set 与带偏移的 Uint8Array,检查值与类型;修改副本不影响原对象;传入函数或自定义类实例断言 CloneError;当运行环境存在原生实现时,验证 deepClone 结果同样正确。

代码示例

export class CloneError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "DataCloneError";
  }
}

function nativeClone<T>(value: T): T | undefined {
  const candidate = (globalThis as { structuredClone?: <U>(input: U) => U }).structuredClone;
  return typeof candidate === "function" ? candidate(value) : undefined;
}

export function deepClone<T>(value: T): T {
  const cloned = nativeClone(value);
  return cloned === undefined ? (cloneFallback(value, new WeakMap()) as T) : cloned;
}

function cloneFallback(value: unknown, seen: WeakMap<object, unknown>): unknown {
  if (value === null) return null;
  const type = typeof value;
  if (type === "function" || type === "symbol") {
    throw new CloneError("value of type " + type + " cannot be cloned");
  }
  if (type !== "object") return value;

  const object = value as object;
  const existing = seen.get(object);
  if (existing !== undefined) return existing;

  if (value instanceof Date) return new Date(value.getTime());
  if (value instanceof RegExp) return new RegExp(value.source, value.flags);
  if (value instanceof Error) {
    const copy = new Error(value.message);
    copy.name = value.name;
    copy.stack = value.stack;
    seen.set(object, copy);
    return copy;
  }
  if (value instanceof ArrayBuffer) return value.slice(0);
  if (value instanceof DataView) {
    const buffer = value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
    return new DataView(buffer);
  }
  if (ArrayBuffer.isView(value)) {
    const view = value as unknown as {
      buffer: ArrayBuffer;
      byteOffset: number;
      byteLength: number;
      constructor: new (buffer: ArrayBuffer) => object;
    };
    const buffer = view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
    return new view.constructor(buffer);
  }
  if (value instanceof Map) {
    const copy = new Map<unknown, unknown>();
    seen.set(object, copy);
    for (const [key, entry] of value) {
      copy.set(cloneFallback(key, seen), cloneFallback(entry, seen));
    }
    return copy;
  }
  if (value instanceof Set) {
    const copy = new Set<unknown>();
    seen.set(object, copy);
    for (const entry of value) copy.add(cloneFallback(entry, seen));
    return copy;
  }
  if (Array.isArray(value)) {
    const copy: unknown[] = [];
    seen.set(object, copy);
    for (const item of value) copy.push(cloneFallback(item, seen));
    return copy;
  }

  const prototype = Object.getPrototypeOf(value);
  if (prototype !== Object.prototype && prototype !== null) {
    throw new CloneError("unsupported object type");
  }
  const copy: Record<PropertyKey, unknown> = Object.create(prototype);
  seen.set(object, copy);
  for (const key of Reflect.ownKeys(object)) {
    const descriptor = Object.getOwnPropertyDescriptor(object, key);
    if (!descriptor || !descriptor.enumerable) continue;
    if (!("value" in descriptor)) {
      throw new CloneError("accessor properties cannot be cloned");
    }
    copy[key] = cloneFallback(descriptor.value, seen);
  }
  return copy;
}