JavaScript 中的结构化克隆算法:处理复杂对象图与循环引用的复制

JavaScript 中的结构化克隆算法:处理复杂对象图与循环引用的复制

各位同仁,大家好。今天我们将深入探讨 JavaScript 中一个至关重要且充满挑战的主题:结构化克隆(Structured Cloning)。在日常开发中,我们经常需要复制对象。然而,简单的复制操作往往不足以应对复杂的数据结构,特别是当对象图包含嵌套、特殊类型数据以及令人头疼的循环引用时。理解并正确应用结构化克隆算法,是驾驭这些复杂场景的关键。

一、浅拷贝与深拷贝:问题的起点

在讨论结构化克隆之前,我们必须先明确“拷贝”的两种基本形式:浅拷贝(Shallow Copy)和深拷贝(Deep Copy)。

1. 浅拷贝

浅拷贝只复制对象的第一层属性。如果属性的值是基本类型(如字符串、数字、布尔值、nullundefinedSymbolBigInt),那么会直接复制这些值。但如果属性的值是引用类型(如对象、数组),那么复制的将是引用本身,而不是引用指向的实际对象。这意味着原对象和新对象的引用类型属性将指向同一个内存地址,修改其中一个会影响另一个。

常见浅拷贝方法:

  • Object.assign({}, originalObject)
  • 扩展运算符(Spread operator):{ ...originalObject }[ ...originalArray ]
  • Array.prototype.slice()
  • Array.prototype.concat()

示例:

const original = {
    a: 1,
    b: { c: 2 },
    d: [3, 4]
};

const shallowCopy = { ...original };

console.log(shallowCopy); // { a: 1, b: { c: 2 }, d: [3, 4] }

shallowCopy.a = 10;
shallowCopy.b.c = 20;
shallowCopy.d.push(5);

console.log(original.a);    // 1 (基本类型,不受影响)
console.log(original.b.c);  // 20 (引用类型,受影响)
console.log(original.d);    // [3, 4, 5] (引用类型,受影响)

2. 深拷贝

深拷贝则会递归地复制对象及其所有嵌套的引用类型属性,直到所有属性都是基本类型。这样,原对象和新对象在内存中是完全独立的,互不影响。

我们的目标,正是实现一种健壮的深拷贝,而结构化克隆算法就是实现这一目标的核心机制。

二、传统深拷贝方法的局限性

structuredClone API 出现之前,JavaScript 开发者通常使用一些“技巧”来尝试实现深拷贝,但它们各有缺陷。

1. JSON.parse(JSON.stringify(object))

这是一种非常流行的深拷贝“黑科技”。它将对象序列化为 JSON 字符串,然后再解析回 JavaScript 对象。

const obj = {
    a: 1,
    b: { c: 2 },
    d: [3, 4]
};

const deepCopy = JSON.parse(JSON.stringify(obj));

console.log(deepCopy); // { a: 1, b: { c: 2 }, d: [3, 4] }

deepCopy.b.c = 20;
console.log(obj.b.c); // 2 (原对象不受影响,深拷贝成功)

然而,这种方法存在严重局限性:

  • 无法处理循环引用: 如果对象图中存在循环引用,JSON.stringify 会抛出 TypeError: Converting circular structure to JSON 错误。
  • 丢失特定类型数据:
    • Date 对象会转换为 ISO 格式的字符串,而不是 Date 对象本身。
    • RegExp 对象会转换为 {} 空对象。
    • MapSet 对象会转换为 {} 空对象。
    • undefinedSymbol、函数(function)会被忽略或转换为 null(在数组中为 null,在对象属性中直接删除)。
    • BigInt 会抛出 TypeError
  • 无法处理原型链: JSON.stringify 不会保留对象的原型链。所有属性都会被复制到新的普通对象上。
  • 无法处理不可枚举属性: JSON.stringify 只会处理可枚举的自有属性。
  • 丢失 Error 对象: Error 对象(如 new Error('message'))会转换为 {}

示例:JSON.parse(JSON.stringify) 的失败案例

const complexObj = {
    num: 123,
    str: "hello",
    bool: true,
    n: null,
    u: undefined, // 会被忽略
    sym: Symbol('id'), // 会被忽略
    bigInt: 123n, // 报错
    date: new Date(),
    regex: /test/gi,
    func: () => console.log('hi'), // 会被忽略
    map: new Map([['key1', 'value1']]),
    set: new Set([1, 2, 3]),
    nested: {
        arr: [1, new Date(), Symbol('test')],
        err: new Error('Something went wrong')
    }
};

