跳到主要内容

高频问答速查

面试前一天过一遍的清单。每条给最短的正确答案,想看推导过程点链接。

JS 语言核心​

typeof null 为什么是 'object'? 早期实现用低 3 位标记类型,对象是 000,而 null 的机器码全 0,于是被判成对象。这是无法修复的历史 bug(修了会破坏大量现存代码)。详见

判断类型的可靠方法? Object.prototype.toString.call(x) 返回 [object Type],能区分 null / Array / Date / RegExp。typeof 只对原始类型可靠,instanceof 跨 iframe 失效(原型链不同)。详见

0.1 + 0.2 !== 0.3 为什么? IEEE 754 双精度用二进制存小数,0.1 和 0.2 都是无限循环二进制,截断后产生误差。判等用 Math.abs(a-b) < Number.EPSILON,金额用整数分或 decimal 库。详见

原型链是什么? 每个对象有 __proto__ 指向其构造函数的 prototype,查属性时沿这条链向上找直到 null。instanceof 就是判断右侧的 prototype 是否在左侧的原型链上。详见

🔥 this 的指向规则? 按优先级:new 绑定 > 显式绑定(call/apply/bind)> 隐式绑定(谁调用指向谁)> 默认绑定(严格模式 undefined,否则全局)。箭头函数没有自己的 this,它在定义时捕获外层作用域的 this,且无法被 call 改变。详见

闭包是什么?为什么会内存泄漏? 函数能访问其词法作用域外的变量,即使外层函数已返回 —— 因为变量被闭包引用,GC 不能回收。泄漏不是闭包的锅,是不该长期持有的引用没解除(比如定时器里引用了大对象却不 clearInterval)。详见

var / let / const 的区别? 作用域(函数 vs 块)、提升(var 提升并初始化为 undefined,let/const 提升但处于暂时性死区 TDZ,访问报错)、重复声明(var 允许)、全局声明是否挂到 window(var 会)。const 限制的是绑定不可变,不是值不可变。详见

深拷贝怎么写? structuredClone() 是现代标准答案(支持循环引用、Map/Set/Date,但不支持函数和 Symbol)。JSON.parse(JSON.stringify()) 会丢失 undefined / 函数 / Symbol,把 Date 变字符串,NaN/Infinity 变 null,且循环引用直接抛错。手写要用 WeakMap 处理循环引用。详见

== 的转换规则? null == undefined 为 true(且两者不等于其他任何值);对象与原始值比较时对象先 valueOf/toString;其余情况都转数字。实践里一律用 ===,只有判「是否 null 或 undefined」时用 == null。详见

事件循环与异步​

🔥 事件循环的执行顺序? 同步代码 → 清空所有微任务(microtask:Promise 回调、queueMicrotask、MutationObserver)→ 渲染 → 取一个宏任务(macrotask:setTimeout、事件回调、I/O)→ 再清空微任务 → 循环。关键点:微任务是一次清空整个队列(期间新增的也会执行完),宏任务是一轮一个。详见

async/await 的本质? 是 Generator + Promise 的语法糖。await 会暂停函数执行,把后续代码包装成微任务挂在 Promise 的 then 上。await 右侧的表达式是同步执行的,只有之后的代码才异步 —— 这是输出顺序题的核心考点。详见

Node 和浏览器的事件循环区别? Node 有六个阶段(timers / pending / poll / check / close 等),setImmediate 在 check 阶段先于 timers;process.nextTick 优先级高于所有微任务。Node 11 之后对齐了浏览器行为(每个宏任务后清空微任务),之前是每阶段结束才清。详见

Promise.all / allSettled / race / any 的区别? all 全部成功才成功、一个失败立刻失败;allSettled 永不失败,返回每项的状态;race 取最先敲定的(无论成败);any 取最先成功的,全失败才抛 AggregateError。详见

手写 Promise 的难点在哪? 三个状态不可逆、then 要返回新的 Promise 以支持链式、then 回调必须异步执行(用微任务)、pending 时要把回调存进数组、resolve 收到 thenable 要递归展开。详见

