2025-09-04 高级前端面试准备指南
一、 技术深度与广度 (JavaScript/TypeScript/HTML/CSS)
JavaScript
异步编程: 手写 Promise.all、Promise.race、Promise.allSettled。并说明错误处理机制。
// 全部成功才算成功 Promise.myAll = function (promises) { return new Promise((resolve, reject) => { var result = new Array(promises.length).fill(null); var count = 0; if (promises.length === 0) { resolve(result); return; } promises.forEach((ele, index) => { Promise.resolve(ele) .then((res) => { count++; result[index] = res; if (count === promises.length) { resolve(result); } }) .catch((err) => { reject(err); }); }); }); }; // 第一个敲定的决定最终状态 Promise.myRace = function (promises) { return new Promise((resolve, reject) => { promises.forEach((ele) => { Promise.resolve(ele) .then((res) => { resolve(res); }) .catch((err) => { reject(err); }); }); }); }; // 等待所有敲定,永不失败,只报告所有最终状态 Promise.myAllSettled = function (promises) { return new Promise((resolve, reject) => { var result = new Array(promises.length); var count = 0; promises.forEach((ele, index) => { Promise.resolve(ele) .then((res) => { result[index] = { status: "resolved", value: res, }; }) .catch((err) => { result[index] = { status: "rejected", value: err, }; }) .finally(() => { count++; if (count == promises.length) { resolve(result); } }); }); }); };async/await 的原理是什么?和 Generator 的关系?
- async/await 是 Generator 和 Promise 的语法糖,其原理是基于 Generator 的暂停/恢复特性,由引擎内置的执行器来自动管理异步流程。
- 它吸收了 Generator 可以暂停执行的核心优势,但是摒弃了其复杂的迭代器概念,专门用于异步操作,返回值是更友好的 Promise。
- 相比直接使用 Generator,它最大的优势是内置自动执行,原生返回 Promise 以及可以使用 try/catch 来进行错误处理,让异步代码的书写阅读都非常接近同步代码。
- babel 等编译器在转换低版本 JS 时,其实就是将 async/await 转换成类似的 Generator 函数和一个自动执行器。
事件循环(Event Loop)机制能详细说一下吗?宏任务、微任务的执行顺序?Node.js 和浏览器的事件循环有什么区别? 可以概括为一个无限的循环,每一次循环称为一个 "tick",其步骤如下:
- 执行同步代码:从调用栈中执行最新的任务(函数),直到调用栈清空。
- 执行微任务:调用栈清空后,事件循环会立即依次执行微任务队列中的所有任务,直到微任务队列被清空。
- 渲染 (浏览器特有):如果需要渲染页面(浏览器环境),会在此刻进行 UI Render。
- 取一个宏任务:从宏任务队列中取出最先进入队列的一个任务(回调函数),将其放入调用栈中执行。
- 回到步骤 1:开始新一轮的事件循环。
总起:“事件循环是 JS 实现异步非阻塞的核心机制,它通过循环检查调用栈和任务队列来调度执行代码。” 流程:“它的基本流程是:执行同步代码(宏任务)-> 清空所有微任务 -> (浏览器可能渲染) -> 取一个宏任务执行 -> 开始新循环。” 区别:“浏览器和 Node.js 的事件循环主要区别在于架构和阶段划分。Node.js 基于 libuv 库,有 timers、poll、check 等更复杂的阶段。并且在 Node 中,微任务会在每个阶段之间执行,且 process.nextTick 优先级最高。” 强调关键:“最重要的概念是微任务优先于宏任务,且在一个宏任务结束后会清空整个微任务队列。”
核心原理: V8 引擎的垃圾回收机制是怎样的? 主要有以下几种方式,第一标记清除法,在函数里标记已使用的变量,在删除的时候就可以直接删除未被标记的变量。 引用计数法,简化理解就是变量对象不在被其他对象引用时,引用计数为 0,这个时候就会清除该对象变量。 总之,V8 的 GC 是一个高度优化的复杂系统,通过分代、多种算法结合以及增量标记等优化手段,在保证内存回收的前提下,尽可能地减少对主线程的影响,提升应用性能。
TypeScript:
- 为什么选择 TypeScript?它在大型项目中的优势是什么?
我选择 TS 主要是因为它的静态类型系统能为大型项目带来可维护性和稳健性的巨大提升。主要有以下几个点:
- 类型安全。它能在编译阶段就发现大部分类型错误,而不是等到运行时,大大减少了调试时间。
- 代码即文档。类型定义本身就是最好的文档,这对于大型项目和新成员入职至关重要,极大提高了代码的可读性。
- 强大的 IDE 支持。比如 vscode,可以智能补全,代码导航,安全重构,能够显著提高开发效率和体验
- 渐进式采用。TS 和 JS 可以共存,方便旧系统迁移,降低风险
- TS 倡导设计先行,有助于构建出更清晰,约束更好的架构 总结:对于大型,人数多的项目,使用 TS 会是一个比较好的点,虽然前期可能会增加一些培训学习的成本以及编写类型的时间,但是长远来看,优点远远大于缺点
- 泛型(Generics)的应用?如何设计一个灵活的泛型函数/接口? 泛型核心是参数化类型,它允许我们编写灵活,可复用的组件,这些组件可以与多种类型一起工作,而不会丢失类型信息。它主要应用场景包括:创建通用函数/接口,使用 extends 约束泛型类型以确保安全,还可以用 keyof 来限制操作对象属性
- 类型守卫(Type Guards)、类型别名(Type)与接口(Interface)的区别? 类型守卫主要是 in,typeof,instanceof 操作符,是运行时的一种检查。类型别名主要是给类型创建另外一个名字/快捷方式等。接口专门用于声明 JS 对象,它描述了一个对象应该有哪些属性,方法,核心思想是定义契约。 “Interface 的核心优势是声明合并和被类实现,更适合定义对象形状和面向对象开发。” “Type 的核心优势是能轻松定义联合、交叉、元组等复杂类型,更灵活,适用于各种类型操作。”
二、 框架与生态 (React/Vue 为主)
React :
原理: Virtual DOM 和 Diff 算法的原理?Key 的作用是什么? 虚拟 dom 是一个轻量级的 JS 对象,它是真实 dom 的抽象表示。react 使用它来模拟真实的 dom 树,如何工作呢:
- 初始渲染:当组件首次渲染时,react 会根据组件的 render 方法创建一个完整的 virtual DOM 树。
- 状态更新:当组件的状态或者接受的属性发生变化时,组件会重新渲染,生成一颗新的 virtual DOM 树
- 对比差异:react 不会直接操作真实 DOM,而是会将心的 virtual DOM 树和上一次的旧树进行精确的比较,找出两者之间的差异。这个过程就是“Diffing”。
- 局部更新(patching):计算出差异后,react 会将这些最小化的变更批量应用到真实的 DOM 上。
diff 算法核心:
- 跨层级比较:如果两个元素的类型不同,react 会直接销毁整个旧的子树并重建新的子树。Tree Diff 会忽略跨层级的移动操作,通常只进行同层比较
- 相同类型的组件:如果两个组件的类型相同,react 会保留组件实例,只更新其变化的 props,并递归比较其子节点
- 列表比较(key 的作用):当比较子节点列表时,react 会使用 key 属性来识别哪些元素是新增,移动或删除,这也是 diff 算法处理列表最为关键的一环。
React 合成事件(Synthetic Event)的原理?与原生事件的区别? react 合成事件是一套为了性能,兼容性和未来扩展性而设计的浏览器原生事件的跨浏览器包装器。其核心原理是事件委托,通过在根部元素监听所有事件,再由 react 进行统一分发和处理。开发者在使用时几乎感知不到差异,反而获得了更好的开发体验。 为什么 React 要设计合成事件?
- 跨浏览器一致性:这是最主要的原因。React 通过合成事件抹平了不同浏览器在事件模型、API 上的差异,让开发者无需再写繁琐的兼容性代码。
- 性能优化:利用事件委托,React 避免了在每个 DOM 节点上直接绑定大量事件监听器,减少了内存开销和管理成本。
- 赋能未来:这套抽象层为 React 实现更高级的特性(如异步渲染、并发模式)打下了基础。React 可以完全控制事件的触发和处理时机。
Fiber 架构是什么?解决了什么问题? 是一种数据结构,Fiber 是一个 JS 对象,代表了一个工作单元,是 virtual DOM 的进化版,包含了比之前更丰富的组件信息。为了解决之前版本 CPU 瓶颈,一些耗时任务会阻塞渲染(页面卡死),或者无关紧要的渲染阻塞了一些重要更新。对此 Fiber 引入了优先级调度,用户操作,动画属于高优先级,数据拉取,大型列表渲染则属于低优先级。高优先级的更新可以插队,中断低优先级渲染,然后更新到用户电脑上,从而让用户感到应用流畅不卡顿。 一言以蔽之:Fiber 架构通过将渲染工作拆分成可中断、可优先级调度的小任务,解决了大规模数据更新时的界面卡顿问题,并为 React 的并发未来奠定了坚实的基础。
Hooks: useState 和 useRef 的区别? usestate 用于管理需要触发组件重新渲染的数据 useRef 用于管理不需要触发重新渲染的,在组件整个生命周期内保持不变的可变值,或者用于直接访问 DOM 元素。
useEffect 和 useLayoutEffect 的区别?依赖数组的作用? useEffect 是异步的,它会在浏览器完成绘制之后执行,不会阻塞浏览器的渲染过程。 useLayoutEffect 是同步的,它会在所有的 DOM 变更之后,但浏览器绘制之前同步执行,会阻塞浏览器的绘制。 依赖数组的核心作用:
- 性能优化:通过跳过不必要的副作用执行,避免每次渲染都做昂贵的操作(网络请求,大量计算)
- 避免无限循环:如果 Effect 内部会修改依赖项,但没有正确声明依赖,会导致 Effect 不断执行
- 保证状态总是最新的:正确声明依赖能确保 Effect 回调函数中使用的 state 和 props 是最新的。 这里可以使用 eslint 里的加强依赖,来让代码自动提示需要添加的依赖项
如何自定义一个 Hook?它解决了什么逻辑复用问题?
解决“包装地狱”:直接在组件内部调用 Hook,不会增加额外的组件层级,保持了组件树的扁平化和清晰。
function MyComponent() { const theme = useTheme(); // 没有嵌套! const user = useAuth(); // 没有嵌套! const data = useDataFetch(); // 没有嵌套! // ... 使用 theme, user, data }聚合相关逻辑:将与同一功能相关的所有逻辑(state、effect、事件处理等)集中管理在一个地方,而不是分散在各个生命周期里。这大大提高了代码的可维护性和可读性。
清晰的数据流:Hook 的输入(参数)和输出(返回值)非常明确,所有传递到组件的数据都是一目了然的,避免了 HOC 可能带来的隐式 props 注入问题。
易于组合和创建:Hook 就是普通的函数,可以轻松组合多个 Hook 来创建更强大的新 Hook,逻辑复用变得非常简单和灵活。
自定义 Hook 使你可以再不改变组件层级结构的情况下,复用状态逻辑,从而实现了关注点分离,让代码更清晰,更易于测试和维护
// 自定义本地存储的Hook import { useState } from "react"; // 1. 创建自定义 Hook:useLocalStorage function useLocalStorage<T>(key: string, initialValue: T) { let storedValue; try { const item = window.localStorage.getItem(key); storedValue = item ? JSON.parse(item) : initialValue; } catch (error) { storedValue = initialValue; } const [storage, setStorage] = useState(storedValue); // 定义一个更新函数 const setValue = (value: T) => { try { let temp = value; if (typeof value === "function") { temp = value(storage); } setStorage(temp); window.localStorage.setItem(key, JSON.stringify(value)); } catch (error) { console.error( "Error setting localStorage key “" + key + "”: ", error ); } }; // 4. 返回组件需要的数据和更新函数 return [storage, setValue]; } export default useLocalStorage; // 使用 const [price, setPrice] = useLocalStorage("price", "");Hooks 的使用规则是什么?为什么会有这些规则?
- 只在最顶层使用 Hook
- 不要在循环,条件判断或嵌套函数中调用 Hook
- 必须在 React 函数组件或自定义 Hook 的顶层无条件调用
- 只在 React 函数中调用 Hook
- 在 React 的函数组件中调用 Hook 在自定义 Hook 中调用其他 Hook
React 依赖于 Hook 的调用顺序,因为内部维护了一个 Hook 的列表,用来存储组件内所有 Hook 的状态。React 完全依赖 Hook 的调用顺序来追踪和管理每个 Hook 对应的状态。 如果不按照这种规则来,则会导致 Hook 状态表混乱,数据胡乱更新。
性能优化: 如何分析和优化 React 应用性能?(React DevTools, Profiler API) React.memo, useMemo, useCallback 的使用场景和区别? 不要滥用。只有当以下两种情况同时满足时才使用: 1. 计算/创建成本很高。 2. 该值/函数作为 props 传递给被 memo 包裹的子组件,或者它是其他 useEffect/useMemo 的依赖项。 React.memo 是一个高阶组件,用于优化整个组件的重渲染。它记忆的是组件的渲染结果。 useMemo 是一个 Hook,用于优化组件内部的昂贵计算。它记忆的是一个值。
如何避免不必要的重渲染?
- 使用 React.memo 进行组件记忆(Memoization)
- 使用 useMemo 和 useCallback 保持引用稳定
- 谨慎使用 Context API
- 使用不可变数据
我的优化流程通常是:首先审视状态设计,尝试通过状态下沉来缩小渲染范围 -> 然后使用 Profiler 找到渲染瓶颈 -> 对昂贵的、Props 稳定的子组件使用 React.memo -> 最后,为了确保 memo 生效,使用 useCallback 和 useMemo 来稳定作为 Props 传递的函数和对象引用。 我始终认为,良好的组件和状态结构设计是最好的性能优化,它远比事后到处添加 memo 要有效和优雅。
三、 工程化与架构
构建工具: Webpack 和 Vite 的原理和对比?为什么 Vite 更快?
为什么 Vite 更快,因为:
- 开发服务器启动快
- 根本原因还是 Vite 不需打包源代码。直接将 index.html 和模块的原始 ESM 请求交给浏览器,让浏览器自己执行模块加载和链接。省去了最耗时的打包步骤
- 依赖预构建使用 esbuild
- Vite 使用 esbuild 来处理第三方依赖的预构建,其速度比基于 JS 的打包器快数十倍
- 按需编译
- 浏览器请求什么模块,Vite 才编译什么模块。极大减少了不必要的编译工作,与项目大小解耦。而 webpack 则是全量编译
- 高效热更新
- 基于 ESM 的 HMR 机制更加轻量,只需要重新请求单个模块,而不是重新构建一个包含该模块的 chunk,更新速度自己更快。
Webpack 的构建流程(Loader, Plugin 的作用和编写)?如何优化构建速度和产出物?
如何进行代码分割(Code Splitting)和懒加载?
- 基于路由的分割
- 每个页面均使用 import()语法
- 基于组件的分割
- 对于不是立即需要的组件,可以进行分割,例如模态框,选项卡,折叠组件等。
- 配置多个入口
- 防止重复
- 使用 SplitChunksPlugin 将 node_modules 打包成一个单独的 chunk,然后缓存起来。
- 开发服务器启动快
性能优化 (综合性问题): 从输入 URL 到页面展示的整个过程中,可以从哪些方面进行性能优化?(这是一个经典问题,可以从前端、网络、服务端等多个角度回答) 首屏加载时间(FCP, LCP)如何优化? 如何监控线上的性能指标?(Web Vitals)
- 前端安全: XSS(跨站脚本攻击)的原理、分类(反射型、存储型、DOM 型)和防范措施? CSRF(跨站请求伪造)的原理和防范措施?(SameSite Cookie, Token 验证等)
架构设计: 如何设计一个前端组件库?需要考虑哪些方面?(样式方案、按需加载、文档、测试等) “设计一个前端组件库是一个系统工程,我会从以下几个核心方面来考虑:
首先明确定位与设计原则:包括目标用户、设计风格(如遵循 Material Design 或 Ant Design)以及 API 设计要保证简单性和一致性。
关键技术选型:
- 我会选择 React + TypeScript 作为技术基础,以保证类型的安全和开发体验。
- 样式方案上,权衡后我会选择 Sass/Less 这类预处理器,通过在构建时输出 CSS 文件来获得更好的性能和主题定制能力,同时用 BEM 规范解决样式隔离问题。
工程化构建:
- 使用 Rollup 进行打包,分别输出 ES Module、CommonJS 和 UMD 格式,以适配不同环境。
- 核心目标是支持 Tree Shaking 和按需加载。我会开发一个 Babel 插件,让用户能通过简单配置自动实现按需引入,极大优化产物体积。
开发与质量保障:
- 使用 Storybook 进行组件的交互式开发和展示,它将作为我们的开发环境和文档预览环境。
- 测试策略上,会结合 Jest(单元测试)、React Testing Library(组件测试)和 Cypress(E2E 测试)来保证组件的质量和稳定性。
文档与发布:
- 文档会使用 dumi 或 VitePress 来生成完整的文档站点,包含示例、API 和指南。
- 发布流程会自动化,并严格遵守语义化版本(SemVer),使用 changesets 等工具来管理版本号和生成变更日志。
四、 项目经验与软实力
- 项目介绍: 让你介绍一个最有挑战的项目。 STAR 原则: Situation (背景),Task (任务),Action (行动),Result (结果)。 重点突出:你遇到的最大技术挑战是什么?你是如何分析和解决的?(考察解决问题的能力) 项目的技术选型是怎么做的?为什么选择这个技术栈?(考察架构和决策能力)
- 团队协作: 你是如何带领或影响团队的?如何做代码审查(Code Review)? 如何保证项目的代码质量和可维护性?(ESLint, Prettier, 单元测试, Git 规范等)
- 学习与总结: 你是如何保持技术学习的?最近关注哪些前端新技术?
五、 coding 能力
- 算法与数据结构: 难度一般在 LeetCode 中等难度,常见于数组、字符串、链表、二叉树相关题目。重点考察思路清晰、代码健壮。
手写代码: 场景题:比如用 React 实现一个无限滚动列表,并考虑性能优化。 思路分析
- 使用 Intersection Observer API 检测滚动触底,实现无限加载
- 实现虚拟化渲染,只渲染可视区域内的列表项
- 添加数据缓存机制,避免重复请求
- 使用 React.memo 和 useCallback 优化组件性能
- 添加加载状态和错误处理
性能优化措施
- 虚拟化渲染:只渲染可视区域内的列表项,大幅减少 DOM 节点数量
- 数据缓存:使用 Map 缓存已加载的数据,避免重复请求
- 使用 React.memo:避免列表项不必要的重渲染
- 使用 useCallback:缓存回调函数,避免不必要的函数重建
- Intersection Observer:使用现代 API 检测滚动触底,性能优于滚动事件监听
- 缓冲区域:在可视区域上下方渲染额外的缓冲项,减少滚动时的空白
其中 IntersectionObserver 是一个现代的浏览器 API,用于异步观察目标元素与其祖先元素或视口(viewport)的交叉状态(intersection)。它非常适合实现懒加载、无限滚动、广告曝光统计等功能。
附录
| 特性 | Webpack | Vite (开发环境) |
|---|---|---|
| 核心原理 | 打包(Bundle):先打包,后服务 | 按需编译(On-demand):先服务,后编译 |
| 启动速度 | 慢。需要先构建完整的依赖图并打包,项目越大越慢。 | 极快。无需打包源码,服务器秒开。 |
| 热更新 | 慢。修改文件后,需要重新构建受影响的部分 bundle。 | 快。基于 ESM,仅失联并重新请求改变的模块,边界更小。 |
| 生产构建 | 使用 Webpack 自身,功能强大且插件生态极其丰富。 | 使用 Rollup,配置简单,输出质量高。 |
| 生态成熟度 | 极高。经过多年发展,插件和 Loader 生态非常完善,能处理各种复杂场景。 | 快速增长。生态已足够支撑主流开发,但一些非常小众的场景可能不如 Webpack。 |
| 适用场景 | 任何规模的项目,尤其是历史悠久、配置复杂的大型企业级项目。 | 尤其中小型、模块化项目(Vue/React/TS),追求极致开发体验。 |