Loading...
Loading...
Site Owner
Published on 2026-05-21
详解structuredClone算法支持的类型与不支持的类型、与JSON.parse对比、递归循环引用处理及深拷贝完整方案

结构化克隆算法支持的类型(Map/Set/Date/RegExp/ArrayBuffer/Error 等)与不支持的类型(Function/DOM 节点/Symbol/原型链);与 JSON.parse(JSON.stringify()) 的对比;postMessage、IndexedDB、History API 中的隐式使用。
结构化克隆算法(Structured Clone Algorithm)是 HTML 规范定义的一套深拷贝标准,用于在不同上下文(Worker、IndexedDB 等)之间传递数据。
1. 维护一个"已访问"Map:original → clone(处理循环引用)
2. 检查值类型:
- 原始值 → 直接返回
- 可克隆对象 → 创建新实例,递归克隆属性
- Transferable → 转移(不克隆)
- 不可克隆 → 抛出 DataCloneError
3. 对象属性只克隆"自有可枚举"属性(不克隆原型链方法)
structuredClone() 是 Web 标准的全局函数(浏览器和 Node.js 17+ 均支持),直接使用结构化克隆算法。
// 基础用法
const original = {
name: 'Alice',
scores: [100, 95, 88],
meta: { joined: new Date('2020-01-01') }
};
const clone = structuredClone(original);
// 完全独立的深拷贝
clone.scores.push(99);
clone.meta.joined.setFullYear(2021);
console.log(original.scores.length); // 3(不受影响)
console.log(original.meta.joined.getFullYear()); // 2020(不受影响)
console.log(original === clone); // false
// transfer 选项:指定要转移(而非克隆)的对象
const buffer = new ArrayBuffer(1024);
const view = new Uint8Array(buffer);
view[0] = 42;
const cloned = structuredClone(
{ data: buffer, name: 'test' },
{ transfer: [buffer] } // buffer 被转移
);
console.log(buffer.byteLength); // 0(已转移,被 detached)
console.log(cloned.data.byteLength); // 1024
console.log(new Uint8Array(cloned.data)[0]); // 42
const testClone = structuredClone({
// 基本类型
number: 42,
string: 'hello',
boolean: true,
null_: null,
undefined_: undefined,
bigint: 9007199254740993n,
// 内置对象
date: new Date(),
regexp: /abc/gi,
error: new Error('test'),
// 集合
map: new Map([['key', 'value']]),
set: new Set([1, 2, 3]),
mapNested: new Map([[{}, new Set([1, 2])]]),
// 类型化数组 & Buffer
buffer: new ArrayBuffer(8),
u8: new Uint8Array([1, 2, 3]),
f64: ([.]),
: ( ()),
: ([]),
: [, [, []]],
: { : { : { : } } },
});
circular = {};
circular. = circular;
clonedCircular = (circular);
.(clonedCircular. === clonedCircular);
// ① 函数
try {
structuredClone({ fn: () => {} });
} catch (e) {
console.log(e.name); // "DataCloneError"
}
// ② DOM 节点
try {
structuredClone(document.body);
} catch (e) {
console.log(e.name); // "DataCloneError"
}
// ③ Symbol(键或值)
try {
structuredClone({ [Symbol('id')]: 1 });
} catch (e) {
console.log(e.name); // "DataCloneError"
}
try {
structuredClone(Symbol('id'));
} catch (e) {
console.log(e.name); // "DataCloneError"
}
// ④ Proxy(会克隆底层对象,但丢失 Proxy 行为)
const proxy = new Proxy({ x: 1 }, {
get(t, k) { console.log('get', k); t[k]; }
});
cloned = (proxy);
{
( ());
} (e) {
.(e.);
}
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
distance() {
return Math.sqrt(this.x ** 2 + this.y ** 2);
}
}
const p = new Point(3, 4);
const cloned = structuredClone(p);
console.log(p.distance()); // 5(✅)
console.log(cloned.distance); // undefined(❌ 原型方法丢失)
console.log(cloned instanceof Point); // false
console.log(Object.getPrototypeOf(cloned) === Object.prototype); // true
| 特性 | structuredClone | JSON.parse(JSON.stringify()) |
|---|---|---|
| Date | ✅ 保留 Date 类型 | ❌ 变成字符串 |
| RegExp | ✅ 保留 | ❌ 变成 {} |
| Map/Set | ✅ 保留 | ❌ 变成 {} / [] |
| undefined | ✅ 保留 | ❌ 属性被删除 |
| NaN/Infinity | ✅ 保留 | ❌ 变成 null |
| 循环引用 | ✅ 支持 | ❌ 抛出错误 |
| 函数 | ❌ 抛出 DataCloneError | ❌ 属性被删除 |
| 原型链 | ❌ 丢失 | ❌ 丢失 |
| Symbol 键 | ❌ 抛出 DataCloneError | ❌ 忽略 |
| BigInt | ✅ 支持 | ❌ 抛出 TypeError |
| 性能 | 较慢(更安全) | 快(针对简单对象) |
| Error | ✅ 保留 message/stack | ❌ 变成 {} |
const original = {
date: new Date('2024-01-15'),
regex: /hello/gi,
map: new Map([['key', 42]]),
set: new Set([1, 2, 3]),
undef: undefined,
nan: NaN,
infinity: Infinity,
fn: () => 'hello', // 函数
};
// structuredClone
let scClone;
try {
scClone = structuredClone(original);
} catch (e) {
console.log('structuredClone 失败:', e.message);
// "DataCloneError: () => 'hello' could not be cloned."
}
// JSON
const jsonClone = JSON.parse(JSON.stringify(original));
console.log({
date: jsonClone.date, // "2024-01-15T00:00:00.000Z"(字符串!)
regex: jsonClone.regex,
: jsonClone.,
: jsonClone.,
: jsonClone,
: jsonClone.,
: jsonClone.,
: jsonClone,
});
结构化克隆在以下 API 中自动使用,开发者不需要手动调用:
// 发送时序列化,接收时反序列化
worker.postMessage({ data: someObject }); // 内部使用结构化克隆
// 存储和读取时自动序列化/反序列化
const store = db.transaction('items', 'readwrite').objectStore('items');
store.add({ date: new Date(), map: new Map() }); // Date 和 Map 正确保存
// state 对象使用结构化克隆序列化
history.pushState({ user: { id: 1 }, visited: new Date() }, '', '/page');
// ⚠️ 函数和 DOM 节点不能放入 state!
new Notification('提醒', { data: { id: 1, tags: new Set(['urgent']) } });
const { port1, port2 } = new MessageChannel();
port1.onmessage = ({ data }) => console.log(data);
port2.postMessage({ value: new Date() }); // 自动克隆
/**
* 深拷贝方案汇总
*/
// 方案 1:JSON 序列化(最快,但限制多)
const json = (obj) => JSON.parse(JSON.stringify(obj));
// 方案 2:structuredClone(推荐,原生支持)
const sc = (obj) => structuredClone(obj);
// 方案 3:Lodash cloneDeep(兼容最广,但需要引入库)
import { cloneDeep } from 'lodash-es';
// 方案 4:手写深拷贝(可定制,应对特殊场景)
function deepClone(value, seen = new WeakMap()) {
// 原始类型和 null
if (value === null || typeof value !== 'object' && typeof value !== 'function') {
return value;
}
// 循环引用检测
if (seen.has(value)) return seen.get(value);
// 函数:直接返回引用(无法复制)
if (typeof value === 'function') return value;
// 特殊内置类型
if (value instanceof Date) return new Date(value.getTime());
(value ) (value., value.);
(value ) {
e = value.();
e. = value.;
(value.) e. = (value., seen);
e;
}
(.(value)) {
value.();
}
(value ) value.();
(value ) {
cloned = ();
seen.(value, cloned);
( [k, v] value) {
cloned.((k, seen), (v, seen));
}
cloned;
}
(value ) {
cloned = ();
seen.(value, cloned);
( v value) {
cloned.((v, seen));
}
cloned;
}
(.(value)) {
cloned = [];
seen.(value, cloned);
( i = ; i < value.; i++) {
cloned[i] = (value[i], seen);
}
cloned;
}
cloned = .(.(value));
seen.(value, cloned);
ownKeys = [
....(value),
....(value),
];
( key ownKeys) {
descriptor = .(value, key);
(descriptor. !== ) {
.(cloned, key, {
...descriptor,
: (descriptor., seen),
});
} {
.(cloned, key, descriptor);
}
}
cloned;
}
// 全面的边界测试套件
function runCloneTests(cloneFn, name) {
const tests = [
{
desc: '基础对象',
input: { a: 1, b: 'hello', c: true, d: null },
check: (original, clone) => {
return JSON.stringify(original) === JSON.stringify(clone)
&& original !== clone;
},
},
{
desc: 'Date 保留类型',
input: new Date('2024-01-15'),
check: (o, c) => c instanceof Date && c.getTime() === o.getTime(),
},
{
desc: 'RegExp 保留',
input: /hello/gi,
check: (o, c) => c instanceof RegExp && c.source === o.source && c.flags === o.flags,
},
{
desc: 'Map 保留',
: ([[, ], [, { : }]]),
: c && c. === o.,
},
{
: ,
: ([, , , { : }]),
: c && c. === o.,
},
{
: ,
: { : , : },
: c && c. === ,
},
{
: ,
: { : },
: .(c.),
},
{
: ,
: ( { o = {}; o. = o; o; })(),
: {
{ c. === c; }
{ ; }
},
},
{
: ,
: { : { : { : { : { : [, , ] } } } } },
: {
c......();
o...... === ;
},
},
{
: ,
: ( { s = (); o = {}; o[s] = ; o; })(),
: {
oKeys = .(o);
cKeys = .(c);
cKeys. === oKeys.;
},
},
];
.();
passed = ;
( { desc, input, check } tests) {
{
clone = (input);
ok = (input, clone);
.();
(ok) passed++;
} (e) {
.();
}
}
.();
}
( .(.(v)), );
( (v), );
(deepClone, );
// 需要手写深拷贝的场景:
// ① 保留原型链
class CustomError extends Error {
constructor(msg, code) {
super(msg);
this.code = code;
}
}
const e = new CustomError('失败', 404);
const sc = structuredClone(e); // instanceof CustomError → false(原型丢失)
const dc = deepClone(e); // instanceof CustomError → true ✅
// ② 克隆包含函数的对象(保留引用)
const store = {
state: { count: 0 },
increment() { this.state.count++; },
};
// structuredClone 会抛出 DataCloneError
// deepClone 保留函数引用,但函数不会被复制
const storeCopy = deepClone(store); // increment 是同一引用
// ③ Symbol 属性的保留
const sym = Symbol('private');
const obj = { [sym]: 'secret', public: 'visible' };
const sc2 = structuredClone(obj); // DataCloneError
const dc2 = (obj);
reactive = {
: ,
() { .; },
() { . = v; },
};
dc3 = (reactive);
dc3. = ;
Q1:structuredClone 比 JSON.parse(JSON.stringify()) 慢多少?
通常慢 2-5 倍(取决于数据结构复杂度)。JSON 对于简单对象有 V8 的高度优化路径。但 structuredClone 在处理 ArrayBuffer、Map、Set 等场景时更优,且不存在 JSON 的语义错误(丢失 undefined、NaN 变 null 等)。
选择标准:优先使用 structuredClone,仅在性能 profiling 证明其是瓶颈,且数据满足 JSON 限制时,才降级使用 JSON 方案。
Q2:postMessage 中同时传递普通对象和 ArrayBuffer 时,各自的序列化方式?
普通对象(非 Transferable):使用结构化克隆(深拷贝,O(n))。ArrayBuffer 若在 transfer 列表中:使用所有权转移(O(1),原持有方 detached);若不在 transfer 列表:同样深拷贝。
Q3:为什么 structuredClone 不支持 Symbol 键?
Symbol 是唯一且不可复制的标识符,其语义本身就是"全局唯一"。结构化克隆的目的是在上下文间传递可重建的数据。Symbol 无法在另一个上下文中重建同一 Symbol(除非是 Symbol.for() 的全局 Symbol),因此算法选择抛出错误而非静默丢失。
Q4:history.pushState(state) 的 state 大小有限制吗?
有。规范推荐上限为 640KB,Chrome 实际实现约 2MB。且 state 使用结构化克隆序列化存储在内存中,过大的 state 会增加内存压力。建议 state 中只存 ID,真实数据存在 store 或 URL 参数中。