为什么 setTimeout(fn, 0) 不是立即执行? 规范规定嵌套层级超过 5 层时最小延迟 4ms;且它只是「把任务放进宏任务队列」,实际执行要等当前同步代码和所有微任务跑完。需要尽快执行且不要渲染间隔用 queueMicrotask。详见

浏览器原理​

🔥 从输入 URL 到页面显示? DNS 解析 → TCP 握手(HTTPS 再加 TLS)→ 发请求 → 服务端响应 → 解析 HTML 构建 DOM → 解析 CSS 构建 CSSOM → 合成 Render Tree → Layout(布局)→ Paint(绘制)→ Composite(合成)。详见

重排和重绘的区别? 重排(reflow) 是几何属性变化需要重新计算布局,必然触发重绘;重绘(repaint) 只是外观变化(颜色、阴影)。只有 transform 和 opacity 能跳过两者直接走合成线程。读取 offsetTop / getBoundingClientRect 会强制同步布局(layout thrashing),要读写分离。详见

CSS 和 JS 会阻塞什么? CSS 阻塞渲染但不阻塞 DOM 解析(因为要避免无样式闪烁);JS 阻塞 DOM 解析(因为脚本可能改 DOM),且会等待前面的 CSS 下载完(脚本可能读样式)。defer 在 DOMContentLoaded 前按顺序执行,async 下载完立即执行不保证顺序。详见

事件冒泡、捕获、委托? 捕获从 window 向下到目标,冒泡从目标向上到 window。addEventListener 第三参数为 true 时在捕获阶段触发。事件委托把监听挂在父元素上,靠 event.target 判断实际来源 —— 省内存且支持动态子元素。stopPropagation 阻止冒泡,stopImmediatePropagation 还阻止同元素上的其他监听器。详见

localStorage / sessionStorage / cookie / IndexedDB? 容量 510MB / 510MB / 4KB / 大(数百 MB);生命周期 永久 / 标签页 / 可设过期 / 永久;是否随请求发送 否 / 否 / 是 / 否。cookie 的 HttpOnly 防 XSS 读取、SameSite 防 CSRF、Secure 限 HTTPS。详见

XSS 和 CSRF 怎么防? XSS 是注入脚本 → 输出转义 + CSP + HttpOnly;CSRF 是借用户凭证发请求 → SameSite=Lax + CSRF token + 校验 Origin。React 默认转义,但 dangerouslySetInnerHTML 和 href="javascript:" 仍是漏洞口。详见

网络与缓存​

🔥 HTTP 缓存策略? 强缓存(Cache-Control: max-age / 旧的 Expires)不发请求直接用本地;协商缓存(ETag/If-None-Match、Last-Modified/If-Modified-Since)发请求但服务端可返 304。优先级:Cache-Control > Expires,ETag > Last-Modified(后者精度只到秒)。实践:HTML 用 no-cache,带 hash 的静态资源用 max-age=31536000, immutable。详见

HTTP/1.1 → 2 → 3 解决了什么? 1.1 有队头阻塞(一个连接上请求必须按序响应);2 引入二进制分帧 + 多路复用解决了应用层队头阻塞,加上头部压缩 HPACK 和服务端推送,但 TCP 层的队头阻塞还在(丢一个包全部等待);3 换成基于 UDP 的 QUIC,彻底消除,且连接建立只需 1 RTT(0-RTT 可复用)。详见

HTTPS 握手过程? 客户端发支持的加密套件和随机数 → 服务端返证书 + 随机数 → 客户端验证证书链、生成预主密钥并用服务端公钥加密发送 → 双方用三个随机数算出对称密钥 → 之后用对称加密通信。非对称加密只用于协商密钥(慢),数据传输用对称加密。TLS 1.3 把握手压到 1 RTT。详见

跨域为什么存在?怎么解决? 同源策略限制的是脚本读取响应,请求其实发出去了(简单请求)。解法:CORS(服务端设 Access-Control-Allow-Origin,带 cookie 时不能用 * 且要 credentials)、代理(开发用 devServer.proxy,生产用 nginx)、postMessage(跨窗口)。JSONP 只支持 GET 且有安全风险,已过时。详见

