搜索文章

前端面试 P081:StrictMode 的检查与版本边界JavaScript 函数传参:值传递与对象引用前端面试 P018:手写柯里化与参数个数边界前端面试 P010:普通函数、箭头函数与 this前端面试 P088:Immer 草稿与结构共享前端面试 P006:对象遍历与属性键的选择前端面试 P009:执行上下文、调用栈与词法作用域前端面试 P094:SSE 分片解码与事件边界前端面试 P097:请求并发池、任务启动与失败释放前端面试 P076:Fiber 的协作调度与不可中断边界前端面试 P013:节流、防抖与取消待执行任务前端面试 P033:Object.create、描述符与共享属性前端面试 P057:Vue Router 配置、历史模式与导航前端面试 P030:构造函数与 new 的返回规则前端面试 P077:key、组件身份与状态重置前端面试 P039:同源策略、CORS 与请求是否发出前端面试 P032:instanceof、原型链与自定义判断前端面试 P047:Proxy 与 Vue 2 属性拦截的区别前端面试 P019:函数组合与同步数据管道前端面试 P004:手写深拷贝、循环引用与实现边界前端面试 P059:导航守卫、顺序与权限边界前端面试 P083:函数式更新、队列与纯度前端面试 P014:call、apply、bind 与模拟实现边界前端面试 P012:高阶函数、柯里化与部分应用前端面试 P041:事件委托、目标匹配与 React 事件前端面试 P048:track、trigger、动态依赖与清理前端面试 P066:自定义指令与 DOM 资源清理前端面试 P075:props 只读、状态归属与派生数据前端面试 P062:Pinia 创建、实例范围与使用前端面试 P073:JSX 编译、React 元素与表达式前端面试 P026:Promise 延迟与顺序任务队列前端面试 P016:纯函数、副作用与参数共享前端面试 P094:流式文本的 SSE 与 WebSocket 取舍前端面试 P064:storeToRefs 与 action 解构纠错前端面试 P044:Composition API 与组合式函数前端面试 P051:副作用清理与过期请求前端面试 P021:事件循环、微任务与渲染机会前端面试 P003:原始值、对象共享与赋值行为前端面试 P098:用户偏好存储、TTL 与命名空间前端面试 P056:插槽作用域与 slot props前端面试 P011:闭包、变量绑定与循环回调前端面试 P027:async 错误处理与 return await前端面试 P036:CSP、nonce 与防护边界前端面试 P007:隐式转换、ToPrimitive 与空数组比较前端面试 P035:继承方式对照与寄生组合式继承前端面试 P025:手写 Promise 的最小状态机前端面试 P079:受控表单与非受控输入前端面试 P072:虚拟 DOM 的价值与性能边界前端面试 P005:数组方法、可变性与回调副作用前端面试 P100:Proxy 沙箱为何不是安全边界前端面试 P040:发布订阅、事件总线与退订前端面试 P053:props、组件事件与 v-model 约定前端面试 P008:相等比较、NaN 与 Object.is前端面试 P065:Pinia 最小变更与对象替换的取舍前端面试 P087:Redux 单向数据流与状态边界前端面试 P002:数据类型与 typeof 的陷阱前端面试 P085:useReducer 与 useState 的实际取舍前端面试 P078:React 合成事件与旧事件池纠错前端面试 P025:Promise 解析过程与实现验收前端面试 P043:Vue 渐进式与声明式 UI前端面试 P091:SWR、TanStack Query 与服务端缓存前端面试 P074:函数组件快照与类实例的输出纠错前端面试 P071:UI 是状态映射与渲染纯度前端面试 P054:provide、inject 与跨层级依赖前端面试 P061:何时引入全局状态管理前端面试 P097:请求去重、缓存键与过期策略前端面试 P086:Context 拆分与订阅更新边界前端面试 P024:Promise 并发组合与空输入边界前端面试 P089:Redux middleware、next 与异步返回值前端面试 P022:Promise 状态、链式调用与错误传播前端面试 P084:状态提升、单一来源与范围选择前端面试 P034:class、super 与语法糖的边界前端面试 P058:路由参数、query 与组件 props前端面试 P020:定时器延迟、轮询重叠与清理前端面试 P029:对象原型、prototype 与属性查找前端面试 P095:AI SDK、编排框架与生成 UI 工具前端面试 P092:Signals、细粒度依赖与 React 集成边界前端面试 P046:Vue 响应式陷阱与 ref 自动解包纠错前端面试 P090:Zustand、Jotai 与状态作用域前端面试 P100:微前端 JS 隔离方案与资源生命周期前端面试课程:学习目录与纠错记录前端面试 P023:async、await、异步恢复与并发前端面试 P068:Vue 2 与 Vue 3 的列表 diff 区别前端面试 P015:IIFE、作用域隔离与现代模块前端面试 P063:Pinia 状态、getter 与 action前端面试 P052:Vue 条件、列表与表单指令前端面试 P050:Vue 生命周期、DOM 时机与 SSR前端面试 P060:路由 meta、合并与界面配置前端面试 P045:ref、reactive、computed 与 watch前端面试 P096:令牌无感刷新、重试与安全边界前端面试 P093:MCP 架构与前端集成场景前端面试 P080:组合、children 与自定义 Hook前端面试 P082:useState 更新请求、快照与批处理前端面试 P067:Vue 模板编译与运行时边界前端面试 P031:手写 new 与 Reflect.construct 的区别前端面试 P069:nextTick 等待 DOM 刷新与绘制的区别前端面试 P055:默认插槽、具名插槽与内容接口前端面试 P038:嵌套定时器的最小延迟与调度选择前端面试 P099:倒计时截止时间、漂移与完成事件前端面试 P019:记忆化缓存与递归调用的纠错前端面试 P049:Vue 与 React 的更新范围和批处理前端面试 P004:浅拷贝、深拷贝与 structuredClone前端面试 P042:Vue 面试导学与学习顺序前端面试 P096:共享刷新 Promise 与并发失败恢复前端面试 P070:React 面试导学与复习主线前端面试 P028:异步输出顺序的逐步推演前端面试 P001:var、let、const 与暂时性死区前端面试 P037:内存泄漏的快照与引用链排查前端面试 P017:手写 bind、重复绑定与构造调用你好,这里是 Henry 的自留地给想法找一个落脚的地方,也给那些解决过的问题留下一点记录。
Esc 关闭
全部文章

