前端面试体系
一、高频手写题
手写题是前端面试的"入场券",考查 coding 基本功和语言特性理解。
1.1 Promise.all
function promiseAll(promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises)) {
return reject(new TypeError('argument must be an array'))
}
let count = 0
const results = []
if (promises.length === 0) return resolve(results)
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i])
.then(value => {
results[i] = value
count++
if (count === promises.length) resolve(results)
})
.catch(reject)
}
})
}关键点:维护结果数组保持顺序、计数器追踪完成状态、空数组直接 resolve。
1.2 防抖(Debounce)
function debounce(fn, delay = 300) {
let timer = null
return function (...args) {
clearTimeout(timer)
timer = setTimeout(() => fn.apply(this, args), delay)
}
}适用场景:搜索输入、窗口 resize、按钮防重复提交。
1.3 节流(Throttle)
function throttle(fn, interval = 300) {
let last = 0
return function (...args) {
const now = Date.now()
if (now - last >= interval) {
last = now
fn.apply(this, args)
}
}
}适用场景:滚动事件、鼠标移动、拖拽。
1.4 深拷贝
function deepClone(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj
if (obj instanceof Date) return new Date(obj)
if (obj instanceof RegExp) return new RegExp(obj)
if (hash.has(obj)) return hash.get(obj)
const clone = Array.isArray(obj) ? [] : {}
hash.set(obj, clone)
for (const key of Reflect.ownKeys(obj)) {
clone[key] = deepClone(obj[key], hash)
}
return clone
}关键点:循环引用(WeakMap)、特殊对象(Date/RegExp)、Symbol 属性。
1.5 数组扁平化
// reduce 实现
function flatten(arr) {
return arr.reduce((acc, cur) =>
acc.concat(Array.isArray(cur) ? flatten(cur) : cur), []
)
}
// 指定深度
function flattenDepth(arr, depth = 1) {
return arr.reduce((acc, cur) =>
acc.concat(depth > 0 && Array.isArray(cur)
? flattenDepth(cur, depth - 1) : cur), []
)
}1.6 函数柯里化
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args)
}
return function (...args2) {
return curried.apply(this, args.concat(args2))
}
}
}1.7 继承(寄生组合式)
function inherit(child, parent) {
child.prototype = Object.create(parent.prototype)
child.prototype.constructor = child
Object.setPrototypeOf(child, parent)
}
function Parent(name) {
this.name = name
}
Parent.prototype.say = function () {
console.log(this.name)
}
function Child(name, age) {
Parent.call(this, name)
this.age = age
}
inherit(Child, Parent)1.8 发布-订阅模式(EventEmitter)
class EventEmitter {
constructor() {
this.events = {}
}
on(event, fn) {
(this.events[event] || (this.events[event] = [])).push(fn)
return this
}
emit(event, ...args) {
(this.events[event] || []).forEach(fn => fn(...args))
return this
}
off(event, fn) {
const cbs = this.events[event]
if (!cbs) return this
if (!fn) { delete this.events[event]; return this }
this.events[event] = cbs.filter(cb => cb !== fn)
return this
}
once(event, fn) {
const wrapper = (...args) => {
fn(...args)
this.off(event, wrapper)
}
this.on(event, wrapper)
return this
}
}1.9 JSONP 封装
function jsonp({ url, params, callbackName }) {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
window[callbackName] = function (data) {
resolve(data)
delete window[callbackName]
document.body.removeChild(script)
}
const queryStr = Object.entries(params)
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
.join('&')
script.src = `${url}?${queryStr}&callback=${callbackName}`
script.onerror = reject
document.body.appendChild(script)
})
}1.10 AJAX 封装
function ajax({ url, method = 'GET', data = null, headers = {} }) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open(method, url)
Object.entries(headers).forEach(([k, v]) => xhr.setRequestHeader(k, v))
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) return
if (xhr.status >= 200 && xhr.status < 300) {
resolve({ data: xhr.responseText, status: xhr.status })
} else {
reject(new Error(xhr.statusText))
}
}
xhr.onerror = reject
xhr.send(data)
})
}1.11 instanceof
function myInstanceof(left, right) {
let proto = Object.getPrototypeOf(left)
const prototype = right.prototype
while (proto) {
if (proto === prototype) return true
proto = Object.getPrototypeOf(proto)
}
return false
}1.12 call / apply / bind
Function.prototype.myCall = function (ctx, ...args) {
ctx = ctx ?? window
const key = Symbol()
ctx[key] = this
const result = ctx[key](...args)
delete ctx[key]
return result
}
Function.prototype.myApply = function (ctx, args) {
ctx = ctx ?? window
const key = Symbol()
ctx[key] = this
const result = ctx[key](...(args || []))
delete ctx[key]
return result
}
Function.prototype.myBind = function (ctx, ...args) {
const fn = this
return function (...args2) {
return fn.myCall(ctx, ...args, ...args2)
}
}1.13 排序算法
// 快速排序
function quickSort(arr) {
if (arr.length <= 1) return arr
const pivot = arr[0]
const left = [], right = []
for (let i = 1; i < arr.length; i++) {
arr[i] < pivot ? left.push(arr[i]) : right.push(arr[i])
}
return [...quickSort(left), pivot, ...quickSort(right)]
}
// 归并排序
function mergeSort(arr) {
if (arr.length <= 1) return arr
const mid = Math.floor(arr.length / 2)
const left = mergeSort(arr.slice(0, mid))
const right = mergeSort(arr.slice(mid))
return merge(left, right)
}
function merge(left, right) {
const result = []
while (left.length && right.length) {
result.push(left[0] <= right[0] ? left.shift() : right.shift())
}
return [...result, ...left, ...right]
}二、原理问答
2.1 事件循环(Event Loop)
JavaScript 是单线程语言,通过事件循环机制实现异步非阻塞。
宏任务(MacroTask):script 整体代码、setTimeout、setInterval、I/O、UI rendering、setImmediate(Node)。
微任务(MicroTask):Promise.then/catch/finally、MutationObserver、queueMicrotask、process.nextTick(Node)。
执行顺序:
- 执行当前宏任务(同步代码)
- 清空微任务队列
- 执行下一个宏任务
经典题目:
console.log(1)
setTimeout(() => console.log(2))
Promise.resolve().then(() => console.log(3))
console.log(4)
// 输出:1 4 3 22.2 闭包(Closure)
闭包是指函数能够访问其外部作用域变量的能力——即使外部函数已经执行完毕。
用途:数据私有化、函数柯里化、模块模式、防抖节流。
内存问题:闭包会持有外部变量的引用,若使用不当可导致内存泄漏。需在不需要时解除引用。
2.3 原型链
每个函数都有 prototype 属性,每个对象都有 __proto__ 属性(即 [[Prototype]])。原型链实现了属性查找和继承。
obj -> Object.prototype -> null
func -> Function.prototype -> Object.prototype -> null
arr -> Array.prototype -> Object.prototype -> nullhasOwnProperty 可以区分自身属性和原型链属性。
2.4 Virtual DOM
Virtual DOM 是真实 DOM 的 JavaScript 对象表示。操作 JS 对象比操作 DOM 快得多(DOM 操作涉及重排重绘)。
流程:
- 数据变更 → 生成新 VNode
- diff 新旧 VNode 得到 patch
- 批量执行 DOM 操作
优势:跨平台(同一 VNode 可渲染到不同平台)、声明式 UI、批量更新。
2.5 diff 算法
React 的 diff 策略(O(n) 复杂度):
- 同层比较:只对同层级节点比较,不跨层
- 类型判断:节点类型不同则直接替换
- key 优化:通过 key 识别可复用的节点
Vue 的 diff 策略类似,但采用双端指针比较(头-头、尾-尾、头-尾、尾-头),对列表重排序更高效。
2.6 响应式原理
Vue 2:Object.defineProperty 递归劫持 getter/setter,结合依赖收集(Dep + Watcher)。
Vue 3:Proxy 直接代理整个对象,惰性处理深层响应,性能更好。
React:不可变数据 + 状态提升,通过 setState 触发 reconcile 流程。
2.7 HTTP 缓存
| 类型 | 头部 | 说明 |
|---|---|---|
| 强制缓存 | Cache-Control: max-age=3600 | 过期前直接使用缓存,不请求 |
| 协商缓存 | Last-Modified / If-Modified-Since | 基于时间戳 |
| 协商缓存 | ETag / If-None-Match | 基于内容哈希,优先级高于时间戳 |
缓存策略:频繁变动的资源用 no-cache(协商缓存),静态资源用 max-age=31536000 + 文件名哈希。
2.8 Webpack 构建流程
- 初始化:读取配置,创建 Compiler 对象
- 编译:从入口文件递归解析依赖,调用 Loader 转换模块
- 构建:生成 Chunk 依赖图,调用 Plugin 做自定义处理
- 输出:将 Chunk 渲染成文件,写入 output 目录
核心概念:Entry、Output、Loader、Plugin、Chunk、Module。
2.9 模块化
| 规范 | 语法 | 加载方式 | 特点 |
|---|---|---|---|
| ES Module | import / export | 静态(编译时) | 支持 tree-shaking |
| CommonJS | require / module.exports | 动态(运行时) | 值拷贝 |
| AMD | define / require | 异步 | 浏览器端 |
| UMD | 兼容 CJS + AMD + 全局 | 通用 | 兼容性 |
2.10 CORS(跨域资源共享)
浏览器同源策略的例外机制。通过 HTTP 头部控制跨域访问:
简单请求:直接发请求,服务端返回 Access-Control-Allow-Origin。
预检请求(OPTIONS):请求包含自定义头部或非简单 Content-Type 时先发 OPTIONS,服务端返回允许的 methods/headers/origin。
常见头部:Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers、Access-Control-Allow-Credentials。
三、项目亮点
3.1 性能优化
- 加载性能:Code Splitting、懒加载、资源预加载/预连接、CDN、Gzip/Brotli
- 运行时性能:虚拟列表、Web Worker 处理大数据、requestAnimationFrame 动画、避免强制回流
- 渲染性能:合成层优化、CSS contain、图片懒加载、字体优化(font-display)
- 缓存策略:Service Worker 离线缓存、HTTP 强缓存 + 版本哈希
3.2 架构设计
- 微前端:qiankun / Module Federation 实现多团队独立部署
- 组件库:原子设计方法论、按需加载、主题定制
- 状态管理:单向数据流、模块化 Store、中间件链
- 分层架构:UI 层 → 业务逻辑层 → 数据层 → 基础设施层
3.3 工程化
- Monorepo:pnpm workspace + Changesets 管理多包
- CI/CD:GitHub Actions / GitLab CI 自动化流水线
- Code Quality:ESLint + Prettier + Husky + commitlint + lint-staged
- 自动化测试:Jest + Testing Library + Cypress + MSW 模拟
3.4 难点突破
典型场景与方案:
| 难点 | 解决方案 |
|---|---|
| 海量数据列表 | 虚拟滚动(如 react-window / vue-virtual-scroller) |
| 复杂表单联动 | 配置式表单 + 联动引擎 + 异步校验 |
| 大文件上传 | 分片上传 + 断点续传 + 秒传(hash 校验) |
| 实时协作 | CRDT / OT 算法 + WebSocket 连接管理 |
| 图表渲染卡顿 | Canvas 渲染 + LOD(细节层级) + Web Worker |
3.5 质量保障
- 单元测试:覆盖工具函数/Hooks/组件状态
- 组件测试:Testing Library 用户行为视角
- E2E 测试:Cypress/Playwright 核心流程覆盖
- 监控告警:Sentry 错误追踪 + 性能埋点(FP/FCP/LCP/CLS)+ 自定义告警规则
3.6 数据驱动
- AB 测试:特征标识 + 实验分组 + 指标回收
- 埋点体系:事件埋点 + 曝光埋点 + 时长埋点
- 数据看板:实时数据可视化 + 下钻分析
3.7 面试话术模板
STAR 法则结构:
S(情境):项目背景,团队规模,业务目标 T(任务):你的角色,负责的模块,面临的挑战 A(行动):具体做了什么,技术选型依据,实现方案 R(结果):量化数据(性能提升百分比,人效提升,Bug 率下降)
示例:
"在电商中台项目中(S),我负责商品详情页的性能优化(T)。通过分析 Lighthouse 报告发现首屏加载超 5 秒,主要瓶颈是图片体积过大和冗余 JS。我实施了图片 WebP 转换 + 懒加载 + 路由级 Code Splitting(A),最终首屏时间降至 1.8 秒,LCP 改善 64%,转化率提升 12%(R)。"
四、系统设计
4.1 组件设计
原则:
- 单一职责:一个组件只做一件事
- 接口最小化:props 尽量少,提供合理默认值
- 组合优于继承:通过 children/slots 实现扩展
- 受控 vs 非受控:对外提供受控模式,对内支持非受控
通用组件设计模板:
interface SelectProps {
value?: string
defaultValue?: string
onChange?: (value: string) => void
options: Array<{ label: string; value: string }>
disabled?: boolean
placeholder?: string
}4.2 状态管理设计
分层模型:
视图层(React/Vue Components)
↕
业务状态层(Zustand/Pinia — 领域模型)
↕
服务层(API Client + 数据转换)
↕
基础设施层(LocalStorage/IndexedDB/WebSocket)选型建议:
- 小型项目:useState + useContext / provide-inject
- 中型项目:Zustand / Pinia
- 大型项目:Redux Toolkit / Vuex + 领域模型分层
4.3 权限系统
前端权限模型:
- 路由权限:动态路由表,根据角色/权限过滤
- 组件权限:自定义指令/directive 控制显隐
- API 权限:请求拦截器校验 token + 服务端鉴权
典型流程:
登录 → 获取 token → 存储 token → 请求用户权限信息
→ 动态生成可访问路由 → 渲染对应菜单和页面4.4 国际化方案
- i18n 库:react-i18next / vue-i18n
- 翻译管理:Lokalise / Crowdin 协作平台
- 自动提取:babel-plugin 提取代码中的翻译 key
- 动态加载:按语言包懒加载,避免首屏体积过大
- SEO:多语言 URL 路径 + hreflang 标签
4.5 SSR 方案
选型对比:
| 方案 | 框架 | 适合场景 |
|---|---|---|
| Next.js | React | 内容型/电商/中后台 |
| Nuxt 3 | Vue | 内容型/营销页 |
| Razzle / Vite SSR | 通用 | 自定义需求 |
| Astro | 多框架 | 内容密集型 |
关键考量:首屏速度、SEO 需求、TTI 优化、同构代码维护成本。
4.6 监控体系
三层监控模型:
- 性能监控(RUM):FP / FCP / LCP / CLS / TTFB / FID
- 错误监控:JS 运行时错误、Promise 未捕获、资源加载失败、API 异常
- 业务监控:PV/UV、页面停留时长、转化漏斗、核心链路成功率
工具栈:Sentry + Web Vitals + 自定义埋点 SDK + 数据可视化看板。
4.7 低代码平台设计
核心架构:
编辑器层(拖拽 + 属性面板 + 大纲树)
↕
渲染引擎层(JSON → 组件树 → VNode)
↕
组件库层(原子组件 → 业务组件 → 模版)
↕
出码层(JSON Schema → 源代码 / 部署包)关键设计:组件协议标准化、页面描述 DSL、扩展机制(插件/自定义组件)、版本管理。
五、V8 引擎
5.1 编译流水线
V8 采用 JIT(Just-In-Time)编译策略:
源代码 → Parser(词法/语法分析) → AST → Ignition(解释器)
↓
TurboFan(优化编译器 → 机器码)- Ignition:解释执行字节码,启动快
- TurboFan:对"热代码"(Hot Code)进行优化编译,生成高性能机器码
- 去优化(Deoptimization):当优化假设不成立时回退到字节码
5.2 隐藏类(Hidden Class)
JavaScript 是动态类型语言,V8 通过隐藏类实现类似静态类型的属性访问优化。
function Point(x, y) {
this.x = x // 创建隐藏类 C1
this.y = y // 在 C1 基础上扩展为 C2
}
const p1 = new Point(1, 2)
const p2 = new Point(3, 4) // 复用隐x藏类
p1.z = 5 // 创建新隐藏类 C3,p2 不受影响优化建议:在构造函数中一次性初始化所有属性、保持属性顺序一致、避免动态添加/删除属性。
5.3 内联缓存(Inline Cache / IC)
V8 在字节码执行过程中,对属性访问(load/store)生成 IC 状态机。当同一位置多次访问相同隐藏类的属性时,直接"内联"为快速路径,跳过查找过程。
未初始化 → 单态(Monomorphic) → 多态(Polymorphic) → 巨态(Megamorphic)优化建议:尽量避免同一函数处理多种类型的对象。
5.4 内存管理
V8 将堆内存分为不同的区域(Space):
| 区域 | 用途 | GC 频率 |
|---|---|---|
| New Space(新生代) | 存活短的小对象 | 频繁(Scavenger) |
| Old Space(老生代) | 存活长的对象 | 低频(Mark-Sweep / Mark-Compact) |
| Code Space | 编译后的机器码 | 特殊管理 |
| Large Object Space | 大对象 | 特殊管理 |
5.5 垃圾回收
分代式 GC:
- 新生代(Scavenge):Cheney 算法,From-Space → To-Space 复制,晋升条件(两次存活 / 空间不足)
- 老生代(Mark-Sweep-Compact):三色标记(白/灰/黑)、增量标记、惰性清理、必要时压缩
V8 优化:
- 并行标记:多线程并行标记
- 并发标记:主线程执行 JS,辅助线程并发标记
- 增量标记:将标记过程拆分成小步,穿插在 JS 执行间隙
5.6 优化陷阱
以下代码模式会导致 TurboFan 放弃优化(去优化/bailout):
- try-catch / try-finally:优化器无法精确推断控制流
- with / eval:破坏作用域链静态分析
- 动态添加/删除属性:导致隐藏类频繁变化
- 多态属性访问:同一函数处理超过 4 种不同隐藏类的对象
- arguments 对象非标准使用:如
arguments.push - 位运算溢出:32 位整数溢出的位运算
性能分析工具:node --trace-opt、node --trace-deopt、Chrome DevTools 的 Performance + V8 内部面板。
六、面试技巧
6.1 简历
- 一页原则:重点突出,删除无关信息
- 量化成果:用数字说话("优化后性能提升 50%" 优于 "做了性能优化")
- 技术栈:标注熟练程度(精通/熟练/了解),避免"精通"过于泛滥
- 项目描述:每个项目按"背景-职责-难点-成果"四段式描述
6.2 自我介绍
黄金模板(1-2 分钟):
"面试官好,我是 XXX,有 X 年前端开发经验。近段时间主要在 XX 业务线负责 XX 方向的开发,主导了 XX 项目的重构/落地。技术栈以 React/TypeScript 为主,对性能优化和工程化有比较深入的实践经验。业余时间我喜欢写技术博客和参与开源项目,累计发表了 XX 篇文章。希望能通过这次面试加入贵团队,跟优秀的同学一起成长。"
6.3 STAR 法则
回答"介绍一个做过的项目"时采用:
- S(情境):项目背景、团队规模、时间周期
- T(任务):你的职责和核心目标
- A(行动):技术选型、方案设计、具体实施
- R(结果):量化收益、个人成长
6.4 谈薪技巧
- 调研市场价:BOSS 直聘/牛客/脉脉了解行情
- 给出范围:范围的下限是你的底线,上限是期望值+20%
- 综合考量:base 薪资、股票/期权、年终奖、福利(餐补/房补/补充医疗)
- 不要先出价:让 HR 先给出范围
- 有底气:手上有其他 offer 时谈判空间更大
6.5 反问问题
高质量反问:
- "团队目前的技术栈是什么?未来有迁移或升级计划吗?"
- "对前端岗位的短期期望和长期发展路径是怎样的?"
- "团队目前在工程化和质量保障方面做了哪些建设?"
- "代码评审流程是怎样的?"
- "业务的下一步规划是什么?技术上的主要挑战有哪些?"
避免问:百度能查到的问题、薪资福利(HR 面再问)、过于宽泛的问题。
6.6 线上面试准备
- 环境:安静/光线充足/网络稳定/备好手机热点
- 设备:摄像头正常工作、耳机减少回音、提前测试共享屏幕
- 资料:简历/作品集/笔记放在手边
- 着装:商务休闲即可,避免睡衣
- 心态:把面试看作技术交流,减轻紧张感
七、面试真题分类汇总
7.1 HTML/CSS
| 题目 | 考点 |
|---|---|
| BFC 是什么?如何触发? | 块级格式化上下文,常见触发方式 |
| 垂直居中方案 | flex/grid/absolute+transform/table |
| 盒模型区别 | content-box vs border-box 的计算方式 |
| 三栏布局 | flex/grid/圣杯/双飞翼 |
| CSS 选择器优先级 | !important > 行内 > ID > 类/属性/伪类 > 元素 |
| 移动端适配 | rem/vw/vh / 媒体查询 / flexible 方案 |
7.2 JavaScript
| 题目 | 考点 |
|---|---|
| 变量提升与暂时性死区 | var/let/const 的区别 |
| this 指向 | 默认绑定/隐式绑定/显式绑定/new 绑定/箭头函数 |
| 事件委托 | 利用冒泡机制统一处理子元素事件 |
| Promise 链式调用 | then 返回新 Promise 的特性 |
| async/await 错误处理 | try-catch 或 .catch 链 |
| Map vs WeakMap | 弱引用与内存回收 |
7.3 React
| 题目 | 考点 |
|---|---|
| Fiber 架构 | 可中断渲染、优先级调度、并发模式 |
| Hooks 规则 | 顶层调用、无条件执行 |
| useEffect 闭包陷阱 | stale closure 产生原因与解决 |
| useMemo/useCallback | 性能优化 vs 过度优化的权衡 |
| React.memo 原理 | 浅比较 props 避免不必要的重渲染 |
| 合成事件 | 事件池、事件委托到 root |
7.4 Vue
| 题目 | 考点 |
|---|---|
| Composition API vs Options API | 逻辑复用、类型推断、组织方式 |
| ref 和 reactive 区别 | 原始值 vs 对象、响应式丢失 |
| nextTick 原理 | 微任务队列、DOM 更新后回调 |
| v-for key 的作用 | diff 算法的节点复用 |
| 组件通信方式 | props/emit / provide-inject / vuex/pinia / event bus |
7.5 网络
| 题目 | 考点 |
|---|---|
| HTTPS 握手过程 | TLS 1.3 握手、证书验证、对称加密 |
| TCP 三次握手/四次挥手 | 状态变化、TIME_WAIT 作用 |
| HTTP/2 多路复用 | 二进制分帧、流优先级、头部压缩 |
| 强缓存与协商缓存 | 头部优先级、缓存位置(memory/disk) |
| 跨域解决方案 | CORS / JSONP / Proxy / postMessage |
7.6 工程化
| 题目 | 考点 |
|---|---|
| Webpack vs Vite | 打包 vs 原生 ESM、HMR 原理差异 |
| Tree-shaking 原理 | ESM 静态分析、sideEffects 配置 |
| 按需加载原理 | import() 动态导入、Chunk 拆分 |
| Loader vs Plugin | Loader 转换模块、Plugin 介入生命周期 |
| Source Map | eval / cheap-source-map 等策略的选择 |
附录
以上内容覆盖了前端面试的主要知识体系。建议结合自己的实际项目经历,将理论知识与实践经验相结合,形成有深度的个人技术表达。