中级前端面试常用问题及回答

0 阅读21分钟

面试题库与参考回答


目录

  1. JavaScript 基础与底层原理
  2. TypeScript 深度
  3. CSS 与布局
  4. 浏览器核心原理
  5. Vue 深度
  6. React 深度
  7. 前端工程化
  8. 性能优化
  9. 安全
  10. 数据结构和算法
  11. 系统设计
  12. 开放性问题和反向提问

一、JavaScript 基础与底层原理

1.1 执行上下文与作用域链

Q: 什么是执行上下文?JavaScript 代码执行时经历了什么?

A: 执行上下文是 JavaScript 引擎执行代码时的运行环境,主要分为三种:

全局执行上下文:代码首次运行时的最外层环境,创建全局对象 window(浏览器)或 global(Node.js),并设置 this 指向全局对象。

函数执行上下文:每次调用函数时创建,每个函数都有自己独立的上下文。

Eval 执行上下文:不推荐使用。

执行过程

  1. 创建阶段:创建变量对象(VO/AO),建立作用域链,确定 this 指向
  2. 执行阶段:逐行执行代码,对变量赋值,执行函数调用

作用域链:由当前执行上下文的变量对象 + 外层嵌套函数的变量对象组成,用于解析变量时的查找顺序(就近原则)。

var a = 1
function outer() {
  var b = 2
  function inner() {
    var c = 3
    console.log(a, b, c) // 1, 2, 3 — 作用域链向上追溯
  }
  inner()
}
outer()

Q: let、const、var 的区别是什么?

A:

特性varletconst
作用域函数作用域块级作用域块级作用域
变量提升声明提升,初始化不提升(undefined)声明提升,但存在暂时性死区同 let
重复声明允许不允许不允许
全局属性会成为 window 属性不会不会
初始值可无可无必须有

暂时性死区(TDZ)

console.log(a) // Cannot access 'a' before initialization
let a = 1

let 在创建阶段到执行阶段之间存在 TDZ,此期间访问变量会报错。

为什么不用 var

  • 函数作用域导致变量泄露(如循环闭包问题)
  • 可重复声明造成不可预期覆盖
  • 没有块级作用域,if/for 块内的变量污染外部

1.2 闭包与内存

Q: 什么是闭包?有什么应用场景?有什么潜在问题?

A: 闭包是指函数能够记住并访问其词法作用域(外部函数的变量),即使该函数在其词法作用域之外执行。

function makeCounter() {
  let count = 0  // 私有变量
  return {
    increment: () => ++count,
    getCount: () => count
  }
}
const counter = makeCounter()
counter.increment() // 1
counter.increment() // 2
counter.getCount()  // 2 — count 无法被外部直接访问

应用场景

  1. 模块化:封装私有变量,暴露公共 API
  2. 函数工厂:创建带预设参数的函数
  3. 防抖/节流:保存定时器状态
  4. 缓存:保存计算结果(记忆化)
  5. 循环中的异步:在 setTimeout 中保存正确的循环变量

潜在问题——内存泄漏

// 问题:大型对象被闭包持有无法释放
function heavy() {
  const bigData = new Array(1000000)
  return () => bigData[0] // bigData 无法被 GC
}

解决方案

  • 手动置 null:bigData = null
  • 避免在不需要时创建闭包
  • Vue/React 中及时清理定时器、事件监听

1.3 原型与继承

Q: JavaScript 的原型链是如何工作的?如何实现继承?

A: JavaScript 通过原型链实现对象间的继承关系。

核心概念

  • 每个对象都有一个 [[Prototype]](浏览器中通过 proto 访问)
  • 函数有一个 prototype 属性,指向一个包含 constructor 的对象
  • 当访问对象的属性时,如果对象本身没有,会沿原型链向上查找

原型链:Object.prototype 是链的顶端,null 是原型链的终点。

function Animal(name) {
  this.name = name
}
Animal.prototype.speak = function() {
  console.log(this.name + ' makes a noise.')
}

function Dog(name, breed) {
  Animal.call(this, name) // 借用构造函数
  this.breed = breed
}
// 原型链继承
Dog.prototype = Object.create(Animal.prototype)
Dog.prototype.constructor = Dog

const dog = new Dog('Rex', 'Shepherd')
dog.speak() // Rex makes a noise.
dog instanceof Animal // true

ES6 Class 继承

class Animal {
  constructor(name) { this.name = name }
  speak() { console.log(this.name + ' speaks') }
}

class Cat extends Animal {
  constructor(name, color) {
    super(name)
    this.color = color
  }
}

1.4 this 指向

Q: this 的指向有哪些规则?如何改变 this?

A: JavaScript 中 this 的指向取决于调用方式:

规则一:默认绑定(独立函数调用)

function foo() { console.log(this) }
foo() // 严格模式下 undefined,非严格模式下 window

规则二:隐式绑定(对象方法调用)

const obj = { name: 'A', foo() { console.log(this.name) } }
obj.foo() // 'A' — this 指向 obj

规则三:显式绑定(call/apply/bind)

function foo() { console.log(this.name) }
const obj = { name: 'B' }
foo.call(obj)   // 'B'
foo.apply(obj)  // 'B'
const fn = foo.bind(obj)
fn()            // 'B'

规则四:new 绑定

function Person(name) { this.name = name }
const p = new Person('C')
p.name // 'C'

规则五:箭头函数

  • 箭头函数没有自己的 this,它继承外层第一个普通函数的 this
  • 箭头函数无法用 call/apply/bind 改变 this

改变 this 的优先级: new > 显式绑定(bind/call/apply)> 隐式绑定 > 默认绑定


1.5 事件循环与异步

Q: 解释 JavaScript 的事件循环机制。什么是宏任务和微任务?

A: JavaScript 是单线程的,通过事件循环实现异步操作。

核心概念

  • 调用栈(Call Stack):执行同步代码
  • 任务队列(Task Queue):宏任务队列
  • 微任务队列(Microtask Queue):Promise 回调、MutationObserver、queueMicrotask

执行顺序

  1. 执行同步代码(调用栈清空)
  2. 执行所有微任务(清空微任务队列)
  3. 执行一个宏任务(从任务队列取一个)
  4. 重复步骤 2-3
console.log('1')         // 同步
setTimeout(() => console.log('2'), 0)  // 宏任务
Promise.resolve().then(() => console.log('3')) // 微任务
console.log('4')         // 同步

// 输出顺序: 1 → 4 → 3 → 2

常见宏任务:setTimeout、setInterval、I/O、UI 渲染、setImmediate(Node)

常见微任务:Promise.then/.catch/.finally、MutationObserver、queueMicrotask

async/await 原理

  • async 函数返回一个 Promise
  • await 后的代码变为微任务
