中级前端工程师面试题
中级前端工程师面试的考察重点、常见题目,以及如何用即时 AI 反馈练习。
中级阶段的考察重点
考察框架深度、状态管理、性能基础,以及独立交付功能的能力。
前端工程师常见面试题示例
- 技术面讲讲浏览器的事件循环,以及微任务和宏任务的区别。好回答应覆盖
- 事件循环机制:执行栈、任务队列
- 宏任务与微任务的定义与优先级
- 常见的宏任务来源:setTimeout、setInterval、I/O、UI渲染
- 常见的微任务来源:Promise.then、MutationObserver、queueMicrotask
- 微任务在事件循环中的执行时机:当前宏任务结束后、下一个宏任务前
查看范例答案
浏览器的事件循环是协调同步任务与异步任务执行的核心机制。主线程执行同步代码,遇到异步任务(如setTimeout、Promise)会将其对应的回调放入任务队列。事件循环持续从任务队列取出回调执行。宏任务包括整体script、setTimeout、setInterval、I/O、UI渲染等,每个事件循环只执行一个宏任务。微任务如Promise.then、MutationObserver、queueMicrotask则在一个宏任务执行完毕后、下一个宏任务开始前执行,且会清空整个微任务队列。这导致微任务具有更高优先级,且能阻塞UI渲染。常见陷阱:在微任务中递归创建微任务会导致页面卡顿。面试中可能追问async/await的语义,实际上await后的代码会作为微任务调度。
- 技术面什么会触发 React 重新渲染?如何避免不必要的重渲染?好回答应覆盖
- 触发重新渲染的因素:state/props变化、forceUpdate、context变更
- React使用浅比较决定是否渲染(默认全渲染)
- 避免方法:React.memo、useMemo、useCallback
- ShouldComponentUpdate在类组件中的作用
- key属性帮助diff优化
查看范例答案
React组件重新渲染的触发条件主要包括:组件自身的state通过setState更新(或useState dispatch)、父组件重新传递的props变化(即使引用相同也可能因父渲染而连带子渲染)、使用forceUpdate强制更新,以及Context值变化导致消费组件重新渲染。默认情况下,React会递归渲染所有子组件,无论props是否实际改变。优化手段有:使用React.memo包裹函数组件,通过浅比较props跳过渲染;在类组件中实现shouldComponentUpdate或继承PureComponent;使用useMemo缓存计算结果、useCallback缓存函数引用以保持子组件memo的有效性;给列表元素使用稳定的key值以复用DOM节点。常见陷阱:将内联对象或函数作为props传递会破坏memo的效果,需要使用useCallback或抽取常量。
- 技术面描述 CSS 层叠是如何解决冲突规则的。好回答应覆盖
- 层叠规则:优先级按来源、选择器特异性、顺序
- 来源排序:用户!important > 作者!important > 作者普通 > 用户普通 > 默认
- 特异性计算:内联样式 > ID > 类/属性/伪类 > 元素/伪元素
- 相同特异性时后声明的生效
- !important的滥用与覆盖策略
查看范例答案
CSS层叠(Cascade)是浏览器解决多个样式规则冲突的算法。它按三个维度依次判断:先看样式来源,优先级从高到低为:用户代理!important > 用户!important > 作者!important > 作者普通声明 > 用户普通声明 > 用户代理默认;其次比较选择器的特异性(Specificity),内联样式1000、ID选择器0100、类/属性/伪类0010、元素/伪元素0001,按位比较;最后按声明出现顺序,后写的覆盖先写的。!important可提升声明至最高优先级,但应避免滥用,因为会增加维护难度。面试常问:相同特异性下后声明的样式覆盖前面的,因此将通用样式放在前、特定样式在后可避免覆盖问题。
- 编程实现一个 debounce 函数,并说明什么时候该用它。好回答应覆盖
- debounce函数实现:延迟执行、取消、立即执行选项
- 适用场景:搜索输入、窗口resize、按钮防止重复点击
- 与throttle的区别:连续触发时只执行最后一次 vs 固定频率执行
查看范例答案
Debounce函数用于限制函数在短时间内被频繁调用,只有当触发停止后等待指定延迟才执行一次。实现原理:每次调用时清除之前的定时器,再设置新定时器。可选立即执行版本(leading)在首次触发时立即执行,后续等待延迟。常见应用场景:搜索框输入(等待用户停止输入后发送请求)、窗口resize事件(避免频繁计算布局)、按钮点击防止重复提交。与throttle不同,debounce会合并多次为一次,而throttle保证固定间隔执行一次。使用时注意:如果延迟设置过长会导致反馈延迟;需要提供取消能力(如组件卸载时清除定时器)。代码示例中使用了尾调用和leading选项。
参考代码javascript /** * 创建一个debounce函数 * @param {Function} func 要执行的函数 * @param {number} wait 延迟毫秒数 * @param {boolean} [immediate=false] 是否立即执行一次 * @returns {Function} debounced函数 */ function debounce(func, wait, immediate = false) { let timeoutId = null; let result; const debounced = function (...args) { const context = this; const callNow = immediate && !timeoutId; if (timeoutId) clearTimeout(timeoutId); timeoutId = setTimeout(() => { timeoutId = null; if (!immediate) { result = func.apply(context, args); } }, wait); if (callNow) { result = func.apply(context, args); } return result; }; // 提供取消功能 debounced.cancel = function () { if (timeoutId) clearTimeout(timeoutId); timeoutId = null; }; return debounced; } - 编程实现一个支持键盘操作的无障碍自动补全组件。好回答应覆盖
- 无障碍属性:role='combobox'、aria-expanded、aria-autocomplete
- 键盘支持:方向键导航选项、回车选择、Esc关闭列表
- 状态管理:输入值、下拉显示、高亮索引、选项列表
- 焦点管理:保持焦点在输入框,使用aria-activedescendant指向高亮选项
- 常见问题:屏幕阅读器朗读、选项的动态更新
查看范例答案
实现一个无障碍自动补全组件,首先需要合理的ARIA属性:给输入框设置role='combobox'和aria-autocomplete='list',同时用aria-expanded控制下拉是否展开,并用aria-controls指向选项列表的id。选项列表容器设置role='listbox',每个选项设置role='option',并用aria-selected标记当前选中项。键盘支持必须包括:上下方向键移动高亮(通常用activeDescendant或aria-activedescendant属性指向高亮选项的id),Enter或点击选择并关闭列表,Esc关闭列表并将焦点留在输入框。需要管理内部状态:是否显示下拉、选项数据、高亮索引、输入值。用useEffect监听输入变化过滤选项,更新列表。焦点管理:当打开列表时,通过aria-activedescendant让屏幕阅读器读出高亮选项,无需移动实际焦点。边界情况:输入为空时清空列表;通过远程数据时需考虑请求防抖。以下是用React+TypeScript的简化实现。
参考代码tsx import React, { useState, useRef, useCallback, useEffect } from 'react'; interface Option { label: string; value: string; } const AutoComplete: React.FC<{ options: Option[] }> = ({ options }) => { const [inputValue, setInputValue] = useState(''); const [isOpen, setIsOpen] = useState(false); const [highlightedIndex, setHighlightedIndex] = useState(-1); const inputRef = useRef<HTMLInputElement>(null); const listRef = useRef<HTMLUListElement>(null); const filteredOptions = options.filter(o => o.label.toLowerCase().includes(inputValue.toLowerCase()) ); const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { setInputValue(e.target.value); setIsOpen(true); setHighlightedIndex(-1); }; const handleKeyDown = useCallback((e: React.KeyboardEvent) => { if (!isOpen) return; switch (e.key) { case 'ArrowDown': e.preventDefault(); setHighlightedIndex(prev => Math.min(prev + 1, filteredOptions.length - 1) ); break; case 'ArrowUp': e.preventDefault(); setHighlightedIndex(prev => Math.max(prev - 1, 0)); break; case 'Enter': e.preventDefault(); if (highlightedIndex >= 0) { const selected = filteredOptions[highlightedIndex]; setInputValue(selected.label); setIsOpen(false); } break; case 'Escape': e.preventDefault(); setIsOpen(false); setHighlightedIndex(-1); break; } }, [isOpen, highlightedIndex, filteredOptions]); const handleOptionClick = (option: Option) => { setInputValue(option.label); setIsOpen(false); setHighlightedIndex(-1); inputRef.current?.focus(); }; // 提供activeDescendant的值 const activeDescendant = highlightedIndex >= 0 ? `option-${highlightedIndex}` : undefined; return ( <div> <input ref={inputRef} type="text" value={inputValue} onChange={handleInputChange} onKeyDown={handleKeyDown} role="combobox" aria-expanded={isOpen} aria-autocomplete="list" aria-haspopup="listbox" aria-controls="listbox-id" aria-activedescendant={activeDescendant} aria-label="请选择" /> {isOpen && ( <ul ref={listRef} id="listbox-id" role="listbox" tabIndex={-1} > {filteredOptions.map((option, index) => ( <li key={option.value} id={`option-${index}`} role="option" aria-selected={index === highlightedIndex} onClick={() => handleOptionClick(option)} style={{ background: index === highlightedIndex ? '#eee' : 'transparent', cursor: 'pointer' }} > {option.label} </li> ))} </ul> )} </div> ); }; export default AutoComplete; - 系统设计为一个实时协同文档编辑器设计前端架构。好回答应覆盖
- 核心需求:低延迟、一致性、离线支持、冲突解决
- 技术选型:CRDT或OT算法,WebSocket通信,操作转换
- 前端架构:文档模型、操作同步模块、本地状态与应用状态分离
- 数据流:用户操作生成操作对象 -> 发送到服务端 -> 接收确认/其他用户操作 -> 应用
- 扩展性:选区同步、光标协同、性能考虑(操作压缩、频率限制)
查看范例答案
实时协同文档编辑器前端架构需满足多用户同时编辑、低延迟同步、冲突解决、离线容错等核心需求。技术选型上,常用CRDT(无冲突复制数据类型)或OT(操作转换)算法,通常基于WebSocket实现实时通信。前端架构可分为几层:文档模型层(如使用immutable数据结构表示文档内容),操作管理模块(生成、应用、本地暂存操作),网络同步层(发送操作、接收服务端广播),以及UI层(渲染文档、光标/选区显示)。数据流为:用户输入触发操作生成(如插入字符),本地立即应用(乐观更新),同时操作通过网络发送到服务端;服务端使用CRDT/OT合并操作并广播给其他客户端;客户端收到远程操作后与本地待确认操作进行转换后应用,确保最终一致。关键难点是本地操作与远程操作的并发冲突解决,需维护操作顺序和回退机制。扩展方面需实现选区同步(通过发送光标位置/选区范围),以及性能优化如操作节流、压缩、Web Worker处理计算密集型操作。离线支持需在本地存储操作日志,重连后回放。
- 行为面讲一次你优化某个慢页面性能的经历。好回答应覆盖
- 性能问题诊断:使用Chrome DevTools的Performance面板、Lighthouse
- 具体优化手段:减少重排重绘、代码分割、懒加载、缓存
- 衡量指标:首次内容绘制(FCP)、交互时间(TTI)、总阻塞时间(TBT)
- 案例描述:虚拟列表优化长列表、图片懒加载、减少不必要的渲染
查看范例答案
在一次优化一个数据仪表盘页面的经历中,页面加载耗时约8秒,滚动和筛选时卡顿。首先用Chrome Performance面板录制,发现主线程长时间阻塞在JavaScript执行和大量DOM操作上,且存在频繁的重排。分析后发现三个主要问题:1)表格使用了原生DOM渲染了5000+行数据,每行多个单元格,导致渲染树庞大;2)筛选功能每次触发的重排和重绘;3)未对API响应数据进行缓存,每次切换筛选条件都重新请求。优化方案:将表格替换为虚拟列表(react-virtualized),只渲染可视区域约50行,大幅减少DOM节点。对筛选操作使用debounce延迟处理,并将筛选逻辑从渲染线程移到Web Worker(或至少避免同步计算)。引入数据缓存层(如SWR或React Query),相同请求直接使用缓存。结果:页面加载降至2秒,滚动帧率60fps,筛选响应几乎无延迟。教训是性能优化要优先定位瓶颈,避免过早优化。
- 行为面当你和设计师在 UX 取舍上有分歧时,你怎么处理?好回答应覆盖
- 使用STAR方法:Situation、Task、Action、Result
- 分歧点:设计师追求视觉美学,工程师关注实现成本和性能
- 处理方式:数据驱动决策(用户测试、A/B测试)、权衡利弊、给出替代方案
- 沟通技巧:用原型或数据说服,而非主观争论
- 最终结果:达成双赢,既保留设计核心又确保可行性
查看范例答案
在一次项目迭代中,设计师提出一个新功能:在数据表格行内嵌入一个复杂的动画图表以呈现趋势,认为能提升视觉吸引力。但作为前端工程师,我评估后认为该方案会导致大量重排和重绘,尤其在数据量较大时性能堪忧,且开发周期需额外2天。用STAR方法结构化沟通:Situation是用户需要快速浏览多行数据趋势,但现有表格过于单调;Task是平衡视觉效果与性能;Action是我通过原型实现了两种方案:设计师原方案和我提出的简化版静态迷你图表(如小折线图sparkline),并用Lighthouse跑分对比,显示静态方案加载快40%,滚动流畅度更高。同时我提供了替代方案:在用户悬停时再展开动画图表,避免初始渲染负担。设计师认可数据,最终选择了悬停展开的方案,既保留了动效惊喜感,又保障了核心性能。这次经历让我意识到,用数据和原型说话比主观争论更有效,而且积极提供替代方案能体现协作精神。
面试官重点考察什么
JavaScript 基础
闭包、事件循环、promise/async、原型链以及 `this` 绑定。
框架深度
React(或 Vue/Angular)的渲染、协调(reconciliation)、hooks 与状态管理。
CSS 与布局
Flexbox/grid、层叠规则、堆叠上下文与响应式设计。
性能
关键渲染路径、打包体积、懒加载与 Core Web Vitals。
无障碍
语义化 HTML、ARIA、键盘导航与屏幕阅读器支持。
如何准备
- 把渲染过程讲出来——面试官评的是你的推理过程,而不只是答案。
- 即使没被明确问到,也主动提及无障碍和性能。
- 练习在没有 IDE 自动补全的情况下,从头到尾写完一个组件。
常见问题
前端面试常考哪些编程题?
常见的有 DOM 操作、debounce/throttle 之类的工具函数,以及实现一个小型交互组件,比如自动补全或弹窗。
前端面试会考系统设计吗?
中高级会考——通常是偏前端的设计,比如信息流、设计系统或协同编辑器,而不是后端基础设施。
时间紧时如何快速准备前端面试?
夯实 JavaScript 基础,从零写几个组件,并做限时模拟面试,让自己能在压力下讲清楚思路。
用即时 AI 反馈刷前端工程师面试题
Offersly 会根据你的简历和目标岗位定制一场模拟面试,并从相关性、深度、清晰度和正确性四个维度为每个回答打分。