预检请求什么时候触发? 非简单请求时 —— 方法不是 GET/POST/HEAD,或 Content-Type 不是 text/plain/form-data/x-www-form-urlencoded,或带了自定义头。所以发 JSON 必然触发预检。用 Access-Control-Max-Age 缓存预检结果。详见

React 原理​

🔥 Fiber 架构解决什么问题? 旧的 Stack Reconciler 递归遍历不可中断,大组件树会长时间占用主线程导致掉帧。Fiber 把虚拟 DOM 改成链表结构(child / sibling / return),使遍历可以暂停、恢复、放弃,配合时间切片把渲染拆成 5ms 的小块。注意:异步可中断只在并发特性(useTransition、startTransition)下生效,默认的同步更新仍是一口气跑完。详见

Hooks 为什么不能写在条件语句里? Hooks 的状态存在 Fiber 节点的 memoizedState 链表上,靠调用顺序而非名称匹配。条件调用会让顺序错位,第二次渲染时 useState 拿到的是上一个 Hook 的值。详见

useState 和 useRef 的区别? useState 改值触发重渲染、值在渲染间被快照(闭包捕获);useRef 改 .current 不触发重渲染、始终指向同一个可变容器。需要「不参与渲染但要跨渲染保持」的值(定时器 ID、DOM 引用、上一次的值)用 ref。详见

useEffect 和 useLayoutEffect? useEffect 在浏览器绘制之后异步执行,不阻塞渲染;useLayoutEffect 在 DOM 更新后、绘制之前同步执行,能避免视觉闪烁(比如读取布局后立即调整位置),但会阻塞渲染。SSR 下 useLayoutEffect 会警告,因为服务端没有布局阶段。详见

useMemo / useCallback 什么时候该用? 三种情况:依赖项要稳定(传给 memo 子组件或作为 effect 依赖)、计算确实昂贵、要保持引用相等。不该到处用 —— 本身有比较和内存开销,浅层计算上是负优化。React 19 的 Compiler 会自动做这件事。详见

diff 算法的三个假设? ① 不同类型的元素产生不同的树(类型变了直接卸载重建,不再比较子树);② key 相同的元素在不同渲染间是同一个;③ 只做同层比较,不做跨层级移动。推论:用数组 index 当 key,在头部插入时会导致所有元素的 state 错位。详见

React 的合成事件? 17 之前挂在 document 上,17 之后挂在根容器上(支持多版本共存)。不是原生事件直接绑在 DOM 上 —— 所以 e.stopPropagation() 阻止不了同一容器上的原生监听器,且事件触发顺序是「原生捕获 → React 捕获 → 原生冒泡 → React 冒泡」。详见

受控和非受控组件? 受控的值由 state 驱动(value + onChange),每次输入触发重渲染,但能做格式化和联动校验;非受控靠 ref 读 DOM(defaultValue),性能好但状态不在 React 里。大表单性能问题常靠改成非受控 + 提交时统一读取解决。详见

性能优化​

🔥 Core Web Vitals 三个指标? LCP(最大内容绘制,≤2.5s)看首屏加载;INP(交互到下一次绘制,≤200ms,2024 年取代 FID)看响应性;CLS(累积布局偏移,≤0.1)看视觉稳定性。回答优化题要先问优化哪个指标,不同指标手段完全不同。详见

LCP 慢怎么定位和优化? 拆成四段:TTFB(服务端/CDN)、资源加载延迟(有没有被发现得晚)、资源加载时长(体积/压缩)、渲染延迟(有没有被 JS 阻塞)。手段:首屏图片加 fetchpriority="high" 且不要 loading="lazy"、关键资源 preload、字体用 font-display: swap + preconnect、SSR/SSG 让内容直接在 HTML 里。详见

INP 差的根因? 长任务(>50ms)占着主线程,导致交互回调排不上队。手段:拆分长任务(scheduler.yield() 或 setTimeout 让出)、把非紧急更新放进 startTransition、重计算搬去 Web Worker、虚拟列表减少 DOM 节点。详见