async function test() {
  console.log('1')
  await Promise.resolve()
  console.log('2') // 微任务,在下一个宏任务前执行
}
test()
console.log('3') // 同步
// 输出: 1 → 3 → 2

1.6 Promise 与 Generator

Q: Promise 有哪些状态?then/catch/finally 的返回值是什么?

A: Promise 有三种状态:

  • pending:初始状态
  • fulfilled:操作成功完成
  • rejected:操作失败

状态一旦改变就不可逆。

then/catch/finally 返回值

Promise.resolve(1)
  .then(x => x + 1)      // 返回 2(值穿透)
  .then(x => { throw new Error('err') }) // 抛出错误,进入 rejected
  .catch(err => 0)       // 捕获错误,返回 0,进入 fulfilled
  .finally(() => console.log('done')) // 不影响返回值
  .then(x => console.log(x)) // 打印 0

Promise 静态方法

Promise.all([p1, p2, p3])     // 所有成功才成功,一个失败则整体失败
Promise.allSettled([p1, p2])  // 等所有结果(成功/失败),总是成功
Promise.race([p1, p2])        // 返回最快 resolved/rejected 的结果
Promise.any([p1, p2])         // 返回最快 resolved 的,一个失败可接受

1.7 深拷贝与浅拷贝

Q: 如何实现深拷贝?有什么优缺点?

A:

浅拷贝:只复制对象的第一层属性,引用类型仍共享。

const copy1 = { ...obj }
const copy2 = Object.assign({}, obj)

深拷贝

// 方式一:JSON 序列化(简单但有局限)
const deep1 = JSON.parse(JSON.stringify(obj))
// 局限:undefined、函数、Symbol、循环引用、Date、RegExp 无法正确拷贝

// 方式二:递归实现
function deepClone(obj, weakMap = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj
  if (weakMap.has(obj)) return weakMap.get(obj) // 处理循环引用
  if (obj instanceof Date) return new Date(obj)
  if (obj instanceof RegExp) return new RegExp(obj)
  const clone = Array.isArray(obj) ? [] : {}
  weakMap.set(obj, clone)
  for (const key of Object.keys(obj)) {
    clone[key] = deepClone(obj[key], weakMap)
  }
  return clone
}

// 方式三:structuredClone(原生 API,2022+)
const deep2 = structuredClone(obj)

1.8 模块化

Q: ES Module 和 CommonJS 有什么区别?

A:

特性ES ModuleCommonJS
语法import/exportrequire/module.exports
加载方式静态解析,编译时确定动态执行,运行时确定
值拷贝只读绑定(动态映射)值拷贝
thisundefinedmodule.exports
循环引用可以(但值为 undefined)可能卡住
异步/同步异步(浏览器原生支持)同步
// ES Module
export const name = 'A'
export function foo() {}
import { name, foo } from './module'
import * as m from './module'

// CommonJS
const { name } = require('./module')
module.exports = { name }

二、TypeScript 深度

2.1 类型基础

Q: TypeScript 和 JavaScript 的核心区别是什么?为什么要用 TypeScript?

A: TypeScript 是 JavaScript 的超集,主要增加了静态类型检查。

核心区别

  • TS 在编译阶段进行类型检查,JS 在运行时才报错
  • TS 需要编译成 JS 才能在浏览器/Node.js 运行
  • TS 支持接口、泛型、枚举、元组等类型语法

为什么要用 TypeScript

  1. 类型安全:编译期发现类型错误,减少运行时 bug
  2. 代码提示:IDE 智能提示更准确,开发效率高
  3. 可维护性:大型项目中,类型即文档,新人容易上手
  4. 重构友好:修改类型后,编译器告诉你所有调用点
  5. 生态友好:主流框架(Vue3、React)都支持 TypeScript

2.2 泛型

Q: 什么是泛型?有哪些常见的泛型工具类型?

A: 泛型允许创建可复用的组件,同时保持类型安全。

// 泛型函数
function identity<T>(arg: T): T {
  return arg
}
identity<string>('hello')
identity(123) // 类型推断

// 泛型约束
function getLength<T extends { length: number }>(arg: T): number {
  return arg.length
}

// 泛型接口
interface KeyValuePair<K, V> {
  key: K
  value: V
}
const pair: KeyValuePair<string, number> = { key: 'age', value: 25 }

内置工具类型

Partial<T>       // 所有属性变为可选
Required<T>      // 所有属性变为必填
Readonly<T>      // 所有属性变为只读
Pick<T, K>       // 从 T 中选取部分属性
Omit<T, K>       // 从 T 中排除部分属性
Record<K, V>     // 创建键值对类型
Extract<T, U>    // 取交集
Exclude<T, U>    // 取差集
NonNullable<T>   // 排除 null 和 undefined
ReturnType<F>    // 获取函数返回值类型
Parameters<F>    // 获取函数参数类型

2.3 装饰器

Q: 什么是装饰器?在 Vue3 和 Angular 中如何使用?

A: 装饰器是一种特殊类型的声明,用于修改类或类的成员的行为。

类装饰器

function sealed(constructor: Function) {
  Object.seal(constructor)
  Object.seal(constructor.prototype)
}

@sealed
class Greeter {
  greeting: string
  constructor(message: string) {
    this.greeting = message
  }
}

方法装饰器

function log(target: any, key: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value
  descriptor.value = function(...args: any[]) {
    console.log(`Calling ${key} with`, args)
    return original.apply(this, args)
  }
  return descriptor
}

class Calculator {
  @log
  add(a: number, b: number) {
    return a + b
  }
}

Vue3 组合式 API vs 装饰器:Vue3 推荐 Composition API,而 Angular 仍广泛使用装饰器(@Component、@Inject 等)。


三、CSS 与布局

3.1 盒模型与布局

Q: CSS 盒模型是什么?content-box 和 border-box 的区别?

A: CSS 盒模型描述了元素在页面上占据空间的方式。

标准盒模型(W3C)

  • width = content 的宽度
  • 总宽度 = width + padding + border + margin

IE 盒模型(border-box)

  • width = content + padding + border 的总宽度
  • 总宽度 = width + margin
/* 现代浏览器默认 */
box-sizing: content-box;

/* 实际项目推荐 */
box-sizing: border-box; /* padding/border 包含在 width 内 */

Q: Flex 布局和 Grid 布局各自适用什么场景?

A:

Flexbox(一维布局)

  • 适合单一方向排列(行或列)
  • 项目之间的空间分配、对齐
  • 典型场景:导航栏、卡片列表、表单项
/* 常见用法:垂直居中 */
.container {
  display: flex;
  justify-content: center;  /* 主轴居中 */
  align-items: center;      /* 交叉轴居中 */
}

Grid(二维布局)

  • 同时控制行和列
  • 适合页面整体布局、仪表盘、相册
.container {
  display: grid;
  grid-template-columns: 200px 1fr 200px;
  grid-template-rows: auto 1fr auto;
  gap: 20px;
}

