前言
深入浅出函数式编程:从技巧到思维的跃迁
第一章:重新理解函数式编程的本质
- 函数式编程不是“用 map 替代 for 循环”
它是一种以函数为基本构造单元的编程范式,核心是表达“做什么”而非“怎么做” - 纯函数:为什么它能消除副作用?
相同输入 → 相同输出 + 无外部影响,是可测试、可缓存、可并行的基础 - 副作用(Side Effect)的七宗罪
修改全局变量、DOM 操作、网络请求、时间依赖、随机数、异常抛出、日志打印 - 引用透明性:表达式可替换为值的前提
为什么f(x) === f(x)成立时,代码才真正“可推理”? - 声明式 vs 指令式:两种思维模式的对比
users.filter(u => u.age > 18)vs 手写 for 循环 + push
第二章:掌握函数式核心机制
- 高阶函数:函数为何能作为参数和返回值?
基于闭包与[[Environment]]的能力,实现函数的组合与抽象 - 柯里化(Currying):为什么
add(1)(2)(3)是合法的?
将多参数函数转换为单参数函数链,实现参数的“预填充” - 函数组合(Composition):
f(g(x))的优雅写法compose(f, g)(x)
数学中的f ∘ g在 JS 中的实现与结合律优势 - 偏应用(Partial Application) vs 柯里化
固定部分参数生成新函数,但不强制一次只传一个 - 惰性求值(Lazy Evaluation):为什么 Ramda 的操作可以“延迟执行”?
利用闭包与 thunk 实现“按需计算”,避免不必要的中间结果
第三章:穿透不可变性与数据结构
- 不可变性(Immutability):为什么不能
push,而要用concat?
避免状态突变带来的“意料之外”的修改,保证数据流可追踪 - 结构共享(Structural Sharing):Immutable.js 如何高效更新树?
利用持久化数据结构(Persistent Data Structures)复用未变节点 - 递归:函数式中的“循环替代品”
为什么尾递归在 JS 中受限?如何用trampoline避免栈溢出? - 函子(Functor):
map背后的统一抽象
任何能map的类型(Array、Maybe、Observable)都是函子 - Either 与 Maybe:用类型表达“可能失败”的计算
替代if (data) { ... } else { ... },让错误处理变得“可组合”
第四章:深入范畴论与类型系统
- 范畴(Category)的三个要素:对象、态射、恒等
编程中的“类型”是对象,“函数”是态射,“id”是恒等函数 - Monad:为什么 Promise 是 Monad?
flatMap(then)满足左单位律、右单位律、结合律 - Applicative:介于 Functor 与 Monad 之间的抽象
支持多个上下文值的函数应用(如liftA2(add, Maybe(1), Maybe(2))) - 代数数据类型(ADT):如何用 JS 模拟 Sum 与 Product 类型?
Either(Sum)、Tuple(Product)的实现与模式匹配 - 类型类(Typeclass):JS 中的“接口”模拟
如Semigroup(可合并)、Monoid(带单位元的 Semigroup)
第五章:在 JS 中安全实践函数式
- 函数式与闭包的共生关系
柯里化、组合、惰性求值都依赖闭包,但需警惕内存泄漏 - 函数式与性能:深拷贝、递归、中间数组的代价
何时该“妥协”?生产环境中的权衡策略 - 函数式与 React:为什么 Hooks 天然适合 FP?
useState是 State Monad?useReducer是纯函数驱动? - 函数式与 RxJS:Observable 是 Monad 吗?
map、flatMap、filter的函子/单子特性 - 渐进式函数式:如何在现有项目中引入 FP 思维?
从纯函数工具函数开始,逐步替换命令式逻辑
第六章:高级技巧与工程实践
- 自动柯里化:如何让普通函数支持
f(1,2,3)或f(1)(2)(3)?
利用闭包与参数长度检测实现灵活调用 - 函数管道(pipe) vs 组合(compose)
pipe(f, g, h)(x)更符合阅读顺序,compose(h, g, f)(x)符合数学习惯 - 错误传播:如何让
Maybe或Either链式处理失败?
避免层层if (result) return ... - 状态传递:如何在无
this的情况下管理状态?
使用State Monad模拟“可变状态”的纯函数表达 - 测试优势:纯函数为何天生适合单元测试?
无需 mock,输入输出明确,覆盖率高