JavaScript 中的结构化克隆算法:处理复杂对象图与循环引用的复制
各位同仁,大家好。今天我们将深入探讨 JavaScript 中一个至关重要且充满挑战的主题:结构化克隆(Structured Cloning)。在日常开发中,我们经常需要复制对象。然而,简单的复制操作往往不足以应对复杂的数据结构,特别是当对象图包含嵌套、特殊类型数据以及令人头疼的循环引用时。理解并正确应用结构化克隆算法,是驾驭这些复杂场景的关键。
一、浅拷贝与深拷贝:问题的起点
在讨论结构化克隆之前,我们必须先明确“拷贝”的两种基本形式:浅拷贝(Shallow Copy)和深拷贝(Deep Copy)。
1. 浅拷贝
浅拷贝只复制对象的第一层属性。如果属性的值是基本类型(如字符串、数字、布尔值、null、undefined、Symbol、BigInt),那么会直接复制这些值。但如果属性的值是引用类型(如对象、数组),那么复制的将是引用本身,而不是引用指向的实际对象。这意味着原对象和新对象的引用类型属性将指向同一个内存地址,修改其中一个会影响另一个。
常见浅拷贝方法:
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对象会转换为{}空对象。Map、Set对象会转换为{}空对象。undefined、Symbol、函数(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)。Map和Set对象(克隆为新的Map和Set实例,并递归克隆其内容)。- 所有 Typed Arrays(如
Uint8Array,Int32Array等)。 ArrayBuffer和SharedArrayBuffer。Blob,File,FileList。ImageData。DOMException和Error对象(仅复制name,message,stack等标准属性,非标准属性会被忽略)。
- 处理循环引用: 这是其最重要的能力之一,它能正确地复制含有循环引用的对象图,而不会陷入无限循环。
- 保留原型链:
structuredClone()默认情况下只会克隆对象的可枚举自有属性,不会保留原型链。新对象将是普通对象(Object.prototype)。
2. structuredClone() 的局限性
尽管 structuredClone() 功能强大,但它并非万能,有一些类型是无法克隆的:
- 函数(
Function): 函数是可执行的代码,克隆它们没有意义,或可能导致安全问题。 - DOM 节点: 如
document.body。 Error对象(非标准属性): 仅复制标准属性,自定义属性会被忽略。Promise对象: 无法克隆。WeakMap和WeakSet: 无法克隆,因为它们的键是弱引用,克隆它们的状态会破坏弱引用的语义。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)和记忆化。
- 深度优先遍历: 我们需要递归地遍历对象图中的所有属性。
- 记忆化(处理循环引用): 为了避免无限循环和重复克隆,我们需要一个机制来记录哪些对象已经被访问过,并存储它们的克隆副本。当再次遇到同一个对象时,直接返回其已存在的克隆副本,而不是重新克隆。这里通常使用
Map来存储(originalObject -> clonedObject)的映射。
算法步骤概览:
- 处理基本类型和
null: 如果当前值是基本类型(number,string,boolean,null,undefined,BigInt)或Symbol(但structuredClone对Symbol值本身会抛错,我们这里也应如此),直接返回该值。 - 处理不可克隆类型: 如果当前值是函数、
Promise、WeakMap、WeakSet、DOM节点或Symbol值,抛出DataCloneError或TypeError。 - 检查循环引用: 在克隆任何引用类型对象之前,检查
Map中是否已经存在该对象的克隆。如果存在,直接返回已存在的克隆。 - 创建新对象并记录: 根据原始对象的类型(
Date,RegExp,Map,Set,ArrayBuffer,TypedArray,Array,Object,Error等),创建相应类型的新实例。将原始对象和新实例添加到Map中,以便后续的循环引用检查。这一步至关重要,必须在递归克隆其内部属性之前完成。 - 递归克隆属性/元素: 遍历原始对象的所有可枚举属性或数组元素,并递归调用克隆函数来处理它们,将克隆后的值赋给新对象的对应位置。
- 返回新对象。
实现细节:
我们将使用 Object.prototype.toString.call(value) 来精确判断对象类型,因为它比 typeof 和 instanceof 更可靠,能区分 Array、Date、RegExp 等不同类型的对象。
/**
* 自定义结构化克隆函数
* 模仿 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 中的数据不可变性、并发编程和持久化存储等高级场景。