设计模式

高频版 · 来自面试实战和个人笔记

1 设计模式

单一职责与开放封闭原则
单一职责原则:一个类只负责一个功能领域中的相应职责,或只有一个引起它变化的原因。
开放封闭原则:软件实体可扩展、不可修改。对扩展开放,对修改封闭。
单例模式
单例模式即一个类只能构造出唯一实例,意义在于共享、唯一。Redux/Vuex 的 store、JQ 的 $、购物车、登录框都是单例模式的应用。
class SingletonLogin {
    constructor(name, password) {
        this.name = name
        this.password = password
    }
    static getInstance(name, password) {
        if (!this.instance) {
            this.instance = new SingletonLogin(name, password)
        }
        return this.instance
    }
}

let obj1 = SingletonLogin.getInstance('CXK', '123')
let obj2 = SingletonLogin.getInstance('CXK', '321')
console.log(obj1 === obj2)  // true
工厂模式
工厂模式即对创建对象逻辑的封装,可理解为对 new 的封装。JQ 的 $()、React.createElement()、Vue.component() 都是工厂模式的实现。
class User {
  constructor(name, auth) {
    this.name = name
    this.auth = auth
  }
}

class UserFactory {
  static createUser(name, auth) {
    if (auth === 'admin') return new User(name, 1)
    if (auth === 'user') return new User(name, 2)
  }
}

const admin = UserFactory.createUser('cxk', 'admin');
const user = UserFactory.createUser('cxk', 'user');
观察者模式 & 发布订阅模式
观察者模式:观察者监听被观察者的变化,被观察者发生改变时通知所有观察者。发布订阅相较于观察者模式多一个调度中心(事件总线),发布者和订阅者之间没有直接联系,解耦度更高。

观察者模式实现

class Subject {
  constructor() { this.observers = []; }
  subscribe(observer) { this.observers.push(observer); }
  unsubscribe(observer) { this.observers = this.observers.filter(obs => obs !== observer); }
  notify(data) { this.observers.forEach(observer => observer.update(data)); }
}

class Observer {
  constructor(name) { this.name = name; }
  update(data) { console.log(`${this.name} received data: ${data}`); }
}

发布订阅模式实现(带事件中心)

class EventHub {
  constructor() { this.eventMap = {}; }
  subscribe(eventName, callback) {
    if (!this.eventMap[eventName]) this.eventMap[eventName] = [];
    this.eventMap[eventName].push(callback);
  }
  publish(eventName, data) {
    if (this.eventMap[eventName])
      this.eventMap[eventName].forEach(callback => callback(data));
  }
  unsubscribe(eventName, callback) {
    if (this.eventMap[eventName])
      this.eventMap[eventName] = this.eventMap[eventName].filter(cb => cb !== callback);
  }
}
装饰器模式
装饰器模式是对类的包装,动态拓展类的功能。ES7 装饰器、React 高阶组件、react-redux 的 connect() 都是这一模式的实现。
function info(target) {
  target.prototype.name = '张三'
  target.prototype.age = 10
}

@info
class Man {}

let man = new Man()
man.name  // 张三

函数装饰器示例:

function logDecorator(originalFunction) {
  return function(...args) {
    console.log(`Calling function with arguments: ${args}`);
    const result = originalFunction(...args);
    console.log(`Function called. Result: ${result}`);
    return result;
  };
}
const decoratedGreet = logDecorator(greet);
适配器模式
适配器模式将一个接口转换成客户希望的另一个接口,使接口不兼容的类可以一起工作。封装旧 API 是典型场景。
class Adaptee {
  test() { return '旧接口' }
}

class Target {
  constructor() { this.adaptee = new Adaptee() }
  test() {
    let info = this.adaptee.test()
    return `适配${info}`
  }
}

let target = new Target()
console.log(target.test())  // 适配旧接口
代理模式
代理模式为一个对象找一个替代对象,以便对原对象进行访问。事件代理、$.proxy、ES6 Proxy 都是这一模式的实现。
const idol = {
  name: '蔡x抻',
  phone: 10086,
  price: 1000000
}

const agent = new Proxy(idol, {
  get: function(target) {
    return '经纪人电话:10010'
  },
  set: function(target, key, value) {
    if (key === 'price') {
      if (value < target.price) throw new Error('报价过低')
      target.price = value
    }
  }
})

agent.phone        // 经纪人电话:10010
agent.price = 100  // Uncaught Error: 报价过低

2 V8 垃圾回收