选择原则:简单一维排列用 Flex,复杂二维布局用 Grid,可以组合使用。


3.2 居中方案

Q: 如何让一个元素完美居中?列举所有方法。

A:

/* 方法一:Flexbox */
.container {
  display: flex;
  justify-content: center;
  align-items: center;
}

/* 方法二:Grid */
.container {
  display: grid;
  place-items: center; /* 简写,等同于 justify-items + align-items */
}

/* 方法三:绝对定位 + transform */
.box {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

/* 方法四:绝对定位 + margin auto */
.container { position: relative }
.box {
  position: absolute;
  top: 0; bottom: 0; left: 0; right: 0;
  margin: auto;
}

/* 方法五:绝对定位 + calc */
.box {
  position: absolute;
  top: calc(50% - 高度/2);
  left: calc(50% - 宽度/2);
}

/* 方法六:行内元素垂直居中 */
.container {
  height: 200px;
  line-height: 200px;
  text-align: center;
}

/* 方法七:table-cell */
.container {
  display: table-cell;
  vertical-align: middle;
  text-align: center;
}

3.3 BFC 与清除浮动

Q: 什么是 BFC?有哪些创建 BFC 的方式?

A: BFC(Block Formatting Context)是块级格式化上下文,决定了块级盒子的布局规则。

创建 BFC 的方式

  • 根元素 html
  • float 不为 none
  • position 为 absolute 或 fixed
  • display 为 inline-block、table-cell、flex、grid
  • overflow 不为 visible(hidden、auto、scroll)
  • contain 属性为 layout、content、strict

BFC 的特点

  1. 同一个 BFC 内,垂直方向的外边距会合并
  2. BFC 内的浮动不会影响外部布局
  3. BFC 可以包含浮动元素(清除浮动)

BFC 应用场景

/* 清除浮动 */
.parent {
  overflow: hidden; /* 创建 BFC 包含浮动 */
}

/* 防止 margin 合并 */
.parent {
  overflow: hidden;
}
.child {
  margin-top: 20px;
}

3.4 重排与重绘

Q: 什么是重排(reflow)和重绘(repaint)?如何优化?

A: 浏览器渲染流程:HTML → DOM → CSSOM → Render Tree → Layout → Paint → Composite

重排(Reflow):几何属性改变时(宽高、位置、字体大小),浏览器重新计算布局。 重绘(Repaint):外观改变但不影响布局(颜色、背景、visibility),重新绘制像素。

触发重排的操作

  • 添加/删除 DOM 元素
  • 元素位置、尺寸改变
  • 内容变化(文本或图片大小)
  • 浏览器窗口大小改变

优化策略

  1. 批量修改 DOM:使用 documentFragment 或将 DOM 离线(display:none)
  2. 使用 transform/opacity:仅触发 composite,不触发 layout/paint
  3. 避免频繁读取布局属性:读取后缓存
  4. CSS 动画使用 transform:GPU 加速
  5. will-change:提前告知浏览器元素将变化
// 批量修改
const fragment = document.createDocumentFragment()
items.forEach(item => {
  const li = document.createElement('li')
  li.textContent = item
  fragment.appendChild(li)
})
list.appendChild(fragment)

// 使用 transform 动画
element.style.transform = 'translateX(100px)' // 仅重绘
element.style.left = '100px' // 触发重排

四、浏览器核心原理

4.1 浏览器渲染机制

Q: 描述浏览器的渲染过程。从 HTML 到页面显示经历了什么?

A:

完整渲染流水线

  1. HTML 解析:将 HTML 文本解析成 DOM 树
  2. CSS 解析:将 CSS 解析成 CSSOM 树
  3. DOM + CSSOM → Render Tree:只包含可见节点
  4. Layout(布局):计算每个节点的几何信息(位置、大小)
  5. Paint(绘制):将节点绘制成多个图层的绘制记录
  6. Composite(合成):将图层合成为最终画面,上传 GPU 显示

关键优化点

  • CSS 放 head 中,JS 放 body 末尾(defer)
  • 使用 requestAnimationFrame 安排动画
  • 避免同步布局(读写分离)

4.2 浏览器缓存

Q: 浏览器缓存策略有哪些?协商缓存和强缓存的区别?

A:

强缓存:不发送请求,直接使用本地缓存。

Cache-Control: max-age=3600       // 相对时间(HTTP/1.1)
Expires: Wed, 21 Oct 2026 07:28:00 GMT  // 绝对时间(HTTP/1.0)

协商缓存:发送请求,由服务器决定是否使用缓存。

Last-Modified / If-Modified-Since   // 文件修改时间
ETag / If-None-Match               // 文件内容 hash

缓存优先级

  1. Service Worker(最高)
  2. Memory Cache(内存)
  3. Disk Cache(磁盘)
  4. Push Cache(HTTP/2 的 push)

用户行为对缓存的影响

  • 强制刷新(Ctrl+Shift+R):跳过所有缓存
  • 地址栏回车:先检查过期,未过期用缓存
  • F5 刷新:跳过强缓存,走协商缓存

4.3 跨域

Q: 什么是跨域?有哪些解决方案?

A: 浏览器同源策略(Same-Origin Policy)限制了不同源之间的资源访问。

同源定义:协议 + 域名 + 端口 三者相同。

解决方案

方案原理适用场景
CORS服务端设置 Access-Control-Allow-OriginAPI 跨域(最推荐)
JSONP利用 script 标签不受同源限制(仅 GET)老项目兼容
代理服务器Nginx/Node 转发请求到同源前端开发环境
postMessage窗口间消息通信iframe 通信
WebSocket协议本身不受同源限制实时通信
document.domain共享子域名 iframe同主域不同子域

CORS 预检请求

  • 非简单请求(PUT/DELETE/Custom Header)会先发 OPTIONS 预检
  • 需服务器配置 Allow-Methods、Allow-Headers、Allow-Origin

4.4 垃圾回收

Q: 浏览器垃圾回收机制是什么?什么是内存泄漏?

A: JavaScript 使用自动垃圾回收(GC),主要算法:

引用计数(已废弃):对象被引用次数归零时回收。无法处理循环引用。

标记清除(现代浏览器主流)

  • 从根对象(window/global)出发,标记可达对象
  • 清除未被标记的对象

V8 引擎的 GC 优化

  • 分代回收:新生代(短生命周期对象,用 Scavenge)、老生代(长生命周期,用 Mark-Sweep + Mark-Compact)
  • 增量标记:将 GC 过程分片,避免长停顿
  • 并发/并行回收:利用多核并行处理

常见内存泄漏场景

// 1. 全局变量泄漏
function leak() { bigData = new Array(1000000) }

// 2. 定时器未清理
setInterval(() => { /* ... */ }, 1000)
clearInterval(id) // 组件卸载时必须清理

// 3. 事件监听未移除
element.addEventListener('click', handler)
element.removeEventListener('click', handler)

// 4. 闭包持有大对象
function heavy() {
  const data = new Array(1000000)
  return () => data[0]
}

// 5. Vue/React 中:
// - 未清理的订阅/事件监听
// - 组件中创建的定时器
// - 全局 store 的冗余数据

五、Vue 深度

5.1 响应式原理

Q: Vue2 和 Vue3 的响应式原理有什么区别?

A:

Vue2:Object.defineProperty

// Vue2 的响应式实现
function defineReactive(obj, key, val) {
  const dep = new Dep() // 依赖收集
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      dep.depend() // 收集依赖
      return val
    },
    set(newVal) {
      if (val === newVal) return
      val = newVal
      dep.notify() // 通知更新
    }
  })
}