// 尝试 JSON 克隆,会失败或丢失信息
try {
    const jsonCloned = JSON.parse(JSON.stringify(complexObj));
    console.log("JSON 克隆成功 (但可能丢失信息):", jsonCloned);
    console.log("原始日期类型:", complexObj.date instanceof Date); // true
    console.log("克隆日期类型:", jsonCloned.date instanceof Date); // false (变成字符串)
    console.log("原始 RegExp 类型:", complexObj.regex instanceof RegExp); // true
    console.log("克隆 RegExp 类型:", jsonCloned.regex instanceof RegExp); // false (变成普通对象)
    console.log("原始 Map 类型:", complexObj.map instanceof Map); // true
    console.log("克隆 Map 类型:", jsonCloned.map instanceof Map); // false (变成普通对象)
    console.log("克隆中是否存在 undefined/Symbol/Function:", 'u' in jsonCloned, 'sym' in jsonCloned, 'func' in jsonCloned); // false false false
    console.log("克隆 Error 类型:", jsonCloned.nested.err); // {}
} catch (e) {
    console.error("JSON 克隆失败:", e.message); // 会捕获 BigInt 导致的错误
}

// 示例:循环引用
const a = {};
const b = {};
a.b = b;
b.a = a;

try {
    JSON.parse(JSON.stringify(a)); // TypeError: Converting circular structure to JSON
} catch (e) {
    console.error("JSON.stringify 无法处理循环引用:", e.message);
}

三、structuredClone() API:现代解决方案

为了解决上述问题,Web 标准引入了一个强大的全局函数 structuredClone()。这个 API 在 Web Workers 之间、IndexedDB 存储数据、history.pushState 等场景中早已内部使用,现在被暴露为标准的 JavaScript API,允许我们直接进行结构化克隆。

1. structuredClone() 的能力

structuredClone() 算法能够深度复制各种 JavaScript 值,包括:

  • 所有原始类型: number, string, boolean, null, undefined, BigInt, Symbol (作为属性值时会被忽略,但作为顶级值或数组元素时会抛出错误)。
  • 普通对象和数组: 能够处理嵌套结构。
  • 特定内置对象:
    • Date 对象(克隆为新的 Date 实例)。
    • RegExp 对象(克隆为新的 RegExp 实例,保留 flags 和 source)。
    • MapSet 对象(克隆为新的 MapSet 实例,并递归克隆其内容)。
    • 所有 Typed Arrays(如 Uint8Array, Int32Array 等)。
    • ArrayBufferSharedArrayBuffer
    • Blob, File, FileList
    • ImageData
    • DOMExceptionError 对象(仅复制 name, message, stack 等标准属性,非标准属性会被忽略)。
  • 处理循环引用: 这是其最重要的能力之一,它能正确地复制含有循环引用的对象图,而不会陷入无限循环。
  • 保留原型链: structuredClone() 默认情况下只会克隆对象的可枚举自有属性,不会保留原型链。新对象将是普通对象(Object.prototype)。

2. structuredClone() 的局限性

尽管 structuredClone() 功能强大,但它并非万能,有一些类型是无法克隆的:

  • 函数(Function): 函数是可执行的代码,克隆它们没有意义,或可能导致安全问题。
  • DOM 节点:document.body
  • Error 对象(非标准属性): 仅复制标准属性,自定义属性会被忽略。
  • Promise 对象: 无法克隆。
  • WeakMapWeakSet 无法克隆,因为它们的键是弱引用,克隆它们的状态会破坏弱引用的语义。
  • Symbol 值: 如果 Symbol 是对象的属性键,该属性会被忽略。如果 Symbol 本身是需要克隆的值(例如作为数组的一个元素),则会抛出 DataCloneError
  • 对象原型链: 新对象的原型将是 Object.prototype,而不是原始对象的原型。

3. structuredClone() 语法

const clonedValue = structuredClone(value, { transfer });
  • value: 必需,要克隆的值。
  • options: 可选,一个包含配置属性的对象。
    • transfer: 一个可选的 Array,包含可转移对象(如 ArrayBuffer, MessagePort, ReadableStream 等)。这些对象在克隆后会从原始对象中“转移”走,不再可用。这主要用于性能优化,不影响我们对核心克隆算法的理解。

