编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

    • 基础入门

    • 综合案例

    • 专栏博客

      • README
      • 引擎解析编译执行
      • 隐藏类与回收机制
      • 类型隐式转换精算
      • 作用域链闭包原理
      • 函数绑定规则组合
      • 原型链语法糖本质
      • 代理与元编程协议
      • 事件循环承诺机制
      • 工作线程并发调度
      • 页面渲染像素原理
      • 网络接口存储架构
      • 服务端运行时编程
      • 模块系统双轨操作
      • 现代工程链三件套
      • 设计模式函数哲学
        • 1. 案例与疑问引入
          • 1.1 「点击按钮→请求
          • 1.2 顺藤摸到根因
          • 1.3 我们要回答什么
        • 2. 架构全景概览
          • 2.1 设计模式三大家族
          • 2.2 JS 中「模式」
        • 3. 单例与工厂
          • 3.1 IIFE 缓存
          • 3.2 工厂模式:一个函
        • 4. 观察者 vs 发
          • 4.1 EventEmi
          • 4.2 观察者(Subj
          • 4.3 与 Vue 的
        • 5. 策略与命令模式
          • 5.1 策略模式:表单校
          • 5.2 命令模式:撤销
        • 6. 适配器代理装饰
          • 6.1 三者长得像但有本
          • 6.2 TC39 Sta
        • 7. 纯函数与副作用隔离
          • 7.1 相同的输入 →
          • 7.2 为什么 useE
          • 7.3 副作用隔离的四种
        • 8. 不可变数据详解
          • 8.1 structur
          • 8.2 为什么 Reac
        • 9. pipe与May
          • 9.1 把 10 层嵌套
          • 9.2 Maybe
          • 9.3 TC39 Sta
        • 10. 综合案例串讲
          • 10.1 案例真相揭晓
          • 10.2 一个「订单处理管
          • 10.3 设计哲学回扣
          • 10.4 速查表速览
      • 跨端架构终局总结
  • CodeX
  • JavaScript入门
  • 专栏博客
杨充
2026-06-11
目录

设计模式函数哲学

# 15.设计模式与函数式哲学

📍 上接第 14 篇《现代工程链三件套》。工具链已齐全。本文回答:同样的功能,为什么有人写 5 行、有人写 50 行——设计模式与函数式编程如何让代码更有「韧劲」。

# 目录介绍

  • 1. 案例与疑问引入
    • 1.1 「点击按钮→请求
    • 1.2 顺藤摸到根因
    • 1.3 我们要回答什么
  • 2. 架构全景概览
    • 2.1 设计模式三大家族
    • 2.2 JS 中「模式」
  • 3. 单例与工厂
    • 3.1 IIFE 缓存
    • 3.2 工厂模式:一个函
  • 4. 观察者 vs 发
    • 4.1 EventEmi
    • 4.2 观察者(Subj
    • 4.3 与 Vue 的
  • 5. 策略与命令模式
    • 5.1 策略模式:表单校
    • 5.2 命令模式:撤销
  • 6. 适配器代理装饰
    • 6.1 三者长得像但有本
    • 6.2 TC39 Sta
  • 7. 纯函数与副作用隔离
    • 7.1 相同的输入 →
    • 7.2 为什么 useE
    • 7.3 副作用隔离的四种
  • 8. 不可变数据详解
    • 8.1 structur
    • 8.2 为什么 Reac
  • 9. pipe与May
    • 9.1 把 10 层嵌套
    • 9.2 Maybe
    • 9.3 TC39 Sta
  • 10. 综合案例串讲
    • 10.1 案例真相揭晓
    • 10.2 一个「订单处理管
    • 10.3 设计哲学回扣
    • 10.4 速查表速览

# 1. 案例与疑问引入

# 1.1 「点击按钮→请求

一个产品经理走过来:"订单列表页加一个刷新按钮——点一下请求后端、更新页面、禁用按钮直到请求回来。"

你的第一个版本:

// 写法 A:功能能跑——但经不起"变化"
async function handleClick() {
    const btn = document.querySelector('#refresh');
    const list = document.querySelector('#list');
    const errorDiv = document.querySelector('#error');

    btn.disabled = true;
    errorDiv.hidden = true;

    const res = await fetch('/api/orders');
    if (!res.ok) {
        errorDiv.textContent = '加载失败';
        errorDiv.hidden = false;
        btn.disabled = false;
        return;
    }

    const data = await res.json();
    list.innerHTML = data.map(d => `<li>${d.name} - ¥${d.amount}</li>`).join('');
    btn.disabled = false;
}

btn.addEventListener('click', handleClick);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

周一上线。周三,产品经理又来:"需求改了——加上防抖(500ms 内连点只触发一次)、出错自动重试 2 次、加载中显示骨架屏。"

你看着那段代码沉默了——每个新增需求都意味着要在这 20 行里再塞 5 行,到第六次变更时它已经变成 50 行的泥潭。

现在看另一种写法——把"加载态、获取数据、渲染、错误处理"拆成独立模块:

// 写法 B:每个部分独立、可测试、可组合

// ① 加载态管理——纯函数组合(高阶函数)
const withLoading = (btn) => (fn) => async (...args) => {
    btn.disabled = true;
    try {
        return await fn(...args);
    } finally {
        btn.disabled = false;
    }
};

// ② 数据获取——分离业务逻辑
const fetchOrders = () =>
    fetch('/api/orders').then(r => r.ok ? r.json() : Promise.reject(r));

// ③ 渲染——纯展示逻辑
const render = (container) => (data) => {
    container.innerHTML = data.map(d => `<li>${d.name} - ¥${d.amount}</li>`).join('');
};

// ④ 错误处理——独立策略
const showError = (target) => (err) => {
    target.hidden = false;
    target.textContent = err.message || '加载失败';
};

// ⑤ 防抖——通用的工具函数(装饰器模式)
const debounce = (fn, delay) => {
    let timer;
    return (...args) => {
        clearTimeout(timer);
        timer = setTimeout(() => fn(...args), delay);
    };
};

// ⑥ 重试——通用策略(装饰器模式)
const withRetry = (fn, maxRetry = 2) => async (...args) => {
    for (let i = 0; i <= maxRetry; i++) {
        try { return await fn(...args); }
        catch (e) { if (i === maxRetry) throw e; }
    }
};

// ⑦ 组装——pipe 组合(函数式编程)
const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x);

const handleRefresh = pipe(
    debounce,
    withLoading(btn),
    () => withRetry(fetchOrders)().then(render(list))
)(() => {});  // 初始输入为空函数

btn.addEventListener('click', handleRefresh);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

# 1.2 顺藤摸到根因

带着两个版本的差异往下挖:

  • 假设 1:"A 和 B 功能一样,B 只是代码多一点"——错。A 的加载态、数据获取、渲染全部耦合在同一个函数里。B 的每个部分都可以独立修改、独立测试、独立复用。如果另一个页面也需要防抖 + 重试,A 要复制全部代码,B 只需要组合已有的工具函数。

  • 假设 2:"设计模式就是语法改写,换种写法而已"——错。模式的核心不是"怎么写",而是"把什么和什么解耦"。单例模式把"对象创建时机"和"对象使用者"解耦。观察者模式把"数据变化"和"响应行为"解耦。策略模式把"条件分支"和"每种条件的具体处理"解耦。

  • 假设 3:"函数式编程就是不用 class、用 map 代替 for"——错。FP 的本质是"数据流过函数"——数据是不可变的、函数是纯的、组合代替嵌套。React 的渲染是 state → UI 的纯函数映射,Redux 的 reducer 是 (state, action) → newState 的纯函数——它们背后的哲学是一致的。

  • 假设 4:"不可变数据就是 {...obj} 浅拷贝"——不完全是。不可变的核心是结构共享(Structural Sharing)——不是每次都完整深拷贝,而是只复制"改变了的那条路径"上的节点,其余部分共享引用。Immer 正是通过 Proxy 实现了这个。

  • 假设 5:"pipe 就是一行语法糖"——错。pipe 的本质是把嵌套变成并列。h(g(f(x))) 和 pipe(f, g, h)(x) 做的事情完全一样,但前者让你从内向外反着读,后者让你从数据开始正着读——这是认知维度上的降级。

这一段重构里至少藏着 7 个模式/哲学点:

① 高阶层函数(withLoading)替代 try/finally 模板代码        → 第 7 章(纯函数/副作用隔离)
② 策略表(fetchOrders/render/showError 各自独立)              → 第 5.1 节(策略模式)
③ 装饰器模式(debounce/withRetry 不改变原函数签名)            → 第 6.1 节(装饰器 vs 代理 vs 适配器)
④ 命令模式(每次操作封装为对象,天然 undo/redo)               → 第 5.2 节(命令模式)
⑤ 观察者模式(事件监听 btn.addEventListener)                 → 第 4 章(观察者 vs 发布订阅)
⑥ 不可变数据(每次返回新对象,原对象不受影响)                  → 第 8 章(不可变数据)
⑦ 组合(pipe 把分离的函数重新串起来)                          → 第 9 章(pipe/compose)
1
2
3
4
5
6
7

# 1.3 我们要回答什么

这个按钮场景就是本篇的主线案例。我们带着上面 7 个模式/哲学点往下走:

  1. ① 单例/工厂——JS 中如何创建和管理"唯一实例"?
  2. ② 观察者 vs 发布订阅——两者经常被混淆,本质区别在哪?
  3. ③ 策略/命令——策略表替代 if/else,命令栈支持 undo/redo
  4. ④ 适配器/代理/装饰器——三者接口不变但要区分核心目的
  5. ⑤ 纯函数与副作用——为什么 React 把副作用放到 useEffect?
  6. ⑥ 不可变数据——spread/structuredClone/Immer 的选择矩阵
  7. ⑦ pipe/Maybe——如何用组合替代嵌套、用 Maybe 消除 null 防御

本篇路线:

架构总图 (第 2 章) ─→ 三大家族 + 函数式范式的关系
   ↓
单例/工厂 (第 3 章) ─→ 创建对象的方式选择
   ↓
观察者/发布订阅 (第 4 章) ─→ 事件通信的两种模型
   ↓
策略/命令 (第 5 章) ─→ 行为型模式的生产落地
   ↓
适配器/代理/装饰器 (第 6 章) ─→ 结构型模式逐字辨析
   ↓
纯函数/副作用 (第 7 章) ─→ FP 的核心约束
   ↓
不可变数据 (第 8 章) ─→ 三种方案 + React 为什么需要它
   ↓
pipe/Maybe (第 9 章) ─→ 组合与安全链式调用
   ↓
综合案例 (第 10 章) ─→ 订单管线重构 + 设计哲学 + 速查表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

📌 本篇定位:这是 JavaScript 专栏中「代码组织哲学」的总成篇。前 14 篇讲了引擎怎么执行、作用域怎么绑定、事件循环怎么调度、模块怎么组织——本篇回答:在这些基础之上,你的代码应该长成什么形状。读完本篇后,面对任何一段需要重构的旧代码,都能立刻回答:"用哪个模式重写、代价是什么"。

# 2. 架构全景概览

# 2.1 设计模式三大家族

先把面向对象的设计模式和函数式编程范式放在一张全景图里:

┌─────────────────────────────────────────────────────────────┐
│                    面向对象设计模式 (GoF 23)                   │
│                                                             │
│  ┌─────────────┐   ┌─────────────┐   ┌─────────────┐        │
│  │  创建型      │   │  结构型      │   │  行为型      │        │
│  │  (怎么造)    │   │  (怎么拼)    │   │  (怎么聊)    │        │
│  ├─────────────┤   ├─────────────┤   ├─────────────┤        │
│  │ 单例 → 第3章 │   │ 适配器      │   │ 观察者      │        │
│  │ 工厂 → 第3章 │   │ 代理        │   │ 发布订阅    │        │
│  │ 建造者      │   │ 装饰器 → 第6章│   │ 策略 → 第5章 │        │
│  │ 原型        │   │ 外观        │   │ 命令 → 第5章 │        │
│  └──────┬──────┘   │ 桥接        │   │ 模板方法    │        │
│         │          │ 组合        │   │ 迭代器(第7篇)│        │
│         │          └──────┬──────┘   │ 状态        │        │
│         │                 │          └──────┬──────┘        │
│         └─────────┬───────┴─────────┬───────┘              │
│                   │                 │                       │
│                   ▼                 ▼                       │
├─────────────────────────────────────────────────────────────┤
│              函数式编程范式 (FP)                              │
│                                                             │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────┐ │
│  │ 纯函数 → 第7章   │  │ 不可变 → 第8章   │  │ 组合 → 第9章 │ │
│  │ 副作用隔离       │  │ 数据即快照       │  │ pipe/compose │ │
│  │ 引用透明         │  │ 结构共享         │  │ Maybe/Either │ │
│  └─────────────────┘  └─────────────────┘  └─────────────┘ │
│                                                             │
│  共同目标:让代码对"变化"更有韧劲                              │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

两者的交集与互补关系:

维度 OOP 设计模式 函数式编程
抽象单元 对象(状态 + 行为) 函数(输入 → 输出)
变化管理 面向接口编程,封装变化 纯函数 + 不可变数据,消除副作用
代码复用 继承 + 组合 高阶函数 + 函数组合
典型案例 策略模式(策略对象)、观察者模式(Subject/Observer) pipe(fn1, fn2, fn3)(data)、Maybe 链式调用
在 JS 中 模式通过对象实现,但不需要 Java 式的类继承 函数是一等公民,天然支持高阶函数

# 2.2 JS 中「模式」

疑惑:"用了 Vue/React 还需要学设计模式吗?框架不是已经帮我做了?"

论证:框架确实内置了模式——但你不知道它在"替你用了哪个模式"时,调试和优化就无从下手:

Vue 3 内置模式:
  reactive()         → 代理模式(Proxy 拦截对象操作)
  watch()            → 观察者模式(响应式值变化 → 回调)
  computed()         → 策略模式(惰性求值的缓存策略)
  provide/inject     → 依赖注入
  <slot>            → 组合模式(内容分发)
  Pinia store        → 单例模式(全局唯一 store 实例)

React 内置模式:
  useState/useReducer → 命令模式(action → state 的单向转变)
  useEffect          → 副作用隔离(纯渲染 vs 非纯操作)
  useMemo/useCallback → 记忆化(Memoization Pattern)
  Context            → 发布订阅模式(Provider 发布,Consumer 订阅)
  HOC (高阶组件)      → 装饰器模式(增强组件不改变接口)
  Children prop      → 组合模式(组合优于继承)

Redux 内置模式:
  Store              → 单例模式
  dispatch(action)   → 命令模式
  reducer(state, action) → 策略模式(action.type → 对应的处理函数)
  subscribe(listener) → 观察者模式
  Middleware          → 装饰器模式(增强 dispatch 不改变签名)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

结论:框架是模式的应用,模式是框架的设计文档。当你在 React 中写出一个自定义 Hook 时,你可能无意中用了"工厂模式"(返回一组方法)或"策略模式"(根据参数选择行为)。知道这些,就能写出更符合框架哲学的自定义代码——而不是用 React 写出 Java 的风格。

# 3. 单例与工厂

# 3.1 IIFE 缓存

疑惑:Java 里写单例需要 private constructor + static getInstance() + 双重检查锁——JS 里为什么一行 export const db = connect() 就够了?

论证:因为 ES6 模块具有模块级缓存——一个模块无论被 import 多少次,它的顶层代码只执行一次。这个特性天然实现了单例:

// db.js —— 模块文件
import mysql from 'mysql2/promise';

// 这行代码在模块加载时执行一次,结果被缓存
const pool = mysql.createPool({
    host: 'localhost',
    user: 'root',
    database: 'myapp'
});

// 每个 import 这个模块的文件拿到的都是同一个 pool
export default pool;
1
2
3
4
5
6
7
8
9
10
11
12
// userService.js          // orderService.js
import db from './db.js';  import db from './db.js';
// db === 同一个 pool       // db === 同一个 pool
1
2
3

三种单例实现对比:

方式 代码量 懒加载 线程安全 适用场景
ES6 模块顶层导出 1 行 ❌(加载时即创建) ✅(模块单线程加载) 服务初始化、连接池
IIFE + 闭包 5 行 ✅ ✅ 浏览器环境需要懒加载时
Proxy construct trap 6 行 ✅ ✅ 需要拦截 new 操作时

懒加载单例——IIFE + 闭包:

// 只有在第一次调用 getDB() 时才创建实例
const getDB = (() => {
    let instance = null;
    return () => {
        if (!instance) {
            console.log('Creating DB connection...');
            instance = connectDB();
        }
        return instance;
    };
})();

// 适用:初始化成本高、且不一定会用到的实例
const db = getDB();  // ← 第一次调用时才真正创建
1
2
3
4
5
6
7
8
9
10
11
12
13
14

用 Proxy construct trap 实现单例(参见第 07 篇第 4.2 节):

const SingletonDB = new Proxy(Database, {
    construct(target, args, newTarget) {
        if (!SingletonDB._instance) {
            SingletonDB._instance = Reflect.construct(target, args, newTarget);
        }
        return SingletonDB._instance;
    }
});
// new SingletonDB() → 永远返回同一个实例
1
2
3
4
5
6
7
8
9

# 3.2 工厂模式:一个函

疑惑:"工厂模式不就是把 new 包在一个函数里吗?值得一个模式?"

论证:工厂的真正价值不是"隐藏 new",而是把"创建什么"的决策权从调用方手里移走——让调用方不需要知道具体子类的名字:

// ❌ 调用方需要知道所有子类——每次新增图表类型都得改这里
function renderChart(type, data) {
    let chart;
    if (type === 'line')      chart = new LineChart(data);
    else if (type === 'bar')  chart = new BarChart(data);
    else if (type === 'pie')  chart = new PieChart(data);
    else if (type === 'radar')chart = new RadarChart(data);
    // ... 新增一种类型 → 新增一行 if/else → 违反开闭原则
    chart.render();
}

// ✅ 策略表——新增类型只加一行配置,不碰业务逻辑
const chartFactories = {
    line:   (data) => new LineChart(data),
    bar:    (data) => new BarChart(data),
    pie:    (data) => new PieChart(data),
    radar:  (data) => new RadarChart(data),
};

function renderChart(type, data) {
    const factory = chartFactories[type];
    if (!factory) throw new Error(`Unknown chart type: ${type}`);
    factory(data).render();
}

// 新增类型 = 往对象里加一行
chartFactories.heatmap = (data) => new HeatmapChart(data);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

工厂模式的四种 JS 落地方式:

// ① 简单工厂——函数直接返回对象(最常用)
function createUser(type, name) {
    if (type === 'admin') return { name, role: 'admin', permissions: ['all'] };
    if (type === 'guest') return { name, role: 'guest', permissions: ['read'] };
    throw new Error(`Unknown user type: ${type}`);
}

// ② 注册式工厂——运行时动态注册(插件系统的基础)
class ServiceRegistry {
    #services = new Map();
    register(name, factory) { this.#services.set(name, factory); }
    create(name, ...args) {
        const factory = this.#services.get(name);
        if (!factory) throw new Error(`Service '${name}' not registered`);
        return factory(...args);
    }
}
// 第三方插件可以在运行时 registry.register('payment', () => new StripePayment());

// ③ React 的 createElement——工厂模式的语言级实现
React.createElement('div', { className: 'box' }, 'Hello');
// createElement 根据第一个参数(字符串/组件类/函数)决定创建什么

// ④ 测试中的 Factory Function(Fixture Factory)
function createTestOrder(overrides = {}) {
    return {
        id: Math.random(),
        items: [{ name: 'test', price: 100 }],
        status: 'pending',
        ...overrides,  // 允许测试按需覆盖
    };
}
const order = createTestOrder({ status: 'paid' });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 4. 观察者 vs 发

# 4.1 EventEmi

手写一个生产级的 EventEmitter——支持通配符、最大监听数限制、错误隔离:

class EventEmitter {
    #listeners = new Map();  // event → Set<{ fn, once }>
    #maxListeners = 10;

    on(event, fn) {
        if (!this.#listeners.has(event)) {
            this.#listeners.set(event, new Set());
        }
        const listeners = this.#listeners.get(event);

        // 最大监听数告警
        if (listeners.size >= this.#maxListeners) {
            console.warn(`MaxListenersExceeded: ${event} has ${listeners.size} listeners`);
        }

        listeners.add({ fn, once: false });
        return this;  // 支持链式调用
    }

    once(event, fn) {
        if (!this.#listeners.has(event)) {
            this.#listeners.set(event, new Set());
        }
        this.#listeners.get(event).add({ fn, once: true });
        return this;
    }

    off(event, fn) {
        const listeners = this.#listeners.get(event);
        if (!listeners) return this;

        // 找到并删除匹配的监听器
        for (const item of listeners) {
            if (item.fn === fn) {
                listeners.delete(item);
                break;
            }
        }
        if (listeners.size === 0) this.#listeners.delete(event);
        return this;
    }

    emit(event, ...args) {
        const listeners = this.#listeners.get(event);
        if (!listeners) return false;

        const toRemove = [];  // 收集需要删除的 once 监听器

        for (const item of [...listeners]) {  // 拷贝一份,防止回调中修改 listeners
            try {
                item.fn(...args);
            } catch (err) {
                // 错误隔离——一个监听器挂了不影响其他
                console.error(`Error in listener for '${event}':`, err);
            }
            if (item.once) toRemove.push(item);
        }

        // 清理 once 监听器
        for (const item of toRemove) listeners.delete(item);

        return true;
    }

    // 移除所有监听器
    removeAllListeners(event) {
        if (event) this.#listeners.delete(event);
        else this.#listeners.clear();
        return this;
    }

    // 获取事件监听的函数数量
    listenerCount(event) {
        return this.#listeners.get(event)?.size ?? 0;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76

# 4.2 观察者(Subj

疑惑:"观察者模式和发布订阅模式不就是同一个东西吗?"

论证:两者有本质的耦合度差异——这是面试中被问最多的辨析点:

观察者模式 (Observer Pattern):
┌──────────────┐         直接调用          ┌──────────────┐
│   Subject    │ ───────────────────────► │   Observer   │
│ (被观察对象)  │                          │  (观察者)     │
│              │                          │              │
│ + addObs(o)  │  Subject 知道 Observer    │ + update()   │
│ + removeObs()│  的存在,维护 observerList │              │
│ + notify()   │                          │              │
└──────────────┘                          └──────────────┘
耦合关系:Subject 直接持有 Observer 引用

发布订阅模式 (Pub/Sub Pattern):
┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│  Publisher   │────►│  EventBus    │────►│  Subscriber  │
│  (发布者)     │     │  (事件总线)   │     │  (订阅者)     │
│              │     │              │     │              │
│ publisher     │     │ + on(event)  │     │ subscriber   │
│ 不知道谁接收   │     │ + emit(event)│     │ 不知道谁发布  │
└──────────────┘     └──────────────┘     └──────────────┘
耦合关系:Publisher 和 Subscriber 完全不知道对方存在——通过 EventBus 解耦
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

代码对比:

// 观察者模式:Subject 直接维护 Observer 列表
class Subject {
    #observers = new Set();
    addObserver(obs) { this.#observers.add(obs); }
    notify(data) { this.#observers.forEach(obs => obs.update(data)); }
}

// 发布订阅:通过独立的 EventBus
const bus = new EventEmitter();

// publisher.js(不知道谁在听)
bus.emit('order:created', order);

// subscriber.js(不知道谁发的)
bus.on('order:created', (order) => sendEmail(order));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

决策表:

维度 观察者 发布订阅
耦合度 Subject 知道 Observer 接口 双方互不知道
通信方式 Subject 直接调 Observer.update() 通过 EventBus 间接通信
适用场景 单一数据源 → 多个视图(如 Vue watch) 跨模块通信(如微前端子应用通信)
JS 原型 addEventListener / Vue watch EventEmitter / Redux subscribe
优点 简单、高效、无中间层 完全解耦、支持多对多
缺点 Subject 和 Observer 有接口依赖 多了中间层、事件名需全局管理

# 4.3 与 Vue 的

三者都是"观察变化→做出响应"——但抽象层级不同:

EventEmitter:  原始的事件广播——"有人喊了一声,(event, data)"
Vue.watch:     特定响应式值变化 → 回调——"这个值变了就做这件事"
RxJS.Observable: 事件流 + 操作符——"对事件流做 filter/map/debounce/throttle"
1
2
3
// EventEmitter——最底层
bus.on('click', handler);

// Vue watch——数据驱动的特定监听
watch(() => state.count, (newVal, oldVal) => { /* ... */ });

// RxJS——流式处理
fromEvent(btn, 'click')
    .pipe(
        debounceTime(300),        // 防抖
        map(e => e.target.value),  // 转换
        filter(v => v.length > 0), // 过滤
        switchMap(v => fetch(`/api?q=${v}`))  // 异步请求
    )
    .subscribe(data => render(data));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 5. 策略与命令模式

# 5.1 策略模式:表单校

把第 1 章的"错误处理"思想扩展到完整的表单校验场景:

// ================ 校验规则库(策略对象) ================
const validators = {
    // 每种策略是一个函数:value → true(通过) | 错误信息(不通过)
    required: (v) => (v !== '' && v !== null && v !== undefined) || '此项必填',
    email:    (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) || '邮箱格式不正确',
    phone:    (v) => /^1[3-9]\d{9}$/.test(v) || '手机号格式不正确',
    url:      (v) => { try { new URL(v); return true; } catch { return 'URL格式不正确'; } },

    // 高阶策略——策略工厂(返回策略函数)
    minLength: (n) => (v) => v.length >= n || `至少 ${n} 个字符`,
    maxLength: (n) => (v) => v.length <= n || `最多 ${n} 个字符`,
    pattern:   (regex, msg) => (v) => regex.test(v) || msg,
    in:        (...values) => (v) => values.includes(v) || `只允许: ${values.join(', ')}`,

    // 组合策略——一个字段可以有多个规则
    all: (...rules) => (v) => {
        for (const rule of rules) {
            const fn = typeof rule === 'function' ? rule : validators[rule];
            const result = fn(v);
            if (result !== true) return result;
        }
        return true;
    }
};

// ================ 校验引擎(不变的逻辑) ================
function validate(formData, schema) {
    const errors = {};

    for (const [field, rules] of Object.entries(schema)) {
        const value = formData[field];
        const ruleFn = typeof rules === 'function' ? rules : validators.all(...rules);
        const result = ruleFn(value);
        if (result !== true) {
            errors[field] = result;
        }
    }

    return {
        valid: Object.keys(errors).length === 0,
        errors
    };
}

// ================ 使用——声明式 ================
const userSchema = {
    name:     ['required', validators.minLength(2)],
    email:    ['required', 'email'],
    phone:    validators.all('required', 'phone'),  // 组合策略
    website:  'url',
    bio:      validators.maxLength(200),
};

const result = validate(
    { name: '', email: 'bad', phone: '123' },
    userSchema
);
console.log(result.errors);
// → { name: '此项必填', email: '邮箱格式不正确', phone: '手机号格式不正确' }

// 新增规则 = 在 validators 里加一个函数
// 不改 validate 引擎、不改 schema 结构
// → 开闭原则(对扩展开放,对修改关闭)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

# 5.2 命令模式:撤销

命令模式的本质:把每个操作封装为对象,该对象暴露 execute() 和 undo()——命令栈天然支持撤销/重做:

// ================ 命令接口 ================
class CommandManager {
    #undoStack = [];
    #redoStack = [];
    #maxHistory = 50;

    execute(command) {
        command.execute();
        this.#undoStack.push(command);
        this.#redoStack = [];  // 新操作后清空 redo

        // 限制历史记录数量
        if (this.#undoStack.length > this.#maxHistory) {
            this.#undoStack.shift();
        }
    }

    undo() {
        const cmd = this.#undoStack.pop();
        if (!cmd) return;
        cmd.undo();
        this.#redoStack.push(cmd);
    }

    redo() {
        const cmd = this.#redoStack.pop();
        if (!cmd) return;
        cmd.execute();
        this.#undoStack.push(cmd);
    }

    get canUndo() { return this.#undoStack.length > 0; }
    get canRedo() { return this.#redoStack.length > 0; }
}

// ================ 具体命令——文本编辑器 ================
class InsertTextCommand {
    constructor(doc, pos, text) {
        this.doc = doc;
        this.pos = pos;
        this.text = text;
        this.oldText = doc.getText();  // 快照(为简单起见取全文)
    }

    execute() {
        this.doc.insertAt(this.pos, this.text);
    }

    undo() {
        // 恢复到操作前的状态
        this.doc.restore(this.oldText);
    }
}

class DeleteTextCommand {
    constructor(doc, pos, length) {
        this.doc = doc;
        this.pos = pos;
        this.length = length;
        this.deletedText = doc.getTextAt(pos, length);  // 保存被删内容
    }

    execute() {
        this.doc.deleteAt(this.pos, this.length);
    }

    undo() {
        this.doc.insertAt(this.pos, this.deletedText);
    }
}

// ================ 使用 ================
const manager = new CommandManager();
const doc = new Document('Hello');

manager.execute(new InsertTextCommand(doc, 5, ' World'));   // Hello → Hello World
manager.execute(new DeleteTextCommand(doc, 0, 6));           // Hello World → World
manager.undo();  // → Hello World
manager.undo();  // → Hello
manager.redo();  // → Hello World
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

命令模式的关键设计点:

  1. 每个命令必须自己保存足够多的状态以便 undo(不能依赖"反操作"就能还原——insertAt(pos, text) 的 undo 不是 deleteAt(pos, text.length),两者接收的参数不同)
  2. 命令栈天然支持宏命令——把多个命令打包成一个 MacroCommand,一次 undo 全部撤销
  3. 在 React 中 useReducer 就是命令模式——dispatch(action) 是命令,reducer(state, action) 是命令执行器

# 6. 适配器代理装饰

# 6.1 三者长得像但有本

疑惑:三个模式都是"在对象外面包一层"——到底怎么区分?

论证:三者外观相似但目的完全不同:

适配器 (Adapter):
  ┌──────────┐    ┌──────────┐    ┌──────────┐
  │  调用方    │───►│  Adapter │───►│  被适配者  │
  │  expects  │    │  (转换)   │    │ (老接口)  │
  │  InterfaceA│   └──────────┘   └──────────┘
  └──────────┘
  目的:让 A 看起来像 B(接口变化)
  例子:axios 统一浏览器(XHR)和 Node(http)的请求接口

代理 (Proxy):
  ┌──────────┐    ┌──────────┐    ┌──────────┐
  │  调用方    │───►│  Proxy   │───►│  Target  │
  │          │    │  (控制)   │    │ (真实对象)│
  └──────────┘    └──────────┘    └──────────┘
  目的:控制对对象的访问(接口不变)
  例子:Vue 3 reactive() / 图片懒加载

装饰器 (Decorator):
  ┌──────────┐    ┌──────────┐    ┌──────────┐
  │  调用方    │───►│ Decorator│───►│  Target  │
  │          │    │  (增强)   │    │ (原对象)  │
  └──────────┘    └──────────┘    └──────────┘
  目的:动态给对象加行为(接口不变但增强)
  例子:@log / React HOC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

代码对比——同一个功能用三种模式实现:

// ===== 场景:第三方支付 SDK 返回的数据格式和你前端不兼容 =====

// 第三方返回:{ payment_id: 'xxx', amount_in_cents: 2500, status_code: 1 }
// 你的前端需要:{ id: 'xxx', amount: 25.00, status: 'paid' }

// ✅ 适配器——接口改变了(字段名和数据结构都不同)
function paymentAdapter(raw) {
    return {
        id: raw.payment_id,
        amount: raw.amount_in_cents / 100,
        status: { 0: 'pending', 1: 'paid', 2: 'failed' }[raw.status_code],
    };
}

// ===== 场景:大列表中的图片,滚到可视区域才加载 =====

// ✅ 代理——接口不变,只是控制了访问时机
function createLazyImage(src) {
    return new Proxy({}, {
        get(_, key) {
            if (key === 'src') {
                // 滚动到可视区域时才返回真实 URL
                return isInViewport() ? src : 'placeholder.png';
            }
        }
    });
}

// ===== 场景:给所有 API 调用加日志 =====

// ✅ 装饰器——接口不变,在原功能上叠加新行为
function withLog(fn, name) {
    return function(...args) {
        console.log(`[${name}] called with`, args);
        const start = performance.now();
        const result = fn.apply(this, args);
        console.log(`[${name}] returned in ${(performance.now()-start).toFixed(2)}ms`);
        return result;
    };
}
const loggedFetch = withLog(fetch, 'fetch');  // fetch 的接口完全不变
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

决策表:

模式 接口变化? 核心问题 判断口诀
适配器 变 "能不能用"(兼容) A 接口→B 接口
代理 不变 "让不让用"(控制) 拦在中间
装饰器 不变 "能不能更强"(增强) 包在外面

# 6.2 TC39 Sta

疑惑:"JS 的装饰器不就是 Java 的 @Annotation 吗?"

论证:完全不同。Java 注解是元数据标记——不执行代码,只提供信息给反射、编译器、框架读取。JS 装饰器是运行时函数调用——在对象创建/方法定义时实际执行代码:

// TC39 Stage 3 装饰器提案语法(尚未标准化,这里是基于提案的示例)
function logged(method, context) {
    const name = String(context.name);
    // 装饰器返回一个新的方法——实际替换了原方法
    return function(...args) {
        console.log(`[${name}] called`);
        const result = method.apply(this, args);
        console.log(`[${name}] returned ${result}`);
        return result;
    };
}

class API {
    @logged
    fetchUser(id) {
        return fetch(`/api/user/${id}`).then(r => r.json());
    }
}

const api = new API();
api.fetchUser(123);
// → [fetchUser] called
// → [fetchUser] returned [object Promise]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

JS 装饰器 vs Java 注解:

维度 JS 装饰器 Java 注解
执行时机 运行时(定义时立即执行) 编译时(保留在字节码中,运行时通过反射读取)
作用 修改被装饰的目标(替换/增强) 标记目标(不修改行为)
类型 函数 特殊接口
例子 @logged → 实际替换了原方法 @Override → 只告诉编译器"我覆盖了父类方法"

# 7. 纯函数与副作用隔离

# 7.1 相同的输入 →

疑惑:"纯函数不就是没有副作用的函数吗?这有什么好讲的?"

论证:纯函数的核心价值不是"干净"——而是你可以不看它的实现,只看签名就知道它做什么。这不是道德选择,是工程需要:

// ❌ 有副作用——你不知道它改了外部什么
const cache = {};
function getResult(key) {
    if (!cache[key]) {
        cache[key] = heavyCompute(key);  // 修改外部 cache
    }
    return cache[key];
}
// 问题:getResult('a') 第一次返回 42,第二次也返回 42——但两次的"系统状态"不同了
// 你无法通过"看签名"知道它会不会改 cache——必须读实现

// ✅ 纯函数——所有依赖和影响都在参数和返回值里
function compute(key) {
    return key.length * 2;
}
// 签名就是承诺:给你 key → 还你 number。不会改任何东西、不依赖任何外部状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

纯函数的三大工程收益:

  1. 可测试:不需要 mock 全局变量、数据库、文件系统——给输入,断言输出
  2. 可缓存(memoize):相同输入保证相同输出 → 放心缓存结果
  3. 可并行:纯函数之间没有共享状态 → 可以放心放到 Worker 里并行执行
// memoize 的高阶函数实现——只对纯函数有效
function memoize(fn) {
    const cache = new Map();
    return (...args) => {
        const key = JSON.stringify(args);
        if (!cache.has(key)) {
            cache.set(key, fn(...args));
        }
        return cache.get(key);
    };
}

const memoizedCompute = memoize(compute);
memoizedCompute('hello');  // 42(计算)
memoizedCompute('hello');  // 42(缓存命中,不计算)

// ⚠ 如果 compute 不是纯函数——缓存会给你过期/错误的结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 7.2 为什么 useE

React 把副作用隔离到 useEffect 的设计不是偶然——渲染本身应该是纯函数:

React 的心智模型:
  组件 = (props, state) → JSX  (纯函数映射)
  副作用 = useEffect(() => { ... }, [deps])  (集中隔离)
1
2
3
function UserProfile({ userId }) {
    const [user, setUser] = useState(null);

    // 副作用隔离:网络请求不在渲染路径上
    useEffect(() => {
        let cancelled = false;

        fetch(`/api/user/${userId}`)
            .then(r => r.json())
            .then(data => {
                if (!cancelled) setUser(data);  // 防止竞态
            });

        return () => { cancelled = true; };  // 清理函数
    }, [userId]);

    // 渲染路径:纯映射
    return user
        ? <div>{user.name}</div>
        : <div>Loading...</div>;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

为什么渲染要纯:React 在并发模式(Concurrent Mode)下可能多次调用你的渲染函数(discard 不用的结果)——如果渲染函数有副作用(如发网络请求),这些副作用会被重复执行。

# 7.3 副作用隔离的四种

策略 代表 核心思路 适用场景
Monad Haskell / fp-ts 把副作用"包"在一个容器里,通过链式调用传递——副作用在容器内发生,不影响外部纯代码 强类型 FP 语言/库
IoC/DI(控制反转/依赖注入) Angular / NestJS 把外部依赖从构造函数注入——组件不自己 new,由框架提供 企业级后端、测试 mock 方便
Saga Redux-Saga 用 Generator 的 yield 声明式描述副作用——saga 是"副作用的声明",执行由 saga middleware 管理 复杂异步流(竞态、取消、重试)
Effect Hook React 副作用集中声明在 useEffect——框架决定何时执行、何时清理 UI 渲染的副作用
// Saga 示例——"副作用的声明式描述"
function* fetchUserSaga(action) {
    try {
        yield put({ type: 'LOADING_START' });
        const user = yield call(api.fetchUser, action.payload.id);  // 声明式
        yield put({ type: 'FETCH_SUCCESS', payload: user });
    } catch (e) {
        yield put({ type: 'FETCH_ERROR', payload: e.message });
    } finally {
        yield put({ type: 'LOADING_END' });
    }
}
// 每一行 yield 都是"我要做这件事"的声明——saga middleware 去实际执行
1
2
3
4
5
6
7
8
9
10
11
12
13

# 8. 不可变数据详解

# 8.1 structur

疑惑:"不可变数据不就等于深拷贝吗?每次改一个字段就复制整个对象——性能得多差?"

论证:不可变 ≠ 完整深拷贝。真正的不可变数据框架(如 Immer)用的是结构共享(Structural Sharing)——只复制"改变了的那条路径"上的节点:

原始对象:
        root
       /    \
      a      b
     / \    / \
    c   d  e   f

修改 root.a.d = 新值:
        root'           (new)
       /    \
      a'     b ───────── (共享!)
     / \
    c   d'               (new)

只创建了 root'、a'、d' 三个新节点
b 和 c 直接复用原始对象的引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

三种方案的性能矩阵(10 万次深度嵌套对象修改 benchmark):

方案 写法 耗时 内存增量 优点 缺点
{ ...obj, a: { ...obj.a, b: newVal } } 手动展开 最快 (~8ms) 最小(只创建新路径上的节点) 零依赖、TypeScript 类型安全 深层嵌套代码噩梦 (...obj.a.b.c.d)
structuredClone(obj) 然后改 深度拷贝 中等 (~45ms) 最大(每次全对象拷贝) 原生 API、写法简单 不能拷贝函数/类/DOM 节点、每次都全量拷贝
Immer produce(obj, draft => { draft.a.b = newVal }) 像可变一样写 较慢 (~30ms) 中等(自动结构共享) 写法最自然、自动冻结返回对象 额外 4KB 依赖、Proxy 开销

选择建议:

// 浅层对象 → spread(最快、最直接)
const newObj = { ...obj, name: 'new' };

// 深层对象 + 单次修改 → structuredClone(简单直接)
const newObj = structuredClone(obj);
newObj.deep.nested.field = 'new';

// 深层对象 + 频繁修改 → Immer(最自然)
import { produce } from 'immer';
const newObj = produce(obj, draft => {
    draft.a.b.c.d.e = 'new';  // 像改可变对象一样写,但返回不可变快照
});
1
2
3
4
5
6
7
8
9
10
11
12

# 8.2 为什么 Reac

疑惑:"React 内部不能自己深比较吗?为什么非要开发者手动保持不可变?"

论证:React 的 PureComponent / React.memo / useMemo 依赖引用比较(===) 判断数据是否变化。这个设计是有意为之——深比较的代价太大:

// React.memo 的判断逻辑(简化):
function memo(Component) {
    let prevProps = null;
    return function MemoWrapper(props) {
        // 只用 === 比较每个 prop(O(n) 浅比较,n = props 数量)
        if (prevProps && Object.keys(props).every(k => props[k] === prevProps[k])) {
            return prevResult;  // 跳过重新渲染
        }
        prevProps = props;
        prevResult = Component(props);
        return prevResult;
    };
}

// 如果 React 改成深比较:
// 每个组件每次渲染都要深比较所有 props → O(m) (m = props 总节点数)
// → 1000 个组件 × 100 个属性节点 = 10 万次比较/帧 → 掉帧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

正确 vs 错误:

// ❌ 原地修改——React 检测不到变化
state.items.push(newItem);
setState(state);  // state 引用没变 → React 认为"没变化" → 不重新渲染

// ✅ 创建新引用——React 检测到变化
setState({
    ...state,
    items: [...state.items, newItem]
});  // state 引用变了 → React 重新渲染
1
2
3
4
5
6
7
8
9

# 9. pipe与May

# 9.1 把 10 层嵌套

疑惑:pipe(f, g, h)(x) 和 h(g(f(x))) 做的事情完全一样——为什么 pipe 就是"更好"?

论证:因为人的大脑读代码是从左到右、从上到下的。嵌套要求你从内向外反着读:

// ❌ 嵌套——从内向外读:先看最里面的 validate,再往外看 parse...
const result = truncate(normalize(parse(validate(input))));

// ✅ pipe——从左到右读:input → validate → parse → normalize → truncate
const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x);
const process = pipe(validate, parse, normalize, truncate);
const result = process(input);
1
2
3
4
5
6
7

pipe vs compose:

// pipe: 从左到右(数据流的自然方向)
const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x);
pipe(f, g, h)(x);  // = h(g(f(x)))

// compose: 从右到左(数学函数组合的方向)
const compose = (...fns) => (x) => fns.reduceRight((v, f) => f(v), x);
compose(f, g, h)(x);  // = f(g(h(x)))
// 等价于:compose(h, g, f)(x) = f(g(h(x)))  ← 注意参数顺序是反的!
1
2
3
4
5
6
7
8

pipe 在异步场景下的威力:

// 异步 pipe——数据流过 Promise 链
const asyncPipe = (...fns) => (x) =>
    fns.reduce((p, f) => p.then(f), Promise.resolve(x));

const getUserOrders = asyncPipe(
    (id) => fetch(`/api/user/${id}`).then(r => r.json()),
    (user) => fetch(`/api/orders?userId=${user.id}`).then(r => r.json()),
    (orders) => orders.filter(o => o.status === 'paid'),
    (paidOrders) => paidOrders.map(o => ({ id: o.id, total: o.amount }))
);

const result = await getUserOrders(42);
// 42 → 查用户 → 查订单 → 筛选已付款 → 字段映射 → result
1
2
3
4
5
6
7
8
9
10
11
12
13

# 9.2 Maybe

疑惑:"obj?.a?.b?.c 不就是 Maybe 模式吗?还需要自己写?"

论证:可选链 ?. 只能解决"取属性时遇到 null/undefined 短路返回 undefined"——但当你需要对中间值做转换、校验、错误追踪时,它就无能为力了:

// 场景:user.address.city 可能在任何层级为 null
const user = { address: null };

// 方案 A:可选链——只能取,不能转换
const city = user?.address?.city ?? 'Unknown';

// 方案 B:Maybe——可以在链上做任何操作
class Maybe {
    #value;
    constructor(v) { this.#value = v; }
    static of(v) { return new Maybe(v); }
    static nothing() { return new Maybe(null); }

    map(fn) {
        return this.#value == null ? Maybe.nothing() : Maybe.of(fn(this.#value));
    }

    // 用于副作用操作(如日志、DOM 更新)——不影响链
    tap(fn) {
        if (this.#value != null) fn(this.#value);
        return this;
    }

    // flatMap(bind)——防止嵌套 Maybe<Maybe<T>>
    flatMap(fn) {
        return this.#value == null ? Maybe.nothing() : fn(this.#value);
    }

    // 链式短路检查
    chain(...fns) {
        return fns.reduce((m, fn) => m.map(fn), this);
    }

    unwrap(defaultVal) { return this.#value ?? defaultVal; }
}

// 使用示例
const cityName = Maybe.of(user)
    .map(u => u.address)           // null → 短路为 Maybe.nothing()
    .map(a => a.city)              // 不会因为 a 为 null 而崩
    .map(c => c.toUpperCase())
    .tap(name => console.log('City:', name))  // 只在有值时执行
    .unwrap('Unknown');

console.log(cityName);  // → 'Unknown'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

Either 模式——左手错误、右手正确:

// Either: Left(错误) | Right(正确)
// 用于需要"知道哪里出错了"而不仅是"出错了"的场景

class Either {
    #value; #isLeft;
    constructor(v, isLeft) { this.#value = v; this.#isLeft = isLeft; }

    static left(err)  { return new Either(err, true); }
    static right(val) { return new Either(val, false); }

    map(fn)    { return this.#isLeft ? this : Either.right(fn(this.#value)); }
    chain(fn)  { return this.#isLeft ? this : fn(this.#value); }
    fold(leftFn, rightFn) {
        return this.#isLeft ? leftFn(this.#value) : rightFn(this.#value);
    }
}

function parseJSON(str) {
    try {
        return Either.right(JSON.parse(str));
    } catch (e) {
        return Either.left(`Parse error: ${e.message}`);
    }
}

parseJSON('{"name":"Alice"}')
    .map(data => data.name)
    .fold(
        err => console.error(err),      // 错误分支
        name => console.log(name)       // 正确分支
    );
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 9.3 TC39 Sta

// Stage 2 提案——pipe 的语言级支持(尚未标准化,仅供参考)
// 基于 Hack 风格(% 占位符):
const result = input
    |> validate(%)
    |> parse(%, options)
    |> normalize(%)
    |> truncate(%);

// 等价于:
const result = truncate(normalize(parse(validate(input), options)));

// 如果 Stage 2 最终落地(2025年后):
// 1. pipe 从库函数变成语法
// 2. 支持 async:const x = input |> await fetch(%) |> (await %).json()
// 3. 和 try/catch 配合:input |> validate(%) |> try parse(%) catch log(%)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 10. 综合案例串讲

# 10.1 案例真相揭晓

回到第 1 章的按钮重构场景,五个假设现在能逐条作答:

疑问 答案
① A 和 B 功能一样、写法不同而已? 第 1.2:A 的加载态/数据获取/渲染完全耦合。B 通过高阶函数(withLoading/withRetry/debounce)和函数组合(pipe)把每个关注点拆成独立模块——新增需求不改原有模块,只加新模块再组合
② 设计模式就是换个写法? 第 2 章:模式的核心不是"怎么写",而是"把什么和什么解耦"。单例解耦"何时创建"和"使用者";观察者解耦"数据变化"和"响应行为";策略解耦"条件分支"和"具体处理"
③ 单例/策略/观察者/命令在 JS 里的核心落点 第 3-5 章:ES6 模块天然单例(顶层 export);策略表替代 if/else;EventEmitter 手写实现;命令栈支持 undo/redo
④ 适配器/代理/装饰器的区别 第 6.1 节:适配器=改接口(兼容)、代理=控访问(透明拦截)、装饰器=加行为(增强不改变接口)——记住口诀:接口变没变?
⑤ pipe/Maybe/不可变的选择矩阵 第 8.1 + 9.1~9.2:浅层→spread、深层一次性→structuredClone、深层频繁→Immer;pipe 消嵌套、Maybe 消 null 防御——共同哲学:把"怎么处理"从"怎么判断"中分离出来

# 10.2 一个「订单处理管

把第 1 章的思路用到完整业务场景——订单处理管线:

// ========== 重构前:30 行 if/else 混杂 ==========
function processOrder(order) {
    if (!order.items || order.items.length === 0) {
        throw new Error('订单没有商品');
    }

    let total = 0;
    for (const item of order.items) {
        if (item.price < 0) throw new Error('商品价格异常');
        if (item.quantity <= 0) continue;  // 跳过数量为 0 的商品

        let lineTotal = item.price * item.quantity;

        // 不同类型不同折扣
        if (item.type === 'digital') {
            lineTotal *= 0.9;  // 数字商品 9 折
        } else if (item.type === 'member') {
            lineTotal *= 0.85; // 会员商品 85 折
        }
        // 新增类型 → 加一个 else if

        total += lineTotal;
    }

    // 满减
    if (total >= 500) total -= 50;
    else if (total >= 200) total -= 10;

    // 优惠券
    if (order.coupon === 'SAVE20') total -= 20;
    else if (order.coupon === 'SAVE50') total -= 50;

    // 运费
    if (total < 99) total += 10;

    return { ...order, total: Math.max(0, total) };
}

// ========== 重构后:策略 + pipe ==========

// ① 校验管线——策略模式
const itemValidators = {
    noItems: (items) => items?.length > 0 || '订单没有商品',
    validPrice: (items) => items.every(i => i.price >= 0) || '商品价格异常',
};
const validateOrder = (order) => {
    for (const [key, fn] of Object.entries(itemValidators)) {
        const result = fn(order.items);
        if (result !== true) throw new Error(result);
    }
    return order;  // 通过校验,原样返回(保持 pipe 链)
};

// ② 折扣策略——策略表(新增折扣类型只加一行)
const discountStrategies = {
    digital: (total) => total * 0.9,
    member:  (total) => total * 0.85,
    default: (total) => total,
};

// ③ 价格计算——纯函数
const calcTotal = (order) => ({
    ...order,
    total: order.items
        .filter(i => i.quantity > 0)
        .reduce((sum, item) => {
            const lineTotal = item.price * item.quantity;
            const discount = discountStrategies[item.type] || discountStrategies.default;
            return sum + discount(lineTotal);
        }, 0)
});

// ④ 满减策略——策略表
const tierDiscount = (total) => {
    if (total >= 500) return total - 50;
    if (total >= 200) return total - 10;
    return total;
};

// ⑤ 优惠券策略
const couponDiscounts = {
    SAVE20: (t) => t - 20,
    SAVE50: (t) => t - 50,
};
const applyCoupon = (order) => ({
    ...order,
    total: (couponDiscounts[order.coupon] || ((t) => t))(order.total)
});

// ⑥ 运费
const addShipping = (order) => ({
    ...order,
    total: order.total >= 99 ? order.total : order.total + 10
});

// ⑦ 保证非负
const ensureNonNegative = (order) => ({
    ...order,
    total: Math.max(0, order.total)
});

// ⑧ 组装——pipe
const processOrder = pipe(
    validateOrder,
    calcTotal,
    (o) => ({ ...o, total: tierDiscount(o.total) }),
    applyCoupon,
    addShipping,
    ensureNonNegative
);

// 新增需求 = 往 pipe 里加一步——不改任何已有函数
// 例如新增"黑名单用户加收 20% 服务费":
// const blacklistSurcharge = (order) =>
//     blacklist.includes(order.userId)
//         ? { ...order, total: order.total * 1.2 }
//         : order;
// processOrder = pipe(validateOrder, ..., blacklistSurcharge);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118

重构效果对比:

维度 重构前 重构后
总行数 30 行 60 行(但每个函数 5~8 行、独立可读)
新增折扣类型 加一个 else if,摸到 calcTotal 内部 在 discountStrategies 里加一行
新增校验规则 在 processOrder 开头加代码 在 itemValidators 里加一个函数
单元测试 只能测 processOrder 整体(mock 所有依赖) 每个函数独立测试(给输入 → 断言输出)
代码复用 0%(全耦合在一个函数里) tierDiscount / couponDiscounts 可在其他页面复用

# 10.3 设计哲学回扣

整理本篇的三条跨篇适用的设计哲学:

哲学一·「模式不是教条——是对变化的防御」

设计模式不是"每段代码都要套一个模式"。而是——当你发现一处代码"每次需求变更都要改这里"时,模式给了你一个已验证的重构方向。单例解决"怎么创建唯一实例"——但如果你不确定未来会不会需要第二个实例,就不要急着上单例。模式的本质不是代码模板——是"把什么和什么解耦"的经验沉淀。 先感受到痛(需求一变就要改 3 个文件),再找对应的模式解耦——而不是反过来。

哲学二·「纯函数不是规则——是推理的边界」

纯函数的最大价值不是"没有副作用"——而是你可以不看它的实现,只看签名就知道它做什么。纯函数给了你一个"推理边界":输入和输出。你不需要追踪它的调用链、不需要检查全局变量、不需要考虑执行时机。这就是为什么 React 组件、Vue computed、Redux reducer 都倾向纯函数——不是道德选择,是工程需要:当你维护 10 万行代码时,你不可能记住每个函数的副作用清单。

哲学三·「组合是对嵌套的降维打击」

pipe(f, g, h)(x) 和 h(g(f(x))) 做的事情完全一样——但前者让你"从左到右读数据的流动",后者让你"从内到外跟踪嵌套"。组合把"嵌套的嵌套关系"变成了"同级的并列关系"——这是认知上的降维。你的大脑不再需要维护一个"从内向外"的栈,只需要一行一行往下扫。这就是为什么 Unix 管道的 | 操作符存活了 50 年——它让人类可以理解"数据是怎么一步步流过去的"。

# 10.4 速查表速览

速查表 A:七种设计模式 + 框架对应

模式 类别 一句话 JS 原生/框架对应
单例 创建型 全局只有一份实例 ES6 模块顶层 export(天然缓存)
工厂 创建型 把"new 什么"的决定权移走 React.createElement / 注册式工厂
适配器 结构型 让 A 看起来像 B(接口变了) axios 统一浏览器和 Node 的 HTTP
代理 结构型 控制对对象的访问(接口不变) Vue 3 reactive() / ES6 Proxy
装饰器 结构型 给对象加行为(不改变接口) React HOC / TC39 @decorator
观察者 行为型 Subject 直接通知 Observer Vue watch / DOM addEventListener
策略 行为型 策略表替代 if/else 表单校验规则对象 / Array.sort(compareFn)
命令 行为型 操作封装为对象 → undo/redo useReducer / 富文本编辑器命令栈

速查表 B:pipe vs compose

pipe compose
执行顺序 从左到右 从右到左
读法 pipe(f, g, h) = 先 f 再 g 再 h compose(f, g, h) = 先 h 再 g 再 f
直觉 符合"数据流"思维 符合"数学函数组合"思维
适用 数据变换管线 高阶组件组合(React)

速查表 C:不可变方案选择矩阵

场景 推荐方案 原因
浅层对象(≤3 层) { ...obj, field: newVal } 最快、零依赖、TS 类型安全
深层 + 单次修改 structuredClone(obj) 再改 简单直接、原生 API
深层 + 频繁修改 Immer produce(obj, draft => { ... }) 写法最自然、自动结构共享
大数组 push [...arr, newItem] 最快、最简洁
大数组 splice Immer 或手动 [...arr.slice(0,i), ...arr.slice(i+1)] Immer 更可读

60 秒诊断清单:

1. 这段代码"改了一个需求是否要动多个地方"?
   → 是 → 用策略模式(策略表)或工厂模式解耦

2. 两个对象需要通信但不想互相引用?
   → 观察者(Subject 知 Observer)vs 发布订阅(通过 EventBus 隔离)

3. 有 if/else 丛林吗?
   → 是 → 策略表(对象映射)替代

4. 需要 undo/redo 吗?
   → 是 → 命令模式(execute + undo + 命令栈)

5. 第三方 API 格式不兼容?
   → 适配器(写一个转换函数)

6. 渲染慢?浅比较失效?
   → 检查是否直接 mutate 了 state → 用不可变方式更新

7. 函数有副作用导致测试/调试困难?
   → 纯函数 + 副作用隔离(useEffect/Saga/DI)

8. 嵌套调用链太长?
   → pipe/compose → 把嵌套变成并列

9. 满屏的 if (x == null) return null?
   → Maybe 链式调用 / 可选链 ?.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

认知模型——从 if/else 到策略+pipe 的思维转换:

写代码时的判断流:
  看到 if/else → 问:"这些分支会新增吗?" → 会 → 策略对象
  看到 try/finally → 问:"这个模板会重复吗?" → 会 → 高阶函数包装
  看到嵌套调用 h(g(f(x))) → 问:"读起来累吗?" → 累 → pipe(f,g,h)(x)
  看到 if (x == null) return → 问:"这种情况多吗?" → 多 → Maybe 模式
  看到 setState(obj.x = v) → 问:"React 能检测到吗?" → 不能 → 不可变更新
1
2
3
4
5
6

下一步:代码优雅了。但用户不止在浏览器里——跨端才是终极战场。进入 16.跨端架构终局。

上次更新: 2026/06/16, 12:36:20
现代工程链三件套
跨端架构终局总结

← 现代工程链三件套 跨端架构终局总结→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式