ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

前端面试高频八股与手写题复盘:从事件循环到项目实战

前端面试高频八股与手写题复盘:从事件循环到项目实战 最近两周我集中面了五家中型互联网公司岗位都是前端方向一面、二面加两三场技术终面加起来前前后后聊了十几个小时。面完回过头看面试官问的东西虽然看起来五花八门但核心逃不出三类简历项目细节、基础八股、手写题和场景设计题。趁印象还热乎我把这些问题按主题回忆了一遍把回答思路、现场翻车的情况、以及事后复盘出来的更好答法都整理了出来。这篇内容适合正在准备跳槽、尤其是两到五年经验准备冲刺中高级岗位的朋友也适合刚被面试虐完想系统查漏补缺的人。放心这不是“标准答案大全”而是我踩过坑之后觉得真正有用的复盘记录。1. 面试前的准备策略为什么我建议先“复述”再“刷题”1.1 简历上的项目是整场面试的主线我这次有个很深的感受面试官问的技术问题绝大多数是从简历上的项目延伸出来的。比如你在项目里写了“用 Vue 3 重构了管理后台”那 Vue 3 的响应式原理、Vue 2 和 Vue 3 的差异、为什么要换、换完之后遇到什么问题这些问题几乎是必问的。所以准备面试的第一步不是刷题而是把简历上每个项目都过一遍“复述关”。我自己用的方法是四段式复盘项目背景是什么、我负责哪一块、遇到了什么技术难点、最后拿到了什么可量化的结果。每个项目都要能十分钟讲完并且能承接面试官随机切入的追问。注意这里说的“能承接”不是硬背而是你自己真的理解当时的技术选型和实现细节。面试官一旦发现你只是在背台词接下来就会往深里问问到答不上来为止。1.2 面试前必须做完的三件事第一个准备动作是写项目复盘文档。别嫌麻烦Word 或者说 Markdown 里把每个项目按“背景—难点—方案—结果—踩坑”五段写下来写完你会发现很多当时没想清楚的问题浮出来了。比如我之前做过一个数据看板项目复盘时发现自己连“为什么当时选了 Canvas 而不是 SVG”都说不利索这就是面试前要补的洞。第二个动作是基础八股过筛。前端的基础题范围其实很固定事件循环、闭包、原型链、HTTP 缓存、Vue/React 原理、组件通信、性能优化、安全、工程化。我建议不要盲目背题而是按主题整理出“一句话结论 一个例子 一个追问点”的结构。比如事件循环一句话结论是“JS 是单线程宏任务和微任务按规则排队执行”例子就是经典的 setTimeout 和 Promise 顺序题追问点就是“await 后面那行代码什么时候执行”。第三个动作是手写题刻意练习。高频题目就那么几个防抖、节流、深拷贝、Promise.all、数组去重、实现 bind/call/apply、LRU 缓存。我这次面试里真正遇到的就有深拷贝、防抖、Promise.all。手写题不是背代码而是练“现场推理能力”后面我会详细说。2. 高频技术问题盘点面试官到底在问什么2.1 Vue 3 的 Proxy 为什么能替代 Vue 2 的 defineProperty这个问题我连续三家都被问到了基本是前端岗位绕不开的必问题。面试官想看的是你有没有真正理解 Vue 2 响应式的限制以及 Vue 3 为什么要用 Proxy。回答的核心点有三个。第一Vue 2 的响应式是“按属性逐个处理”的通过 Object.defineProperty 递归遍历对象的所有属性把每个属性变成 getter/setter。这意味着对象新增属性、删除属性或者通过下标修改数组元素都没法被拦截所以才有了 Vue.$set、Vue.$delete 这种补丁 API。第二Vue 3 用 Proxy 直接代理整个对象不管是读取属性、新增属性、删除属性还是遍历都能被拦截到所以不再需要 $set数组也能直接通过下标触发更新。第三从性能角度说Vue 2 在初始化时要递归遍历所有属性对象层级越深性能损耗越明显Vue 3 的 Proxy 是懒处理的真正访问到嵌套对象时才对其做响应式代理。我当时的回答还补了一句Proxy 的兼容性不如 defineProperty所以 Vue 3 不支持 IE这也是当初 Vue 2 只能在某些场景里妥协用 defineProperty 的原因之一。面试官一般听到这里就会点头然后再接一个问题那 Proxy 有什么缺点这就要讲到 Proxy 的监听是异步的、以及不能用完全相等的对象判断、还需要配合 Reflect 等等。把这几个点串起来讲基本就能把这个高频问题聊透。2.2 React setState 到底是同步还是异步这个问题有个经典陷阱你不能只说“异步”。我这次遇到的面试官直接给了一个场景问我 setTimeout 里调用 setState 会同步更新还是异步更新。要分情况回答至少要讲清两个维度。第一个维度是 React 版本。React 18 之前在 React 事件处理函数里setState 会被批量合并表现为异步更新状态不会立即变化但在 setTimeout、setInterval、Promise 回调、原生 DOM 事件里它又是同步执行的调用之后立刻能拿到最新状态。第二个维度是 React 18 之后通过 createRoot 渲染的应用里自动批处理覆盖了更多场景setTimeout 和 Promise 里的 setState 也会被合并成批量更新不再是之前那种同步行为。除了结论最好还能说清楚“为什么要这样设计”。批处理的意义是减少不必要的渲染次数把多个 setState 合并成一次更新。而为什么 setTimeout 里不能批处理因为 React 内部是通过一个 isBatchingUpdates 的开关来判断的事件处理函数执行时开关打开离开时关闭setTimeout 执行时开关早就关了所以每次 setState 都会立即触发更新流程。讲清楚这个开关机制面试官才会觉得你是真懂而不是背结论。我这次面试还遇到一个衍生问法useState 和 setState 有什么区别这就要提到函数组件里 useState 的更新是基于闭包的每次渲染拿到的 state 是当前渲染的那次值所以连续调用两次 setCount(count 1) 不会累加必须用 setCount(c c 1) 这种函数式更新。把这个问题绑定到“闭包陷阱”上答出来就很有层次。2.3 事件循环与微任务一道经常出错的题前端面试里事件循环的高频程度不用多说。这次有一场面试面试官直接在白板上写了一段代码让我说输出顺序我印象很深console.log(script start); setTimeout(() { console.log(setTimeout); }, 0); Promise.resolve() .then(() { console.log(promise1); }) .then(() { console.log(promise2); }); async function async1() { console.log(async1 start); await async2(); console.log(async1 end); } async function async2() { console.log(async2); } async1(); console.log(script end);正确的输出顺序是script start - async1 start - async2 - script end - promise1 - promise2 - async1 end - setTimeout。这里最容易被坑的是 async1 end 的位置。很多同学以为 await 只是“等一下”所以 async1 end 会立刻输出其实不是。await 后面的代码会被放进微任务队列await async2() 这行本身会先执行 async2 并输出然后把 await 后面的 async1 end 挂到微任务队列里。而 promise1 和 promise2 已经在微任务队列里排队所以会先输出 promise1、promise2最后才输出 async1 end。这个问题的核心考点有三层宏任务与微任务的分类、微任务队列的执行时机、async/await 在 await 之后的代码如何入队。我在现场回答时把“微任务队列是清空的不是执行一个就切走”这个点说了出来面试官明显态度更好。建议你也把这个细节记牢很多人栽在“以为微任务和宏任务交替执行”上实际上微任务会一口气清空中间插入新的微任务也会继续执行完。2.4 HTTP 缓存强缓存和协商缓存的区别HTTP 缓存是简历里写了性能优化必被追问的知识点。我面的五家里有三家问到问法还不一样有的直接问“强缓存和协商缓存的区别”有的问“Expires 和 Cache-Control 哪个优先”还有的让设计一个缓存策略。回答的结构建议分四层。第一层强缓存浏览器直接读本地缓存不发请求到服务器通过 Cache-Control 的 max-age 和 Expires 控制。Expires 是绝对时间有客户端时间不准的问题Cache-Control 是相对时间优先级更高。第二层协商缓存浏览器向服务器发请求服务器通过 If-Modified-Since/Last-Modified 或 If-None-Match/ETag 来判断资源是否变化没变化就返回 304有变化就返回新资源。第三层Etag 比 Last-Modified 更精确因为 Last-Modified 只能精确到秒而 Etag 是文件指纹内容变化了就变。第四层实际项目中怎么配合使用静态资源用强缓存并加上文件名 hash入口 HTML 用协商缓存或者短缓存避免文件更新后浏览器还在用旧版本。说到这需要补充一个容易被忽略的细节强缓存命中时浏览器控制台看到的是 200 (from disk cache 或者 from memory cache)不是 304。看到 304 说明走了协商缓存。很多面试者分不清这个回答时把两层混在一起说给面试官的印象就很差。3. 现场问题回忆那些我在面试中遇到的题3.1 手写深拷贝被追问到循环引用这次面试有一场直接开着共享文档让我写深拷贝原题很简单写一个 deepClone 函数对象里可能包含嵌套对象和数组。我一开始写了一个很基础的版本function deepClone(obj) { if (obj null || typeof obj ! object) return obj; if (Array.isArray(obj)) return obj.map(item deepClone(item)); const result {}; for (const key in obj) { result[key] deepClone(obj[key]); } return result; }写完面试官立刻追问对象里有循环引用怎么办比如a.self a如果直接用上面这个版本会无限递归导致栈溢出。正确的做法是用一个 WeakMap 缓存已经拷贝过的对象遇到重复引用直接从缓存里取。function deepClone(obj, cache new WeakMap()) { if (obj null || typeof obj ! object) return obj; if (cache.has(obj)) return cache.get(obj); if (obj instanceof Date) return new Date(obj); if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags); const result Array.isArray(obj) ? [] : {}; cache.set(obj, result); for (const key of Object.keys(obj)) { result[key] deepClone(obj[key], cache); } return result; }这里用 WeakMap 而不是普通 Map 的原因也要能说清楚WeakMap 的 key 是弱引用如果对象本身没有被别处引用垃圾回收可以把它回收掉不会造成内存泄漏。而普通 Map 会强引用 key导致被拷贝的对象永远不能被回收。现场还有个小插曲面试官问我“能不能拷贝 Symbol 类型的 key”我当时愣了一下后来想起来 Object.keys 只能拿字符串 key要拿 Symbol 得用 Object.getOwnPropertySymbols。这是加分点如果时间够可以提一下“完整版还需要处理 Symbol 和原型链”。3.2 防抖节流手写与场景选择防抖和节流我这次面了两家都考了。一家让写防抖一家让先说区别再写节流。先说结论防抖是“在事件触发后 wait 时间后再执行如果 wait 时间内又触发了就重新计时”适合搜索框输入、窗口 resize节流是“每隔 wait 时间最多执行一次无论触发多频繁”适合滚动事件、按钮点击限频。防抖的基础实现function debounce(fn, wait 300) { let timer null; return function (...args) { if (timer) clearTimeout(timer); timer setTimeout(() { fn.apply(this, args); timer null; }, wait); }; }这里关键点是 this 指向问题。如果直接用箭头函数写内部调用this 会丢所以要用 fn.apply(this, args) 把返回函数的 this 传进去。这个细节不写出来手写题就白写了。面试官大概率会继续追问如果希望第一次点击立刻执行呢这是防抖的高级用法也就是 leading 选项。需要加一个是否立即执行的参数function debounce(fn, wait 300, immediate false) { let timer null; return function (...args) { if (immediate !timer) { fn.apply(this, args); } if (timer) clearTimeout(timer); timer setTimeout(() { fn.apply(this, args); timer null; }, wait); }; }这种写法在“首次立即执行、后续防抖延迟”的场景里很实用比如直播抢购按钮防重复点击第一次点击必须立刻生效。面试时能写出来这个版本比基础版加分明显。3.3 场景题搜索框请求优化有一家公司的二面没让写代码直接抛给我一个场景搜索框输入关键词需要请求后端接口返回联想结果你怎么做优化这种题没有标准答案但有几个必答的点。第一是防抖控制输入频率等待用户停止输入再发请求比如 300ms。第二是请求竞态用户先输入了 A又输入了 AB前一个请求可能后返回导致页面显示的是 A 的结果。解决方法是在闭包里维护一个自增 id 或者请求序号只有当前序号等于最新序号时才更新结果。或者用 AbortController 取消上一个请求。第三是缓存如果用户输入了相同的关键词直接拿上次的结果不用重新请求。第四是请求失败的处理要有 loading 状态和错误兜底不能让用户输入越快界面越乱。面试官追问我“缓存怎么做”的时候我说可以用一个 Map 存 keyword - promise下次相同关键词直接返回 map 里的 promise既能防抖又天然去重。其实这个思路就是请求的 promise 复用比单纯缓存值更优雅面试官听到这里明显有兴趣。这种场景题考察的不只是 API 背得熟不熟而是你处理真实业务问题时有没有完整的思考链路。3.4 项目复盘线上问题排查与性能优化每次面试最难的不是八股而是把项目经历讲得像“自己做过的”而不是“参与过的”。我这次特意准备了一个线上性能优化案例之前负责的后台管理系统首屏加载要 6 秒多用户反馈特别卡。我一上去先从 Network 面板看加载时序发现最大的问题是打包出了一个 8MB 的 vendor.js。优化方案我讲了三个层面。第一是代码层面路由懒加载、组件按需加载、去掉没用到的依赖第二是构建层面用 webpack 的 splitChunks 拆包把 echarts 这类大库单独抽出来并按需引入第三是缓存层面静态资源加 hash 文件名、配好强缓存让二次访问基本零请求。优化完之后首屏时间从 6 秒降到 2 秒以内。面试官追问了一个很刁钻的点你怎么确认优化之后的结果是有效的而不是你“感觉快了”我当时的回答是用 Lighthouse 在优化前后各跑了五轮取平均值然后用 Performance 面板看 FP、FCP、LCP 三个指标的变化并且从监控平台拉真实用户数据做对比。这种回答方式能让面试官相信你是在真实场景里解决问题而不是背了一套话术。4. 面试翻车实录几个容易扣分的回答方式4.1 项目讲太细被面试官打断有场面试我讲项目时犯了很典型的错误花了五分钟讲技术实现的细节把组件怎么拆分、接口怎么设计的流程全都说了结果面试官皱着眉头问我“所以你在这个项目里到底解决了什么问题”那个瞬间我才意识到面试官不关心你的组件怎么命名、函数怎么组织他要听的是问题、方案、效果。后来的复盘是停止“流水账式讲项目”。正确讲法是先一句话概括项目背景和我的角色再用“我遇到的最难的一个问题是什么、我如何分析、如何解决、效果怎么量化”来讲。如果面试官想继续追问实现细节他自然会打断你。这一点真的很重要建议准备面试的朋友用“录屏自讲”的方式练习你会发现自己在台上废话远比自己想象的多。4.2 手写题卡壳时的现场抢救我这次有一场手写 Promise.all 的时候卡了壳脑子一片空白开头五行写完就不知道后面怎么接。当时我做了两件事一是立刻告诉面试官“让我先理一下思路写个伪代码”二是从最简单的输入输出推起Promise.all 接收一个 promise 数组返回一个新的 promise最后一个 resolve 出去任一 reject 就 reject。其实就是先搭骨架再填肉。先把返回值写出来function myPromiseAll(promises) { return new Promise((resolve, reject) { const results []; let count 0; if (promises.length 0) { resolve(results); return; } promises.forEach((p, index) { Promise.resolve(p).then( (value) { results[index] value; count; if (count promises.length) resolve(results); }, (error) { reject(error); } ); }); }); }虽然最后写完了但我复盘时意识到如果一开始就先说“我只要保证顺序不变、计数完成就 resolve、任何一个 reject 就 reject 这三个点”面试官可能更好理解我的思路。手写题的重点本来就不是默写代码而是现场展示你如何拆解问题。4.3 “还有吗”背后在问什么有场面试面试官问我“组件通信方式有哪些”我噼里啪啦说了 props、自定义事件、Vuex然后面试官问了句“还有吗”我当时以为他只是想让我再背几个于是补充了 provide/inject、$attrs、$refs。后来想想这个“还有吗”其实是在问我有没有对“组件通信”这个问题的分类思维。更好的回答方式应该是按场景分类父传子用 props子传父用自定义事件爷孙组件用 provide/inject兄弟组件用共享状态或者事件总线跨模块用状态管理库跨层访问实例用 ref。如果再加上一句“这些方式各有优缺点比如 props 适合单向数据流状态管理库适合大项目多人协作”就更好了。面对“还有吗”你要理解成“你还有没有更深层的思考”而不是“你还能不能多背几个词”。4.4 回答里的高频雷区面试中有些回答方式真的不要碰。第一个雷区是“我们项目就是这么实现的没考虑过为什么”。这种话一出面试官对你的评价直接封顶了后面再怎么聊都是挽救。哪怕你当时真的就是“照着文档写得”也要转换说法“当时我们做技术选型时对比过 A 和 B因为当时的项目规模和团队情况选了 A后来遇到什么情况我们又补了哪些措施。”把“没思考”包装成“按场景决策”。第二个雷区是强行套原理。有一次面试官问我 Vue 的 computed 实现原理我其实只能背出“有缓存、依赖收集”这种话但硬往深里扯结果被追问两句就露馅了。现在的我会直接说“这块底层细节我还没有深入看过”然后把话头转到我熟悉的应用场景上。诚实承认边界比硬撑要好得多。第三个雷区是答题没有结束感。很多同学在回答完问题后习惯性地“还有”一下越补越多反而让面试官抓不住重点。我现在的做法是结论先行说完结论给一个例子最后一句“这就是我理解的核心”作为收尾。有了结束感面试官才好接下一个问题。5. 简历与题库复盘下一轮面试的优化清单5.1 错题本按主题分类面完之后最重要的一件事是整理错题本。我这次把所有没答好的问题都记下来了然后按“浏览器机制、JS 语言基础、框架原理、工程化、网络、项目表达、场景设计”这几个大类归档。每一题下面记三件事当时怎么答的、正确答案是什么、哪个环节出了问题。比如我在一家公司挂了“async/await 实现原理”错题本里就记下当时只会说“异步函数的语法糖”就卡住了正确答案是 generator promise 的自动执行然后我另外花了一个晚上去搞懂了 co 库的核心逻辑。这种错题本不是形式主义而是下一次面试前最精准的复习资料。5.2 表达训练把“我知道”变成“我能讲出来”面试复盘最大的收获之一是我意识到“心里知道”和“讲出来”之间隔着一条大沟。有个很明显的例子我知道事件循环的微任务和宏任务关系但一开口就说得乱因为脑子里概念太多没有形成表达顺序。我的改进办法是“费曼式练习”。挑一道高频题想象对面坐着一个人用三句话讲明白不能超过五分钟。比如“为什么 setTimeout 不能保证准时执行”这个问题我会先说结论再给原因主线程任务没执行完宏任务队列里的回调就不能开始。这三句话练顺了面试时就不会出现越说越乱的情况。面试表达本来就是一门技术活需要刻意训练。5.3 谈薪与 offer 选择面到最后阶段肯定会遇到谈薪和选 offer 的问题这里也有不少经验教训。我的建议是先别急着给期望薪资让面试官或者 HR 先报预算范围你大概就能判断这家公司的薪资带宽如果必须给数字给一个你满意的范围而不是一个具体值比如“20 到 24k 之间都可以聊”比“我要 22k”要有弹性。选 offer 的时候我这次列的维度是业务方向是不是核心、技术栈是否匹配、直属领导的风格、团队规模、通勤时间、试用期工资折扣。钱很重要但不要只看钱有一个我很在意的点是面试过程中面试官的态度——如果二面面试官全程不耐烦、不断打断你那入职之后的沟通大概率也很累。这种方式虽不绝对但可以作为参考毕竟面试是双选。最后再分享一个小技巧。面试复盘的时候不要只记错题也把那些你回答得好的问题记下来建立自己的“舒适区清单”。下一次面试前先把这些题重新讲一遍给自己建立信心你会发现状态对了后面很多问题都会答得很顺。面试这个东西技术积累是基础节奏和心态也真的会直接影响发挥。
返回列表