Vue2 的问题

  • 无法检测数组下标变化(通过重写数组方法解决)
  • 需要 $set(Vue.set)处理新增属性
  • 深度递归开销大

Vue3:Proxy + Reflect

// Vue3 的响应式实现
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver)
      track(target, key) // 收集依赖
      return typeof res === 'object' ? reactive(res) : res // 深度代理
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver)
      trigger(target, key) // 通知更新
      return result
    }
  })
}

Vue3 的优势

  • Proxy 可以检测数组下标、新增属性、删除属性
  • 无需递归,性能更好
  • 支持 Map、Set、WeakMap、WeakSet

Q: 什么是依赖收集?Vue 如何知道哪个组件需要更新?

A:

响应式系统的核心

  1. Track(追踪):在 get 时收集当前活跃的 Watcher
  2. Trigger(触发):在 set 时通知所有依赖的 Watcher 更新
// 依赖管理器
class Dep {
  constructor() { this.subs = [] }
  addSub(watcher) { this.subs.push(watcher) }
  notify() { this.subs.forEach(w => w.update()) }
}

// 渲染 watcher
class Watcher {
  constructor(vm, expOrFn, callback) {
    this.vm = vm
    this.getter = expOrFn
    this.callback = callback
    this.get() // 执行 getter,将自身设为全局 activeWatcher
  }
  get() {
    pushTarget(this) // 设置 activeWatcher
    this.getter.call(this.vm, this.vm)
    popTarget()     // 恢复上一个 activeWatcher
  }
}

流程

  1. 组件渲染时创建 Watcher,执行 render()
  2. render() 读取响应式数据,触发 get()
  3. get() 中 activeWatcher 订阅 Dep
  4. 数据变化时,Dep 通知所有 Watcher,触发 re-render

5.2 虚拟 DOM

Q: 什么是虚拟 DOM?Vue 为什么使用它?它的性能一定比直接操作 DOM 快吗?

A:

虚拟 DOM(VNode):用 JavaScript 对象描述真实 DOM 结构。

// 真实 DOM
<div class="container">
  <h1>Title</h1>
  <p>Content</p>
</div>

// 虚拟 DOM
{
  tag: 'div',
  props: { class: 'container' },
  children: [
    { tag: 'h1', children: 'Title' },
    { tag: 'p', children: 'Content' }
  ]
}

Vue 为什么使用 VNode

  1. 跨平台:同一套 VNode 可以渲染到 DOM、SSR、Native(Weex)
  2. 减少重排重绘:通过 Diff 算法找出最小更新范围
  3. 更好的开发体验:组件化、模板语法

性能真相

  • 少量操作(1-2个 DOM 变更):直接操作 DOM 更快(VNode 有额外开销)
  • 大量操作:VNode + Diff 算法远快于直接 DOM
  • VNode 不是银弹,滥用仍会性能问题

Diff 算法

  • 同级比较(只看同一层级,不跨级)
  • key 的作用:复用 DOM 节点,减少重排
  • 策略:先比较标签、属性、最后比较子节点

5.3 computed vs watch

Q: computed 和 watch 有什么区别?

A:

特性computedwatch
缓存基于响应式依赖,缓存结果不缓存,每次都执行
适用派生状态(基于已有数据计算)副作用(执行异步操作、调用 API)
返回值必须有返回值不必须有
同步/异步同步支持异步
// computed:计算属性,依赖响应式数据,自动缓存
const fullName = computed(() => firstName.value + ' ' + lastName.value)

// watch:监听数据变化,执行副作用
watch(userName, (newVal, oldVal) => {
  console.log('name changed:', newVal)
})

// watch 深度监听
watch(() => state.obj, (newVal) => { /* ... */ }, { deep: true })

// watchEffect:自动追踪依赖
watchEffect(() => {
  console.log(count.value) // 自动追踪 count
})

5.4 Vue3 Composition API

Q: Composition API 相比 Options API 有什么优势?

A:

逻辑复用

  • Options API 用 mixins:来源不清晰,命名冲突
  • Composition API 用 composables(hooks):来源清晰,逻辑可组合
// composable: useWindowSize.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useWindowSize() {
  const width = ref(window.innerWidth)
  const height = ref(window.innerHeight)

  const handleResize = () => {
    width.value = window.innerWidth
    height.value = window.innerHeight
  }

  onMounted(() => window.addEventListener('resize', handleResize))
  onUnmounted(() => window.removeEventListener('resize', handleResize))

  return { width, height }
}

// 使用:逻辑按功能分组,来源清晰
import { useWindowSize } from './useWindowSize'
import { useWebSocket } from './useWebSocket'

export default {
  setup() {
    const { width, height } = useWindowSize()
    const { messages, send } = useWebSocket()
    // ...
  }
}

其他优势

  • 更好的 TypeScript 支持
  • 代码量减少(无需 data/methods/computed 分开写)
  • 更好的 Tree-shaking
  • 更好的逻辑内聚(相关逻辑集中在一起,而非分散在多个选项中)

5.5 Vue Router

Q: Vue Router 的原理是什么?有哪几种路由模式?

A:

Hash 模式

  • 使用 URL 的 hash(#xxx)作为路由
  • 监听 hashchange 事件
  • 优点:兼容性好,不需要服务器配置
  • 缺点:URL 不好看,有#

History 模式

  • 使用 HTML5 History API(pushState/replaceState)
  • 监听 popstate 事件(浏览器前进/后退)
  • 优点:URL 好看
  • 缺点:需要服务器配置(所有路径都返回 index.html)

Abstract 模式

  • 在非浏览器环境(如 Node.js、SSR)使用

导航守卫

router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth && !isAuthenticated) {
    next('/login')
  } else {
    next()
  }
})

5.6 Vue3 进阶

Q: nextTick 的原理是什么?为什么需要它?

A:

为什么需要 nextTick: Vue 的 DOM 更新是异步的。修改响应式数据后,DOM 不会立即更新,而是在下一个事件循环中批量更新。

