Flyweight Pattern
享元模式的实现与应用场景
问题
什么是享元模式?如何在 JavaScript 中实现?
解答
享元模式通过共享对象来减少内存占用,将对象的状态分为:
- 内部状态:可共享,存储在享元对象中
- 外部状态:不可共享,由客户端传入
基本实现
// 享元类 - 存储内部状态(可共享)
class Flyweight {
constructor(sharedState) {
this.sharedState = sharedState;
}
operation(uniqueState) {
const s = JSON.stringify(this.sharedState);
const u = JSON.stringify(uniqueState);
console.log(`Flyweight: 共享(${s}) 独有(${u})`);
}
}
// 享元工厂 - 管理享元对象的创建和复用
class FlyweightFactory {
constructor() {
this.flyweights = {};
}
getKey(state) {
return state.join('_');
}
getFlyweight(sharedState) {
const key = this.getKey(sharedState);
if (!this.flyweights[key]) {
console.log('创建新享元:', key);
this.flyweights[key] = new Flyweight(sharedState);
} else {
console.log('复用已有享元:', key);
}
return this.flyweights[key];
}
getCount() {
return Object.keys(this.flyweights).length;
}
}
// 使用
const factory = new FlyweightFactory();
// 相同内部状态会复用同一个对象
const fw1 = factory.getFlyweight(['Toyota', 'Camry']);
const fw2 = factory.getFlyweight(['Toyota', 'Camry']); // 复用
const fw3 = factory.getFlyweight(['BMW', 'X5']);
fw1.operation({ owner: 'Alice', plates: 'ABC123' });
fw2.operation({ owner: 'Bob', plates: 'XYZ789' });
console.log('享元对象总数:', factory.getCount()); // 2
实际应用:文本编辑器字符渲染
// 字符享元 - 共享字体样式
class CharacterFlyweight {
constructor(font, size, color) {
this.font = font;
this.size = size;
this.color = color;
}
render(char, x, y) {
console.log(`渲染 "${char}" 在 (${x},${y}) 样式: ${this.font}/${this.size}/${this.color}`);
}
}
// 字符享元工厂
class CharacterFactory {
constructor() {
this.cache = new Map();
}
getCharacter(font, size, color) {
const key = `${font}-${size}-${color}`;
if (!this.cache.has(key)) {
this.cache.set(key, new CharacterFlyweight(font, size, color));
}
return this.cache.get(key);
}
}
// 文档类 - 管理字符
class Document {
constructor() {
this.characters = [];
this.factory = new CharacterFactory();
}
addCharacter(char, font, size, color, x, y) {
const flyweight = this.factory.getCharacter(font, size, color);
// 只存储字符和位置(外部状态)
this.characters.push({ char, flyweight, x, y });
}
render() {
this.characters.forEach(({ char, flyweight, x, y }) => {
flyweight.render(char, x, y);
});
}
}
// 使用
const doc = new Document();
// 1000 个字符,但样式对象只有少数几个
for (let i = 0; i < 1000; i++) {
doc.addCharacter(
String.fromCharCode(65 + (i % 26)),
'Arial',
i % 2 === 0 ? '12px' : '14px',
i % 3 === 0 ? 'red' : 'black',
i * 10,
Math.floor(i / 100) * 20
);
}
console.log('字符数:', doc.characters.length); // 1000
console.log('享元对象数:', doc.factory.cache.size); // 4 (2种大小 × 2种颜色)
DOM 事件委托(享元思想)
// 不使用享元:每个按钮绑定事件
document.querySelectorAll('.btn').forEach(btn => {
btn.addEventListener('click', handleClick); // 创建多个处理函数
});
// 使用享元:事件委托
document.getElementById('container').addEventListener('click', (e) => {
if (e.target.matches('.btn')) {
// 共享一个处理函数,通过 e.target 获取外部状态
const id = e.target.dataset.id;
handleClick(id);
}
});
关键点
- 内部状态共享:多个对象共享相同的内部数据,减少对象数量
- 外部状态分离:不可共享的数据由客户端维护和传入
- 工厂模式配合:通过工厂管理享元对象的创建和缓存
- 适用场景:大量相似对象、对象的大部分状态可外部化、内存敏感的应用
- 前端应用:事件委托、对象池、虚拟列表中的 DOM 复用
目录