跳到主要内容
← 返回文章列表
面试题-基础2026/10/30约 6 分钟读完

JavaScript 浅拷贝、深拷贝与 structuredClone:前端状态更新的正确方式

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

JavaScript 浅拷贝、深拷贝与 structuredClone:前端状态更新的正确方式
文章目录 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);

请回答:

  1. 四个 console.log 分别输出什么?为什么?
  2. { ...source } 到底复制了什么?哪些数据仍然与 source 共享引用?
  3. 如果要求修改副本中的嵌套数据时完全不影响 source,你会怎样实现?
  4. JSON.parse(JSON.stringify(source)) 和 structuredClone(source) 分别存在哪些问题?
  5. 在 Vue、React 等前端状态管理场景中,是否应该每次更新状态都对整个对象进行深拷贝?

我的回答

首次回答

  1. 输出分别是 Alice、["vue", "ai", "go"]、2030、false。展开语法实质上是浅拷贝,会把旧对象中的引用带入新对象。
  2. 复制了 source 的对象内容;tags、createdAt 和 onSave 仍然共享引用。
  3. 可以采用深拷贝,例如 JSON.parse(JSON.stringify(source))。
  4. 可能存在兼容性问题。

补充回答

  1. JSON 序列化后,createdAt 会变成字符串,onSave 仍然存在。
  2. 对包含函数的对象执行 structuredClone(source) 会报错。
  3. 每次深拷贝整个状态的性能较差,但暂时没有想到替代方案。

面试点评

评分

总分: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、onSave

3. JSON 序列化不会保留函数

执行:

const result = JSON.parse(JSON.stringify(source));

结果类似:

{
  user: {
    name: "Alice"
  },
  tags: ["vue", "ai"],
  createdAt: "2026-01-01T00:00:00.000Z"
}

onSave 会被删除:

result.onSave; // undefined

4. 没有给出状态更新的替代方案

前端状态更新通常不需要复制整个对象。更常见的方案是只复制发生变化的路径,未变化的分支继续复用原引用,这种方式称为结构共享。

推荐答案与原理解析

浅拷贝复制了什么

展开运算符会:

  1. 创建一个新的普通对象;
  2. 枚举源对象自身的可枚举属性;
  3. 把每个属性值赋给新对象。

对于基本类型,复制的是值;对于对象、数组、日期和函数,属性值本身是引用,因此复制的是引用值。

可以理解为:

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);
// DataCloneError

structuredClone 也不等于前端状态更新的默认最佳方案,因为它仍然会复制整棵可克隆的数据结构。

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 则可以利用响应式代理直接修改目标属性。

最终总结

这道题需要掌握四个关键结论:

  1. 浅拷贝创建新的外层对象,但嵌套对象仍然共享引用。
  2. JSON 序列化只适合简单 JSON 数据,不能作为通用深拷贝方案。
  3. structuredClone 支持更多数据类型,但不能克隆函数,也不适合替代所有状态更新策略。
  4. 前端状态更新应优先采用结构共享,只复制修改路径,避免深拷贝整个状态。

评论

0 条

评论会在审核后展示,邮箱不会公开。

正在确认登录状态

5000 / 5000