// 不用 nextTick:获取的是旧值
this.message = 'updated'
console.log(this.$refs.el.innerText) // 旧值

// 使用 nextTick:DOM 已更新
this.message = 'updated'
this.$nextTick(() => {
  console.log(this.$refs.el.innerText) // 新值
})

原理:Vue 2 用 Promise + MutationObserver + setTimeout(降级);Vue 3 用 Promise + Scheduler。

执行顺序

  1. 同步代码执行
  2. 微任务(Promise.then, nextTick callback)
  3. 下一个宏任务(setTimeout)

六、React 深度

6.1 Fiber 架构

Q: React Fiber 是什么?解决了什么问题?

A:

Fiber 的核心思想:将渲染工作拆分成小单元,可以中断和恢复。

旧架构(Stack Reconciler)

  • 递归更新,一旦开始不能中断
  • 大型更新会造成主线程阻塞,用户交互卡顿

Fiber 架构

  • 每个 VNode 对应一个 Fiber 节点
  • 每个 Fiber 节点有优先级(动画高、低优先级低)
  • 可中断、可恢复、可复用已完成的工作
  • 双缓冲:一次只更新一个"工作树",完成后才切换

Fiber 的 return:将工作单元的结果返回给父节点,父节点可以收集子节点的完成结果。

时间切片:每帧预留 5ms 给浏览器执行交互,保证不卡顿。


6.2 Hooks

Q: useEffect 和 useLayoutEffect 有什么区别?

A:

特性useEffectuseLayoutEffect
时机DOM 更新后,浏览器渲染前(异步)DOM 更新后,同步执行
是否阻塞渲染
SSR在服务端执行不执行(SSR 时为空函数)
适用场景异步操作、订阅、数据获取同步 DOM 操作、测量布局
// useEffect:异步,不阻塞渲染
useEffect(() => {
  document.title = count
}, [count])

// useLayoutEffect:同步,DOM 操作前执行
useLayoutEffect(() => {
  const rect = ref.current.getBoundingClientRect()
  // 同步测量/修改 DOM,避免闪烁
}, [])

useRef vs useState

  • useState:触发组件重新渲染,异步
  • useRef:修改不触发重渲染,.current 同步更新

Q: 自定义 Hook 有什么应用场景?

A:

// 1. 数据获取
function useFetch(url) {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false))
  }, [url])

  return { data, loading, error }
}

// 2. 媒体查询
function useMediaQuery(query) {
  const [matches, setMatches] = useState(
    () => window.matchMedia(query).matches
  )
  useEffect(() => {
    const mql = window.matchMedia(query)
    const handler = (e) => setMatches(e.matches)
    mql.addEventListener('change', handler)
    return () => mql.removeEventListener('change', handler)
  }, [query])
  return matches
}

// 3. 防抖
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value)
  useEffect(() => {
    const id = setTimeout(() => setDebouncedValue(value), delay)
    return () => clearTimeout(id)
  }, [value, delay])
  return debouncedValue
}

6.3 Redux vs Zustand

Q: Redux 的工作原理?和 Zustand 有什么区别?

A:

Redux 三大原则

  1. 单一数据源(store)
  2. State 只读(通过 action 触发更新)
  3. 纯函数修改(reducer 必须是纯函数)

Redux 流程

用户交互 → dispatch(action) → reducer(newState) → subscribe(通知) → UI 更新

Redux vs Zustand

特性ReduxZustand
代码量多(action、reducer、type、selector)少(一组 store)
中间件支持(redux-thunk、saga)通过 middleware 扩展
DevTools强大的调试工具基础工具
Boilerplate
适用大型复杂应用中小型应用

Zustand 示例

import { create } from 'zustand'

const useStore = create((set) => ({
  count: 0,
  increment: () => set(state => ({ count: state.count + 1 })),
  reset: () => set({ count: 0 })
}))

function Counter() {
  const { count, increment } = useStore()
  return <button onClick={increment}>{count}</button>
}

6.4 React 渲染优化

Q: React 如何避免不必要的重新渲染?

A:

1. React.memo(函数组件)

const MyComponent = React.memo(function MyComponent({ name }) {
  return <div>{name}</div>
})

// 进阶:自定义比较函数
const MyComponent = React.memo(
  function MyComponent({ name, age }) {
    return <div>{name} - {age}</div>
  },
  (prevProps, nextProps) => {
    return prevProps.name === nextProps.name // 只比较 name
  }
)

2. useMemo 和 useCallback

// 缓存计算结果
const expensiveValue = useMemo(() => computeExpensive(a, b), [a, b])

// 缓存回调函数(防止子组件不必要的重渲染)
const handleClick = useCallback(() => {
  doSomething(count)
}, [count])

3. 合理使用 key

  • 列表中用稳定、唯一的 key
  • 避免用数组索引作为 key(删除项目时索引变化,导致错误复用)

4. 状态提升的边界

  • 将频繁变化的状态放在子组件,避免父组件重渲染影响子组件

七、前端工程化

7.1 构建工具

Q: Webpack 和 Vite 的核心区别是什么?为什么 Vite 更快?

A:

Webpack 原理

  • 启动时扫描所有依赖,构建完整的依赖图
  • 将所有模块打包成少量 bundle
  • 开发时需要全量构建,修改后重新构建
  • HMR(热模块替换)需要重新构建相关模块

Vite 原理

  • 利用浏览器的 ES Module 原生支持(ESM)
  • 开发时仅编译当前需要的文件,不打包
  • 生产时用 Rollup 打包

Vite 为什么快

  1. No Bundle:直接提供 ESM,浏览器自己处理模块解析
  2. 按需编译:只编译当前访问的页面
  3. 预构建依赖:node_modules 中的 CommonJS 依赖预构建为 ESM
  4. HMR:模块级别热更新,无需重新加载整个应用

Vite 配置示例

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          three: ['three'],
          vendor: ['vue', 'vue-router']
        }
      }
    }
  },
  optimizeDeps: {
    include: ['three', 'vue'] // 预构建依赖
  }
})

7.2 CI/CD

Q: 前端 CI/CD 流程是怎样的?GitHub Actions 如何配置?

A:

典型 CI/CD 流程

代码提交 → 静态检查(Lint) → 单元测试 → 构建 → E2E 测试 → 部署

GitHub Actions 示例

# .github/workflows/ci.yml
name: CI/CD

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci
      - run: npm run lint
      - run: npm run test:unit
      - run: npm run test:e2e

  deploy:
    needs: lint-and-test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

      - name: Deploy to Server
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SERVER_KEY }}
          script: |
            cd /var/www/app
            git pull
            npm ci
            npm run build
            pm2 restart app

7.3 TypeScript 配置

Q: tsconfig.json 核心配置有哪些?如何优化编译速度和类型检查?

A:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",  // Vite/Webpack 场景用 bundler
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "jsx": "preserve",              // Vue/React 保留 JSX

    // 严格模式
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,

    // 路径别名
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    },

    // 输出
    "outDir": "dist",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,

    // 性能优化
    "skipLibCheck": true,           // 跳过 .d.ts 检查
    "incremental": true,             // 增量编译
    "isolatedModules": true,         // 每个文件独立转译

    // 模块解析
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

八、性能优化

8.1 Core Web Vitals

Q: 什么是 Core Web Vitals?LCP、LCP、CLS 的优化方法?

A:

Core Web Vitals 三大指标

LCP(Largest Contentful Paint):最大内容绘制时间

  • 目标:< 2.5 秒
  • 优化方法:
    • 图片优化(WebP/AVIF、懒加载、预加载)
    • 优化服务器响应时间
    • 移除阻塞渲染的资源
<!-- 预加载关键图片 -->
<link rel="preload" href="hero.webp" as="image">

FID/INP(交互到下一绘制):输入响应时间

  • 目标:< 100ms
  • 优化方法:
    • 减少主线程阻塞(代码分割、懒加载)
    • 优化事件处理器
    • Web Worker 处理耗时计算

CLS(Cumulative Layout Shift):累计布局偏移

  • 目标:< 0.1
  • 优化方法:
    • 给图片和视频设置宽高
    • 避免动态注入内容(广告、弹窗)
    • 使用 transform 做动画
/* 预留图片空间,防止 CLS */
img {
  width: 100%;
  height: auto;
  aspect-ratio: 16/9; /* 固定比例 */
}

8.2 首屏优化

Q: 如何优化首屏加载速度?

A:

核心策略

1. 代码层面

  • 代码分割(Code Splitting):按路由/组件拆分
  • 懒加载(Lazy Loading):dynamic import
  • Tree Shaking:移除未使用的代码
// 路由懒加载
const Home = () => import('./Home.vue')
// 组件懒加载
const HeavyChart = defineAsyncComponent(() => import('./HeavyChart.vue'))

2. 资源层面

  • 压缩图片(WebP、AVIF)
  • 图片懒加载
  • CSS/JS 合并与压缩
  • 使用 CDN
<!-- 图片懒加载 -->
<img loading="lazy" src="image.webp" alt="...">

<!-- 关键 CSS 内联 -->
<style>/* critical CSS */</style>
<!-- 非关键 CSS 异步加载 -->
<link rel="preload" href="non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">

3. 网络层面

  • DNS 预解析:
  • 预连接:
  • HTTP/2 多路复用
  • 缓存策略优化

4. 渲染层面

  • SSR/SSG:首屏直出 HTML
  • 服务端压缩(Gzip/Brotli)

8.3 Vue/React 性能优化

Q: Vue/React 应用中有哪些常用的性能优化手段?

A:

Vue 优化

// 1. v-show vs v-if
// v-if:条件不渲染,适合不频繁切换
// v-show:渲染但隐藏,适合频繁切换

// 2. v-for 加 key,避免用 index
<item v-for="item in list" :key="item.id" />  // ✅
<item v-for="(item, index) in list" :key="index" />  // ❌

// 3. v-memo:缓存模板子树
<div v-memo="[selectedId]">
  <ExpensiveComponent />
</div>

// 4. keep-alive:缓存组件状态
<keep-alive include="Home,About" :max="10">
  <router-view />
</keep-alive>

// 5. shallowRef:只监听第一层
const state = shallowRef({ count: 0, deep: { nested: 1 } })
state.value.deep.nested = 2 // 不触发更新

// 6. defineAsyncComponent:异步组件
const AsyncComp = defineAsyncComponent(() => import('./Comp.vue'))

React 优化

// 1. React.lazy + Suspense
const Home = React.lazy(() => import('./Home'))
<Suspense fallback={<Loading />}>
  <Home />
</Suspense>

// 2. useMemo / useCallback
const memoizedValue = useMemo(() => computeExpensive(a, b), [a, b])
const memoizedFn = useCallback(() => doSomething(count), [count])

// 3. React.memo
const MemoizedComponent = React.memo(Component, (prev, next) => {
  return prev.name === next.name  // 自定义比较
})

// 4. 虚拟列表:大列表只渲染可见区域
import { FixedSizeList } from 'react-window'

九、安全

9.1 XSS 与 CSRF

Q: 什么是 XSS 攻击?有哪些类型?如何防范?

A:

XSS(跨站脚本攻击):攻击者在网页中注入恶意脚本。

三种类型

  1. 存储型 XSS:恶意代码存储在服务器(数据库、评论)
  2. 反射型 XSS:恶意代码通过 URL 参数传递
  3. DOM 型 XSS:纯前端代码漏洞,恶意数据直接操作 DOM
<!-- 反射型 XSS 示例 -->
<!-- URL: https://example.com?q=<script>alert(1)</script> -->
<!-- 服务器将 q 参数直接渲染到页面 -->

防范措施

1. 内容转义

function escapeHtml(str) {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;')
}

2. CSP(内容安全策略)

<meta http-equiv="Content-Security-Policy" content="script-src 'self'; style-src 'self' 'unsafe-inline'">

3. HttpOnly Cookie

  • 设置 Cookie 的 HttpOnly 属性,防止 JavaScript 读取

4. 输入验证 + 输出编码

  • 服务端验证输入格式
  • 输出时根据上下文(HTML/JS/CSS/URL)做不同编码

Q: 什么是 CSRF?如何防护?

A:

CSRF(跨站请求伪造):利用用户已登录的身份,伪造请求执行操作。

攻击流程

  1. 用户登录 A 网站,Cookie 被保存
  2. 用户访问 B 网站,B 网站中有自动提交的表单/脚本
  3. 浏览器自动携带 A 网站的 Cookie 发请求
  4. A 网站认为是用户本人操作,执行请求

防护措施

1. CSRF Token

// 服务端生成随机 token
// 响应 HTML 中包含 token
<input type="hidden" name="csrf_token" value="random_token">

// 提交时验证
app.post('/api/action', (req, res) => {
  if (req.body.csrf_token !== session.csrf_token) {
    return res.status(403).json({ error: 'CSRF' })
  }
  // 处理请求
})

2. SameSite Cookie

Set-Cookie: session=abc123; SameSite=Strict
  • SameSite=Strict:完全禁止跨站携带
  • SameSite=Lax:允许导航跳转携带,禁止子请求携带
  • SameSite=None:允许跨站(需 Secure)

3. 验证来源(Origin/Referer)

const origin = req.headers.origin
const referer = req.headers.referer
if (origin !== 'https://mysite.com') {
  return res.status(403)
}

9.2 其他安全话题

Q: 什么是 CSP?如何配置?

A:

CSP(Content Security Policy):通过 HTTP 响应头或 meta 标签,告诉浏览器允许加载哪些资源。