示例:使用 structuredClone()

// 复杂对象
const complexObj = {
    num: 123,
    str: "hello",
    bool: true,
    n: null,
    u: undefined, // 原始对象中存在,克隆后会保留 (但作为属性值时,JSON.stringify会忽略)
    bigInt: 123n,
    date: new Date(),
    regex: /test/gi,
    map: new Map([['key1', 'value1'], ['key2', { val: 2 }]]),
    set: new Set([1, 2, 'three']),
    buffer: new ArrayBuffer(8),
    uint8: new Uint8Array([10, 20, 30]),
    nested: {
        arr: [1, new Date(), 3],
        err: new Error('Something went wrong')
    }
};

// 循环引用
const a = {};
const b = {};
a.b = b;
b.a = a;
complexObj.circular = a; // 将循环引用添加到复杂对象中

// 克隆
try {
    const cloned = structuredClone(complexObj);
    console.log("structuredClone 成功:", cloned);

    // 验证基本类型
    console.log("原始 num:", complexObj.num, "克隆 num:", cloned.num);
    console.log("原始 bigInt:", complexObj.bigInt, "克隆 bigInt:", cloned.bigInt);

    // 验证日期
    console.log("原始日期:", complexObj.date, "克隆日期:", cloned.date);
    console.log("日期是 Date 实例:", cloned.date instanceof Date); // true

    // 验证 RegExp
    console.log("原始 RegExp:", complexObj.regex, "克隆 RegExp:", cloned.regex);
    console.log("RegExp 是 RegExp 实例:", cloned.regex instanceof RegExp); // true
    console.log("RegExp flags:", cloned.regex.flags); // gi

    // 验证 Map
    console.log("原始 Map size:", complexObj.map.size, "克隆 Map size:", cloned.map.size); // 2 2
    console.log("Map 是 Map 实例:", cloned.map instanceof Map); // true
    console.log("Map 内容:", cloned.map.get('key2').val); // 2 (嵌套对象也被克隆)

    // 验证 Set
    console.log("原始 Set size:", complexObj.set.size, "克隆 Set size:", cloned.set.size); // 3 3
    console.log("Set 是 Set 实例:", cloned.set instanceof Set); // true

    // 验证 ArrayBuffer 和 Typed Array
    console.log("原始 ArrayBuffer 长度:", complexObj.buffer.byteLength, "克隆 ArrayBuffer 长度:", cloned.buffer.byteLength);
    console.log("克隆 ArrayBuffer 实例:", cloned.buffer instanceof ArrayBuffer); // true
    console.log("原始 Uint8Array 值:", complexObj.uint8[0], "克隆 Uint8Array 值:", cloned.uint8[0]);
    console.log("克隆 Uint8Array 实例:", cloned.uint8 instanceof Uint8Array); // true
    // 修改克隆的 Typed Array,不影响原始
    cloned.uint8[0] = 99;
    console.log("修改后克隆 Uint8Array 值:", cloned.uint8[0], "原始 Uint8Array 值:", complexObj.uint8[0]); // 99 10

    // 验证循环引用
    console.log("克隆后的循环引用 (a.b === b):", cloned.circular.b === cloned.circular.b.a); // true

    // 验证 Error 对象
    console.log("原始 Error:", complexObj.nested.err);
    console.log("克隆 Error:", cloned.nested.err);
    console.log("克隆 Error 是 Error 实例:", cloned.nested.err instanceof Error); // true
    console.log("克隆 Error message:", cloned.nested.err.message); // Something went wrong

    // 尝试克隆不可克隆类型 (函数)
    const objWithFn = { fn: () => {} };
    try {
        structuredClone(objWithFn);
    } catch (e) {
        console.error("尝试克隆函数失败:", e.message); // DataCloneError
    }

    // 尝试克隆不可克隆类型 (Symbol 值本身)
    try {
        structuredClone(Symbol('test'));
    } catch (e) {
        console.error("尝试克隆 Symbol 值失败:", e.message); // DataCloneError
    }

} catch (e) {
    console.error("structuredClone 失败:", e.message);
}

四、实现一个自定义的结构化克隆算法

尽管 structuredClone() API 已经非常方便,但理解其底层算法对于成为一名真正的编程专家至关重要。有时,我们可能需要在不支持该 API 的环境中(例如旧版 Node.js 或某些非浏览器环境)实现类似功能,或者需要定制克隆行为(例如,保留原型链、处理特定自定义类实例)。

