前端文档
浏览器相关
Css
JavaScript
Vue
Typescript
React
性能优化
AI 场景
  • Vue
  • React
退出
浏览器相关
Css
JavaScript
Vue
Typescript
React
性能优化
AI 场景
  • Vue
  • React
退出
  • Javascript 面试题

Javascript 面试题

一、语言核心机制

作用域 & 闭包

词法作用域:函数能访问哪些变量,在定义时就确定了。闭包 = 函数 + 它定义时捕获的外层环境引用。

核心点

  • var:声明提升 + 初始化为 undefined,函数作用域,挂 window
  • let/const:只提升声明不初始化(TDZ),块级作用域,访问抛 ReferenceError
  • 循环陷阱:var 所有迭代共享同一个 i,let 每次迭代产生新绑定

项目价值:防抖/节流保存 timer、模块私有变量、memoize 缓存结果、函数工厂

风险:持有大对象 / DOM 节点 / 定时器未清理 → 内存泄漏

  • Vue:onUnmounted 清理
  • React:useEffect return 清理

追问

  • 闭包一定泄漏?→ 不会,只有长期持有无法释放的资源才泄漏
  • React stale closure?→ 异步回调捕获了旧 render 的 state,用 useRef 或调整依赖数组解决
  • 闭包和垃圾回收的关系?→ 只要内部函数还被引用,外层变量就不会被 GC,这是闭包能工作的原因,也是泄漏的根源

this 绑定

this 在调用时决定,不是定义时(箭头函数除外)。

四种规则(优先级从高到低)

优先级规则结果
1new 绑定新创建的对象
2显式绑定 call/apply/bind指定的对象
3隐式绑定 obj.fn()obj
4默认绑定 fn()严格模式 undefined,否则 window

箭头函数:没有自己的 this,捕获定义时所在词法作用域的 this,call/apply/bind 无法改变

项目中丢失 this 的高频场景

  • 解构方法调用:const { login } = store; login() → this 丢失
  • 回调传递:setTimeout(this.fn, 0) → this 丢失,改用箭头函数或 bind
  • 事件回调中的方法:el.addEventListener('click', this.handler) → this 丢失

原型链 & 继承

每个对象有 [[Prototype]],属性查找沿原型链向上直到 null。class 是语法糖,底层仍是原型链。

class vs ES5 的差异

  • ES5 寄生组合继承:Animal.call(this) 继承实例属性 + Object.create(Animal.prototype) 继承原型方法
  • class 等价,但静态方法继承、super、严格模式默认开启、不提升
  • 寄生组合继承是最优方案:只调用一次父构造函数,原型链完整,没有多余属性

instanceof 原理:沿原型链查找 Constructor.prototype 是否存在

  • 跨 iframe 失效:每个 iframe 独立 JS 环境,Array.prototype 不是同一个引用
  • 安全判断类型用 Object.prototype.toString.call(val)

类型系统

== 先看类型,不同类型按规则转换;=== 不转换,直接比较。

== 转换规则(能推导不用背)

  • null == undefined → true(特殊规定),它们不等于其他任何值
  • 有 Boolean → 先转 Number
  • 有 Number + String → String 转 Number
  • 有 Object + 原始值 → Object 调 valueOf/toString

经典题:[] == false → []→''→0,false→0 → true;[] == ![] → ![]→false→0,[]→0 → true

其他

  • typeof null === 'object':历史 bug,null 机器码全 0,和 object 标签 000 一样
  • NaN !== NaN:唯一不等于自身的值,判断用 Number.isNaN()
  • Object.is(+0, -0) → false;Object.is(NaN, NaN) → true(比 === 更精确)

二、异步编程

Promise 深度

Promise 是状态机:pending → fulfilled / rejected,状态一旦改变不可逆。

链式调用

  • .then 返回普通值 → 包装成 resolved Promise 继续传递
  • .then 返回 Promise → 等待该 Promise settle 再继续
  • .then 抛错 → 转为 rejected,被下一个 .catch 捕获

四种静态方法

方法rejected 时机适用场景
Promise.all任意一个 reject 立即 reject全部成功才继续(并行接口)
Promise.race任意一个 settle超时控制
Promise.allSettled永不 reject需要每个结果,不管成败
Promise.any全部 reject 才 reject多个备用接口取最快成功的

