最近一段时间,陆陆续续面了不少公司,也帮朋友做了几轮模拟面试,加上自己在工作里带人、做技术评审,越来越有一种强烈的感受:现在前端面试的“八股文”背得越来越溜,但真正问到技术深度的时候,很多候选人甚至个别面试官,对底层原理的把握其实是不到位的。
我并不是说八股文没有价值。事实上,八股文是知识面的“索引”,一个连event loop、闭包、diff算法都说不清楚的人,很难让人相信他做过有质量的前端项目。但问题在于:很多人把“背得出题”当成了“懂原理”,而不少面试官也停留在“问得出题”的层面,没有继续往下追问“为什么”。
这篇文章我不想站在“面试官”或者“候选人”任何一方去批判谁,而是想以一个从业者的视角,把这些年在面试中观察到的“深度不足”现象掰开揉碎聊一聊。我会结合具体的面试题、具体的回答场景,以及背后的原理分析,说说问题到底出在哪里,以及我们该怎么去应对这种局面。无论你是准备跳槽的候选人,还是需要招人的团队 leader,这篇文章应该都能给你一些参考。
1. 前端八股文的“深度”到底是什么
1.1 八股文不是洪水猛兽,但它有分层
我见过很多人写面经,把“八股文”三个字当成贬义词,好像一问底层就是在“卷”。但我自己的看法是:八股文本身是分层的,不能一概而论。
最基础的一层,是语言和平台层面的知识点。比如var、let、const的区别,==和===的差异,this的指向规则,Promise的状态机这类。这些内容几乎是 JS 这门语言的“公约数”,不管你在什么公司、做什么业务,都得搞清楚。这一层如果不过关,那确实说明基础不牢。
第二层,是框架和工程体系层面的机制。比如React的fiber架构、diff算法、setState的批处理,Vue的响应式依赖收集、nextTick的实现,webpack的构建流程、HMR的原理、tree-shaking的条件等等。这一层已经是“职业前端”和“能写页面的人”之间的分水岭了。
第三层,是跨领域的融会贯通。比如你能不能用浏览器渲染原理去解释requestAnimationFrame为什么比setTimeout更适合做动画,能不能用事件循环去解释setTimeout(0)的不可靠性,能不能用HTTP 缓存的规则去设计一个前端资源发布方案。这一层没有明确的八股文题目,完全靠平时的积累和思考。
问题就出在这里。很多候选人停留在第一层,把第一层的题背得滚瓜烂熟;一部分人能说到第二层,但也只是“背结论”;真正能到第三层的人,我面了这么多,确实不多。更扎心的是,有些面试官自己也只是停留在第一层,问完==和===、闭包是什么、数组去重怎么写,就不知道该问什么了。
1.2 “深度不足”最典型的表现:会背定义,不会讲场景
我在面试里最常遇到的一种情况是:候选人能把闭包的定义背出来——“函数内部访问外部变量”,但当我问他“闭包在实际项目里到底用来干什么?你写过哪些闭包的代码?它和普通函数在内存表现上有什么区别?”的时候,就开始支支吾吾了。
之所以会这样,我总结下来,问题不是出在候选人“不努力”,而是出在学习路径和考核方式上都过度依赖“题目-答案”的范式。大家刷题的时候,看到的都是“解释 xxx 概念”,而不是“你在项目里是怎么用 xxx 解决问题的”。一旦知识点脱离了场景,它就变成了单纯的“背诵文本”,谈不上理解。
比如闭包最常见的实际场景是函数防抖和节流。你自己手写一个debounce,你就会理解为什么返回的函数能一直访问到timer这个变量,为什么这个timer不会因为函数执行完毕就被销毁。你也会理解,如果这个debounce用在 React 组件的useCallback里,依赖数组到底该怎么写,为什么依赖不对的时候防抖会失效。这才是“理解闭包”,而不是“会背闭包”。
所以我在后面的面试里,只要有时间,都会尽量把八股文题目往场景上引。这不是要故意刁难候选人,而是因为只有场景才能检验一个人是否真的理解了这个知识点的来龙去脉。
2. 我亲眼见到的几个“深度不足”现场
2.1 事件循环:背得出“宏任务、微任务”,画不对执行顺序
关于事件循环,最经典的八股文题就是问执行顺序,给出一段代码让你说出打印结果。这种题我几乎每次面试都会问,因为它是 JS 运行时最核心的机制之一,而且它足够“硬”——会就是会,不会就是不会。
让我印象很深的一个例子,是一个自称“有三年 React 经验”的候选人。我问了下面这段代码:
console.log('script start'); setTimeout(() => { console.log('timeout 1'); Promise.resolve().then(() => { console.log('promise 1'); }); }, 0); Promise.resolve().then(() => { console.log('promise 2'); }); setTimeout(() => { console.log('timeout 2'); }, 0); console.log('script end');他很快给出了答案:script start、script end、promise 2、timeout 1、promise 1、timeout 2。这个答案完全正确。
于是我又追加了一个问题:“那如果我在promise 2的微任务里再放一个setTimeout(0),它的打印顺序会发生在timeout 1之前还是之后?为什么?”
他想了想说应该在timeout 1之前,因为微任务先执行。这个答案其实也对了,但当我继续问“浏览器是怎么决定先执行宏任务还是先执行微任务的?微任务队列是在什么时候被清空的?是一次性清空还是只清空一个?”的时候,他说不上来了。
这个案例非常典型。背结论的人知道“先微任务后宏任务”,但不知道“每个宏任务执行完之后,JS 引擎会去清空微任务队列”。他不知道在一次事件循环里,宏任务是“一个一个执行”的,而微任务是“一队一队清空”的。这导致他一旦遇到稍微复杂一点的嵌套代码,就只能凭感觉蒙答案。
“事件循环”这个知识点,我们不应该只停留在“背顺序”,而应该在脑海里建立一张完整的时序图:
- 当前宏任务执行 -> 执行栈清空 -> 检查微任务队列 -> 全部清空微任务 -> 渲染机会(requestAnimationFrame 回调)-> 取下一个宏任务。
把这些串起来,再去分析代码执行顺序,基本上就不会错。而且这样的理解方式,还能帮你解释很多更复杂的工程问题,比如“为什么setTimeout的定时在后台标签页会被节流”、“为什么async/await写不好会造成性能问题”等等。
2.2 闭包与内存:知道“闭包”是什么,不知道“闭包”会带来什么
上面已经提到闭包,这里我想再展开说说。闭包是另一个“看起来人人都会,实则深度差异巨大”的知识点。
基础题通常是:什么是闭包?经典回答:函数内部访问外部变量,内部函数被返回并在外部引用后,形成了对父级作用域的引用,所以父级作用域不会被销毁。
但通常在业务开发里,真正和闭包相关的坑是意外持有导致的内存泄漏。比如下面这段代码:
function createApp() { const heavyData = new Array(10000000).fill('x'); document.getElementById('btn').addEventListener('click', function onClick() { console.log('clicked'); }); }onClick这个函数虽然没有直接引用heavyData,但是因为它和heavyData在同一个作用域里,闭包会把这个作用域整体保留下来。只要事件监听器不解除,heavyData就永远无法被回收。这在 SPA 应用里很常见——页面切换的时候,如果removeEventListener做漏了,内存就会悄悄涨上去。
我也用这道题考过不少候选人。能答出“闭包导致内存泄漏”的不少,但能进一步说出“什么情况下闭包一定会导致内存泄漏?是不是所有闭包都会泄漏?”的人就很少了。其实,只要闭包函数已经没有任何引用,它本身就会被回收,不会泄漏;只有当你把闭包函数挂在一个长期存活的对象上(全局变量、事件监听器、定时器回调等),才可能出现泄漏。
我给大家一个自查的方法:当你在代码里写了一个addEventListener或者setInterval,随手想一下“这个回调函数被谁持有?它的作用域里有没有大对象?”这个习惯养成之后,你就会理解为什么现代前端框架要在useEffect里返回一个清理函数,为什么要用useCallback和useRef控制依赖。这些都是闭包知识的实际应用。
可惜的是,很多候选人画不出这条链路。他们脑子里只有“闭包 = 能用”或“闭包 = 会泄漏”这两个孤立结论,中间的推理过程是缺失的。
2.3 React 渲染优化:都在说 memo,但说不清什么时候该用
React 生态是八股文的重灾区。useMemo、useCallback、memo、diff、Fiber,随便哪个都能拉出来考一考。但真正让我觉得“深度不足”的,不是候选人说不上这些 API,而是他们不知道这些 API 到底是在解决什么问题。
我一般会问这样一道题:
父组件里有一个
useState的计数器和一段很耗时的渲染列表,点击按钮会让计数器加一,进而触发父组件重渲染。子组件接收一个props,并且完全不依赖父组件的 state。请问:子组件会跟着重渲染吗?如何避免?memo是必须的吗?如果用useMemo包住子组件的 JSX,和用memo包住组件有什么区别?
这道题,把“React 函数组件在什么情况下会重渲染”这个核心机制全考了。能完整答上来的人,大概率真的理解 React 的渲染流程;答不上来的人,通常都停留在“用memo可以优化性能”的层面。
说句实话,在大多数业务场景里,memo和useCallback真的没那么重要。与其纠结这些 API,不如先搞清楚一件事:你的组件为什么会重渲染?
在 React 中,函数组件每渲染一次就是“执行一次函数”。只要父组件函数重新执行了,子组件函数也会跟着重新执行,不管它的props有没有变化。memo做的事情,是让 React 在子组件外部做一个“浅比较”,如果props的引用都没变,就跳过子组件函数的执行,复用上一次的渲染结果。
这里关键来了:如果父组件每次渲染都生成一个新的数组/对象/函数传给子组件,那么即使子组件被memo包住了,浅比较也会认为props变了,导致memo失效。所以说“memo必须搭配useCallback/useMemo才能发挥效果”——不是八股文,是真正从机制推导出来的结论。
能推理到这一层的人,在遇到“为什么我加了 memo 没效果”这种实际 bug 时,就能很快定位问题;而停留在“背概念”的候选人,遇到这个问题大概率只能console.log盲猜,甚至回一句“可能 React 版本有 bug”。
2.4 浏览器渲染机制:知道“重排重绘”,答不出优化场景
前端面试十道题里有八道会考浏览器渲染。这是我最喜欢问的方向,因为它横跨HTML、CSS、JS三门语言,而且直接关系到页面性能。
面试里常见的问题是:什么是重排(reflow)和重绘(repaint)?它们有什么区别?如何减少重排?
标准回答一般是:重排是计算元素几何位置,重绘是绘制像素;重排一定会导致重绘,重绘不一定导致重排;减少重排可以用transform代替top、left动画等等。这套答案背下来很容易,但当我追问“为什么transform不会触发重排?它用的是哪个合成层?什么时候transform也救不了你?”的时候,绝大多数人都会卡住。
实际上,transform不触发重排的根本原因,在于浏览器把元素放到了独立的合成层上,transform的变化只是对这个层做 GPU 合成变换,不涉及布局计算。这就好比你有一张打印好的海报,你想把它往右挪一点,你不需要重新排版整张海报,只需要把海报物理移动一下就行。
但这里面有一个很关键的前提:如果这个元素所在的合成层本身非常大,或者页面上有大量合成层,GPU 会吃不消,照样导致卡顿,甚至比重排重绘更严重。这就是为什么“用transform做动画就一定流畅”是个错误结论,它只对了一半。
大家在复习这个知识点的时候,如果能多问自己几个“为什么”,比如“为什么transform不触发重排”“为什么position: fixed某些时候会失效”“为什么content-visibility: auto可以跳过屏幕外元素的渲染”,你的理解深度就会完全不同,面试的时候也自然能说出比别人更立体、更有层次的内容。
3. 为什么会出现“深度不足”的局面
3.1 题库越来越新,但知识体系没有随题而长
观察下来,现在“前端八股文”的题库迭代速度是非常快的。从早期的DOM操作、闭包、原型链,到后来的Vue/React 生命周期、diff 算法,再到现在的微前端、性能优化、TypeScript 类型体操,题目越来越花哨。
但问题在于,很多候选人的知识体系是“块状”的,不是“网状”的。他们背了一堆“块”,但块和块之间没有连起来。比如,背了 “TypeScript 的infer关键字”,却不知道它其实是和“函数返回值类型的推断”相关的;背了 “React 的useSyncExternalStore”,却不知道它解决的其实是“外部 store 和并发渲染的撕裂问题”。
知识碎片化,导致了一个很典型的面试现象:单独问点,都能答上;一旦把几个点串起来,或者换一个业务场景,立刻就“卡壳”。我在一面里常用的一道组合题是:
用
Web Worker处理大量数据的计算,计算完成后把结果传回主线程渲染一个很长的列表。请说说这个过程中有哪些性能瓶颈?分别怎么优化?`
这道题其实没有标准答案,它考察的是Worker 通信、大数据渲染、浏览器内存、事件循环等多方面知识的综合运用。能答好的人,一定是能把“块”连成“网”的人;答不好的人,通常就是被“块”困住的人。
3.2 面试官段位参差:有些人的题目是从面经里抄的
这个问题不太有人愿意说,但作为一面面试官,我观察到的现象是:确实存在一部分前端面试官,他们的考核方式就是“题库搬运”。
这类面试官手里往往有一套自己当年找工作时的面试题合集,或者是一个“前端知识图谱”的 checklist。面试的时候,他们按着 check item 一个一个问:事件循环、闭包、原型链、盒子模型、flex 布局、浏览器缓存、webpack 优化……每个问题只要候选人能说出“关键词”,就算过关。至于候选人回答里的漏洞、混淆、甚至错误的概念,他自己也听不出来。
这就导致一种非常荒诞的循环:面试官水平不足,招进来的人水平也跟着打折扣;这批人再过两年也成为面试官,继续用固定题库筛人。整个行业的技术深度在一部分公司里不升反降。
我当然知道,不是所有公司都这样,很多一线大厂的面试还是很扎实的,会有非常细致的追问环节。但“八股文面试”和“深度面试”的差别,确实肉眼可见地存在。有些公司一面问的都是概念题,二面让写算法,三面聊项目——看起来流程很规范,但实际上每一轮都停留在“你背过没有”的层面,没有人真正去验证候选人“能不能想清楚一个问题”。
3.3 候选人的应试策略放大了问题
还有一方面也值得说,就是候选人的应试策略。
现在的面经文化太发达了,很多候选人花大量时间刷题、背题,这本身没有错。但如果你的重心全放在“怎么应付面试官”,而不是“怎么把原理学透”上面,短期里也许能过面试,长期来看反而是给自己埋雷。
最典型的例子是,“手写 Promise/A+” 这道题。网上有很多版本的“标答”,全都标注着“高频题”。于是很多候选人不管三七二十一,先把代码背下来,面试的时候默写一遍。但当我追问“你实现的.then为什么返回了一个新的 Promise?如果直接return this会有什么问题?resolvePromise里为什么要加called标志?”的时候,就露馅了。
我真心觉得,候选人应该把面试准备当成一次系统性学习的过程,而不是刷题过关。那些反复被问到的点,比如Promise、组件通信、工程化配置,都是这个行业真正需要的核心能力。你花了一周时间把它们真正搞懂,收获的是一辈子的职业基础;你花了一周时间把它们背下来,收获的可能只是入职三个月的试用期。
4. 如何提升“深度”:给候选人、也给面试官的建议
4.1 给候选人:别背题,去推演
我理解大家找工作的压力,也理解“八股文”是绕不开的筛选手段。但我的核心建议是:背题可以,但要在背题之后加一步“推演练习”。
具体来说,看一道题,不要急着看答案,先自己回答一遍,然后问自己三个问题:
- 这个结论是从什么前提推导出来的?
- 如果环境改变(比如从浏览器换成 Node,从 Vue2 换成 Vue3),结论还成立吗?如果不成立,变化在哪?
- 这个知识点和我之前做过哪个项目、遇到过哪个 bug 有关?
举个例子,你在背“webpack的tree-shaking依赖 ES Module 的静态结构”时,可以顺手推演一下:为什么CommonJS不行?因为require是运行时执行、可以写在条件语句里的,编译器无法静态确定你到底引用了哪些导出;而import必须在顶层、模块依赖关系是静态确定的,所以可以分析出哪些导出没用。这样一来,你不仅记住了 “tree-shaking 用什么语法”,还明白了“为什么用这个语法”、“换成 CJS 为什么不好使”。
这种“推演习惯”一旦养成,你再看问题的方式就完全不一样了。你不再是“记住了一个知识点”,而是“推导出了一个知识链”,面试的时候哪怕突然紧张,也能顺着逻辑说下去,而不是卡在“忘了关键词”上。
4.2 给面试官:多问“为什么”,少问“是什么”
作为前端 team lead,我也经常帮团队设计面试题。我一直强调一点:面试官的作用不是打分,而是帮候选人把脑子里的知识“展开”。
如果候选人能答对“宏任务微任务的执行顺序”,你要继续追问“为什么微任务要先执行完”;如果候选人能答出“useMemo可以缓存计算结果”,你要继续追问“缓存的是什么?什么时候缓存会失效?”;如果候选人能说出“script标签加defer可以延迟执行”,你要继续追问“defer和async的区别是什么?如果两个defer的脚本之间存在依赖关系,会出问题吗?”。
深度,从来不是靠问一道“偏题怪题”能问出来的,而是靠追问“为什么”一层一层挖出来的。一个只知道背答案的人,最多能扛住第一层追问;一个真正理解原理的人,能一路回答到“运行时”“编译器”“网络协议”这些底层领域去。
另外我还想提一个建议:面试官在评价候选人的时候,不要把“背出了答案”直接等价于“能力强”。你要看他回答时的状态:是流畅但不过脑,还是边思考边推理?是直接给结论,还是先分析前提再说结论?这中间的差别,比答案本身更能说明问题。
4.3 给团队:把评审当成常态化机制,而不是面试才测深度
最后也想说点团队层面的东西。面试应该是“深度的抽样”,不应该是“深度的全部”。如果一个团队平时根本不聊技术细节,只在招人的时候考深度,那大概率招不到满意的人,也留不住有想法的人。
我自己在团队里推动的两种常态化机制,效果非常不错:
- 每周一次“原理分享”:不限主题,不限难度,但要求讲清楚“一个机制的完整链路”。比如“从输入 URL 到页面展示的整个过程”,从 DNS、TCP 到渲染流水线,每个环节要能回答大家随时的追问。
- Code Review 时多问一层“为什么”:不是只指出“这里写的不对”,而是问“为什么要这样写?如果换一种写法会有什么区别?”如果写代码的人自己说不清楚,那就一起查资料弄明白。
长期坚持下来,团队的平均技术深度会肉眼可见地提升,面试的时候也会轻松很多——因为候选人面对的是一群真正懂行的人,大家能聊到一块去,而不是互相演背题。
5. 我的真实感受与后续打算
说了这么多“深度不足”的问题,我再聊几句个人体会。
我见过不少候选人,笔试做得漂漂亮亮,面试对答如流,但一到实际项目里就各种踩坑——因为“背”和“懂”之间的距离,只有“踩坑”才能弥补。反过来,我也见过一些候选人,八股文基础一般,有些概念甚至说得磕磕绊绊,但你能感觉到他在思考,他在试图用已知的东西去推导未知的东西。这两种候选人,如果让我选,我大概率会选后者。
原因很简单:前端的知识更新太快了。你今天背熟的那套八股文,过两年可能就变了;但你的“理解能力”和“推理能力”,是永远不会过时的。所以我真心建议大家,在准备面试的时候,不要只盯着“高频题”,也别总想着“速成”。遇到一个概念,多问几句“为什么”,多想想“它解决的问题是什么”,多联系一下自己在项目里的实际场景——这个过程可能慢,但它的收益是长期的。
后面我打算写一个系列文章,专门挑一些高频的“八股文”题目,一篇一篇地拆解它们背后的“为什么”,尽量把那些网上搜不到、只能靠实战踩坑才能理解的细节讲透。如果你在面试或者平时工作里遇到过一些“看起来很基础、但细想很迷糊”的问题,欢迎一起讨论交流。踩过多少次坑之后你就会明白,前端这个行业,深度不是靠背出来的,是靠一个个问题“追”出来的。