<meta http-equiv="Content-Security-Policy"
  content="default-src 'self';
           script-src 'self' 'nonce-random123';
           style-src 'self' 'unsafe-inline';
           img-src 'self' data: https://cdn.example.com;
           connect-src 'self' https://api.example.com;
           font-src 'self' https://fonts.gstatic.com;
           frame-ancestors 'none';">

常用指令

  • default-src:默认来源
  • script-src:JavaScript 来源
  • style-src:CSS 来源
  • img-src:图片来源
  • connect-src:AJAX/WebSocket 请求来源
  • frame-ancestors:可被嵌入的来源(防止 clickjacking)

十、数据结构和算法

10.1 常见算法

Q: 手写一个防抖(debounce)和节流(throttle)函数。

A:

// 防抖:在事件触发 n 秒后执行,如果 n 秒内再次触发,重新计时
function debounce(fn, delay, immediate = false) {
  let timer = null
  return function(...args) {
    const context = this
    if (timer) clearTimeout(timer)

    if (immediate && !timer) {
      fn.apply(context, args)
    }

    timer = setTimeout(() => {
      if (!immediate) {
        fn.apply(context, args)
      }
      timer = null
    }, delay)
  }
}

// 使用
const debouncedSearch = debounce((query) => {
  console.log('Searching:', query)
}, 300)
input.addEventListener('input', (e) => debouncedSearch(e.target.value))

// 节流:n 秒内只执行一次,稀释执行频率
function throttle(fn, delay) {
  let last = 0
  return function(...args) {
    const context = this
    const now = Date.now()
    if (now - last >= delay) {
      last = now
      fn.apply(context, args)
    }
  }
}

// 使用
const throttledScroll = throttle(() => {
  console.log('Scrolling:', window.scrollY)
}, 100)
window.addEventListener('scroll', throttledScroll)

Q: 实现一个简易的 Promise。

A:

class MyPromise {
  constructor(executor) {
    this.state = 'pending'
    this.value = undefined
    this.onFulfilledCallbacks = []
    this.onRejectedCallbacks = []

    const resolve = (value) => {
      if (this.state !== 'pending') return
      this.state = 'fulfilled'
      this.value = value
      this.onFulfilledCallbacks.forEach(fn => fn(value))
    }

    const reject = (reason) => {
      if (this.state !== 'pending') return
      this.state = 'rejected'
      this.value = reason
      this.onRejectedCallbacks.forEach(fn => fn(reason))
    }

    try {
      executor(resolve, reject)
    } catch (err) {
      reject(err)
    }
  }

  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      const handle = (fn, fallback) => {
        try {
          if (typeof fn === 'function') {
            const result = fn(this.value)
            resolve(result)
          } else {
            resolve(this.value)
          }
        } catch (err) {
          reject(err)
        }
      }

      if (this.state === 'fulfilled') {
        handle(onFulfilled)
      } else if (this.state === 'rejected') {
        handle(onRejected)
      } else {
        this.onFulfilledCallbacks.push(() => handle(onFulfilled))
        this.onRejectedCallbacks.push(() => handle(onRejected))
      }
    })
  }

  catch(onRejected) {
    return this.then(null, onRejected)
  }

  finally(fn) {
    return this.then(
      value => { fn(); throw value },
      reason => { fn(); throw reason }
    )
  }

  static resolve(value) {
    return new MyPromise(resolve => resolve(value))
  }

  static reject(reason) {
    return new MyPromise((_, reject) => reject(reason))
  }

  static all(promises) {
    return new MyPromise((resolve, reject) => {
      const results = []
      let count = 0
      promises.forEach((p, i) => {
        p.then(value => {
          results[i] = value
          count++
          if (count === promises.length) resolve(results)
        }, reject)
      })
    })
  }

  static race(promises) {
    return new MyPromise((resolve, reject) => {
      promises.forEach(p => p.then(resolve, reject))
    })
  }
}

Q: 实现深拷贝(考虑循环引用和特殊类型)。

A:

function deepClone(target, map = new WeakMap()) {
  // 处理原始类型
  if (target === null || typeof target !== 'object') {
    return target
  }

  // 处理循环引用
  if (map.has(target)) {
    return map.get(target)
  }

  // 处理 Date
  if (target instanceof Date) {
    return new Date(target.getTime())
  }

  // 处理 RegExp
  if (target instanceof RegExp) {
    return new RegExp(target.source, target.flags)
  }

  // 处理 Map
  if (target instanceof Map) {
    const cloneMap = new Map()
    map.set(target, cloneMap)
    target.forEach((v, k) => {
      cloneMap.set(deepClone(k, map), deepClone(v, map))
    })
    return cloneMap
  }

  // 处理 Set
  if (target instanceof Set) {
    const cloneSet = new Set()
    map.set(target, cloneSet)
    target.forEach(v => {
      cloneSet.add(deepClone(v, map))
    })
    return cloneSet
  }

  // 处理数组和普通对象
  const cloneTarget = Array.isArray(target) ? [] : {}
  map.set(target, cloneTarget)

  Object.keys(target).forEach(key => {
    cloneTarget[key] = deepClone(target[key], map)
  })

  return cloneTarget
}

Q: 实现一个函数,将扁平数据结构转成树形结构。

A:

// 输入
const list = [
  { id: 1, name: 'A', parentId: null },
  { id: 2, name: 'B', parentId: 1 },
  { id: 3, name: 'C', parentId: 1 },
  { id: 4, name: 'D', parentId: 2 },
  { id: 5, name: 'E', parentId: 2 },
]

// 输出
function buildTree(list) {
  const map = new Map()
  const result = []

  // 第一遍:建立 id → 节点的映射
  list.forEach(item => {
    map.set(item.id, { ...item, children: [] })
  })

  // 第二遍:建立父子关系
  list.forEach(item => {
    const node = map.get(item.id)
    if (item.parentId === null) {
      result.push(node)
    } else {
      const parent = map.get(item.parentId)
      if (parent) {
        parent.children.push(node)
      }
    }
  })

  return result
}

// 变体:仅用一次遍历(边插入边构建)
function buildTreeOncPass(list) {
  const map = new Map()
  const result = []

  list.forEach(item => {
    const node = { ...item, children: [] }
    map.set(item.id, node)

    if (item.parentId === null) {
      result.push(node)
    } else {
      const parent = map.get(item.parentId)
      if (parent) {
        parent.children.push(node)
      }
    }
  })

  return result
}

10.2 常见数据结构

Q: Map 和 Object 的区别?WeakMap 有什么用?

A:

特性MapObject
键的类型任意值字符串或 Symbol
键的顺序插入顺序(多数实现)无序(除数字键)
迭代可迭代(for...of)不可直接迭代
大小Map.sizeObject.keys().length
性能大量键值对时更优小规模时足够
原型链无(除非手动设置)有(可能有键名冲突)

