JavaScript 函数组合(Compose & Pipe)详解
前言
当你学习 JavaScript 进阶时,一定会遇到函数组合 这个概念。它是函数式编程的核心思想之一,能让你像搭积木一样把小函数拼成大函数。
今天我们来彻底搞懂这段代码:
javascript
const compose = (...functions) => (value) => {
return functions.reduceRight((acc, fn) => fn(acc), value);
};
const pipe = (...functions) => (value) => {
return functions.reduce((acc, fn) => fn(acc), value);
};
看起来很吓人?别怕,我们一层一层拆开。
一、先搞懂三个小函数
javascript
const add5 = x => x + 5; // 加 5
const multiply3 = x => x * 3; // 乘 3
const subtract2 = x => x - 2; // 减 2
它们各自做的事情非常简单:
javascript
add5(10); // 15
multiply3(15); // 45
subtract2(45); // 43
如果我们想依次执行这三步呢?
最笨的写法:嵌套调用
javascript
subtract2(multiply3(add5(10)));
// 从最里面开始算:
// 第1步:add5(10) → 15
// 第2步:multiply3(15) → 45
// 第3步:subtract2(45) → 43
问题:当函数多了,嵌套会变成这样 👇
javascript
// 😵 可读性极差
h(g(f(e(d(c(b(a(value))))))));
函数组合就是为了解决这个问题!
二、什么是函数组合?
函数组合 = 把多个小函数,合并成一个新函数。
就像工厂的流水线:
原料(10) → [加5] → [乘3] → [减2] → 成品(43)
每个工位(函数)只做一件简单的事,串起来就能完成复杂的任务。
三、compose 逐层拆解
完整代码
javascript
const compose = (...functions) => (value) => {
return functions.reduceRight((acc, fn) => fn(acc), value);
};
这一行代码包含了三个知识点,我们逐个击破。
知识点 1:剩余参数 ...functions
javascript
const compose = (...functions) => { /* ... */ };
...functions 把所有传入的参数收集成一个数组:
javascript
compose(subtract2, multiply3, add5);
// functions = [subtract2, multiply3, add5]
知识点 2:返回一个新函数(柯里化)
javascript
const compose = (...functions) => (value) => { /* ... */ };
compose 并不直接计算结果,而是返回一个新函数 ,这个新函数等待接收 value。
javascript
// 第一步:传入要组合的函数,得到一个新函数
const composed = compose(subtract2, multiply3, add5);
// composed 现在是一个函数:(value) => { ... }
// 第二步:传入具体的值,开始计算
composed(10); // 43
等价于:
javascript
// compose(subtract2, multiply3, add5) 返回了这个:
const composed = (value) => {
const functions = [subtract2, multiply3, add5];
return functions.reduceRight((acc, fn) => fn(acc), value);
};
知识点 3:reduceRight 从右往左执行
javascript
functions.reduceRight((acc, fn) => fn(acc), value);
reduceRight 和 reduce 的区别:
| 方法 | 方向 |
|---|---|
reduce |
从左 往右 → |
reduceRight |
从右 往左 ← |
完整执行过程
javascript
const composed = compose(subtract2, multiply3, add5);
composed(10);
此时 functions = [subtract2, multiply3, add5],value = 10
reduceRight 从右往左遍历数组:
functions 数组: [subtract2, multiply3, add5]
←←← 从右往左
| 轮次 | acc(累加器) |
fn(当前函数) |
计算 fn(acc) |
结果 |
|---|---|---|---|---|
| 初始 | 10(value) | --- | --- | 10 |
| 第 1 轮 | 10 | add5 |
add5(10) |
15 |
| 第 2 轮 | 15 | multiply3 |
multiply3(15) |
45 |
| 第 3 轮 | 45 | subtract2 |
subtract2(45) |
43 |
最终返回 43 ✅
图解
compose(subtract2, multiply3, add5)(10)
执行方向:从右到左 ←←←
←─────────────────────────
│ │
subtract2 ← multiply3 ← add5 ← 10
│ │ │
│ │ 10+5=15
│ 15*3=45
45-2=43
结果:43
四、pipe 逐层拆解
完整代码
javascript
const pipe = (...functions) => (value) => {
return functions.reduce((acc, fn) => fn(acc), value);
};
和 compose 唯一的区别 :用的是 reduce(从左往右),而不是 reduceRight。
执行过程
javascript
const piped = pipe(add5, multiply3, subtract2);
piped(10);
此时 functions = [add5, multiply3, subtract2],value = 10
reduce 从左往右遍历数组:
functions 数组: [add5, multiply3, subtract2]
→→→ 从左往右
| 轮次 | acc(累加器) |
fn(当前函数) |
计算 fn(acc) |
结果 |
|---|---|---|---|---|
| 初始 | 10(value) | --- | --- | 10 |
| 第 1 轮 | 10 | add5 |
add5(10) |
15 |
| 第 2 轮 | 15 | multiply3 |
multiply3(15) |
45 |
| 第 3 轮 | 45 | subtract2 |
subtract2(45) |
43 |
最终返回 43 ✅
图解
pipe(add5, multiply3, subtract2)(10)
执行方向:从左到右 →→→
─────────────────────────────→
│ │
10 → add5 → multiply3 → subtract2
│ │ │
10+5=15 15*3=45 45-2=43
结果:43
五、compose vs pipe 对比
两者结果相同,但函数的书写顺序相反:
javascript
// compose:从右往左(像数学写法)
const composed = compose(subtract2, multiply3, add5);
// ←── ←── ←── 执行方向
// pipe:从左往右(像阅读顺序)
const piped = pipe(add5, multiply3, subtract2);
// ──→ ──→ ──→ 执行方向
两者都是对 10 执行:先加5,再乘3,最后减2。
compose pipe
(subtract2, multiply3, add5) (add5, multiply3, subtract2)
←←← 从右往左 →→→ 从左往右
书写顺序: 结果 ← 过程 ← 起点 起点 → 过程 → 结果
类比: 数学表达式 f(g(x)) 流水线:x → g → f
该用哪个?
compose |
pipe |
|
|---|---|---|
| 执行方向 | 右 → 左 | 左 → 右 |
| 阅读习惯 | 像数学公式 f(g(x)) |
像自然语言,从上到下 |
| 使用场景 | 函数式编程传统写法 | 更直观,更多人喜欢 |
💡 推荐 :大多数场景下
pipe更容易阅读,因为执行顺序和书写顺序一致。
六、为什么要用函数组合?
场景:处理用户数据
javascript
// 一堆小函数,每个只做一件事
const trim = str => str.trim();
const toLowerCase = str => str.toLowerCase();
const split = str => str.split(' ');
const join = arr => arr.join('-');
// ❌ 嵌套写法(难读)
const result = join(split(toLowerCase(trim(" Hello World "))));
// ✅ pipe 写法(清晰!)
const slugify = pipe(trim, toLowerCase, split, join);
const result = slugify(" Hello World ");
// "hello-world"
执行过程
" Hello World "
│
▼ trim
"Hello World"
│
▼ toLowerCase
"hello world"
│
▼ split
["hello", "world"]
│
▼ join
"hello-world"
好处
✅ 每个函数只做一件事(单一职责)
✅ 函数可以复用(trim、toLowerCase 到处都能用)
✅ 像搭积木一样组合(需要新功能?加一个函数就行)
✅ 容易测试(每个小函数单独测试)
✅ 可读性强(一眼看出数据处理流程)
七、自己动手实现一遍
如果 reduce 让你头晕,可以先用 for 循环实现:
pipe 的 for 循环版本
javascript
const pipe = (...functions) => (value) => {
let result = value;
for (let i = 0; i < functions.length; i++) {
result = functions[i](result); // 把上一步的结果传给下一个函数
}
return result;
};
compose 的 for 循环版本
javascript
const compose = (...functions) => (value) => {
let result = value;
for (let i = functions.length - 1; i >= 0; i--) { // 从后往前
result = functions[i](result);
}
return result;
};
是不是一下就理解了?reduce 和 reduceRight 只是上面 for 循环的简写而已。
八、总结
javascript
// compose:从右往左执行
const compose = (...fns) => (val) => fns.reduceRight((acc, fn) => fn(acc), val);
// pipe:从左往右执行
const pipe = (...fns) => (val) => fns.reduce((acc, fn) => fn(acc), val);
| 概念 | 说明 |
|---|---|
| 函数组合 | 把多个小函数合并成一个大函数 |
compose |
从右往左执行,像数学写法 |
pipe |
从左往右执行,像流水线 |
| 核心思想 | 上一个函数的输出 = 下一个函数的输入 |
reduceRight |
从数组最后一项开始归纳 |
reduce |
从数组第一项开始归纳 |
| 柯里化 | 先传函数,返回新函数,再传值 |
一句话总结:函数组合就是搭建数据的流水线------数据从一头进去,经过层层加工,从另一头出来。 🏭
逐层拆解 const pipe = (...fns) => (val) => fns.reduce((acc, fn) => fn(acc), val)
这一行代码看着很唬人,其实它用了三个语法叠加在一起。我们一个一个拆。
语法一:箭头函数
javascript
// 普通函数
function add(a, b) {
return a + b;
}
// 箭头函数(简写)
const add = (a, b) => a + b;
当箭头函数只有一个表达式 时,可以省略 {} 和 return。
javascript
// 这两种写法完全等价
const double = (x) => { return x * 2; };
const double = x => x * 2;
语法二:返回函数的函数(柯里化)
一个函数可以返回另一个函数:
javascript
// 普通写法
function greet(greeting) {
return function(name) {
return greeting + ', ' + name;
};
}
// 箭头函数写法
const greet = (greeting) => (name) => greeting + ', ' + name;
怎么用?
javascript
// 第一次调用:传入 greeting,返回一个新函数
const sayHello = greet('你好');
// sayHello 现在是:(name) => '你好' + ', ' + name
// 第二次调用:传入 name,得到最终结果
sayHello('张三'); // '你好, 张三'
sayHello('李四'); // '你好, 李四'
// 也可以连着调用
greet('你好')('张三'); // '你好, 张三'
一步步理解连续箭头
javascript
const a = (x) => (y) => (z) => x + y + z;
这其实是三层函数嵌套:
javascript
// 等价于:
function a(x) {
return function(y) {
return function(z) {
return x + y + z;
};
};
}
调用方式:
javascript
a(1)(2)(3); // 6
// 拆开看:
const step1 = a(1); // (y) => (z) => 1 + y + z
const step2 = step1(2); // (z) => 1 + 2 + z
const step3 = step2(3); // 1 + 2 + 3 = 6
语法三:剩余参数 ...
javascript
const test = (...args) => {
console.log(args);
};
test(1, 2, 3); // [1, 2, 3]
test('a', 'b'); // ['a', 'b']
...args 把所有传入的参数收集成一个数组。
现在拆解 pipe
原始代码
javascript
const pipe = (...fns) => (val) => fns.reduce((acc, fn) => fn(acc), val);
第一步:展开成普通函数
javascript
function pipe(...fns) {
// fns 是一个数组,比如 [add5, multiply3, subtract2]
return function(val) {
// val 是传入的初始值,比如 10
return fns.reduce(function(acc, fn) {
return fn(acc);
}, val);
};
}
第二步:用 for 循环替代 reduce
javascript
function pipe(...fns) {
return function(val) {
let result = val; // 从初始值开始
for (let i = 0; i < fns.length; i++) {
result = fns[i](result); // 把上一步结果传给下一个函数
}
return result;
};
}
第三步:看看实际调用
javascript
const add5 = x => x + 5;
const multiply3 = x => x * 3;
const subtract2 = x => x - 2;
// 第一次调用:传入三个函数
const piped = pipe(add5, multiply3, subtract2);
// 此时 fns = [add5, multiply3, subtract2]
// piped 是一个函数:(val) => { ... }
// 第二次调用:传入值
piped(10);
// val = 10
// 开始执行 reduce...
第四步:模拟 reduce 的执行
fns = [add5, multiply3, subtract2]
初始值 val = 10
轮次 acc(累积值) fn(当前函数) fn(acc) 新的acc
─────────────────────────────────────────────────────────
1 10 add5 add5(10) 15
2 15 multiply3 multiply3(15) 45
3 45 subtract2 subtract2(45) 43
最终返回:43
用一张图理解全貌
pipe(add5, multiply3, subtract2)(10)
│ │
│ 第一次调用 │ 第二次调用
│ │
▼ ▼
(...fns) => (val) => fns.reduce(...)
fns = [add5, multiply3, subtract2] val = 10
│
▼
reduce 开始流水线:
10 ──→ add5 ──→ multiply3 ──→ subtract2 ──→ 43
+5=15 *3=45 -2=43
再来几个例子加深理解
例子 1:最简单的 ------ 只有一层箭头
javascript
const double = x => x * 2;
double(5); // 10
例子 2:两层箭头
javascript
const add = a => b => a + b;
add(3)(4); // 7
// 等价于
function add(a) {
return function(b) {
return a + b;
};
}
例子 3:两层箭头 + 剩余参数
javascript
const sum = (...nums) => (multiplier) => {
const total = nums.reduce((acc, n) => acc + n, 0);
return total * multiplier;
};
sum(1, 2, 3)(2);
// nums = [1, 2, 3], multiplier = 2
// total = 6
// 6 * 2 = 12
例子 4:就是 pipe
javascript
const pipe = (...fns) => (val) => fns.reduce((acc, fn) => fn(acc), val);
// 拆开:
const pipe = (...fns) => {
// 返回一个新函数
return (val) => {
// 从左到右依次执行每个函数
return fns.reduce((acc, fn) => {
return fn(acc);
}, val);
};
};
总结
这一行代码:
javascript
const pipe = (...fns) => (val) => fns.reduce((acc, fn) => fn(acc), val);
其实就是三个简单语法叠在一起:
| 语法 | 在代码中的位置 | 作用 |
|---|---|---|
箭头函数 => |
整行都在用 | 简写函数 |
剩余参数 ...fns |
(...fns) |
收集所有传入的函数为数组 |
| 返回函数(柯里化) | => (val) => |
第一次传函数,第二次传值 |
| reduce | .reduce((acc, fn) => fn(acc), val) |
依次执行每个函数 |
🎯 核心思想 :看到连续的
=>,就知道这是"返回函数的函数",需要分多次调用 。第一个()传第一批参数,第二个()传第二批参数,以此类推。
后记
2026年4月16日12点42分于上海,在opus 4.6辅助下完成。