JavaScript 中的结构化克隆(Structured Clone)算法:针对超大型循环引用对象的深度优先遍历优化

各位同仁,各位对JavaScript深层机制充满求知欲的开发者们,大家下午好!

今天,我们将共同深入探讨JavaScript中一个强大而又复杂的特性——结构化克隆(Structured Clone)算法。尤其是,我们将聚焦于如何针对超大型、包含复杂循环引用的对象,通过优化深度优先遍历(Depth-First Traversal, DFS)策略,来实现高效且健壮的结构化克隆。这不仅仅是一个理论话题,它在前端框架的状态管理、Web Worker之间的数据传递、以及复杂数据结构的持久化等多个场景下都扮演着核心角色。

引言:深拷贝的迷思与结构化克隆的崛起

在JavaScript的世界里,数据拷贝是一个基础而又频繁的操作。我们经常会遇到需要复制一个对象或数组,但又不想让副本与原对象相互影响的情况。这就是所谓的“深拷贝”问题。

浅拷贝(Shallow Copy) 很容易实现,例如使用展开运算符 ...Object.assign()。它只复制了对象或数组的第一层属性,对于嵌套的引用类型,副本仍然与原对象共享引用。

const originalShallow = { a: 1, b: { c: 2 } };
const shallowCopy = { ...originalShallow };

shallowCopy.a = 10;
shallowCopy.b.c = 20; // 这会影响 originalShallow

console.log(originalShallow); // { a: 1, b: { c: 20 } }
console.log(shallowCopy);    // { a: 10, b: { c: 20 } }

深拷贝(Deep Copy) 的目标是创建一个完全独立的对象副本,包括所有嵌套的引用类型。这意味着副本中的任何修改都不会影响原对象。

JSON.parse(JSON.stringify()) 的局限性

structuredClone() API 出现之前,开发者们常常使用一个“技巧”来实现深拷贝:

const originalDeep = { a: 1, b: { c: 2 } };
const deepCopyJSON = JSON.parse(JSON.stringify(originalDeep));

deepCopyJSON.a = 10;
deepCopyJSON.b.c = 20;

console.log(originalDeep);    // { a: 1, b: { c: 2 } }
console.log(deepCopyJSON); // { a: 10, b: { c: 20 } }

这个方法简单粗暴,在很多情况下确实有效。然而,它的局限性非常明显,使其无法成为通用的深拷贝解决方案:

  1. 不支持特定类型:无法克隆 Date 对象(会变成日期字符串)、RegExp 对象(会变成空对象 {})、MapSetErrorundefinedBigIntSymbol函数Promise 等。
  2. 丢失原型链:克隆后的对象会失去原有的原型链,所有对象都变成了纯粹的 {}
  3. 无法处理循环引用:如果对象中存在循环引用,JSON.stringify() 会抛出 TypeError: Converting circular structure to JSON 错误。

structuredClone() API 的诞生与能力

为了解决这些痛点,Web标准引入了 structuredClone() 全局函数。它提供了一种标准的、健壮的、能够处理复杂数据结构的深拷贝机制。

structuredClone() 的能力远超 JSON.parse(JSON.stringify())

  • 支持广泛的内置类型:除了原始类型、普通对象和数组,它还能正确克隆 DateRegExpMapSetArrayBufferTypedArrayBlobFileImageDataError 等类型。
  • 保留原型链(部分):对于常见的内置类型,它能保留其正确的构造函数和原型。但对于自定义类的实例,它会克隆为纯粹的对象,丢失原始类的方法。
  • 自动处理循环引用:这是其最重要的特性之一。它能自动检测并正确处理对象图中的循环引用,避免无限递归。
  • 支持可转移对象(Transferable Objects):在某些环境中(如Web Workers),structuredClone() 还可以通过 transfer 选项实现零拷贝的数据传输,这是一种高级优化。
const obj = {
  date: new Date(),
  regex: /foo/g,
  map: new Map([['a', 1]]),
  set: new Set([1, 2, 3]),
  arr: [],
  circular: null
};
obj.circular = obj; // 制造一个循环引用

const clonedObj = structuredClone(obj);

console.log(clonedObj.date instanceof Date); // true
console.log(clonedObj.regex instanceof RegExp); // true
console.log(clonedObj.map instanceof Map);     // true
console.log(clonedObj.circular === clonedObj); // true (克隆后的对象内部的循环引用指向克隆后的对象自身)