核心思想:深度优先遍历与记忆化(Memoization)

自定义结构化克隆算法的核心是深度优先遍历(DFS)和记忆化。

  1. 深度优先遍历: 我们需要递归地遍历对象图中的所有属性。
  2. 记忆化(处理循环引用): 为了避免无限循环和重复克隆,我们需要一个机制来记录哪些对象已经被访问过,并存储它们的克隆副本。当再次遇到同一个对象时,直接返回其已存在的克隆副本,而不是重新克隆。这里通常使用 Map 来存储 (originalObject -> clonedObject) 的映射。

算法步骤概览:

  1. 处理基本类型和 null 如果当前值是基本类型(number, string, boolean, null, undefined, BigInt)或 Symbol(但 structuredCloneSymbol 值本身会抛错,我们这里也应如此),直接返回该值。
  2. 处理不可克隆类型: 如果当前值是函数、PromiseWeakMapWeakSetDOM 节点或 Symbol 值,抛出 DataCloneErrorTypeError
  3. 检查循环引用: 在克隆任何引用类型对象之前,检查 Map 中是否已经存在该对象的克隆。如果存在,直接返回已存在的克隆。
  4. 创建新对象并记录: 根据原始对象的类型(Date, RegExp, Map, Set, ArrayBuffer, TypedArray, Array, Object, Error 等),创建相应类型的新实例。将原始对象和新实例添加到 Map 中,以便后续的循环引用检查。这一步至关重要,必须在递归克隆其内部属性之前完成。
  5. 递归克隆属性/元素: 遍历原始对象的所有可枚举属性或数组元素,并递归调用克隆函数来处理它们,将克隆后的值赋给新对象的对应位置。
  6. 返回新对象。

实现细节:

我们将使用 Object.prototype.toString.call(value) 来精确判断对象类型,因为它比 typeofinstanceof 更可靠,能区分 ArrayDateRegExp 等不同类型的对象。

/**
 * 自定义结构化克隆函数
 * 模仿 structuredClone 的行为,处理复杂对象图和循环引用
 *
 * @param {any} value - 要克隆的值
 * @param {Map<object, object>} [clonedMap=new Map()] - 用于跟踪已克隆对象的映射,处理循环引用
 * @returns {any} - 克隆后的值
 * @throws {DOMException} - 如果遇到不可克隆的类型
 */