微任务执行顺序:同步 → 微任务(Promise.then)→ 渲染 → 宏任务(setTimeout)


async/await 陷阱

循环串行陷阱

  • forEach 不等待 await,三个请求实际并行
  • 串行用 for...of,并行用 Promise.all + map

错误处理选择

  • try/catch:多个 await 共用一个处理,但无法区分哪一步出错
  • 独立 .catch():每步单独处理,能精确定位

未捕获 rejection

  • 浏览器:window.addEventListener('unhandledrejection', handler)
  • Node 15+:未捕获直接退出进程

实际工程问题

并发请求限流:维护一个执行中的 Set,达到上限时用 Promise.race 等最快完成的释放槽位,再补充新任务

竞态条件(搜索框)

  • 方案 1:AbortController 取消上一个请求(推荐)
  • 方案 2:版本号标记,回调时对比是否是最新请求,不是则丢弃

轮询 vs 长连接

方式特点适用
短轮询 setInterval简单,无效请求多低频更新
长轮询挂起请求到有数据中频
SSE EventSource服务端推送,断线重连消息流/日志
WebSocket全双工,最实时聊天/协同

三、事件循环

单线程协调同步、异步和渲染。核心:一个宏任务 → 清空所有微任务 → 可能渲染 → 下一个宏任务。

宏任务:setTimeout setInterval MessageChannel I/O UI事件微任务:Promise.then/catch/finally queueMicrotask MutationObserver

完整一轮顺序

  1. 执行当前宏任务(同步代码)
  2. 清空微任务队列(含微任务里新产生的微任务)
  3. 如需渲染:执行 requestAnimationFrame → 样式计算 → Layout → Paint
  4. 取下一个宏任务

高频易错点

  • rAF 不是宏任务也不是微任务,在渲染阶段执行,在微任务之后
  • MutationObserver 是微任务
  • setTimeout(fn, 0) 实际有 1ms+ 延迟,嵌套 5 层以上强制 4ms
  • queueMicrotask 和 Promise.resolve().then 效果相同,都是微任务

四、内存管理

垃圾回收

V8 分代回收:新生代(短命对象,Scavenge)+ 老生代(长命对象,标记清除 + 标记整理)。

  • 标记清除:从 GC Root 出发标记所有可达对象,清除未标记的
  • 引用计数:循环引用时引用计数永远 > 0,无法回收 → 现代引擎不用此方案

内存泄漏

常见场景

  • 定时器未清除:setInterval 的回调一直持有外部变量
  • 事件监听未解绑:组件销毁后监听仍在
  • 游离 DOM:从 DOM 移除但全局变量还持有引用
  • 闭包持有大对象:事件回调捕获了不必要的大数据

WeakMap / WeakRef:弱引用,不阻止 GC,适合以 DOM 或对象为 key 的私有数据缓存

Chrome DevTools 排查流程

  1. Memory → Heap Snapshot
  2. 操作一次 → 快照 1;重复操作 → 快照 2
  3. Comparison 视图,过滤 + 新增对象
  4. 点击可疑对象 → 看 Retainers 链,找到谁持有引用

五、现代特性

Proxy & Reflect

Proxy 在对象和外界之间加拦截层,可定义 13 种操作的自定义行为。Vue 3 响应式核心。

Vue 3 换 Proxy 的原因

对比Object.definePropertyProxy
拦截粒度属性级,逐个定义对象级,一次代理
新增/删除属性无法感知(需 $set)自动拦截
数组变化只能劫持特定方法完整支持
初始化性能递归遍历(差)惰性代理(好)

Reflect 的意义:让对象操作变函数形式;配合 Proxy 保持正确的 receiver(解决继承时 this 问题);失败返回 false 而不是抛异常

Proxy 局限:无法代理私有字段 #field;Date/Map 等内置对象的内部槽无法被代理正确转发


Generator & Iterator

Generator = 可暂停的函数,每次 yield 暂停并向外传值,next() 恢复。本质是协程。

  • async/await 底层就是 Generator + 自动执行器(Promise 驱动 next())
  • for...of 原理:调用对象的 [Symbol.iterator]() 获取迭代器,反复调用 .next() 直到 done: true
  • 自定义迭代器:对象实现 [Symbol.iterator] 方法即可被 for...of / 展开运算符消费