console.log(clonedObj.date === obj.date); // false (是深拷贝)

然而,structuredClone() 也有其局限性:

  • 不支持函数(Functions):函数不可被结构化克隆。
  • 不支持Symbol:Symbol值会被忽略。
  • 不支持Promise、WeakMap、WeakSet:这些对象的状态或引用特性使其不适合结构化克隆。
  • 不支持DOM节点:DOM元素通常有自己的克隆方法(如 cloneNode())。
  • 不支持特定内置对象:如 window 对象。

结构化克隆的核心机制:深度优先遍历与循环引用处理

要实现一个像 structuredClone() 那样强大而健壮的深拷贝算法,我们需要解决两个核心问题:

  1. 遍历所有可克隆的属性和子对象:这通常通过深度优先遍历(DFS)或广度优先遍历(BFS)来实现。
  2. 处理循环引用:这是深拷贝中最棘手的问题,必须避免无限递归。

朴素的深度优先遍历克隆实现(递归版本)

让我们首先构建一个基础的递归深拷贝函数,它能够处理基本类型、对象、数组,并初步引入循环引用检测。

核心思想:

  • 基本类型:直接返回。
  • 非对象类型(Date, RegExp等):需要特殊处理,创建新实例。
  • 对象/数组
    • 创建一个新的空对象或数组作为副本。
    • 遍历原对象的每个属性/元素。
    • 递归地克隆每个属性的值,并赋值给副本。
  • 循环引用检测:使用一个 MapWeakMap 来存储已克隆的原始对象和它们对应的副本。在克隆任何引用类型之前,先检查它是否已经在 Map 中。如果在,直接返回 Map 中存储的副本;如果不在,则在创建副本后立即将其存入 Map,然后再进行递归克隆。
/**
 * 这是一个简化的、递归版本的深拷贝实现,用于说明概念。
 * 它不完全符合 structuredClone 的所有特性,例如,它不会克隆 Map/Set/Date/RegExp 等特殊对象,
 * 并且在处理非普通对象时可能丢失原型链。
 * 主要目的是演示递归 DFS 和循环引用处理。
 *
 * @param {*} obj 待克隆的对象。
 * @param {WeakMap<object, object>} [seen] 用于检测循环引用的 WeakMap。
 * @returns {*} 克隆后的对象。
 */
function deepCloneNaiveRecursive(obj, seen = new WeakMap()) {
    // 1. 处理基本类型和 null
    if (obj === null || typeof obj !== 'object') {
        return obj;
    }

    // 2. 处理 Date 对象
    if (obj instanceof Date) {
        return new Date(obj.getTime());
    }

    // 3. 处理 RegExp 对象
    if (obj instanceof RegExp) {
        return new RegExp(obj);
    }

    // 4. 处理循环引用
    // 如果这个对象已经被处理过,直接返回之前克隆的副本,避免无限循环
    if (seen.has(obj)) {
        return seen.get(obj);
    }

    // 5. 初始化克隆结果:根据类型创建新的对象或数组
    const clone = Array.isArray(obj) ? [] : {};

    // 6. 将原对象和其副本存入 seen Map,用于后续的循环引用检测
    // 这一步必须在递归克隆子属性之前完成
    seen.set(obj, clone);

    // 7. 递归克隆对象的属性或数组的元素
    for (const key in obj) {
        // 确保只处理对象自身的属性,而不是原型链上的属性
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
            clone[key] = deepCloneNaiveRecursive(obj[key], seen);
        }
    }

    return clone;
}

// 示例测试
const original = {
    a: 1,
    b: {
        c: 2,
        d: [3, 4],
        e: new Date(),
        f: /test/g
    },
    g: null
};
original.b.h = original; // 制造一个循环引用

const cloned = deepCloneNaiveRecursive(original);

console.log("--- Naive Recursive Clone Test ---");
console.log("Original:", original);
console.log("Cloned:", cloned);

