行为型模式:迭代器 / 命令 / 职责链 / 中介者 / 模板方法 / 状态
行为型模式关心对象之间如何分配职责与协作。观察者和策略已单独成篇,这里是另外六个。
一、迭代器模式(Iterator)
提供一种方法顺序访问一个聚合对象中的元素,而不暴露它的内部表示。
JS 已经把这个模式内置到语言里了:只要对象实现了 Symbol.iterator,就能用 for...of、展开运算符、解构。
class Range {
constructor(start, end, step = 1) {
Object.assign(this, { start, end, step });
}
[Symbol.iterator]() {
let current = this.start;
const { end, step } = this;
return {
next: () => current < end
? { value: (current += step) - step, done: false }
: { value: undefined, done: true },
[Symbol.iterator]() { return this; }, // 让迭代器自身也可迭代
};
}
}
[...new Range(0, 5)]; // [0, 1, 2, 3, 4]
for (const n of new Range(0, 3)) console.log(n);
用生成器写同一个东西要简单得多:
class Range {
constructor(start, end, step = 1) { Object.assign(this, { start, end, step }); }
*[Symbol.iterator]() {
for (let i = this.start; i < this.end; i += this.step) yield i;
}
}
迭代器真正的价值是"惰性"
它可以表示无限序列,也可以让每个元素用到时才计算:
function* naturals() { let n = 0; while (true) yield n++; } // 无限
function* take(iter, n) { for (const x of iter) { if (n-- <= 0) return; yield x; } }
[...take(naturals(), 5)]; // [0, 1, 2, 3, 4]
数组方法(map/filter)每一步都会生成完整的中间数组,而迭代器链是逐元素流过的,处理大数据集时内存优势明显。
异步迭代器用于流式数据(分页拉取、SSE、大文件读取):
async function* fetchPages(url) {
let page = 1;
while (true) {
const data = await fetch(`${url}?page=${page++}`).then(r => r.json());
if (!data.list.length) return;
yield* data.list; // 把每一项逐个抛出
}
}
for await (const item of fetchPages('/api/items')) console.log(item);
二、命令模式(Command)
把一个请求封装成对象,从而可以参数化、排队、记录日志,并支持撤销。
关键在于把"做什么"和"谁来做"解耦:按钮只管发命令,不关心谁执行、怎么执行。
// 每个命令封装 execute / undo 一对操作
const createAddTextCommand = (editor, text) => ({
name: 'addText',
execute() { editor.content += text; },
undo() { editor.content = editor.content.slice(0, -text.length); },
});
class CommandManager {
constructor() { this.history = []; this.redoStack = []; }
execute(command) {
command.execute();
this.history.push(command);
this.redoStack = []; // 执行新命令后,重做栈失效
}
undo() {
const command = this.history.pop();
if (!command) return;
command.undo();
this.redoStack.push(command);
}
redo() {
const command = this.redoStack.pop();
if (!command) return;
command.execute();
this.history.push(command);
}
}
撤销/重做是命令模式的杀手级应用:编辑器、画板、表单历史都靠它。此外还有:
- 宏命令:把多个命令组合成一个(配合组合模式);
- 请求排队/重放:把命令序列化存起来,离线时排队、恢复网络后重放;
- Redux 的 action 就是命令对象——
{ type, payload }描述"要做什么",reducer 负责"怎么做",因此才有时间旅行调试。
JS 里的简化形态
不需要撤销时,一个函数就是一个命令,不必定义类:
const commands = { save: () => {}, delete: () => {} };
button.onclick = () => commands[button.dataset.action]?.();
需要 undo 时才值得升级成对象。
三、职责链模式(Chain of Responsibility)
让多个对象都有机会处理请求,把它们连成一条链,请求沿链传递直到被处理。
识别信号:一长串 if/else if 在依次尝试处理同一个请求。
// ❌ 优惠逻辑层层嵌套,新增一档要改函数本身
function order(type, isPaid, count) {
if (type === 1) { if (isPaid) return '500 元定金,得 100 优惠券'; return order(3, isPaid, count); }
if (type === 2) { /* ... */ }
}
// ✅ 每个环节只关心自己,处理不了就交给下一个
const NEXT = Symbol('nextSuccessor');
class Chain {
constructor(fn) { this.fn = fn; this.successor = null; }
setNext(chain) { this.successor = chain; return chain; } // 返回 next 便于链式串联
pass(...args) {
const result = this.fn(...args);
if (result === NEXT) return this.successor?.pass(...args);
return result;
}
}
const order500 = new Chain((type, isPaid) =>
type === 1 && isPaid ? '500 元定金预购,得 100 优惠券' : NEXT);
const order200 = new Chain((type, isPaid) =>
type === 2 && isPaid ? '200 元定金预购,得 50 优惠券' : NEXT);
const orderNormal = new Chain((_type, _isPaid, count) =>
count > 0 ? '普通购买' : '库存不足');
order500.setNext(order200).setNext(orderNormal);
order500.pass(1, true, 500); // '500 元定金预购,得 100 优惠券'
order500.pass(3, false, 500); // '普通购买'
前端里最重要的职责链是中间件(洋葱模型)——Koa、Express、Redux middleware、axios 拦截器都是它:
function compose(middlewares) {
return function (ctx) {
let index = -1;
function dispatch(i) {
if (i <= index) return Promise.reject(new Error('next() 被重复调用'));
index = i;
const fn = middlewares[i];
if (!fn) return Promise.resolve();
return Promise.resolve(fn(ctx, () => dispatch(i + 1)));
}
return dispatch(0);
};
}
compose([
async (ctx, next) => { console.log('1 前'); await next(); console.log('1 后'); },
async (ctx, next) => { console.log('2 前'); await next(); console.log('2 后'); },
])({});
// 1 前 → 2 前 → 2 后 → 1 后
职责链的两个风险
- 请求可能走到链尾也没人处理——必须设计好兜底节点或明确的"未处理"返回值;
- 调试困难:出问题时不知道断在哪一环。给每个节点起名字、在开发环境打印链路,会省下大量时间。
其他应用:DOM 事件冒泡本身就是一条天然的职责链;表单校验的多级规则;错误处理的逐层上抛。
四、中介者模式(Mediator)
用一个中介对象封装一系列对象之间的交互,使各对象不必显式地相互引用。
它把网状的多对多依赖,变成星形的一对多:
改造前:A↔B, A↔C, A↔D, B↔C, B↔D, C↔D (n 个对象 → n(n-1)/2 条关系)
改造后:A↔M, B↔M, C↔M, D↔M (n 条关系)
// 表单联动:选了国家才能选城市,两者都填完才能提交
const formMediator = {
components: {},
register(name, comp) { this.components[name] = comp; comp.mediator = this; },
// 所有联动逻辑集中在这里,组件之间互不引用
notify(sender, event) {
const { country, city, submitBtn } = this.components;
if (sender === 'country' && event === 'change') {
city.setOptions(getCities(country.value));
city.setDisabled(!country.value);
}
submitBtn.setDisabled(!country.value || !city.value);
},
};
现实中的对应物:Redux/Vuex 的 store(组件不互相通信,都通过 store)、聊天室服务端、飞机与塔台。
中介者的代价
所有交互逻辑都集中到中介者里,它很容易膨胀成一个难以维护的上帝对象。当中介者本身开始变得复杂时,说明该按业务域把它拆开了。
与发布订阅的区别:事件中心只负责转发(不懂业务),中介者封装了协作规则(懂业务)。
五、模板方法模式(Template Method)
在父类中定义算法骨架,把某些步骤延迟到子类实现。
不变的是流程,变的是步骤的具体实现。
class Beverage {
// 模板方法:固定流程,不允许子类改
make() {
this.boilWater();
this.brew(); // 抽象步骤
this.pourInCup();
if (this.customerWantsCondiments()) { // 钩子方法:给子类可选的干预点
this.addCondiments();
}
}
boilWater() { console.log('把水煮沸'); }
pourInCup() { console.log('倒进杯子'); }
customerWantsCondiments() { return true; } // 默认实现,子类可覆盖
brew() { throw new Error('子类必须实现 brew'); }
addCondiments() { throw new Error('子类必须实现 addCondiments'); }
}
class Coffee extends Beverage {
brew() { console.log('用沸水冲泡咖啡'); }
addCondiments() { console.log('加糖和牛奶'); }
}
class Tea extends Beverage {
brew() { console.log('用沸水浸泡茶叶'); }
addCondiments() { console.log('加柠檬'); }
customerWantsCondiments() { return false; } // 通过钩子跳过某一步
}
JS 里更自然的写法是高阶函数
模板方法依赖继承,而 JS 可以直接把"变化的步骤"作为参数传进去,这也是组合优于继承的体现:
const makeBeverage = ({ brew, addCondiments, wantsCondiments = true }) => () => {
console.log('把水煮沸');
brew();
console.log('倒进杯子');
if (wantsCondiments) addCondiments();
};
const makeTea = makeBeverage({ brew: () => console.log('泡茶叶'), addCondiments: () => {}, wantsCondiments: false });
React 的生命周期、Vue 的钩子函数、各类构建工具的插件钩子,本质上都是模板方法:框架定好流程,你填空。
六、状态模式(State)
允许一个对象在内部状态改变时改变它的行为。
识别信号:一个方法里全是 if (this.state === 'xxx'),而且每加一个状态就要改遍所有方法。
// ❌ 每个方法都要判断当前状态,状态一多就是灾难
class Player {
play() { if (this.state === 'paused') { /* ... */ } else if (this.state === 'stopped') { /* ... */ } }
pause() { if (this.state === 'playing') { /* ... */ } /* ... */ }
}
// ✅ 把每个状态封装成对象,行为随状态自动切换
const states = {
stopped: {
play(player) { console.log('开始播放'); player.setState('playing'); },
pause() { console.log('已停止,无法暂停'); },
},
playing: {
play() { console.log('已在播放中'); },
pause(player) { console.log('暂停'); player.setState('paused'); },
},
paused: {
play(player) { console.log('继续播放'); player.setState('playing'); },
pause() { console.log('已经是暂停状态'); },
},
};
class Player {
constructor() { this.state = 'stopped'; }
setState(name) { this.state = name; }
play() { states[this.state].play(this); }
pause() { states[this.state].pause(this); }
}
好处是:新增一个状态只需要往 states 里加一项;每个状态的行为集中在一处,一眼能看全;非法的状态转移被自然地挡住了。
这正是**有限状态机(FSM)**的思想。复杂场景直接用现成的状态机库(XState):订单状态流转、上传/下载流程、多步表单、动画编排都非常适合。
与策略模式的区别:策略由外部选择且互不感知,状态由内部驱动切换且通常知道下一个状态是谁。
回到 👉 设计原则与模式总览
