前端行业这几年变化快,面试的要求也跟着水涨船高。尤其到了2026年这个节点,单纯背八股文已经很难过面试了,考察的重点越来越偏向“原理深度 + 工程落地 + 场景应变”这三样东西。我平时也帮团队筛简历、做技术面,见过太多候选人简历写得很漂亮,结果一问到底层就卡壳。这个系列我会按专题整理一些真正值得反复琢磨的前端面试真题,有题目、有思路、也有我作为面试官角度的真实评判标准。第一篇先聊几个高频出现、又最容易暴露水平的经典题。
1. 内容整体设计与思路拆解
1.1 为什么前端面试越来越难“临时抱佛脚”
先讲一个现象。现在的前端面试题,尤其是中大型公司的,早就不满足于“这道题考什么 API”了。面试官更关心你在面对一个模糊问题时的思考路径。比如问“防抖和节流区别”是基础题,但问“一个搜索框,输入时既要实时联想又要避免请求风暴,你怎么设计”就完全是另一回事了——前者考记忆,后者考工程判断。
我参与过的面试场次里,淘汰率最高的往往不是不会写代码的人,而是“会写但讲不清为什么”的人。你让他手写一个深拷贝,他能写出来,但问他“为什么要处理循环引用”“Symbol 类型要不要拷贝”“原型上的属性怎么办”,他就开始含糊了。这说明他平时写代码是照着网上的代码敲的,没有真正理解其背后的设计逻辑。
所以这个系列的内容,我会刻意把“题目”和“题目背后的设计意图”放在一起讲。你如果只想要标准答案,网上搜一堆面试题总结就行;但如果你想把面试官聊到“这个人确实有两下子”,那你需要的是思维链,不是答案本身。这也是我写这篇长文的初衷。
1.2 一个可复用的前端面试知识框架
既然要做系统准备,我们得先建一个“地图”。前端面试的知识点看似零散,但我习惯把它们分成六层:
- 基础语言层:JavaScript 核心机制(原型链、闭包、异步、事件循环)、ES6+ 语法、TypeScript 类型系统。
- 渲染与样式层:CSS 盒模型、布局方案、响应式、BFC、动画性能。
- 框架应用层:React/Vue 的核心原理、组件设计、状态管理、跨端方案。
- 工程化与构建层:打包工具、模块化、代码规范、CI/CD、Monorepo。
- 性能与质量层:性能指标、优化手段、监控告警、错误处理。
- 软技能与场景层:项目复盘、架构设计、团队协作、业务理解。
面试题不管怎么出,都能归到这六层里。你的准备优先级应该按“基础语言层 > 框架应用层 > 工程化与性能 > 其他”这样来排。把最底层的东西吃透,上层就算忘了一些细节,也能推导出来;反过来,只背上层框架的 API,底层一问就露馅。
这份地图也提醒我们:面试准备不是刷题,是补知识体系。刷题能帮你熟悉“考题长什么样”,但知识体系才能帮你应对“没见过的题”。我见过太多人刷了300道题,结果面试官换个问法就懵了,原因就是底层的“为什么”没有打通。
2. 核心细节解析与实操要点
2.1 真题一:请解释事件循环(Event Loop),并说说宏任务与微任务的执行时机
这题几乎是所有前端面试的“必考题”,但回答的质量天差地别。基础回答是:JavaScript 是单线程的,事件循环负责调度任务,宏任务执行完会清空微任务队列。这个回答只能算及格。
面试官真正想听到的,是你对以下细节的理解:
- 为什么要有微任务?微任务和宏任务的划分标准是什么?
Promise、async/await、setTimeout、requestAnimationFrame、MutationObserver分别属于哪类任务?- 在浏览器和 Node.js 里的事件循环有什么区别?
- 一个宏任务队列里的多个任务执行完,一定会执行微任务吗?是执行一个微任务还是一个队列的微任务?
实际面试中我习惯这么问:setTimeout回调里嵌套setTimeout,再混入几个Promise.resolve(),让你说出打印顺序。这题没有难度,但能直接暴露候选人有没有真正“跟过”代码的执行过程,而不是靠背结论。
我给一个容易记住的口诀:一个宏任务执行完,清空整个微任务队列,然后再取下一个宏任务;微任务中产生的微任务,也会在当前宏任务结束前全部执行完。关键点在于“微任务队列是逐个执行还是整体清空”——整体清空。所以你在微任务里不断产生新微任务,就会造成所谓的“微任务死循环”,页面直接卡死。
实操建议:自己在浏览器控制台里多写几个任务混合的例子,先猜输出再验证。这不是浪费时间,这个思维模式会跟着你从面试用到线上排查性能问题。
2.2 真题二:手写实现 call / apply / bind,并说明它们的区别和底层原理
这题在面试里出现频率极高,考察的是你对 JavaScript 函数调用机制和 this 绑定的理解。手写这三兄弟,其实核心就一句话:改变函数执行时的 this 指向。
实现call的思路很直接:
Function.prototype.myCall = function(context, ...args) { // 如果 context 是 null/undefined,默认指向全局对象 context = context ?? globalThis; // 用 Symbol 创建唯一 key,避免覆盖 context 上的属性 const key = Symbol('key'); context[key] = this; const result = context[key](...args); delete context[key]; return result; };这里最关键的是Symbol的使用。如果你直接给context添加一个普通属性,万一这个对象本身有同名属性就被覆盖了,面试时写出Symbol会是一个加分项。然后apply只是把参数形式从展开的数组变成数组整体传进去。
bind稍微复杂一点,因为它返回的是一个新函数,而且支持“参数分两次传”:
Function.prototype.myBind = function(context, ...bindArgs) { const fn = this; return function(...callArgs) { return fn.apply(context, [...bindArgs, ...callArgs]); }; };但要注意,这个简版实现没有处理“用 new 调用绑定函数”的情况。原生bind生成的函数如果被new了,this 会指向新创建的对象,而不是我们绑定的 context;而且原型链也要保留。能主动讲到这一层的候选人,我会觉得他是真的读过相关规范。
2.3 真题三:什么是闭包?闭包的实际应用场景有哪些?
闭包这个概念被讲滥了,但最常见的答案是“函数里返回一个函数,内部函数能访问外部函数的变量”——这个回答太浅了。面试官想听的是,你能不能把闭包的本质说透。
闭包的本质是“函数 + 它的词法环境”的组合。当一个函数被定义时,它会记住自己出生的那个作用域;即使这个作用域已经执行完毕,只要内部函数还存活着,那些变量就不会被垃圾回收。这在 JavaScript 里是底层机制,不是语法糖。
实际应用场景大概有以下几类:
- 数据私有化:用闭包模拟私有变量,模块模式就是典型。
- 柯里化和函数工厂:让函数预设一些参数,生成新函数。
- 事件处理和回调:比如循环中绑定点击事件,用闭包捕获当前索引。
- 防抖和节流:保存定时器 id 和状态,本质上也是闭包。
我面试时喜欢追一句:闭包会造成内存泄漏吗?这是一个陷阱题。闭包本身不会泄漏,泄漏是因为你“长期持有本该释放的引用”。比如一个不再使用的 DOM 节点被闭包引用着,它就无法被回收。这才是真正的风险点。能分辨“内存占用”和“内存泄漏”的人,通常对 V8 的垃圾回收机制有基本的了解。
2.4 真题四:什么是事件冒泡和事件捕获?如何阻止事件传播?
这是 DOM 部分的经典题,也是很多初级候选人的分水岭。现代浏览器的事件流分为三个阶段:捕获阶段、目标阶段、冒泡阶段。默认情况下事件是在冒泡阶段触发的,但你可以在addEventListener的第三个参数里传true让它在捕获阶段触发。
这题的进阶考法是:有一个父元素和子元素,都绑定了点击事件,父子的事件执行顺序是什么?如果中途执行了stopPropagation(),后面的阶段还会不会触发?“阻止冒泡”和“阻止默认行为”的区别是什么?
再往深了问:事件委托的原理是什么?手写一个支持捕获和冒泡的通用事件委托?
事件委托的实现要点:
function delegate(container, selector, eventType, handler) { container.addEventListener(eventType, function(e) { let target = e.target; // 向上遍历,看是否匹配 selector while (target && target !== container) { if (target.matches(selector)) { handler.call(target, e); return; } target = target.parentNode; } }); }常见的问题是:如果子元素里还有更深的子元素,点击最深那个,怎么让事件命中中间层的目标?答案是用closest()方法配合e.target,或者做向上遍历判断。这种代码写业务时经常用到,但很多候选人只是“用过 addEventListener”,让他自己实现一个 delegate 就卡住了。
2.5 真题五:CSS 的 BFC 是什么?它解决了什么问题?
CSS 题里 BFC(Block Formatting Context,块级格式化上下文)是经久不衰的考点。很多人只知道“overflow: hidden 能清除浮动”,但如果问“为什么 overflow: hidden 能清除浮动”,就答不上来了。
BFC 可以理解为一个独立的渲染区域,这个区域内部元素的布局不会影响外部元素。触发 BFC 的条件有:float不是none、position是absolute或fixed、display是inline-block或flex、overflow不是visible等。
它的典型应用场景有三个:
- 清除浮动:父元素建立了 BFC,浮动子元素的高度就能被父元素感知,父元素不再塌陷。
- 防止 margin 合并:两个兄弟元素的上下 margin 会叠在一起,如果给其中一个包一层 BFC,两个 margin 就不会粘连。
- 阻止元素被浮动元素覆盖:一个普通流中的元素,如果旁边有浮动元素,它可能会被盖住;让它变成 BFC,它就不会跑进浮动元素的领地。
我建议候选人把 BFC 的原理和“为什么能解决这些视觉问题”一起理解,而不是只记触发条件。面试官加问一句“flex 容器里的子元素,margin 还会合并吗”时,如果你能答出来“flex 容器内部不会产生 margin 合并”,这题就拿下大半了。
2.6 真题六:谈谈 Vue 3 的响应式原理和 Vue 2 的核心区别
框架题是前端面试的重头戏,而 Vue 是中文社区里用户量最大的框架之一。Vue 2 的响应式基于Object.defineProperty,Vue 3 基于Proxy。这俩的区别,基本是面试必问。
Object.defineProperty的问题是:
- 只能劫持对象属性的 getter 和 setter,新增属性和删除属性无法被侦测。
- 数组的索引操作无法被完整侦测,Vue 2 只能 hack
push、pop等方法。 - 初始化时需要递归遍历所有属性,对象层级深了性能会有影响。
- 需要
Vue.set和Vue.delete来处理动态属性。
Proxy的优势是:
- 可以直接监听整个对象,包括属性的新增、删除和修改。
- 支持数组索引和 length 变化的监听。
- 惰性代理,访问到某个属性时才做深度代理,初始渲染性能更好。
- 还有
Reflect配合使用,这在规范层面更安全。
但这题还有一个隐藏考点:Proxy的兼容成本和性能开销。你不能只说 Proxy 好,还得知道它为什么好、代价是什么。Proxy 在每次属性访问时都有额外的代理层开销,所以 Vue 3 内部做了不少优化(比如利用WeakMap缓存代理、避免重复代理等)。
如果你用 React,面试官大概率会问“React 的 render 函数、Hooks 的原理、为什么不能在条件语句里调用 Hook”。无论是 Vue 还是 React,核心是理解框架的“数据流过视图”的通路,而不是背 API。
3. 实操过程与核心环节实现
3.1 一个模拟面试题:实现一个支持并发限制的任务调度器
前面讲的都是知识点,咱们来一道更综合的手写题,这类题目现在很受面试官偏爱。题目如下:
实现一个
Scheduler,支持add(promiseCreator)方法,限制同时执行的并发数为 2。也就是说,最多只有两个任务在跑,其他任务排队等待。
这题考的不只是 Promise,还考了队列、异步控制和边界处理。我在面试中给过这道题,能完整写出来的候选人不到三成。
核心实现思路:
class Scheduler { constructor(limit) { this.limit = limit; this.queue = []; // 等待队列 this.running = 0; // 正在执行的任务数 } add(promiseCreator) { return new Promise((resolve, reject) => { // 把任务包一层,放进队列 this.queue.push({ promiseCreator, resolve, reject }); this._run(); }); } _run() { while (this.running < this.limit && this.queue.length) { const { promiseCreator, resolve, reject } = this.queue.shift(); this.running++; promiseCreator() .then(resolve, reject) .finally(() => { this.running--; this._run(); }); } } } // 测试代码 const scheduler = new Scheduler(2); const timeout = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const addTask = (ms, name) => { scheduler.add(() => timeout(ms).then(() => console.log(name))); }; addTask(1000, '任务1'); addTask(500, '任务2'); addTask(300, '任务3'); addTask(200, '任务4'); // 输出顺序:任务2、任务3、任务1、任务4这段代码的关键点有几个。第一,add方法返回一个 Promise,把外层的 resolve/reject 和内部任务绑定起来;第二,每次任务执行完,用finally确保计数减一;第三,_run用while循环检查是否可以继续启动新任务,而不是单次if判断。写在finally里而不是then里,是为了保证“即使任务 reject 了,也能继续跑下一个任务”。
面试官经常会追问:“如果某个任务一直 pending,会发生什么?”答案是后面的任务会一直排队,这就是并发控制最朴素的体现。“怎么给任务加超时?”你可以在promiseCreator()外层包一个Promise.race,在超时时间后 reject 掉。这题写顺了,说明你对 Promise 和异步控制的理解已经能落地了。
3.2 结合性能优化:如何设计一个前端通用请求缓存层
这个“题”不是纯粹的手写算法题,而是一个场景题,非常贴近真实工作。要求是:写一个请求缓存模块,要求命中缓存时不再发起请求,并处理并发请求的合并。
这题我基本是在招“高级前端”时会问的。先说背后的痛点:你很难保证业务代码里不会有人重复调用同一个接口,尤其是列表页和详情页来回跳转的场景。没有缓存层,重复请求既浪费流量,又加重服务端压力。
简单版的缓存层实现:
const cache = new Map(); function requestWithCache(url) { if (cache.has(url)) { return cache.get(url); // 命中缓存,直接返回 } const promise = fetch(url) .then(response => response.json()) .then(data => { // 过一段时间让缓存失效,避免数据永远不更新 setTimeout(() => cache.delete(url), 5000); return data; }) .catch(err => { // 失败要删掉缓存,否则下次永远拿不到正确数据 cache.delete(url); throw err; }); cache.set(url, promise); return promise; }这里有几个细节非常能体现候选人的工程经验。第一,缓存的对象是 Promise 而不是数据本身,这样并发请求会复用同一个 Promise,避免“同时发出两个相同的请求”。第二,Promise 失败后要主动删除缓存,否则下次请求会拿到同一个失败的 Promise。第三,要有过期时间,用setTimeout或者记录时间戳都行。
进阶版还可以封装一个clear()方法,支持缓存清理;也可以让缓存 key 包含请求参数,把 GET 请求的url + query序列化作为 key;还可以支持缓存优先级、最大缓存条数等等。这题没有标准答案,但能把你对“浏览器缓存、HTTP 缓存、内存缓存”的理解全部串起来。
3.3 工程化实战:手写一个 Vite 插件实现按需导入
工程化是 2026 年前端面试绕不开的板块。为了不写太长的代码,我拿一个 Vite 插件举例,给大家看看“工程化面试题”的形态。题目是:实现一个 Vite 插件,让项目的某些大型模块能够在编译时自动按需导入,而不是全量打包。
在真实的面试场景里,我不会要求候选人口述完整插件代码,但会让他讲清楚“Vite 插件的生命周期是什么”“transform钩子有什么用”“import.meta.glob和动态导入的区别”这类问题。这个方向能看出来候选人日常是否深入过构建链路。
一个简单的按需导入思路:
// vite-plugin-virtual-module 的简化版思路 export default function virtualLoader() { return { name: 'virtual-loader', resolveId(id) { if (id === 'virtual:big-lib') { return '\0virtual:big-lib'; } }, load(id) { if (id === '\0virtual:big-lib') { // 只是模拟:真正场景里,这里会根据当前页面按需生成导出 return `export { default as Button } from 'big-lib/button'`; } }, }; }工程化题目的核心不是背插件 API,而是理解“构建工具到底在做什么”。它做的是:把模块之间的依赖关系理清,确定每个模块的转换方式,最后生成浏览器能跑的代码。你理解了这一个主线,剩下的都是在这个主线上做文章。Vite 的开发服务器快,是因为它把“预构建依赖”和“源码按需编译”分开了,这背后是 esbuild 和 Rollup 的分工。能讲清楚这条链路,面试官对你的工程化能力的看法会明显不一样。
4. 常见问题与排查技巧实录
4.1 候选人在手写题里最容易犯的错
我总结了过去几年面试候选人的手写代码问题,高频错误集中在这么几个点上:
- 忘记处理边界情况。比如手写深拷贝,只处理了普通对象和数组,没处理
Date、RegExp、Map、Set;手写new,没处理构造函数返回对象的情况。 - 手写
call/apply/bind,用普通字符串做临时 key,导致原对象属性被覆盖。 - 手写防抖节流,完全不考虑参数透传和 this 绑定,只是把 setTimeout 包了一层。
- 数组去重,只会用
Set,问“如果元素是对象,需要用某个字段去重,怎么做”就愣了。
这些错误的本质,不是编码能力不行,而是“系统性地思考问题”的能力不足。手写题不是背代码,面试官要看你有没有“把每种情况都过一遍”的习惯。我建议候选人写完代码之后,自己用一两分钟在脑子里跑一遍测试用例,把null、undefined、空对象、嵌套结构、循环引用全部过一遍。这习惯在真实代码 review 里也很重要。
4.2 面试时答不出题的正确应对方式
除了知识点本身,我还想谈谈心理层面的问题。面试时最怕的不是答不出,而是答不出后直接放弃。我在面试中见过两种情况,结果截然不同:
一种是候选人对某个问题完全没概念,直接说“我不会”。这种回答虽然诚实,但让面试官没有追问的空间,只能跳到下一题。
另一种是候选人说“这个概念我不太熟,但我可以根据我的理解猜一下,比如...”。这种态度我会给加分,因为编码这件事,本质就是面对不确定问题时的推理过程。哪怕他猜得不对,只要推理链条是合理的,我会更愿意相信他做事的方式是对的。
所以我的建议是:遇到不会的题,不要沉默,把你能想到的关联点说出来。比如问到你不会的WeakRef,你可以说“我了解垃圾回收机制,知道 WeakRef 是为了不阻止对象被回收,但我在业务里没实际用过它”。这比你直接答“不会”要强得多。
4.3 面试官最反感的几种“背题式”回答
这节是我作为面试官的真心话合集。很多候选人的简历很优秀,但一开口我就知道他在背题,典型表现如下:
- 回答得像百度百科。比如问事件循环,张口就是“事件循环是 JavaScript 的执行模型,分为内存堆和调用栈,Web API 提供异步能力,回调进入任务队列...”每个词都对,但连起来就是没解释“事件循环把任务从队列拿到调用栈”的完整过程。
- 不回答问题本身。面试官问“这个项目的性能优化是怎么做的”,他背了一堆“预加载、懒加载、CDN、图片压缩”,但没有一个点跟他的项目有关。
- 机械套模板。问“为什么选择 Vue 而不是 React”,他答“Vue 上手简单、模板直观、中文文档好”,完全没提团队情况、业务类型、构建规模这些背景因素。
我想给候选人的建议是:把知识变成自己的语言。每一次面试准备,都试着用一句话把概念讲给你身边不懂技术的人听——如果你能让他们听懂,说明你真的理解了;如果你只能复述定义,那大概率就是背题式的回答。这个训练我每天都在用,效果很好。
4.4 如何在手写题中“边写边讲”拿高分
很多面试题不要求你写得多快,更看重你的表达过程。同样是手写防抖,有两种候选人:
第一种,默不作声开始写,30 秒写完,然后说“写完了”。 第二种,先问面试官:“这个防抖需要支持立即执行吗?需要取消防抖吗?”然后边写边说:“我先保存 this 和参数,因为 setTimeout 回调里 this 会丢;然后清除上一次的定时器;最后用call透传参数。”
如果你是面试官,你会选谁?答案不言而喻。边写边讲的好处是:第一,面试官能跟着你的思路走,你在“展示思考过程”而不是“展示记忆结果”;第二,你可以通过提问澄清需求,给自己争取思考时间,同时展示沟通能力;第三,就算写错了,面试官也知道你是“思路对但细节漏了”,而不是“完全不会”。
面试官视角下,“能沟通的候选人”和“只会写代码的候选人”之间,隔着一条巨大的鸿沟。尤其是团队招人,沟通顺畅度几乎和技术能力同等重要。所以下次面试,别做哑巴,大胆把思考过程说出来。
4.5 前端面试准备的工具清单与避坑指南
最后分享一个面试准备期的工具组合,这些都是我自己用过、觉得真实有效的:
- 浏览器控制台:手写题一定要在控制台里验证,别凭感觉。
- TypeScript Playground:练习类型体操、看类型推导结果,又快又直观。
- React/Vue 官方文档的“进阶”章节:别只停留在组件基础,
useMemo、Portals、Suspense、组合式函数这些都要吃透。 - 性能分析工具:Lighthouse、Chrome DevTools 的 Performance 面板,亲手跑几次性能测试,比背十条优化建议有用得多。
- 代码片段管理工具:整理自己的代码片段库,面试前过一遍,既是复习也是建立信心。
避坑方面最重要的一条:别迷信“面经大全”类的资料。那种资料能帮你应付“题库型面试”,但应对不了“场景型面试”。我见过有人背了几百道题,结果面试官问“你上一个项目里有哪些让你印象深刻的 bug,你是怎么定位的”,他一句话说不出来。这种项目经历题,只有平时真的在做项目、记笔记、复盘,才能在面试时娓娓道来。
《前端专业面试真题》这个系列,我打算按“JavaScript 核心机制、CSS 与渲染原理、框架原理、工程化与性能、场景与综合面试”这几个专题一路写下去。这一篇先打底,把最常考的几类题和答题思路串了一遍。如果你准备面试,不要急着背答案,先按我第一节的知识框架盘一遍自己的薄弱点,再逐个专题去补。面试这东西,说到底就是一场“带着知识体系和思维方式去聊天”的过程,你准备得越像“梳理自己”,越不像“刷题”,效果反而越好。