console.log("Are they different objects?", original !== cloned); // true
console.log("Are nested objects different?", original.b !== cloned.b); // true
console.log("Is circular reference preserved?", cloned.b.h === cloned); // true
console.log("Is Date object cloned?", cloned.b.e instanceof Date); // true
console.log("Is Date object value same?", cloned.b.e.getTime() === original.b.e.getTime()); // true
console.log("Is RegExp object cloned?", cloned.b.f instanceof RegExp); // true
console.log("Is RegExp object value same?", cloned.b.f.source === original.b.f.source); // true

// 修改克隆对象,检查是否影响原对象
cloned.a = 100;
cloned.b.c = 200;
cloned.b.d[0] = 300;
cloned.b.e.setFullYear(2050);

console.log("Original after cloned modification:", original);
console.log("Cloned after modification:", cloned);

超大型循环引用对象带来的性能挑战

上述递归实现虽然原理清晰,但在面对超大型、深度非常深的循环引用对象时,会暴露出严重的性能和稳定性问题:

  1. 栈溢出(Stack Overflow):JavaScript引擎对递归调用的栈深度是有限制的。当对象图的嵌套层级非常深时(例如,链表或树结构有数万层),过多的函数调用会导致调用栈溢出,程序崩溃。
  2. 内存开销(Memory Overhead):每次函数调用都会在调用栈上创建一个新的栈帧,存储局部变量、参数和返回地址。深层递归会积累大量的栈帧,消耗大量内存。
  3. CPU开销(CPU Overhead):函数调用和返回本身就存在一定的开销。虽然现代JS引擎对尾调用优化(Tail Call Optimization, TCO)有所支持,但在非尾调用场景下,这种开销依然存在。
  4. WeakMapMap 的性能:尽管 WeakMap 在垃圾回收方面有优势,但在大型对象图中,如果需要克隆的对象数量巨大,其查找和插入操作仍可能成为瓶颈。

为了解决栈溢出问题,我们必须放弃递归,转而采用迭代式(Iterative) 的深度优先遍历。

深度优先遍历的优化策略:迭代式 DFS 与栈管理

迭代式 DFS 的核心思想是用一个显式的数据结构(通常是一个数组,充当栈)来模拟递归调用的过程。这样,我们就能完全控制栈的深度,避免JavaScript引擎的调用栈限制。

核心思想与栈元素设计

在迭代式 DFS 中,我们不再依赖函数调用栈来记录“待处理”的状态。取而代之的是,我们手动维护一个栈,每次循环从栈中取出一个任务来处理。

一个任务可能包含以下信息:

  • source: 原始对象或数组。
  • target: 原始对象或数组的克隆副本。
  • keys: (针对对象)原始对象的所有可枚举属性键数组。
  • index: (针对对象)当前正在处理的属性键在 keys 数组中的索引。
  • isArray: 布尔值,指示 source 是否为数组。

通过 index,我们可以在处理完一个子节点后,回到父节点继续处理下一个子节点,这正是递归的本质。

算法流程概述

  1. 初始化

    • 创建一个 WeakMapMap (seen) 来跟踪已克隆的对象,防止循环引用。
    • 创建一个数组 (stack) 作为我们的手动调用栈。
    • 处理根对象:如果根对象是引用类型,创建其副本,将其存入 seen,然后将一个任务(包含原始根对象、副本、以及其属性/索引信息)推入 stack。如果根对象是基本类型,直接返回。
  2. 主循环while (stack.length > 0)

    • stack 顶部弹出一个任务 currentTask
    • 根据 currentTask 中的 isArray 标志,判断当前处理的是数组还是普通对象。
    • 遍历子节点
      • currentTask 中获取下一个待处理的属性键或数组索引。
      • 获取对应的子值 childSource
      • 处理 childSource
        • 基本类型/不可克隆类型:直接赋值给 currentTask.target 的对应属性/索引。
        • 已克隆的引用类型(在 seen 中):直接从 seen 中获取其副本,赋值给 currentTask.target 的对应属性/索引。
        • 未克隆的引用类型
          • 创建 childSource 的副本 childTarget
          • childSourcechildTarget 存入 seen
          • currentTask 重新推回 stack(因为它还有其他子节点可能未处理),并更新其 index
          • 创建一个新的任务,将 childSourcechildTarget 推入 stack,以便在下一次循环中处理 childTarget 的子节点。
  3. 返回结果:当 stack 为空时,表示所有可达节点都已处理完毕,返回根对象的克隆副本。

