行为型模式:迭代器 / 命令 / 职责链 / 中介者 / 模板方法 / 状态

行为型模式关心对象之间如何分配职责与协作观察者策略已单独成篇,这里是另外六个。

一、迭代器模式(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 后

职责链的两个风险

  1. 请求可能走到链尾也没人处理——必须设计好兜底节点或明确的"未处理"返回值;
  2. 调试困难:出问题时不知道断在哪一环。给每个节点起名字、在开发环境打印链路,会省下大量时间。

其他应用: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):订单状态流转、上传/下载流程、多步表单、动画编排都非常适合。

策略模式的区别:策略由外部选择且互不感知,状态由内部驱动切换且通常知道下一个状态是谁。


回到 👉 设计原则与模式总览

上次更新:
贡献者: Joe