function customStructuredClone(value, clonedMap = new Map()) {
    // 1. 处理基本类型和 null
    if (value === null || typeof value !== 'object') {
        // 包括 number, string, boolean, null, undefined, BigInt
        // Symbol 类型作为值时,structuredClone 会抛出 DataCloneError。我们这里也模拟
        if (typeof value === 'symbol') {
            throw new DOMException(
                "Symbol values cannot be cloned.",
                "DataCloneError"
            );
        }
        return value;
    }

    // 2. 检查是否已经克隆过 (处理循环引用)
    if (clonedMap.has(value)) {
        return clonedMap.get(value);
    }

    // 获取值的内部 [[Class]] 属性,用于精确类型判断
    const type = Object.prototype.toString.call(value);

    // 3. 处理不可克隆的类型
    // structuredClone 对于 Function, Promise, WeakMap, WeakSet, DOM Nodes 等会抛出 DataCloneError
    switch (type) {
        case '[object Function]':
        case '[object Promise]':
        case '[object WeakMap]':
        case '[object WeakSet]':
            // 简单模拟 DOMException,实际上 DOMException 构造函数需要浏览器环境
            throw new DOMException(
                `${type.slice(8, -1)} objects cannot be cloned.`,
                "DataCloneError"
            );
        // 对于 DOM 节点,我们可以通过检查 instanceof Node 来判断,但这里简化为只处理 JS 内置类型
        // 如果需要更严格的模拟,可以添加 if (value instanceof Node) 检查
    }

    // 4. 根据类型创建新的实例并记录到 clonedMap 中 (关键步骤:在递归前记录)
    let clonedInstance;
    switch (type) {
        case '[object Array]':
            clonedInstance = [];
            clonedMap.set(value, clonedInstance); // 立即记录,处理数组内部的循环引用
            for (let i = 0; i < value.length; i++) {
                clonedInstance[i] = customStructuredClone(value[i], clonedMap);
            }
            return clonedInstance;

        case '[object Date]':
            clonedInstance = new Date(value.getTime());
            clonedMap.set(value, clonedInstance);
            return clonedInstance;

        case '[object RegExp]':
            clonedInstance = new RegExp(value.source, value.flags);
            clonedMap.set(value, clonedInstance);
            return clonedInstance;

        case '[object Map]':
            clonedInstance = new Map();
            clonedMap.set(value, clonedInstance); // 立即记录,处理 Map 内部的循环引用
            for (const [key, val] of value.entries()) {
                clonedInstance.set(
                    customStructuredClone(key, clonedMap),
                    customStructuredClone(val, clonedMap)
                );
            }
            return clonedInstance;

        case '[object Set]':
            clonedInstance = new Set();
            clonedMap.set(value, clonedInstance); // 立即记录,处理 Set 内部的循环引用
            for (const item of value.values()) {
                clonedInstance.add(customStructuredClone(item, clonedMap));
            }
            return clonedInstance;

        case '[object ArrayBuffer]':
            // 克隆 ArrayBuffer
            clonedInstance = value.slice(0); // slice 创建一个新的 ArrayBuffer 副本
            clonedMap.set(value, clonedInstance);
            return clonedInstance;

        // 处理所有 Typed Arrays (Uint8Array, Int32Array, Float64Array, etc.)
        case '[object Int8Array]':
        case '[object Uint8Array]':
        case '[object Uint8ClampedArray]':
        case '[object Int16Array]':
        case '[object Uint16Array]':
        case '[object Int32Array]':
        case '[object Uint32Array]':
        case '[object Float32Array]':
        case '[object Float64Array]':
        case '[object BigInt64Array]':
        case '[object BigUint64Array]':
            // TypedArray 的克隆需要其底层的 ArrayBuffer 的克隆
            const clonedBuffer = customStructuredClone(value.buffer, clonedMap);
            // 使用原始 TypedArray 的构造函数创建新的 TypedArray 视图
            clonedInstance = new value.constructor(
                clonedBuffer,
                value.byteOffset,
                value.length
            );
            clonedMap.set(value, clonedInstance);
            return clonedInstance;

        case '[object Error]':
        case '[object EvalError]':
        case '[object RangeError]':
        case '[object ReferenceError]':
        case '[object SyntaxError]':
        case '[object TypeError]':
        case '[object URIError]':
        case '[object AggregateError]':
        case '[object DOMException]': // 仅复制标准属性
            // 创建新的 Error 实例,并复制标准属性
            clonedInstance = new (value.constructor)(value.message);
            // structuredClone 会复制 name, message, stack 等标准属性,自定义属性会被忽略
            // 这里我们只复制主要的几个
            clonedInstance.name = value.name;
            if (value.stack) clonedInstance.stack = value.stack;
            // 如果是 AggregateError,需要克隆 errors 属性 (数组)
            if (type === '[object AggregateError]' && value.errors) {
                 clonedInstance.errors = customStructuredClone(value.errors, clonedMap);
            }
            clonedMap.set(value, clonedInstance);
            return clonedInstance;

        // 处理 Blob, File, ImageData 等 Web API 对象
        // structuredClone 可以处理,但自定义实现复杂。这里简化为直接返回或抛错
        // 实际上,这些对象通常是 immutable 的,或者有专门的 API 来复制
        case '[object Blob]':
        case '[object File]':
        case '[object FileList]':
        case '[object ImageData]':
            // 对于这些类型,structuredClone 会创建新的实例,但自定义实现可能需要宿主环境API
            // 简单起见,我们选择直接返回原始对象(这不是深克隆),或抛出错误
            // 为了更接近 structuredClone,应该尝试克隆,但JS层面难以实现
            console.warn(`Warning: Type ${type.slice(8, -1)} is being returned directly, not deeply cloned.`);
            clonedMap.set(value, value); // 记录以避免循环,但实际未克隆
            return value;

        case '[object Object]': // 普通对象
            clonedInstance = {};
            clonedMap.set(value, clonedInstance); // 立即记录,处理对象内部的循环引用
            for (const key in value) {
                // 确保只复制自有属性,并且是可枚举的
                if (Object.prototype.hasOwnProperty.call(value, key)) {
                    clonedInstance[key] = customStructuredClone(value[key], clonedMap);
                }
            }
            return clonedInstance;

        default:
            // 遇到其他未知类型,例如自定义类的实例,structuredClone 会复制其可枚举属性
            // 但不保留原型链。这里我们采取类似普通对象的处理方式。
            // 如果需要保留原型链,则需要更复杂的逻辑,例如 Object.create(Object.getPrototypeOf(value))
            clonedInstance = {}; // 默认创建一个普通对象
            clonedMap.set(value, clonedInstance);
            for (const key in value) {
                if (Object.prototype.hasOwnProperty.call(value, key)) {
                    clonedInstance[key] = customStructuredClone(value[key], clonedMap);
                }
            }
            return clonedInstance;
    }
}

