侧边栏壁纸
博主头像
一笑痕

仙人之下我无敌,
仙人之上一换一。

  • 累计撰写 52 篇文章
  • 累计收到 7 条评论

structuredClone 能克隆什么,不能克隆什么

2026-9-15 / 0 评论 / 13 阅读

structuredClone 能克隆什么,不能克隆什么

前几年做深拷贝,标准答案基本是 JSON.parse(JSON.stringify(obj))。这招的问题在于它会偷偷改数据:Date 变成字符串,undefined 和函数直接消失,碰上循环引用还会报错。

后来 structuredClone 进了标准,Chrome 98 之后都能用。一开始以为终于有正经的深拷贝了,用了一阵子才发现它也有自己的脾气。下面 13 条检查项,每条一句结论,紧跟着的是脚本里的实际输出,不是从文档抄的。第 13 条是取舍。

能克隆的

01 嵌套对象和数组都是深拷贝,改副本动不到原对象

const src = { a: 1, b: { c: [1, 2, 3] } };
const cp = structuredClone(src);
cp.b.c.push(4);
// 原对象 b.c = [1,2,3] / 副本 b.c = [1,2,3,4]

02 循环引用也能克隆,JSON 方案在这一步直接报错

const cyc = { name: 'x' };
cyc.self = cyc;
const c = structuredClone(cyc);
c.self === c; // true

03 内置类型逐个还原,Map 和 Set 不会变成空对象

类型 结果
Date instanceof Date 为 true,时间值一致
Map 还原成 Map,值是深拷贝
Set 还原成 Set
BigInt 还是 bigint
Uint8Array 还是 Uint8Array
TypeError 还原了,name 和 message 都在

Map 和 Set 这一条挺重要。JSON 方案会把它们变成空对象 {},而且不报错,等发现的时候通常已经在线上跑了。

前三条都属于日常最常见的数据形态,克隆结果和原件完全断开,改哪边都不影响另一边。后面十条讲的是会变形或者会抛错的地方。

会失效的那几类

04 函数、Symbol、WeakMap、Proxy 直接抛异常,不返回 undefined

structuredClone({ fn: () => 1 });
// DOMException: () => 1 could not be cloned.

structuredClone({ s: Symbol('x') });
// DOMException: Symbol(x) could not be cloned.

structuredClone(new WeakMap());
// DOMException: #<WeakMap> could not be cloned.

structuredClone(new Proxy({ a: 1 }, {}));
// DOMException: #<Object> could not be cloned.

弱引用类型克隆不了是合理的,克隆出来的弱引用该指向谁呢。Proxy 克隆不了也好理解,代理的目标和陷阱函数没法序列化。

这些值都报同一种错,问题直接落在调用点上,比悄悄改写容易发现。脚本里每一项都套了 try/catch,抛出来的构造函数名和 message 原样打在控制台,文章里的报错原文就是那几行输出。

05 异常类型是 DOMException,按 TypeError 分支去写会漏

实测下来有个坑:在 Node 里写 catch (e) { if (e instanceof TypeError) ... } 是抓不到的,错误处理分支根本没进去。它抛的是 DOMException

06 类实例的原型会丢,数据全在而方法没了

class User {
  constructor(n) { this.name = n; }
  greet() { return 'hi ' + this.name; }
}

const u = structuredClone(new User('金铭'));
u instanceof User;  // false
u.name;             // '金铭'  —— 数据在
u.greet;            // undefined —— 方法没了

数据全都复制过来了,但 prototype 没跟过来。规范里的说法是「结构化克隆」,不是「对象克隆」,它只处理数据,不处理行为。结果就是克隆出来的东西长得像 User,实际是个普通对象,调用它的方法时才报错,而且报错位置往往离克隆的地方很远。

07 对象上有方法就别用它,补原型能把方法救回来

const u2 = structuredClone(plainUser);  // 先拿数据快照
Object.setPrototypeOf(u2, User.prototype);

类里有私有字段(#field)的话还是救不了,私有字段不在结构化克隆的范围内。

08 getter 会被求值,变成可写的数据属性

const withGetter = {
  _x: 1,
  get x() { return this._x * 10; },
};
const g = structuredClone(withGetter);
g.x;  // 10
Object.getOwnPropertyDescriptor(g, 'x');
// { value: 10, writable: true, enumerable: true, configurable: true }

克隆读的是属性值,不是属性描述符。原本那个只读的计算属性,克隆完变成了一个可写的数据属性。代码如果依赖 getter 做校验或者惰性计算,克隆之后就失效了。

09 不可枚举的属性会整个丢掉

const hidden = {};
Object.defineProperty(hidden, 'secret', { value: 42, enumerable: false });
structuredClone(hidden);  // {} —— secret 没了

10 冻结状态不保留,克隆出来完全可写

const f = structuredClone(Object.freeze({ a: 1 }));
Object.isFrozen(f);  // false

拿冻结对象当配置常量传给别处、指望它防篡改的话,克隆一次就白冻了。

11 稀疏数组的洞会变成显式的 undefined

const sp = structuredClone([1, , 3]);
1 in sp;   // true —— 原来是个洞,现在是个真实存在的 undefined

长度没错,但 in 判断和 Object.keys 的结果会变。

12 RegExp 的 lastIndex 会被重置

const re = /ab/g;
re.lastIndex = 5;
const c = structuredClone(re);
c.lastIndex;  // 0,不是 5

gy 标志的正则用 exec 连续匹配时依赖 lastIndex。克隆一个匹配到一半的正则,进度就丢了,下次又从字符串开头开始找。这个坑不常见,真碰上了很难查。

13 取舍:纯数据用它,行为别指望它

日常的纯数据,比如接口返回的 JSON、配置对象、要存进 IndexedDB 的东西,structuredClone 比 JSON 方案好,直接用。对象上有方法、有 getter、有私有字段,或者依赖冻结状态,这一类我不推荐用:它给出来的是一份数据的快照,不是那个对象的副本。至于 JSON.parse(JSON.stringify()),现在没有理由再用它了:克隆不了的值它会静默改写,structuredClone 至少会大声报错。

    🤞 分享