前端面试 P004:浅拷贝、深拷贝与 structuredClone

来源:方格前端讲面试 · P4:深浅拷贝的本质(含常见实现),16 分 15 秒。本集的手写部分另见 前端面试 P004:手写深拷贝、循环引用与实现边界。

先区分赋值与拷贝

const original = { a: 1, nested: { count: 2 } };
const same = original;

console.log(same === original); // true
same.nested.count = 20;
console.log(original.nested.count); // 20

这里没有创建另一个对象。对象赋值只让两个变量访问同一个对象,见 前端面试 P003:原始值、对象共享与赋值行为。

浅拷贝只复制一层属性值

const original = { a: 1, nested: { count: 2 } };
const copy = { ...original };

copy.a = 10;
copy.nested.count = 20;

console.log(copy === original); // false
console.log(copy.nested === original.nested); // true
console.log(original.a); // 1
console.log(original.nested.count); // 20

外层对象不同,但 nested 属性仍指向同一个子对象。原始值属性重新赋值不会影响另一份对象;通过共享的子对象修改属性则会相互可见。

常见浅拷贝包括对象展开 { ...obj }、Object.assign({}, obj),以及数组的 [...array]、array.slice() 等。它们的细节并不完全一致,不能只因为都“浅”就认为行为相同。例如对象展开和 Object.assign 对目标对象 setter 的处理有区别。

JSON 序列化不等于通用深拷贝

JSON.parse(JSON.stringify(value)) 适合满足 JSON 数据约束的场景,不适合任意 JavaScript 值。

const original = {
  date: new Date('2026-01-01T00:00:00Z'),
  missing: undefined,
  fn: () => 1,
  symbol: Symbol('id'),
  number: NaN,
};

const copy = JSON.parse(JSON.stringify(original));
console.log(typeof copy.date); // string
console.log(Object.hasOwn(copy, 'missing')); // false
console.log(Object.hasOwn(copy, 'fn')); // false
console.log(Object.hasOwn(copy, 'symbol')); // false
console.log(copy.number); // null

还要注意:

  • 对象属性里的 undefined、函数、Symbol 值通常被忽略;数组中的相应位置通常变成 null。
  • 循环引用会抛错。
  • 没有定制序列化处理时,BigInt 会抛错。
  • Map、Set、原型及属性描述符等不能通过这套过程完整保留。
  • toJSON 等自定义行为也会影响结果。

structuredClone 适合支持的可克隆数据

const original = {
  nested: { count: 2 },
  date: new Date('2026-01-01T00:00:00Z'),
};
original.self = original;

const copy = structuredClone(original);
copy.nested.count = 20;

console.log(original.nested.count); // 2
console.log(copy.self === copy); // true
console.log(copy.date instanceof Date); // true
console.log(copy.date.getTime() === original.date.getTime()); // true

结构化克隆能处理循环引用,以及 Date、Map、Set、ArrayBuffer 等支持的类型,因此通常比 JSON 往返更适合复制这些数据。

需要纠正:Error 对象可以结构化克隆

视频约 09:11 的口述和课件把 Error 对象列为不能克隆的类型,这不正确。标准支持 Error 类型,name、message 等错误信息可以被保留;自定义扩展字段是否保留不能据此一概保证。

const copy = structuredClone(new TypeError('bad input'));
console.log(copy instanceof TypeError); // true
console.log(copy.message); // bad input

视频最后还建议复杂场景用 Lodash.cloneDeep“拷贝函数”。这里也不能理解为复制出一个行为独立的新函数:克隆库通常不会重建函数闭包;例如对象属性中的函数可能继续共享同一个函数对象。选库仍需要确认它支持的类型和语义。

对“深拷贝首选”的边界补充

视频推荐现代 JavaScript 中优先考虑 structuredClone,这个方向对支持的数据很实用,但不能理解为“任何对象都能完整复制”。

  • 函数、DOM 节点等不能直接结构化克隆,可能抛 DataCloneError。
  • 自定义类的原型、私有字段、访问器和属性描述符等,不会按普通对象的全部行为原样重建。
  • 复制对象图时,会保留副本内部原本的共享关系,而不是为每个属性重复制造一份不同对象。
  • 传输 ArrayBuffer 会使源缓冲区分离;SharedArrayBuffer 的底层内存仍可共享。这些是具体类型的语义,不能一概背成“所有内存完全独立”。

因此,先确定数据类型、是否需要保留类行为、是否有循环引用,再选择工具。更新某个字段时,也不一定需要深拷贝整个对象。

面试回答思路

浅拷贝创建新的外层对象,但嵌套对象仍可能共享;深拷贝需要根据支持的数据类型递归或按相应算法复制对象图,并处理循环引用。对象展开、Object.assign 是常见浅拷贝,JSON 往返有数据丢失和循环引用等限制,structuredClone 能处理多种内建类型但也有明确边界。

参考与关联

看看其他文章 →