代码实现:基于迭代式 DFS 的结构化克隆优化

现在,让我们来构建一个更接近 structuredClone() 能力的迭代式深拷贝函数。它将处理更多的内置类型,并采用手动栈管理来避免递归限制。

/**
 * 结构化克隆的迭代式深度优先遍历实现。
 * 旨在模拟 structuredClone() 的行为,处理各种内置类型和循环引用,
 * 并通过迭代而非递归来避免栈溢出。
 *
 * @param {*} source 待克隆的对象或值。
 * @returns {*} 克隆后的对象或值。
 */
function structuredCloneOptimized(source) {
    // 1. 处理基本类型和 null
    if (source === null || typeof source !== 'object') {
        return source;
    }

    // 2. 处理不可克隆的类型 (structuredClone() 会抛出 DataCloneError)
    // 这里我们简单返回 undefined 或抛出错误,具体行为取决于需求
    if (typeof source === 'function' || typeof source === 'symbol' ||
        source instanceof Promise || source instanceof WeakMap || source instanceof WeakSet) {
        // structuredClone 会抛出 DataCloneError
        // throw new DOMException("Function, Symbol, Promise, WeakMap, WeakSet cannot be cloned.", "DataCloneError");
        console.warn(`Attempted to clone an unsupported type: ${typeof source}. It will be ignored.`);
        return undefined; // 或者根据需求返回null、{},或直接抛出错误
    }

    // WeakMap 用于存储已克隆的原始对象及其对应的副本,处理循环引用。
    // 使用 WeakMap 可以让不再引用的原对象被垃圾回收,避免内存泄漏。
    const seen = new WeakMap();

    // 手动维护的栈,用于模拟递归的深度优先遍历。
    // 栈中的每个元素都是一个任务对象,包含:
    // {
    //   source: 原始对象/数组,
    //   target: 克隆后的对象/数组,
    //   keys: (针对对象) 原始对象的属性键数组,
    //   index: 当前正在处理的属性键/数组索引
    // }
    const stack = [];

    let targetRoot; // 存储最终的克隆结果

    // 初始处理根对象
    // 如果根对象已经是 seen 中,说明是某种循环引用(不太可能发生在这里,但作为安全检查)
    if (seen.has(source)) {
        return seen.get(source);
    }

    // 根据源对象的类型创建其副本,并存储到 seen 中
    if (source instanceof Date) {
        targetRoot = new Date(source.getTime());
    } else if (source instanceof RegExp) {
        targetRoot = new RegExp(source);
    } else if (source instanceof Map) {
        targetRoot = new Map();
    } else if (source instanceof Set) {
        targetRoot = new Set();
    } else if (source instanceof ArrayBuffer) {
        // ArrayBuffer 需要特殊处理,创建一个新的 ArrayBuffer 并拷贝内容
        targetRoot = source.slice(0);
    } else if (ArrayBuffer.isView(source)) { // TypedArrays, DataView
        // TypedArrays 和 DataView 基于 ArrayBuffer,需要创建新的视图并拷贝内容
        // 假设这里只处理 ArrayBufferView,对于 Blob/File/ImageData 等需要更复杂的逻辑
        targetRoot = new source.constructor(source.buffer.slice(0), source.byteOffset, source.byteLength);
    } else if (Array.isArray(source)) {
        targetRoot = [];
    } else { // 普通对象
        targetRoot = Object.create(Object.getPrototypeOf(source)); // 尝试保留原型链
    }

    seen.set(source, targetRoot); // 立即将根对象及其副本存入 seen Map

    // 对于需要进一步遍历其内容的类型,将其推入栈
    if (source instanceof Map) {
        // Map 的键值对需要特殊处理,因为键也可以是引用类型
        // 我们可以将 Map 视为一个特殊的迭代任务
        stack.push({
            source: source,
            target: targetRoot,
            type: 'Map',
            iterator: source.entries(), // 使用迭代器可以逐个处理键值对
            done: false
        });
    } else if (source instanceof Set) {
        // Set 的值需要特殊处理
        stack.push({
            source: source,
            target: targetRoot,
            type: 'Set',
            iterator: source.values(),
            done: false
        });
    } else if (typeof source === 'object') { // 普通对象或数组
        const keys = Array.isArray(source) ? Array.from({ length: source.length }, (_, i) => i) : Object.keys(source);
        stack.push({
            source: source,
            target: targetRoot,
            keys: keys,
            index: 0,
            isArray: Array.isArray(source)
        });
    }

    // 主循环:处理栈中的任务
    while (stack.length > 0) {
        const currentTask = stack[stack.length - 1]; // 查看栈顶任务,不弹出
        const { source: currentSource, target: currentTarget } = currentTask;

        if (currentTask.type === 'Map') {
            const { iterator, target } = currentTask;
            let entry = iterator.next();
            if (entry.done) {
                stack.pop(); // 所有键值对已处理完毕,弹出 Map 任务
                continue;
            }

            const [originalKey, originalValue] = entry.value;

            // 克隆键和值
            const clonedKey = structuredCloneOptimized(originalKey); // 递归调用,这里可以优化为循环
            const clonedValue = structuredCloneOptimized(originalValue); // 递归调用,这里可以优化为循环

            target.set(clonedKey, clonedValue);
            // 保持 Map 任务在栈顶,直到所有条目处理完毕
            continue;
        } else if (currentTask.type === 'Set') {
            const { iterator, target } = currentTask;
            let entry = iterator.next();
            if (entry.done) {
                stack.pop(); // 所有值已处理完毕,弹出 Set 任务
                continue;
            }

            const originalValue = entry.value;
            const clonedValue = structuredCloneOptimized(originalValue); // 递归调用,这里可以优化为循环
            target.add(clonedValue);
            // 保持 Set 任务在栈顶,直到所有条目处理完毕
            continue;
        }

        // 普通对象或数组的遍历逻辑
        if (currentTask.index < currentTask.keys.length) {
            const key = currentTask.keys[currentTask.index];
            const childSource = currentSource[key];

            // 推进索引,为下一次迭代做准备
            currentTask.index++;

            // 处理子节点
            if (childSource === null || typeof childSource !== 'object') {
                // 基本类型或 null,直接赋值
                currentTarget[key] = childSource;
            } else if (seen.has(childSource)) {
                // 如果子对象已经被克隆过(循环引用),直接赋值其副本
                currentTarget[key] = seen.get(childSource);
            } else {
                // 未克隆的引用类型,需要创建副本并压入栈
                let childTarget;

                if (childSource instanceof Date) {
                    childTarget = new Date(childSource.getTime());
                } else if (childSource instanceof RegExp) {
                    childTarget = new RegExp(childSource);
                } else if (childSource instanceof Map) {
                    childTarget = new Map();
                } else if (childSource instanceof Set) {
                    childTarget = new Set();
                } else if (childSource instanceof ArrayBuffer) {
                    childTarget = childSource.slice(0);
                } else if (ArrayBuffer.isView(childSource)) {
                    childTarget = new childSource.constructor(childSource.buffer.slice(0), childSource.byteOffset, childSource.byteLength);
                } else if (Array.isArray(childSource)) {
                    childTarget = [];
                } else { // 普通对象
                    childTarget = Object.create(Object.getPrototypeOf(childSource));
                }

                seen.set(childSource, childTarget); // 立即存入 seen Map

                // 将子节点的副本赋值给当前对象的对应属性
                currentTarget[key] = childTarget;

                // 将子节点任务推入栈,以便后续处理其子属性
                if (childSource instanceof Map) {
                    stack.push({
                        source: childSource,
                        target: childTarget,
                        type: 'Map',
                        iterator: childSource.entries(),
                        done: false
                    });
                } else if (childSource instanceof Set) {
                    stack.push({
                        source: childSource,
                        target: childTarget,
                        type: 'Set',
                        iterator: childSource.values(),
                        done: false
                    });
                } else if (typeof childSource === 'object') {
                    const childKeys = Array.isArray(childSource) ? Array.from({ length: childSource.length }, (_, i) => i) : Object.keys(childSource);
                    stack.push({
                        source: childSource,
                        target: childTarget,
                        keys: childKeys,
                        index: 0,
                        isArray: Array.isArray(childSource)
                    });
                }
            }
        } else {
            // 当前对象的(所有或已处理部分的)子属性已处理完毕,从栈中弹出
            stack.pop();
        }
    }

    return targetRoot;
}

