compose / pipe 函数
约 1133 字大约 4 分钟
2026-04-16
1. 为什么需要函数组合
在日常开发中,我们经常需要对数据进行一系列处理:
const data = [1, 2, 3, 4, 5];
// 传统的嵌套调用,可读性差
const result = console.log(
double(
square(
filterEven(data)
)
)
);
// 想要理解这个流程,必须从里往外读函数组合(compose)可以将多个函数串联起来,形成一个清晰的数据处理管道:
const result = compose(console.log, double, square, filterEven)(data);
// 从右到左执行:filterEven -> square -> double -> console.log这样做的好处:
- 可读性更好:数据流向清晰
- 易于维护:每个函数职责单一
- 方便调试:可以在任意位置插入日志
- 易于扩展:新增或移除处理步骤很方便
2. 手写 compose 函数
2.1 基础版本
理解 compose 的关键在于:从右到左依次执行每个函数,将前一个函数的返回值传递给下一个函数。
function compose(...fns) {
return function(x) {
return fns.reduceRight((acc, fn) => {
return fn(acc);
}, x);
};
}
// 使用
const composed = compose(double, square);
composed(3); // square(3) = 9, double(9) = 18reduceRight 从数组末尾开始向左遍历,正好实现了从右到左执行的效果。
2.2 箭头函数简化
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);2.3 执行流程图解
以 compose(double, square, add) 为例,执行 compose(double, square, add)(3, 2):
add(3, 2) -> 5
|
v
square(5) -> 25
|
v
double(25) -> 503. 手写 pipe 函数
pipe 和 compose 的区别在于执行方向:
- compose:从右到左执行(符合数学上的函数组合)
- pipe:从左到右执行(更符合自然语言的阅读顺序)
const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);
// 使用
const piped = pipe(add, square, double);
piped(3, 2); // add(3, 2) = 5, square(5) = 25, double(25) = 50对比
const data = [1, 2, 3, 4, 5];
// compose: 从右往左
compose(console.log, double, square, filterEven)(data);
// pipe: 从左往右(数据流更直观)
pipe(filterEven, square, double, console.log)(data);两者结果相同,只是阅读顺序不同。个人更喜欢 pipe,因为它的数据流向从左到右,更符合自然语言习惯。
4. 支持多个参数的版本
前面的实现只支持单个初始值。但在实际使用中,函数往往需要多个参数。
// 基础版本只能处理单参数
compose(double, add)(3, 2); // add(3, 2) = 5, double(5) = 10 ✓
// 但如果想这样用就出问题了
compose(console.log, add); // 只收到第一个参数 3改进版本,让每个函数都能接收到完整的参数:
const compose = (...fns) => (...args) =>
fns.reduceRight((acc, fn) => fn(...acc), args);
const pipe = (...fns) => (...args) =>
fns.reduce((acc, fn) => fn(...acc), args);
// 使用
compose(console.log, add)(3, 2);
// add(3, 2) = 5, console.log(5)
pipe(add, console.log)(3, 2);
// add(3, 2) = 5, console.log(5)5. 处理初始值的情况
当没有初始值时,第一个函数会接收到多个参数:
const add = (a, b) => a + b;
const double = a => a * 2;
const square = a => a * a;
pipe(
add, // 接收 1, 2
double, // 接收 3
square // 接收 6
)(1, 2); // 最终结果 36如果想从某个初始值开始处理,可以传入初始值:
const sum = (...nums) => nums.reduce((a, b) => a + b, 0);
const addOne = a => a + 1;
const double = a => a * 2;
pipe(
sum, // 接收 [1, 2, 3],返回 6
addOne, // 接收 6,返回 7
double // 接收 7,返回 14
)(1, 2, 3); // 结果 146. 使用场景举例
6.1 数据处理链
const users = [
{ name: 'Alice', age: 25, active: true },
{ name: 'Bob', age: 30, active: false },
{ name: 'Charlie', age: 20, active: true },
];
const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);
const getActiveUserNames = pipe(
users => users.filter(u => u.active), // 过滤活跃用户
users => users.map(u => u.name), // 提取名字
names => names.map(n => n.toUpperCase()), // 转为大写
console.log // 输出
);
getActiveUserNames(); // ['ALICE', 'CHARLIE']6.2 验证链
const isNotEmpty = str => str.trim().length > 0;
const isEmail = str => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
const isLongEnough = str => str.length >= 6;
const validate = pipe(
isNotEmpty, // 先检查非空
isLongEnough, // 再检查长度
isEmail // 最后检查邮箱格式
);
console.log(validate(' ')); // false (非空检查失败)
console.log(validate('abc')); // false (长度检查失败)
console.log(validate('abc@')); // false (邮箱检查失败)
console.log(validate('abc@def.gh')); // true6.3 表单处理
const trim = str => str.trim();
const toLowerCase = str => str.toLowerCase();
const replaceSpaces = str => str.replace(/\s+/g, '-');
const normalizeInput = pipe(trim, toLowerCase, replaceSpaces);
normalizeInput(' Hello World '); // 'hello-world'7. lodash 延伸
lodash 提供了类似的功能:
_.flow(fns...)等同于 pipe,从左到右执行_.flowRight(fns...)等同于 compose,从右到左执行
import { flow, flowRight } from 'lodash';
const addOneThenDouble = flow(addOne, double);
const doubleThenAddOne = flowRight(addOne, double);实现一个简单的 flow
function flow(...fns) {
return function(...args) {
return fns.reduce((acc, fn) => {
return typeof acc === 'function' ? acc(...args) : fn(acc);
});
};
}