ES2020+ 实用特性

可选链 ?. 和空值合并 ??

  • ?.:左侧为 null/undefined 立即短路返回 undefined,不抛错
  • ??:只在左侧为 null/undefined 时取右值(区别于 ||,|| 对 0 / '' / false 也触发)

深克隆方案对比

方案缺陷
JSON.parse(JSON.stringify)丢 undefined/函数/Symbol,Date 变字符串,不支持循环引用
手写递归 + WeakMap处理循环引用,支持 Date/RegExp/Map/Set
structuredClone不支持函数/DOM 节点,部分环境不支持

逻辑赋值

  • &&=:左侧 truthy 才赋值
  • ||=:左侧 falsy 才赋值
  • ??=:左侧 null/undefined 才赋值(最精确,推荐)

六、请求层设计

如何封装统一 request?

目标:业务代码只关心入参和返回值,鉴权、错误处理、loading 全部在封装层处理。

核心分层

  • 底层:axios 实例,统一 baseURL、timeout、headers
  • 请求拦截器:注入 token、添加 traceId、标记 loading
  • 响应拦截器:统一解包(res.data)、状态码判断、错误抛出
  • 业务层:只调用封装后的方法,不接触 axios 细节

追问

  • 为什么不直接用 axios?→ 便于统一切换底层库(fetch/axios),集中维护鉴权和错误逻辑
  • 如何支持不同 baseURL?→ 创建多个 axios 实例,各自配置

如何处理 token 过期和刷新?

无感刷新:请求返回 401 时,自动用 refresh token 换新 token,然后重放原请求,业务层无感知。

实现要点

  • 拦截 401 响应,标记"正在刷新"状态
  • 刷新期间的其他请求进队列等待,不能重复触发刷新
  • 刷新成功 → 用新 token 重放队列里所有请求
  • 刷新失败 → 清除 token,跳转登录页
  • refresh token 本身过期 → 不重试,直接登出

追问

  • 为什么要防重复刷新?→ 并发请求都 401 时,同时触发多次刷新会导致 refresh token 失效
  • refresh token 放哪里?→ httpOnly Cookie 最安全,防 XSS 窃取

如何处理重复请求、取消请求、重试、loading?

重复请求去重

  • 用请求的 url + method + params 生成唯一 key
  • 同 key 的请求未完成时,取消新请求或取消旧请求(按场景选)
  • 用 AbortController 或 axios 的 CancelToken 实现取消

取消请求

  • AbortController:controller.abort() 取消,signal 传给 fetch
  • 路由跳转时取消当前页所有请求:维护一个 controller 集合,beforeEach 时全部 abort

重试

  • 网络错误或 5xx 才重试,4xx 不重试(客户端错误重试没意义)
  • 指数退避:第 1 次等 1s,第 2 次等 2s,第 3 次等 4s,避免瞬间打爆服务
  • 最多重试 3 次,超过直接报错

loading

  • 请求拦截器计数 +1,响应拦截器计数 -1,为 0 时关闭 loading
  • 支持单个接口单独控制(传 silent: true 跳过全局 loading)

如何做接口错误统一收敛?

分层处理

  • 网络层错误(超时/断网):响应拦截器统一 toast 提示
  • 业务错误(code !== 0):拦截器统一处理,特殊 code 单独映射
  • 特殊码统一跳转:401 → 登录,403 → 无权限页,500 → 错误页
  • 业务层只处理成功场景,不写 catch(错误已在拦截器兜底)

追问

  • 有些接口需要自己处理错误怎么办?→ 传 customError: true 跳过拦截器的错误处理,业务自己 catch
  • 如何上报接口错误?→ 在拦截器里统一接入 Sentry / 埋点,带上 traceId、url、status、耗时

七、高性能 JS

Long Task 是什么?如何处理?

Long Task:JS 执行超过 50ms 的任务。会阻塞主线程,导致页面无法响应输入和渲染,用户感知到卡顿。

为什么 50ms?:浏览器每帧 16.6ms,JS 超过 50ms 就会跨越多帧,用户感知到明显延迟

检测:Chrome Performance 面板红色标记 / PerformanceObserver 监听 longtask 类型

