JavaScript 浅拷贝、深拷贝与 structuredClone:前端状态更新的正确方式
通过一道对象复制代码题,梳理 JavaScript 浅拷贝、JSON 深拷贝、structuredClone 和 transferable object 的边界,并解释 React 结构共享与 Vue 3 响应式更新的差异。

文章目录 21
题目信息
- 领域:JavaScript、前端状态管理
- 难度:L2 基础应用(中级偏低)
- 预计用时:10–15 分钟
- 题型:代码题、场景题
- 考察方向:分析对象复制后的引用关系,理解常见深拷贝方案的数据类型边界,并根据前端状态更新场景选择合适的复制策略。
面试题
阅读下面的代码:
const source = {
user: {
name: "Alice"
},
tags: ["vue", "ai"],
createdAt: new Date("2026-01-01"),
onSave() {
console.log("saved");
}
};
const copy = { ...source };
copy.user.name = "Bob";
copy.tags.push("go");
copy.createdAt.setFullYear(2030);
console.log(source.user.name);
console.log(source.tags);
console.log(source.createdAt.getFullYear());
console.log(copy === source);请回答:
- 四个
console.log分别输出什么?为什么? { ...source }到底复制了什么?哪些数据仍然与source共享引用?- 如果要求修改副本中的嵌套数据时完全不影响
source,你会怎样实现? JSON.parse(JSON.stringify(source))和structuredClone(source)分别存在哪些问题?- 在 Vue、React 等前端状态管理场景中,是否应该每次更新状态都对整个对象进行深拷贝?
我的回答
首次回答
- 输出分别是
Alice、["vue", "ai", "go"]、2030、false。展开语法实质上是浅拷贝,会把旧对象中的引用带入新对象。 - 复制了
source的对象内容;tags、createdAt和onSave仍然共享引用。 - 可以采用深拷贝,例如
JSON.parse(JSON.stringify(source))。 - 可能存在兼容性问题。
补充回答
- JSON 序列化后,
createdAt会变成字符串,onSave仍然存在。 - 对包含函数的对象执行
structuredClone(source)会报错。 - 每次深拷贝整个状态的性能较差,但暂时没有想到替代方案。
面试点评
评分
总分:49 / 100
- 正确性:23 / 40
- 原理理解:9 / 20
- 工程应用:7 / 20
- 边界与权衡:4 / 10
- 面试表达:6 / 10
回答正确的部分
四个日志输出判断正确:
Alice
["vue", "ai", "go"]
2030
false同时正确识别了以下内容:
- 展开运算符执行的是浅拷贝;
- 修改嵌套对象会影响原对象;
- JSON 序列化会把
Date转换成字符串; structuredClone遇到函数会报错;- 对整个大型状态进行深拷贝存在性能成本。
遗漏和错误
1. 外层对象和嵌套引用的关系说反了
浅拷贝会创建新的外层对象:
copy === source; // false但它只复制第一层属性值。属性值如果是对象,复制过去的仍然是同一个引用:
copy.user === source.user; // true
copy.tags === source.tags; // true
copy.createdAt === source.createdAt; // true
copy.onSave === source.onSave; // true因此,共享的是嵌套对象引用,而不是外层对象。
2. 遗漏了 user
仍然共享引用的属性包括:
user、tags、createdAt、onSave3. JSON 序列化不会保留函数
执行:
const result = JSON.parse(JSON.stringify(source));结果类似:
{
user: {
name: "Alice"
},
tags: ["vue", "ai"],
createdAt: "2026-01-01T00:00:00.000Z"
}onSave 会被删除:
result.onSave; // undefined4. 没有给出状态更新的替代方案
前端状态更新通常不需要复制整个对象。更常见的方案是只复制发生变化的路径,未变化的分支继续复用原引用,这种方式称为结构共享。
推荐答案与原理解析
浅拷贝复制了什么
展开运算符会:
- 创建一个新的普通对象;
- 枚举源对象自身的可枚举属性;
- 把每个属性值赋给新对象。
对于基本类型,复制的是值;对于对象、数组、日期和函数,属性值本身是引用,因此复制的是引用值。
可以理解为:
source ── user ───────┐
├── 同一个对象
copy ─── user ────────┘JSON 序列化的边界
JSON.parse(JSON.stringify(value)) 只适合结构简单、完全符合 JSON 数据模型的数据。
常见问题包括:
Date变成字符串;- 函数属性被删除;
undefined和Symbol属性被删除;BigInt会导致序列化报错;- 循环引用会导致报错;
NaN和Infinity会变成null;- 原型和类实例信息丢失。
因此,它不是通用的深拷贝方案。
structuredClone 的边界
structuredClone 可以处理许多 JSON 无法正确处理的数据:
Date;Map;Set;- 循环引用;
ArrayBuffer;- 部分 TypedArray。
但它不能克隆函数:
structuredClone(source);
// DataCloneErrorstructuredClone 也不等于前端状态更新的默认最佳方案,因为它仍然会复制整棵可克隆的数据结构。
React:只复制修改路径
假设状态如下:
const state = {
user: {
profile: {
name: "Alice",
age: 20
},
permissions: ["read"]
},
posts: [
{ id: 1, title: "JavaScript" }
]
};只修改用户名时,可以写成:
const nextState = {
...state,
user: {
...state.user,
profile: {
...state.user.profile,
name: "Bob"
}
}
};此时:
nextState !== state; // true
nextState.user !== state.user; // true
nextState.user.profile !== state.user.profile; // true
nextState.posts === state.posts; // true
nextState.user.permissions === state.user.permissions; // true只有发生变化的路径产生新对象,未变化的 posts 和 permissions 继续复用。
嵌套状态较复杂时,可以使用 Immer 简化不可变更新:
const nextState = produce(state, draft => {
draft.user.profile.name = "Bob";
});Vue 3:利用响应式代理
Vue 3 的 reactive 使用 Proxy 跟踪属性读取与修改,通常可以直接更新目标属性:
const state = reactive({
user: {
profile: {
name: "Alice"
}
}
});
state.user.profile.name = "Bob";Vue 会通知依赖该属性的视图更新,不需要为了触发响应式而深拷贝整个状态。
因此需要区分框架行为:
- React 通常依赖不可变更新和引用变化;
- Vue 3 可以跟踪响应式代理内部属性的修改。
递进追问与补答
追问
请只复制发生变化的路径,把用户名修改为 "Bob",并保证:
nextState.posts === state.posts;
nextState.user.permissions === state.user.permissions;我的补答
const nextState = structuredClone(state, {
transfer: [state.user.permissions, state.posts]
});
nextState.user.profile.name = "Bob";追问点评
评分:20 / 100
这段代码知道先生成新对象再修改用户名,但存在两个关键问题。
第一,普通数组不是 transferable object:
transfer: [state.user.permissions, state.posts]permissions 和 posts 都是普通数组,把它们放入 transfer list 通常会抛出 DataCloneError。
transfer 主要用于转移 ArrayBuffer 等对象的所有权:
const buffer = new ArrayBuffer(1024);
const cloned = structuredClone(
{ buffer },
{ transfer: [buffer] }
);第二,即使移除 transfer,structuredClone(state) 也会复制全部嵌套数据:
nextState.posts === state.posts; // false
nextState.user.permissions ===
state.user.permissions; // false这不符合结构共享要求。正确方式仍然是沿着修改路径逐层展开,其余分支继续复用原引用。
推荐面试口述
展开运算符只创建新的外层对象,并复制第一层属性值。如果属性值是对象、数组、日期或函数,复制过去的仍然是原引用,所以修改嵌套数据会影响原对象。
JSON 序列化不适合作为通用深拷贝,因为 Date 会变成字符串、函数会被删除、循环引用会报错。structuredClone 能处理 Date、Map、Set 和循环引用,但不能克隆函数。
前端状态更新时也不应该默认深拷贝整个对象,因为这会产生额外的时间和内存开销,并让未变化的数据引用也发生改变。React 中通常只复制被修改路径上的对象,实现结构共享;Vue 3 则可以利用响应式代理直接修改目标属性。
最终总结
这道题需要掌握四个关键结论:
- 浅拷贝创建新的外层对象,但嵌套对象仍然共享引用。
- JSON 序列化只适合简单 JSON 数据,不能作为通用深拷贝方案。
structuredClone支持更多数据类型,但不能克隆函数,也不适合替代所有状态更新策略。- 前端状态更新应优先采用结构共享,只复制修改路径,避免深拷贝整个状态。
相关文章
- 面试题-基础
Vue 3 响应式解构:为什么替换 user 后出现“李四 / 王五”
通过一道 Vue 3 代码题,区分 reactive 对象解构、嵌套属性修改与对象替换的行为,并用 toRef 保持源属性关联。文章保留首次作答及追问补答,说明从误判到纠正的过程。
- 面试题-基础
Cookie 登录接口中的 CSRF:同源策略、CORS、SameSite 与 CSRF Token
通过修改邮箱的跨站表单攻击,梳理 CSRF 的成立条件,解释为什么“读不到响应”不代表攻击失败,并比较 SameSite、CSRF Token、Origin 校验与限流的职责边界。
- 面试题-基础
面试题:AI 前端中的模型 API Key 安全
本题考察模型调用链路设计,以及前端环境变量、BFF 和接口安全。
评论
0 条评论会在审核后展示,邮箱不会公开。