自定义克隆算法测试:

// 示例1:基本类型和简单对象
const simpleObj = {
    a: 1,
    b: 'hello',
    c: true,
    d: null,
    e: undefined,
    f: 123n,
    g: [1, {x:10}]
};
const clonedSimple = customStructuredClone(simpleObj);
console.log("自定义克隆 - 简单对象:", clonedSimple);
clonedSimple.g[1].x = 20;
console.log("原始简单对象嵌套值:", simpleObj.g[1].x); // 10 (深拷贝成功)

// 示例2:复杂对象,包含日期、正则、Map、Set、ArrayBuffer、TypedArray
const complexObjWithAllTypes = {
    date: new Date(),
    regex: /pattern/i,
    map: new Map([['k1', 'v1'], ['k2', { sub: 'value' }]]),
    set: new Set([10, 'text', { obj: true }]),
    buffer: new ArrayBuffer(16),
    uint16: new Uint16Array([1, 2, 3, 4]),
    err: new TypeError('My custom type error')
};

const clonedComplex = customStructuredClone(complexObjWithAllTypes);
console.log("自定义克隆 - 复杂对象:", clonedComplex);

console.log("日期实例:", clonedComplex.date instanceof Date); // true
console.log("正则实例:", clonedComplex.regex instanceof RegExp); // true
console.log("Map实例:", clonedComplex.map instanceof Map); // true
console.log("Set实例:", clonedComplex.set instanceof Set); // true
console.log("ArrayBuffer实例:", clonedComplex.buffer instanceof ArrayBuffer); // true
console.log("Uint16Array实例:", clonedComplex.uint16 instanceof Uint16Array); // true
console.log("Error实例:", clonedComplex.err instanceof TypeError); // true

// 验证 Map 内部嵌套对象是否克隆
const originalMapNested = complexObjWithAllTypes.map.get('k2');
const clonedMapNested = clonedComplex.map.get('k2');
clonedMapNested.sub = 'new value';
console.log("原始 Map 嵌套值:", originalMapNested.sub); // value (深拷贝成功)

// 验证 TypedArray 独立性
clonedComplex.uint16[0] = 99;
console.log("原始 Uint16Array 值:", complexObjWithAllTypes.uint16[0]); // 1
console.log("克隆 Uint16Array 值:", clonedComplex.uint16[0]); // 99

// 示例3:循环引用
const objA = {};
const objB = { id: 'B' };
objA.propB = objB;
objB.propA = objA;
objA.name = 'A';

const clonedA = customStructuredClone(objA);
console.log("自定义克隆 - 循环引用:", clonedA);
console.log("克隆 A.propB.propA === 克隆 A:", clonedA.propB.propA === clonedA); // true
console.log("克隆 A.name:", clonedA.name); // A
console.log("克隆 B.id:", clonedA.propB.id); // B

// 示例4:不可克隆类型
const objWithUnclonable = {
    f: () => {},
    s: Symbol('test'), // 作为属性值时,structuredClone会保留属性,但值无法克隆,这里我们模拟DataCloneError
    p: Promise.resolve(1)
};
try {
    customStructuredClone(objWithUnclonable);
} catch (e) {
    console.error("自定义克隆 - 尝试克隆不可克隆类型失败:", e.message); // DataCloneError
}
try {
    customStructuredClone(objWithUnclonable.f);
} catch (e) {
    console.error("自定义克隆 - 尝试克隆函数值失败:", e.message); // DataCloneError
}
try {
    customStructuredClone(Symbol('direct symbol clone'));
} catch (e) {
    console.error("自定义克隆 - 尝试克隆 Symbol 值失败:", e.message); // DataCloneError
}

关于 WeakMap 的考虑:

在我们的 customStructuredClone 函数中,使用 Map 来存储 clonedMap 是为了清晰地展示算法原理。Map 会强引用键和值,这意味着即使原始对象不再被其他地方引用,只要它还在 clonedMap 中,就不会被垃圾回收。对于大型、短生命周期的克隆操作,这通常不是问题。

然而,如果需要克隆的对象图非常庞大,并且克隆操作本身是长期存在的,或者原始对象可能在克隆后很快变得不可达,那么使用 WeakMap 作为 clonedMap 可能更有优势。WeakMap 的键是弱引用,如果一个键指向的对象不再被其他地方引用,那么它就可以被垃圾回收,并自动从 WeakMap 中移除。这有助于避免内存泄漏。

但需要注意的是,WeakMap 的键必须是对象,不能是基本类型。由于我们的 clonedMap 还需要存储基本类型(比如在 Map/Set 的键/值中可能出现基本类型),所以 WeakMap 不适合直接作为 clonedMap 使用。对于只存储对象引用的 visited 映射,WeakMap 是一个更好的选择。在我们的 customStructuredClone 实现中,clonedMap 存储的是 (originalObject -> clonedObject) 的映射,键和值都是对象,所以理论上 WeakMap 是可以用于这种场景的。

// 如果只用于跟踪对象,WeakMap 是一个更优的选择
// 但由于我们的 Map 同时也存储了克隆结果,并且可能需要在克隆 Map/Set 的键/值时递归使用,
// Map 仍然是更直接的选择。
// 如果要用 WeakMap,可能需要更精细的逻辑,例如在 WeakMap 中存储一个 Map,
// 或者只用 WeakMap 来跟踪“已访问”的对象,而克隆结果存储在其他地方。

五、性能考量与使用场景

1. 性能对比:structuredClone() vs. 自定义实现

  • structuredClone() 作为浏览器或 Node.js 环境的内置 API,它通常使用 C++ 等底层语言实现,经过高度优化。其性能远超任何 JavaScript 实现,尤其是在处理大型、复杂对象图时。
  • 自定义实现: 无论多么精巧的 JavaScript 实现,都会因为 JavaScript 引擎的解释执行、大量的类型检查、递归调用和 Map 操作而产生显著的性能开销,通常比原生实现慢一个数量级。

结论: 优先使用原生的 structuredClone() API。只有在目标环境不支持该 API 或有非常特殊的定制需求时,才考虑使用自定义实现。

2. 结构化克隆的使用场景

结构化克隆不仅仅是为了深拷贝一个对象供应用内部使用,它在 JavaScript 生态中有许多重要的应用场景:

  • Web Workers 之间的数据传递 (postMessage): 当通过 postMessage 在主线程和 Worker 之间发送数据时,数据会自动进行结构化克隆。
  • IndexedDB 存储: 存储到 IndexedDB 数据库中的数据也需要通过结构化克隆算法来处理。
  • history.pushState / history.replaceState 浏览器历史记录状态对象在存储时也会进行结构化克隆。
  • 浏览器剪贴板 API (navigator.clipboard.write): 当写入复杂数据到剪贴板时。
  • 共享内存 (SharedArrayBuffer): structuredClone 能够正确处理 SharedArrayBuffer,并支持 transferList 选项来转移 ArrayBuffer 等可转移对象,提高性能。
  • 状态管理: 在 Redux、Vuex 等状态管理库中,为了确保不可变性,有时需要深度复制状态对象。虽然通常会使用 Immer 或其他库来优化性能,但在某些场景下,直接的深拷贝是必要的。
  • 撤销/重做功能: 实现应用程序的撤销/重做功能时,通常需要保存状态的深度副本。
  • 测试: 在单元测试中,为了避免测试用例之间的副作用,经常需要深度复制输入数据或模拟对象。

六、总结与展望

结构化克隆算法是 JavaScript 中处理复杂对象图和循环引用的核心机制。它通过深度优先遍历和记忆化技术,确保了对象图的完整复制,并正确处理了各种内置类型。原生 structuredClone() API 提供了最高效、最可靠的解决方案,我们应优先使用。而理解其底层原理,不仅能帮助我们更好地利用这个 API,也能在特定需求下构建健壮的自定义克隆逻辑。掌握结构化克隆,意味着我们能够更自信地处理 JavaScript 中的数据不可变性、并发编程和持久化存储等高级场景。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注