// --- 复杂测试用例 ---
// 构造一个超大型循环引用对象,模拟链表或树结构,深度达到10000层
function createDeepCyclicObject(depth) {
    let head = { value: 0 };
    let current = head;
    let tail = null;

    for (let i = 1; i < depth; i++) {
        const next = { value: i };
        current.next = next;
        current.prev = current; // 制造双向引用
        current = next;
    }
    tail = current;
    tail.next = head; // 制造循环引用:最后一个节点指向头节点
    head.prev = tail; // 头部节点也指向尾部

    return head;
}

console.time("Deep Clone Iterative DFS (Depth 10000)");
const deepCyclicObject = createDeepCyclicObject(10000);
const clonedDeepCyclicObject = structuredCloneOptimized(deepCyclicObject);
console.timeEnd("Deep Clone Iterative DFS (Depth 10000)");

// 验证克隆结果
console.log("--- Deep Cyclic Object Clone Test ---");
console.log("Original head value:", deepCyclicObject.value);
console.log("Cloned head value:", clonedDeepCyclicObject.value);
console.log("Are heads different objects?", deepCyclicObject !== clonedDeepCyclicObject); // true

// 验证循环引用是否正确克隆
let currentOriginal = deepCyclicObject;
let currentCloned = clonedDeepCyclicObject;
for (let i = 0; i < 5; i++) { // 检查前几个节点
    console.log(`Node ${i} (Original):`, currentOriginal.value);
    console.log(`Node ${i} (Cloned):`, currentCloned.value);
    console.log(`Node ${i} (Cloned) prev === currentCloned ?`, currentCloned.prev === currentCloned); // 检查 prev 引用是否正确指向克隆后的对象
    currentOriginal = currentOriginal.next;
    currentCloned = currentCloned.next;
}
console.log("Cloned tail.next === cloned head?", currentCloned.prev.next === clonedDeepCyclicObject); // 验证循环引用
console.log("Cloned head.prev === cloned tail?", clonedDeepCyclicObject.prev === currentCloned.prev); // 验证循环引用
console.log("Are values identical?", deepCyclicObject.value === clonedDeepCyclicObject.value); // true
// 验证修改克隆对象不会影响原对象
clonedDeepCyclicObject.value = 999;
console.log("Original head value after cloned modification:", deepCyclicObject.value); // 0
console.log("Cloned head value after modification:", clonedDeepCyclicObject.value); // 999