分代回收
V8 的 GC 采用分代回收。大多数对象「朝生夕死」,所以将内存分为新生代和老生代分别处理。
  • 新生代:刚创建的对象,来得快死得也快。频繁、小成本地清理。
  • 老生代:存活多轮的对象,回收频率更低,但回收更重。
Scavenge(新生代)
新生代使用 Scavenger(半空间复制)算法。将新生代分为 From 和 To 两块区域。
  • 新对象分配在 From 空间
  • GC 时从根对象找存活对象,复制到 To 空间
  • 没被复制的默认是垃圾
  • 交换 From/To 角色
  • 存活多轮的对象会晋升(promote)到老生代

Minor GC 便宜,因为只扫描新生代,通过 write barrier 和 remembered set 记录「老指新」引用,无需扫描整个老生代。

Mark-Sweep & Mark-Compact(老生代)
老生代使用 Major GC,流程:
  • Mark(标记):从根遍历,把还能访问到的对象标记为「活着」
  • Sweep(清除):把没标记的对象对应内存回收到空闲链表
  • Compact(压缩):碎片多时把活对象往一起挪,减少内存碎片
增量标记与 Orinoco
为减少 GC 停顿,V8 引入 Orinoco 工程:
  • Parallel:多线程一起做 GC
  • Incremental:把 GC 工作拆成多小段
  • Concurrent:后台线程并发标记,尽量不阻塞主线程

口播模板:V8 的 GC 采用分代回收。新生代用 Scavenger 复制式回收,成本低、消除碎片。存活多轮晋升老生代。老生代用 Mark-Sweep 和 Mark-Compact。为减少停顿引入并行、增量、并发 GC,官方称作 Orinoco。

3 内存泄漏

JS 中的内存泄漏场景
  • 未解除的事件监听器:元素移除时未 removeEventListener,监听器仍持有引用
  • 未清理的全局变量:尽量避免,用 let/const,不用时置 null
  • 闭包:确保只保留需要的引用
  • DOM 引用未释放:移除 DOM 后将引用置 null
  • 定时器:用 clearInterval/clearTimeout 清理
  • 缓存:用 WeakMap/WeakSet 或定期清理
// 事件监听器清理
button.removeEventListener('click', handleClick);

// 定时器清理
clearInterval(intervalId);

// DOM 引用释放
element = null;
React 中的内存泄漏
三方面:请求、定时器、事件。组件卸载后未完成的异步请求会尝试更新已卸载组件,导致警告或内存泄漏。

1. 取消未完成的异步请求(isMounted 或 AbortController)

useEffect(() => {
  let isMounted = true;
  fetchData().then(response => {
    if (isMounted) setData(response);
  });
  return () => { isMounted = false; };
}, []);

2. AbortController 取消 fetch

useEffect(() => {
  const controller = new AbortController();
  fetch('https://api.example.com/data', { signal: controller.signal })
    .then(r => r.json())
    .then(setData)
    .catch(err => { if (err.name !== 'AbortError') throw err; });
  return () => controller.abort();
}, []);

3. useEffect cleanup 清除定时器与事件

useEffect(() => {
  const intervalId = setInterval(() => console.log('tick'), 1000);
  return () => clearInterval(intervalId);
}, []);

useEffect(() => {
  const handleResize = () => console.log('resized');
  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, []);

4 进程与线程

进程 vs 线程
  • 进程:资源分配的基本单位。每个进程有独立的内存空间、系统资源。创建/销毁开销大。进程崩溃通常不影响其他进程。
  • 线程:进程中的执行路径,共享进程资源。创建/销毁开销小。线程崩溃可能导致整个进程崩溃。
  • 通信:进程间用 IPC(消息队列、管道、共享内存等);线程间直接共享内存。
浏览器多进程架构
现代浏览器采用多进程架构,典型包括:
  • Browser 进程:负责 UI、网络、存储等
  • Renderer 进程:每个标签页一个,负责页面渲染、JS 执行
  • GPU 进程:GPU 加速
  • Plugin 进程:插件

多进程隔离保证单个标签页崩溃不会影响其他标签页。

Web Worker
Web Worker 让 JS 在后台线程运行,不阻塞主线程。Worker 与主线程通过 postMessage 通信,共享数据需序列化(结构化克隆)。
// 主线程
const worker = new Worker('worker.js');
worker.postMessage({ data: 'hello' });
worker.onmessage = (e) => console.log(e.data);

// worker.js
self.onmessage = (e) => {
  self.postMessage({ result: e.data.data + ' processed' });
};