CLS 怎么修? 给图片和视频写死宽高或 aspect-ratio(最常见的一条)、给广告/嵌入位预留空间、避免在已有内容上方动态插入元素、字体用 size-adjust 减少 fallback 切换时的跳动、动画用 transform 而非改 top/height。详见

首屏优化的完整清单? 按收益排:SSR/SSG 出 HTML → 路由级代码分割 → 关键 CSS 内联 + 其余异步 → 图片用 WebP/AVIF + 响应式 srcset + 懒加载非首屏 → CDN + 强缓存 → 预连接第三方域 → Tree Shaking 和依赖瘦身(lodash-es 按需引入、dayjs 换 moment)。详见

工程化​

Webpack 的构建流程? 读配置 → 从 entry 出发构建依赖图(用 loader 转换每个模块)→ 生成 chunk → 用 plugin 在各生命周期钩子做优化 → 输出 bundle。loader 是文件转换器(链式执行,从右到左),plugin 靠 Tapable 钩子介入整个生命周期。详见

🔥 Vite 为什么快? 开发时不打包 —— 利用浏览器原生 ESM,请求哪个模块才编译哪个,用 esbuild(Go 写的,比 JS 工具快一个数量级)做依赖预构建和 TS 转译。所以冷启动与项目大小基本无关。代价:生产构建仍用 Rollup(为了更好的产物优化),开发和生产两套管线,偶发行为不一致。详见

Tree Shaking 的前提? 必须是 ESM(静态可分析的 import/export,CommonJS 的 require 是动态的),且模块无副作用 —— 靠 package.json 的 sideEffects: false 声明。常见失效原因:被 Babel 转成了 CJS、import 整个库再取属性、库本身没出 ESM 产物。详见

HMR 的原理? 构建工具起 WebSocket 通知浏览器哪个模块变了 → 客户端运行时拉取新模块 → 沿依赖图往上找到能「接受」更新的边界(import.meta.hot.accept)→ 替换并重新执行。React Fast Refresh 在此基础上保留组件 state。详见

ESM 和 CommonJS 的本质区别? ESM 是编译时确定依赖、导出的是值的引用(绑定)、顶层 await 可用、静态可分析;CJS 是运行时加载、导出的是值的拷贝、可条件 require。循环依赖时 ESM 得到未初始化的绑定(TDZ 报错),CJS 得到部分完成的对象。详见

CSS 与布局​

BFC 是什么?怎么触发?解决什么? 块级格式化上下文 —— 一块独立的渲染区域,内部布局不影响外部。触发:overflow 非 visible、float 非 none、position: absolute/fixed、display: flex/grid/inline-block、根元素。解决三件事:外边距重叠、包含浮动元素(高度塌陷)、阻止被浮动元素覆盖(两栏自适应)。详见

垂直居中有哪些方案? Flex(align-items: center,首选)、Grid(place-items: center,最短)、绝对定位 + transform: translateY(-50%)(不知道高度时)、line-height 等于容器高度(仅单行文本)。详见

flex: 1 等于什么? flex-grow: 1; flex-shrink: 1; flex-basis: 0%。注意 flex-basis: 0% 和 auto 的区别 —— 前者忽略内容本身尺寸平均分配,后者先按内容撑开再分剩余空间。等宽列要用 flex: 1 而不是 flex: auto。详见

层叠上下文和 z-index 失效? z-index 只在同一层叠上下文内比较。position: relative/absolute + z-index 非 auto、opacity < 1、transform 非 none、filter、will-change 都会创建新的层叠上下文。常见坑:父元素设了 opacity: 0.99,子元素的 z-index: 9999 也盖不过父元素的兄弟。详见

怎么做高性能动画? 只动 transform 和 opacity(跳过 layout 和 paint,直接在合成线程跑,不占主线程)。will-change 提前提升图层但别滥用(每个图层占显存)。避免动 width/height/top/left。检查手段:DevTools 的 Layers 面板和 Rendering 里的 Paint flashing。详见

TypeScript​

interface 和 type 的区别? interface 可声明合并(同名自动合并,适合扩展第三方库类型)、只能描述对象形状;type 可定义联合类型、元组、条件类型、映射类型,但不能合并。实践:对外暴露的对象用 interface,其余用 type。详见