WeakMap

  • 键必须是对象(不能是原始值)
  • 键是弱引用,不阻止 GC
  • 不可遍历(没有 size、keys 等)

WeakMap 应用场景

// 1. 私有属性
const privateData = new WeakMap()

class User {
  constructor(name) {
    privateData.set(this, { name, createdAt: Date.now() })
  }
  getName() {
    return privateData.get(this).name
  }
}

// 2. DOM 节点关联数据(不阻止 DOM 被 GC)
const cache = new WeakMap()
cache.set(domNode, computedStyles)
// 当 domNode 被移除,cache 中的关联数据也被回收

Q: 什么是发布-订阅模式?和观察者模式有什么区别?

A:

发布-订阅模式

class EventEmitter {
  constructor() {
    this.events = {}
  }

  on(event, listener) {
    if (!this.events[event]) {
      this.events[event] = []
    }
    this.events[event].push(listener)
    return () => this.off(event, listener) // 返回取消订阅函数
  }

  emit(event, ...args) {
    if (this.events[event]) {
      this.events[event].forEach(listener => listener(...args))
    }
  }

  off(event, listener) {
    this.events[event] = this.events[event].filter(l => l !== listener)
  }

  once(event, listener) {
    const wrapper = (...args) => {
      listener(...args)
      this.off(event, wrapper)
    }
    this.on(event, wrapper)
  }
}

// 使用
const emitter = new EventEmitter()

emitter.on('message', (msg) => console.log('Handler 1:', msg))
const unsubscribe = emitter.on('message', (msg) => console.log('Handler 2:', msg))

emitter.emit('message', 'Hello')  // 两个 handler 都执行
unsubscribe()                       // 取消订阅
emitter.emit('message', 'World')  // 只有 Handler 1 执行

发布-订阅 vs 观察者模式

特性发布-订阅观察者
中间层有 EventEmitter 中心无中间层
耦合度发布者和订阅者解耦目标和观察者耦合
管理统一管理事件订阅观察者自行注册
典型应用Vue 事件总线、Node EventEmitterVue 响应式、DOM 事件

十一、系统设计

11.1 设计一个实时协作白板

Q: 如何设计一个支持多人实时协作的白板应用?

A:

核心架构

┌──────────────────────────────────────────────────┐
│                    前端应用                        │
│  Canvas/Three.js + WebSocket + Operation Log     │
└────────────────────────┬─────────────────────────┘
                         │ WebSocket
┌────────────────────────┴─────────────────────────┐
│              WebSocket Gateway (Node.js)           │
│  - 广播消息到所有客户端                             │
│  - 处理房间逻辑                                    │
└────────────────────────┬─────────────────────────┘
                         │
┌────────────────────────┴─────────────────────────┐
│                    消息队列                         │
│         Redis Pub/Sub 或 Kafka                    │
└──────────────────────────────────────────────────┘

核心技术

  1. WebSocket 长连接:实时双向通信
  2. OT(Operational Transform)算法:解决并发冲突
    • 用户 A 和 B 同时修改同一个图形
    • 服务器通过 OT 转换,保证最终一致性
  3. Canvas 增量更新:只传输操作,而非全量画面
  4. Canvas 重播:新用户加入时,回放操作历史

数据结构设计

// 操作日志
{
  id: 'op-001',
  type: 'MOVE_SHAPE',
  shapeId: 'shape-001',
  from: { x: 100, y: 100 },
  to: { x: 200, y: 200 },
  timestamp: 1699999999999,
  userId: 'user-001'
}

降级策略

  • WebSocket 断开时切换到 HTTP 长轮询
  • 离线时记录本地操作,上线后同步

11.2 设计一个前端监控SDK

Q: 如何设计一个前端监控 SDK(性能、错误、行为)?

A:

监控维度

  1. 性能指标:FP、FCP、LCP、FID/INP、CLS、TTFB
  2. 错误监控:JS Error、Promise Rejection、资源加载失败
  3. 行为数据:PV/UV、点击热力图、页面停留时长
  4. 自定义事件:接口耗时、用户操作路径

SDK 架构

class MonitorSDK {
  constructor(options) {
    this.appId = options.appId
    this.endpoint = options.endpoint || '/api/report'
    this.queue = []
    this.maxQueueSize = options.maxQueueSize || 100
    this.flushInterval = options.flushInterval || 5000

    this.initPerformance()
    this.initError()
    this.initBehavior()
    this.startFlush()
  }

  // 性能监控
  initPerformance() {
    new PerformanceObserver((list) => {
      list.getEntries().forEach(entry => {
        this.report('performance', {
          name: entry.name,
          value: entry.value,
          rating: entry.rating
        })
      })
    }).observe({ entryTypes: ['paint', 'navigation', 'longtask'] })
  }

  // 错误监控
  initError() {
    window.addEventListener('error', (e) => {
      this.report('error', {
        type: 'js',
        message: e.message,
        filename: e.filename,
        lineno: e.lineno,
        colno: e.colno
      })
    })

    window.addEventListener('unhandledrejection', (e) => {
      this.report('error', {
        type: 'promise',
        message: e.reason?.message || e.reason
      })
    })
  }

  // 行为数据
  initBehavior() {
    document.addEventListener('click', (e) => {
      this.report('behavior', {
        type: 'click',
        x: e.clientX,
        y: e.clientY,
        target: e.target.tagName
      })
    })
  }

  // 数据上报
  report(type, data) {
    this.queue.push({
      appId: this.appId,
      type,
      data,
      timestamp: Date.now(),
      url: location.href,
      userId: this.getUserId()
    })

    if (this.queue.length >= this.maxQueueSize) {
      this.flush()
    }
  }

  async flush() {
    if (this.queue.length === 0) return
    const data = [...this.queue]
    this.queue = []
    await fetch(this.endpoint, {
      method: 'POST',
      body: JSON.stringify(data),
      keepalive: true  // 页面关闭时也发送
    })
  }

  startFlush() {
    setInterval(() => this.flush(), this.flushInterval)
  }
}

// 使用
const monitor = new MonitorSDK({
  appId: 'my-app',
  endpoint: '/api/monitor'
})

十二、开放性问题和反向提问

反向提问建议

Q: 你有什么问题想问我们的?

A:

技术相关

  • 前端团队目前的技术栈是什么?有计划引入新技术吗?
  • 团队如何做代码审查和技术债务管理?
  • 遇到技术难题时,团队如何做决策?

业务相关

  • 公司目前的核心业务方向是什么?
  • 前端在产品中的定位是怎样的?
  • 有哪些技术挑战是前端团队正在面对的?

团队相关

  • 前端团队有多少人?组织架构是怎样的?
  • 新人入职后的成长路径是什么?
  • 团队的技术分享氛围如何?

发展相关

  • 公司对技术专家的培养路径是怎样的?
  • 有没有技术晋升通道?