函数式编程
函数式编程核心概念(纯函数/不可变数据/一等公民)
#tech / dev / pattern
#type / concept
#status / growing
函数式编程
一句话定义
函数式编程 (FP) 是一种编程范式,将计算视为函数的组合,强调纯函数、不可变数据、避免副作用。
核心机制
1. 纯函数
输入相同则输出必然相同,且不产生副作用。
// 纯函数
function add(a, b) {
return a + b;
}
// 非纯函数 — 依赖外部状态
let discount = 0.1;
function getPrice(price) {
return price * (1 - discount); // discount 可能被外部修改
}
// 非纯函数 — 有副作用
function addToList(list, item) {
list.push(item); // 修改了输入
return list;
}
// 纯化版本
function addToPure(list, item) {
return [...list, item]; // 返回新数组
}
2. 不可变数据
数据创建后不修改,通过函数生成新数据。
const user = { name: 'Alice', age: 25 };
// 错误:直接修改
user.age = 26;
// 正确:生成新对象
const updated = { ...user, age: 26 };
// 数组不可变
const nums = [1, 2, 3];
const added = [...nums, 4]; // 而非 nums.push(4)
const filtered = nums.filter(n => n > 1); // 而非 nums.splice(...)
3. 函数是一等公民
函数可以赋值给变量、作为参数传递、作为返回值。
const greet = (name) => `Hello, ${name}`;
const apply = (fn, value) => fn(value);
apply(greet, 'Alice'); // "Hello, Alice"
map / filter / reduce
这三者是 FP 在 JavaScript 中最常用的体现:
const orders = [
{ product: 'A', price: 100, qty: 2 },
{ product: 'B', price: 50, qty: 3 },
{ product: 'C', price: 200, qty: 1 },
];
// 链式调用 = 函数组合
const total = orders
.filter(o => o.price > 60) // 筛选
.map(o => o.price * o.qty) // 转换
.reduce((sum, v) => sum + v, 0); // 聚合
// 200 + 200 = 400
函数组合 (Composition)
// pipe:从左到右执行
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);
const trim = (s) => s.trim();
const lower = (s) => s.toLowerCase();
const split = (sep) => (s) => s.split(sep);
const process = pipe(trim, lower, split(' '));
process(' Hello World '); // ['hello', 'world']
// compose(从右到左,数学惯例)
const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);
柯里化 (Currying)
// 普通函数
function add(a, b) { return a + b; }
add(1, 2); // 3
// 柯里化
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return (...moreArgs) => curried(...args, ...moreArgs);
};
}
const curriedAdd = curry(add);
curriedAdd(1)(2); // 3
curriedAdd(1, 2); // 3
// 实用场景:创建专用函数
const add10 = curriedAdd(10);
add10(5); // 15
add10(20); // 30
常用术语
| 术语 | 定义 |
|---|---|
| 纯函数 | 相同输入 → 相同输出,无副作用 |
| 副作用 | 修改外部状态(DOM、全局变量、IO) |
| 高阶函数 | 接收或返回函数的函数(map、filter、compose) |
| 柯里化 | 将多参数函数转为链式单参数调用 |
| 不可变性 | 数据创建后不修改,变更时生成新副本 |
| 组合 | 将小函数拼装成复杂逻辑 |
何时 FP 有帮助 vs 拖后腿
有帮助:
- 状态管理(Redux / Vuex 的 reducer 必须是纯函数)
- 数据转换管道(ETL、API 数据映射)
- 测试(纯函数天然易测,无需 mock)
- 并发安全(不可变数据没有竞态问题)
拖后腿:
- 性能敏感路径(频繁创建新对象有 GC 压力)
- IO 密集型代码(数据库、文件操作本质上是副作用)
- 过度抽象(满屏 compose/pipe/curry 降低可读性)
- 团队不熟悉时(FP 术语增加认知负担)
边界与常见误解
- FP 不是”不能有副作用”:而是将副作用推到边界(IO 层),核心逻辑保持纯。
- 不可变不是”完全不改”:而是不修改已存在的数据,用新数据代替。Immer 等库用 Proxy 实现写时复制。
forEach不是 FP:forEach返回undefined,强调的是执行副作用;map/filter/reduce才是 FP 风格。