unknown 和 any? any 关闭类型检查(可赋给任何类型、可随意访问属性),unknown 是类型安全的顶层类型 —— 能接收任何值,但使用前必须收窄(typeof 判断或类型断言)。API 响应应该标 unknown 而不是 any。详见

泛型约束和条件类型? extends 在泛型里是约束(<T extends object>),在类型位置是条件判断(T extends U ? X : Y)。条件类型遇到联合类型会分发(distributive),用 [T] extends [U] 包一层可以关掉分发。详见

infer 干什么? 在条件类型里声明一个待推断的类型变量。type Return<T> = T extends (...args: any) => infer R ? R : never 就是 ReturnType 的实现。Awaited、Parameters、UnwrapArray 都是同一个套路。详见

协变和逆变在哪会踩到? 函数参数是逆变的(接受更宽的参数类型才算子类型),返回值是协变的。但 TS 默认对函数参数用双变(bivariant)以兼容现实代码,开 strictFunctionTypes 才严格。数组是协变的,所以 Dog[] 能赋给 Animal[],这其实不安全(可以往里推 Cat)。详见

Next.js 与 SSR​

🔥 四种渲染模式怎么选? CSR 纯客户端(SEO 差、首屏慢);SSR 每次请求在服务端渲染(数据实时、TTFB 受服务端影响);SSG 构建时生成静态 HTML(最快、CDN 可缓存、但内容变了要重新构建);ISR SSG + 后台按需再生成。判断依据:内容多久变一次、是否因人而异、SEO 重不重要。详见

水合(hydration)是什么?为什么会不匹配? 服务端输出 HTML,客户端 React 接管这些 DOM 并绑定事件。不匹配的典型原因:渲染里用了 Date.now() / Math.random()、读了 window 或 localStorage、服务端和客户端的时区或语言不同。表现是控制台警告 + 整棵子树被客户端重渲染。详见

RSC(React Server Components)和 SSR 的区别? SSR 是把组件渲染成 HTML 字符串,组件代码仍会打进客户端 bundle 用于水合;RSC 是组件只在服务端运行,输出序列化后的 RSC payload,代码完全不进客户端 bundle。所以 RSC 能直接读数据库、能减小 bundle,但不能用 state 和事件处理。详见

'use client' 的边界怎么理解? 它标记的是客户端 bundle 的入口,不是「这个文件在客户端跑」—— 被它 import 的所有模块都会进客户端。所以要尽量把它下推到叶子组件(比如只有那个按钮是客户端组件),而不是整页标上。详见

App Router 的缓存有几层? 请求记忆(单次渲染内 dedupe)、Data Cache(跨请求持久,fetch 默认,用 revalidate 控制)、Full Route Cache(构建时的 HTML/RSC)、Router Cache(客户端内存)。改了后台数据前台不更新,多半是其中某层没失效 —— 纯 SSG 站点则必须重新构建。详见


容易答错的版本差异​

被问到时答老结论会被纠正的几条:

题过时答案现在的答案
交互性能指标FIDINP(2024-03 起正式取代 FID)
深拷贝JSON.parse(JSON.stringify())structuredClone()(原生、支持循环引用)
useMemo 要不要手动加到处加React 19 Compiler 自动做,手动加只在特定场景
React 事件挂在哪document根容器(React 17 起,为支持多版本共存)
Node 微任务时机每个阶段结束才清空每个宏任务后就清空(Node 11 起对齐浏览器)
HTTP/2 服务端推送推荐用已被 Chrome 移除,改用 103 Early Hints
移动端适配rem + js 动态算根字号视口单位 + clamp(),或容器查询
CSS 居中绝对定位 + 负 marginFlex / Grid,place-items: center
判断数组instanceof ArrayArray.isArray()(跨 iframe 安全)
请求库XHR / axiosfetch + AbortController(原生、支持流)

:::tip 怎么用这页 从上往下念一遍,卡住的那条点进去看推导。能把 🔥 那几条在 90 秒内讲清楚(不看文档、被打断还能接上),前端八股这块基本就够了。 :::