// --- 更多类型测试 ---
const complexOriginal = {
    num: 123,
    str: 'hello',
    bool: true,
    n: null,
    u: undefined, // structuredClone会忽略undefined属性,我们的实现会保留
    date: new Date('2023-01-01T12:00:00Z'),
    regex: /^abcd+$/gi,
    arr: [1, { x: 10 }, [true, false]],
    map: new Map([['key1', 'val1'], [1, { nested: 'mapval' }]]),
    set: new Set([10, 'setval', { a: 1 }]),
    buffer: new Uint8Array([1, 2, 3]).buffer,
    error: new Error('test error'),
    custom: {
        prop: 'custom val',
        method: () => console.log('This will be ignored or warned') // 函数不可克隆
    }
};
complexOriginal.map.set(complexOriginal.set, complexOriginal.arr); // 嵌套复杂引用
complexOriginal.custom.ref = complexOriginal; // 另一个循环引用

console.time("Complex Object Clone");
const clonedComplex = structuredCloneOptimized(complexOriginal);
console.timeEnd("Complex Object Clone");

console.log("n--- Complex Object Clone Test ---");
console.log("Original:", complexOriginal);
console.log("Cloned:", clonedComplex);

console.log("Date cloned and same value:", clonedComplex.date.toISOString() === complexOriginal.date.toISOString());
console.log("RegExp cloned and same source:", clonedComplex.regex.source === complexOriginal.regex.source);
console.log("Array nested object cloned:", clonedComplex.arr[1].x === 10 && clonedComplex.arr[1] !== complexOriginal.arr[1]);
console.log("Map cloned and has original keys/values:", clonedComplex.map.get('key1') === 'val1' && clonedComplex.map.get(1).nested === 'mapval');
console.log("Set cloned and has original values:", clonedComplex.set.has(10) && Array.from(clonedComplex.set).some(item => typeof item === 'object' && item.a === 1));
console.log("ArrayBuffer cloned and has same content:", new Uint8Array(clonedComplex.buffer)[0] === 1 && clonedComplex.buffer !== complexOriginal.buffer);
console.log("Error cloned and same message:", clonedComplex.error.message === complexOriginal.error.message);
// 验证循环引用在复杂对象中
console.log("Custom object circular ref preserved:", clonedComplex.custom.ref === clonedComplex);
// 验证函数被忽略
console.log("Custom object method is undefined (ignored):", clonedComplex.custom.method === undefined);

