structuredClone 与序列化边界
Site Owner
发布于 2026-05-21
详解structuredClone算法支持的类型与不支持的类型、与JSON.parse对比、递归循环引用处理及深拷贝完整方案

structuredClone 与序列化边界
结构化克隆算法支持的类型(Map/Set/Date/RegExp/ArrayBuffer/Error 等)与不支持的类型(Function/DOM 节点/Symbol/原型链);与 JSON.parse(JSON.stringify()) 的对比;postMessage、IndexedDB、History API 中的隐式使用。
目录
- 结构化克隆算法(SCA)
- structuredClone API
- 支持 vs 不支持的类型
- 与 JSON 深拷贝的对比
- 隐式使用场景
- 深拷贝方案全景对比
- 手写验证:深拷贝边界测试
- 何时仍需手写深拷贝
- 深度追问
结构化克隆算法(SCA)
结构化克隆算法(Structured Clone Algorithm)是 HTML 规范定义的一套深拷贝标准,用于在不同上下文(Worker、IndexedDB 等)之间传递数据。
设计目标
- 支持比 JSON 更丰富的类型(循环引用、类型化数组、Date、Map 等)
- 不支持无法被序列化的类型(函数、DOM 节点)
- 正确处理循环引用(记忆已克隆节点,不无限递归)
算法核心步骤
1. 维护一个"已访问"Map:original → clone(处理循环引用)
2. 检查值类型:
- 原始值 → 直接返回
- 可克隆对象 → 创建新实例,递归克隆属性
- Transferable → 转移(不克隆)
- 不可克隆 → 抛出 DataCloneError
3. 对象属性只克隆"自有可枚举"属性(不克隆原型链方法)
structuredClone API
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
Transferable 支持
// 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
支持 vs 不支持的类型
✅ 支持克隆的类型
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: new Float64Array([Math.PI]),
dataView: new DataView(new ArrayBuffer(4)),
blob: new Blob(['hello']),
// 结构
array: [1, [2, [3]]],
object: { a: { b: { c: 1 } } },
});
// 循环引用
const circular = {};
circular.self = circular;
const clonedCircular = structuredClone(circular); // ✅ 不会栈溢出
console.log(clonedCircular.self === clonedCircular); // true(保持循环结构)
❌ 不支持克隆的类型(抛出 DataCloneError)
// ① 函数
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); return t[k]; }
});
const cloned = structuredClone(proxy); // 克隆的是 { x: 1 },无 Proxy
// cloned.x 不会触发 get trap
// ⑤ WeakMap / WeakSet / WeakRef
try {
structuredClone(new WeakMap());
} catch (e) {
console.log(e.name); // "DataCloneError"
}
⚠️ 克隆时丢失原型链
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
与 JSON 深拷贝的对比
全面对比表
| 特性 | 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, // {}(丢失!)
map: jsonClone.map, // {}(丢失!)
set: jsonClone.set, // (丢失,变成空数组或{})
undef: 'undef' in jsonClone, // false(属性被删除!)
nan: jsonClone.nan, // null(变成 null!)
infinity: jsonClone.infinity, // null
fn: 'fn' in jsonClone, // false(函数被忽略!)
});
隐式使用场景
结构化克隆在以下 API 中自动使用,开发者不需要手动调用:
1. postMessage(Web Workers)
// 发送时序列化,接收时反序列化
worker.postMessage({ data: someObject }); // 内部使用结构化克隆
2. IndexedDB
// 存储和读取时自动序列化/反序列化
const store = db.transaction('items', 'readwrite').objectStore('items');
store.add({ date: new Date(), map: new Map() }); // Date 和 Map 正确保存
3. History API
// state 对象使用结构化克隆序列化
history.pushState({ user: { id: 1 }, visited: new Date() }, '', '/page');
// ⚠️ 函数和 DOM 节点不能放入 state!
4. Notification API
new Notification('提醒', { data: { id: 1, tags: new Set(['urgent']) } });
5. MessageChannel
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());
if (value instanceof RegExp) return new RegExp(value.source, value.flags);
if (value instanceof Error) {
const e = new value.constructor(value.message);
e.stack = value.stack;
if (value.cause) e.cause = deepClone(value.cause, seen);
return e;
}
// 类型化数组
if (ArrayBuffer.isView(value)) {
return new value.constructor(value.buffer.slice(0));
}
if (value instanceof ArrayBuffer) return value.slice(0);
// Map
if (value instanceof Map) {
const cloned = new Map();
seen.set(value, cloned);
for (const [k, v] of value) {
cloned.set(deepClone(k, seen), deepClone(v, seen));
}
return cloned;
}
// Set
if (value instanceof Set) {
const cloned = new Set();
seen.set(value, cloned);
for (const v of value) {
cloned.add(deepClone(v, seen));
}
return cloned;
}
// 数组
if (Array.isArray(value)) {
const cloned = [];
seen.set(value, cloned);
for (let i = 0; i < value.length; i++) {
cloned[i] = deepClone(value[i], seen);
}
return cloned;
}
// 普通对象(保留原型链)
const cloned = Object.create(Object.getPrototypeOf(value));
seen.set(value, cloned);
// 克隆自有属性(包括 Symbol 键)
const ownKeys = [
...Object.getOwnPropertyNames(value),
...Object.getOwnPropertySymbols(value),
];
for (const key of ownKeys) {
const descriptor = Object.getOwnPropertyDescriptor(value, key);
if (descriptor.value !== undefined) {
Object.defineProperty(cloned, key, {
...descriptor,
value: deepClone(descriptor.value, seen),
});
} else {
// getter/setter:直接复制描述符
Object.defineProperty(cloned, key, descriptor);
}
}
return 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 保留',
input: new Map([['key', 'value'], [1, { nested: true }]]),
check: (o, c) => c instanceof Map && c.size === o.size,
},
{
desc: 'Set 保留',
input: new Set([1, 2, 3, { x: 1 }]),
check: (o, c) => c instanceof Set && c.size === o.size,
},
{
desc: 'undefined 值保留',
input: { a: undefined, b: 1 },
check: (o, c) => 'a' in c && c.a === undefined,
},
{
desc: 'NaN 保留',
input: { value: NaN },
check: (o, c) => Number.isNaN(c.value),
},
{
desc: '循环引用不崩溃',
input: (() => { const o = {}; o.self = o; return o; })(),
check: (o, c) => {
try { return c.self === c; }
catch { return false; }
},
},
{
desc: '深层嵌套',
input: { a: { b: { c: { d: { e: [1, 2, 3] } } } } },
check: (o, c) => {
c.a.b.c.d.e.push(4);
return o.a.b.c.d.e.length === 3;
},
},
{
desc: 'Symbol 键',
input: (() => { const s = Symbol('id'); const o = {}; o[s] = 42; return o; })(),
check: (o, c) => {
const oKeys = Object.getOwnPropertySymbols(o);
const cKeys = Object.getOwnPropertySymbols(c);
return cKeys.length === oKeys.length;
},
},
];
console.log(`\n=== ${name} ===`);
let passed = 0;
for (const { desc, input, check } of tests) {
try {
const clone = cloneFn(input);
const ok = check(input, clone);
console.log(`${ok ? '✅' : '❌'} ${desc}`);
if (ok) passed++;
} catch (e) {
console.log(`❌ ${desc}(异常:${e.message})`);
}
}
console.log(`通过 ${passed}/${tests.length}`);
}
// 对比各方案
runCloneTests(v => JSON.parse(JSON.stringify(v)), 'JSON 深拷贝');
runCloneTests(v => structuredClone(v), 'structuredClone');
runCloneTests(deepClone, '手写 deepClone');
// 结果示例:
// === JSON 深拷贝 ===
// ✅ 基础对象
// ❌ Date 保留类型
// ❌ RegExp 保留
// ❌ Map 保留
// ❌ Set 保留
// ❌ undefined 值保留
// ❌ NaN 保留
// ❌ 循环引用不崩溃(异常:Converting circular structure to JSON)
// ✅ 深层嵌套
// ❌ Symbol 键
// 通过 2/10
// === structuredClone ===
// ✅ 基础对象
// ✅ Date 保留类型
// ✅ RegExp 保留
// ✅ Map 保留
// ✅ Set 保留
// ✅ undefined 值保留
// ✅ NaN 保留
// ✅ 循环引用不崩溃
// ✅ 深层嵌套
// ❌ Symbol 键(DataCloneError)
// 通过 9/10
何时仍需手写深拷贝
// 需要手写深拷贝的场景:
// ① 保留原型链
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 = deepClone(obj); // { [sym]: 'secret', public: 'visible' } ✅
// ④ getter/setter 的处理
const reactive = {
_value: 0,
get value() { return this._value; },
set value(v) { this._value = v; },
};
const dc3 = deepClone(reactive);
dc3.value = 10; // ✅ setter 仍有效
深度追问
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 参数中。