处理思路

  • 分片执行:把大任务拆成小块,用 requestIdleCallback 或 setTimeout(fn, 0) 在空闲时执行
  • 移到 Web Worker:计算密集型任务完全脱离主线程
  • 减少不必要的同步计算:缓存结果、惰性计算、按需触发

如何处理 10 万条数据?

核心原则:不要一次性渲染 10 万个 DOM,浏览器撑不住。

方案对比

方案原理适用
虚拟列表只渲染可视区域的 DOM,滚动时复用列表展示(推荐)
分页 / 懒加载按需加载数据数据可分页
时间切片用 requestIdleCallback 分批插入 DOM必须全部渲染时
Web Worker计算在 Worker,结果传回主线程渲染数据需要大量计算

虚拟列表核心:监听滚动位置,计算起始/结束索引,只渲染这个范围内的条目,用 padding 或 transform 撑开容器高度


JSON 大数据解析卡顿怎么办?

  • 大 JSON 的 JSON.parse 是同步的,会阻塞主线程
  • 方案 1:放进 Web Worker 解析,完成后 postMessage 传回
  • 方案 2:后端分页返回,避免一次传大 JSON
  • 方案 3:流式解析库(如 stream-json),适合 Node 端
  • 方案 4:用 structuredClone 替代序列化传递(Worker 内通信)

Web Worker 适合什么场景?

Web Worker 在独立线程运行,不能操作 DOM,通过 postMessage 和主线程通信。

适合:大数据计算、JSON 解析、图片处理、加密/解密、复杂排序过滤、Excel 导出生成

不适合:需要操作 DOM、依赖全局状态、计算量小(通信开销反而更大)

追问

  • SharedArrayBuffer 是什么?→ Worker 和主线程共享内存,避免数据拷贝开销,但需要跨域隔离头
  • Worker 如何调试?→ Chrome DevTools Sources 面板有独立的 Worker 线程

如何拆分长任务?

requestIdleCallback:浏览器空闲时执行,适合低优先级任务(日志、预计算),不保证执行时机

requestAnimationFrame:每帧执行,适合和渲染相关的任务(动画、DOM 更新批处理)

setTimeout(fn, 0):简单粗暴的切片,兼容性好,但最小延迟约 4ms

scheduler.postTask(新 API):可以指定优先级(user-blocking / user-visible / background)

选择原则:和渲染相关用 rAF → 低优先级后台任务用 rIC → 通用切片用 setTimeout → 需要优先级控制用 scheduler


八、业务数据结构

扁平数组转树

后端常返回带 id / pid 的扁平列表,前端需要转成嵌套树结构用于菜单、组织架构、评论等。

思路:先用 Map 建立 id → node 的索引,再遍历一次:有 pid 的挂到父节点的 children,没有 pid 的是根节点

时间复杂度:O(n),比递归查找的 O(n²) 快得多,数据量大时差距明显

注意点:pid 可能是 0 或 null 或 undefined,判断根节点时要统一处理;children 默认给 [] 还是不加,根据业务决定


树转扁平数组

思路:DFS 或 BFS 遍历,每个节点去掉 children,push 进结果数组

场景:权限勾选后把树结构传给后端、搜索过滤时需要遍历所有节点


权限树查找

场景:用户有一组权限 code,需要在权限树中找出所有匹配节点,或判断是否有某个权限

思路

  • 初始化时把权限 code 存进 Set,查找 O(1)
  • 树形权限:父节点权限包含子节点时,需要递归展开所有子 code 再存 Set
  • 按钮级权限:直接 permissionSet.has(code) 判断

菜单路由生成

思路:后端返回用户有权限的菜单树 → 前端递归转成 vue-router / react-router 的 routes 配置 → addRoute 动态注册

注意点

  • 菜单显示和路由权限是两件事,菜单可以隐藏但路由要保留(或反过来)
  • 刷新时路由丢失:动态路由在 router.beforeEach 里判断,没有则重新拉取并注册

字典缓存设计

字典(数据字典):状态码 → 文字的映射,如 1 → 启用,0 → 禁用。

常见问题:每个组件各自请求字典接口 → 重复请求、数据不一致

