设计模式
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' });
};