// 验证Map中嵌套复杂引用
const originalSetInMap = Array.from(complexOriginal.map.keys()).find(k => k instanceof Set);
const clonedSetInMap = Array.from(clonedComplex.map.keys()).find(k => k instanceof Set);
console.log("Map key (Set) is cloned and different object:", originalSetInMap !== clonedSetInMap);
const originalArrInMap = complexOriginal.map.get(originalSetInMap);
const clonedArrInMap = clonedComplex.map.get(clonedSetInMap);
console.log("Map value (Array) is cloned and different object:", originalArrInMap !== clonedArrInMap);
console.log("Map value (Array) is the same cloned array as clonedComplex.arr:", clonedArrInMap === clonedComplex.arr);

代码解释:

  1. 处理基本类型和不可克隆类型:函数开始时,首先处理 null、原始类型(string, number, boolean等),以及 structuredClone() 不支持克隆的类型(function, symbol, Promise 等)。对于不支持的类型,我们选择发出警告并返回 undefined,实际 structuredClone() 会抛出 DataCloneError
  2. seen WeakMap:这是解决循环引用的关键。它存储了所有已经开始克隆过程的引用类型对象(作为键)和它们对应的克隆副本(作为值)。在处理任何引用类型之前,我们都先检查 seen,如果已存在,直接返回副本,避免重复克隆和无限循环。WeakMap 的好处是,当原始对象不再被其他地方引用时,它可以被垃圾回收,不会造成内存泄漏。
  3. stack 数组:这是迭代式 DFS 的核心。它模拟了递归调用栈,每个元素是一个“任务”对象,包含了处理当前节点所需的所有上下文信息。
  4. 根对象初始化:对最顶层的 source 对象进行初始化处理。根据其类型创建 targetRoot,将其存入 seen,并根据其是否为 Map/Set/普通对象/数组,将其相应的任务推入 stack
  5. 主循环 while (stack.length > 0)
    • 栈顶任务处理:我们总是处理 stack 中最顶层的任务。
    • Map/Set 类型处理:这些类型需要特殊处理,因为它们的键/值也可以是引用类型。我们使用它们的 entries()values() 迭代器来逐个处理元素。为了避免在处理每个 Map/Set 元素时都创建新的栈任务,我们让 Map/Set 任务保持在栈顶,并不断调用 iterator.next() 来获取下一个元素。一旦迭代器完成,就弹出该任务。
    • 普通对象/数组处理:对于这些类型,任务对象中包含 keys 数组和 index。我们按 index 顺序遍历 keys
      • 子节点处理
        • 如果子节点是基本类型或 null,直接赋值。
        • 如果子节点是已经存在于 seen 中的引用类型(即循环引用),直接赋值其副本。
        • 如果子节点是未克隆的引用类型:
          • 根据其类型创建 childTarget 副本。
          • childSourcechildTarget 立即存入 seen
          • childTarget 赋值给 currentTarget 的对应属性。
          • 关键步骤:将 childSourcechildTarget 封装成一个新的任务,并推入 stack。这意味着我们将暂停当前父对象的处理,转而深入处理这个子对象。父对象任务仍在栈中,等待子对象处理完毕后继续。
    • 任务完成:当一个普通对象或数组的所有 keys 都处理完毕后(即 currentTask.index >= currentTask.keys.length),从 stack 中弹出该任务。
  6. 原型链保留:对于普通对象,我们使用 Object.create(Object.getPrototypeOf(source)) 来尝试保留原型链。对于 DateRegExp 等内置类型,我们通过其构造函数创建新实例。
  7. ArrayBufferTypedArray:这些需要通过 slice(0) 方法来创建新的缓冲区,确保数据的独立性。

性能考量与进一步优化