方案

  • 全局请求一次,存 Pinia / Vuex / 全局变量
  • 按需懒加载:第一次用到某类字典时请求,后续命中缓存
  • 字典 Map:Map<typeCode, Map<value, label>>,查询 O(1)
  • 时效控制:字典变化频率低,可以加本地缓存(localStorage + 版本号),减少请求

表格 schema 数据转换

后端返回字段名和前端展示名不一致,或需要格式化,统一用 schema 配置驱动

核心思路:定义列配置数组(字段名、标题、格式化函数),渲染时统一走 schema 处理,业务代码不写硬编码的字段映射

追问

  • 为什么不直接在模板里处理?→ 逻辑分散,字段改了要改多处;schema 集中管理,改一处全部生效
  • 如何处理嵌套字段?→ 用 lodash _.get(row, 'a.b.c') 或自己实现路径取值

九、模块化与工程设计

ESM 和 CommonJS 区别

对比CommonJS(CJS)ES Module(ESM)
语法require / module.exportsimport / export
加载时机运行时动态加载编译时静态分析
输出值的拷贝值的实时绑定(live binding)
异步不支持支持 import() 动态导入
环境Node.js 原生浏览器原生,Node 12+ 支持
thismodule 对象undefined

live binding:ESM 导出的变量,模块内修改后,导入方拿到的是最新值;CJS 是拷贝,修改后导入方不变


Tree shaking 为什么依赖 ESM?

Tree shaking = 打包时自动删除未使用的代码。

原因:ESM 是静态结构,import/export 在编译时就能确定依赖关系,打包工具(Rollup/Webpack)可以静态分析哪些导出没有被使用,直接删掉

CJS 的 require 是运行时动态执行,打包工具无法静态确定哪些会被用到,所以 tree shaking 不了

追问

  • 为什么有时候 tree shaking 没生效?→ 副作用没标记(package.json 的 sideEffects 字段)、或用了 CJS 库、或 Babel 把 ESM 编译成 CJS 了
  • sideEffects: false 是什么?→ 告诉打包工具这个包没有副作用,可以放心删未使用的导出

循环依赖有什么问题?

A 引用 B,B 引用 A,形成循环。

CJS:循环依赖时,require 返回的是未完成的对象(半成品),可能拿到 undefined,很难排查

ESM:通过 live binding 支持循环,但仍然要注意初始化顺序,变量在使用时可能还未赋值

如何避免:把公共部分抽成第三个模块 C,A 和 B 都引用 C,打破循环


如何设计 utils,而不是垃圾桶?

utils 最常见的问题:所有人都往里扔,最后变成一个什么都有、没有人敢删的文件。

原则

  • 按职责分文件:date.ts / string.ts / array.ts / request.ts,不放同一个 utils.ts
  • 只放纯函数:无副作用,输入确定输出确定,方便测试和 tree shaking
  • 有明确归属的逻辑不放 utils:业务逻辑放 composables/hooks,请求放 api 层
  • 定期清理:用 ESLint 的 no-unused-vars 或打包分析找出没人用的函数

如何设计 SDK / 插件机制?

SDK:给外部使用的封装库。插件机制:让使用者扩展核心功能而不修改核心代码。

SDK 设计要点

  • 最小化对外暴露:只暴露必要的 API,内部实现细节不导出
  • 初始化配置化:SDK.init({ apiKey, baseURL, timeout })
  • 链式调用 / Builder 模式:让调用更流畅
  • 错误处理统一:SDK 内部消化异常,对外抛出语义清晰的错误

插件机制核心:定义钩子(hook)点,插件注册时把回调挂到对应钩子,核心流程执行时依次调用

  • Vue 插件:app.use(plugin) → 调用 plugin.install(app)
  • Webpack 插件:Tapable 的 hooks,compiler.hooks.emit.tap('MyPlugin', callback)
  • 自己实现:维护一个 hooks Map,register(hookName, fn) 注册,call(hookName, ...args) 触发

追问

  • 插件和中间件的区别?→ 中间件是线性链路(洋葱模型),插件是挂载到特定事件点,两者都是扩展机制
  • 如何保证插件执行顺序?→ 注册时带优先级,或按注册顺序严格执行
最近更新: 2026/2/7 15:43
Contributors: milly980810@gmail.com, weak