这个迭代式 DFS 实现已经解决了递归栈溢出的问题,并且能够正确处理循环引用和多种内置类型。然而,对于极致性能和特定场景,我们还可以考虑以下几点:

  1. WeakMapMap 的选择
    • WeakMap:如果原始对象在克隆后可能不再被其他地方引用,使用 WeakMap 可以让它们被垃圾回收,避免内存泄漏。这是 structuredClone 内部通常采用的策略。
    • Map:如果克隆过程中需要保留对所有原始对象的强引用(例如,你需要一个完整的原始对象到克隆对象的映射表),或者某些不可作为 WeakMap 键的原始类型(如 symbol 或数字),则 Map 是更好的选择。在我们的实现中,只要原始对象是引用类型,它就可以作为 WeakMap 的键。
  2. 内置类型处理的精细化structuredClone() 支持的内置类型远不止我们示例中列出的这些(例如 Blob, File, ImageData, DOMException 等)。实现一个完全兼容的 structuredClone 需要处理所有这些类型,并遵循它们的特定克隆语义。
  3. 可转移对象(Transferable Objects)structuredClone() API 支持一个 transfer 选项,用于将 ArrayBuffer 等可转移对象从一个执行上下文(如主线程)转移到另一个(如Web Worker),而不是复制。转移是零拷贝操作,性能极高。这涉及到更底层的内存管理,超出了纯JavaScript实现的范畴,通常由浏览器引擎原生支持。
  4. 深度与广度的平衡:虽然我们选择了 DFS,但在某些特定的对象图结构下,BFS(广度优先遍历)可能在内存访问模式上略有优势。然而,对于避免栈溢出而言,迭代式 DFS 和迭代式 BFS 都是有效的解决方案。
  5. 内存预分配:对于数组,如果能预先知道其长度,可以提前创建定长数组,减少动态扩容的开销。
  6. JIT 优化:原生的 structuredClone() 是由浏览器引擎的C++代码实现的,经过高度优化,能够利用JIT编译等底层机制,其性能通常远超任何纯JavaScript实现。

对比 structuredClone() API 与自定义实现

特性/方面 structuredClone() API (原生) structuredCloneOptimized() (自定义迭代式 JS 实现)
性能 极高,由浏览器C++引擎实现,经过高度优化。 较高,但受限于JS引擎的执行速度,通常不如原生API。
栈溢出 无此问题,原生实现通常不使用JS调用栈。 通过迭代式DFS避免JS调用栈溢出,能处理任意深度的对象。
循环引用 完美处理。 完美处理,使用 WeakMapMap
支持类型 广泛,包括 Date, RegExp, Map, Set, ArrayBuffer, TypedArray, Blob, File, ImageData, Error, DOMException等。 较广,示例中实现了常用类型,但完全覆盖需要更多代码。
原型链 对于内置类型保留,对于自定义类实例会丢失(克隆为纯对象)。 对于内置类型保留,对于自定义类实例会丢失(克隆为纯对象)。
不可克隆类型 抛出 DataCloneError (如 Function, Symbol, Promise, DOM 节点)。 可自定义行为(如警告并返回 undefined 或抛出错误)。
可转移对象 支持 transfer 选项,实现零拷贝数据传输 (Web Workers)。 纯JS无法实现零拷贝传输,只能进行深拷贝。
安全沙箱 在受限环境中(如Web Workers),提供安全的跨上下文通信。 仅在当前JS执行上下文中运行。
实现复杂度 对开发者而言是简单API调用。 较高,需要深入理解数据结构和算法。
适用场景 生产环境中的首选,尤其是在Web Workers、IndexedDB等需要结构化克隆的场景。 学习和理解结构化克隆原理,或者在特定环境(如Node.js旧版本)下需要类似功能时。

总结

我们今天深入探讨了JavaScript结构化克隆算法的内部机制,重点关注了如何通过迭代式深度优先遍历来优化超大型、包含复杂循环引用对象的克隆过程。通过手动管理一个栈和利用 WeakMap 来跟踪已克隆的对象,我们成功地避免了递归可能导致的栈溢出问题,并实现了对多种内置类型和循环引用的健壮处理。虽然纯JavaScript实现的性能难以超越原生 structuredClone() API,但理解其底层原理对于深入掌握JavaScript数据处理和优化技巧至关重要。

发表回复

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