各位观众老爷,大家好!今天咱们来聊聊JavaScript里两个非常有趣,而且在函数式编程中举足轻重的家伙:compose
和 pipe
。 这俩兄弟,一个从右往左,一个从左往右,专门负责把一堆函数像流水线一样串起来。 准备好了吗?咱们这就开始!
1. 函数组合:为什么要这么干?
先问大家一个问题:你们平时写代码,是不是经常会遇到这样的情况?
const number = 5;
const doubled = number * 2;
const squared = doubled * doubled;
const result = squared + 10;
console.log(result); // 110
这段代码很简单,对一个数字进行了翻倍、平方,最后加10。 但如果逻辑更复杂一些,或者需要多次复用这个流程,代码就会变得冗长而难以维护。
函数组合就是用来解决这个问题的。 它的核心思想是:把多个函数像搭积木一样组合起来,形成一个新的函数,这个新函数可以一次性完成所有操作。
用函数组合的方式,上面的代码可以这样写(先别管 compose
是什么,后面会详细解释):
const double = x => x * 2;
const square = x => x * x;
const addTen = x => x + 10;
const composedFunction = compose(addTen, square, double); // 注意顺序!
const result = composedFunction(5);
console.log(result); // 110
看到了吗? 我们把 double
、square
和 addTen
这三个函数组合成了一个新的函数 composedFunction
。 调用 composedFunction(5)
就能得到和之前一样的结果。
这样做的好处是什么呢?
- 代码更简洁: 避免了中间变量,代码更易读。
- 可复用性更高: 可以把常用的函数组合封装成一个独立的函数,方便在不同的地方使用。
- 更易于测试: 每个小函数都是独立的,更容易进行单元测试。
- 更贴近函数式编程的思想: 函数组合是函数式编程的核心概念之一。
2. compose
:从右向左的函数组合
compose
函数的作用是将多个函数从右向左依次执行,并将前一个函数的返回值作为后一个函数的输入。
用人话说,就像叠罗汉,最下面的函数先执行,然后把结果给上面的函数,依次往上,直到最上面的函数执行完毕。
一个简单的 compose
函数的实现如下:
const compose = (...fns) => {
return (arg) => {
return fns.reduceRight((result, fn) => {
return fn(result);
}, arg);
};
};
这段代码看着可能有点晕,咱们来一步步解释:
...fns
: 使用剩余参数语法,将所有传入的函数收集到一个数组fns
中。return (arg) => { ... }
:compose
函数返回一个函数,这个函数接收一个参数arg
,这个参数将作为最右边(也就是第一个执行)的函数的输入。fns.reduceRight((result, fn) => { ... }, arg)
: 使用reduceRight
方法从右向左遍历fns
数组。result
: 累积值,初始值为arg
。fn
: 当前遍历到的函数。fn(result)
: 将result
作为fn
的输入,并返回fn
的执行结果,作为下一次reduceRight
的result
。
简单来说,reduceRight
就是从数组的最后一个元素开始,依次将每个元素(这里是函数)应用到累积值上,最终返回一个累积值。
现在,让我们用 compose
函数来改造之前的例子:
const double = x => x * 2;
const square = x => x * x;
const addTen = x => x + 10;
const composedFunction = compose(addTen, square, double);
const result = composedFunction(5);
console.log(result); // 110
在这个例子中,compose(addTen, square, double)
的执行顺序是:
double(5)
得到 10square(10)
得到 100addTen(100)
得到 110
最终 composedFunction(5)
的结果就是 110。
compose
的一些注意事项
- 参数类型:
compose
组合的函数需要满足一个条件:前一个函数的返回值类型必须是后一个函数可以接受的参数类型。 否则,就会出错。 - 执行顺序:
compose
的执行顺序是从右向左的,这对于理解函数组合至关重要。 - 参数数量:
compose
组合的函数可以是单参数函数,也可以是多参数函数,但只有最右边的函数可以接收多个参数。 比如,compose(f, g, h)
,h
可以接收多个参数,但g
和f
只能接收一个参数。
3. pipe
:从左向右的函数组合
pipe
函数和 compose
函数的功能类似,都是将多个函数组合成一个函数。 唯一的区别是,pipe
函数的执行顺序是从左向右的。
用人话说,pipe
就像一条管道,数据从管道的一端流入,经过一系列函数的处理,最终从管道的另一端流出。
一个简单的 pipe
函数的实现如下:
const pipe = (...fns) => {
return (arg) => {
return fns.reduce((result, fn) => {
return fn(result);
}, arg);
};
};
这段代码和 compose
函数的代码几乎一样,唯一的区别是使用了 reduce
方法而不是 reduceRight
方法。
现在,让我们用 pipe
函数来改造之前的例子:
const double = x => x * 2;
const square = x => x * x;
const addTen = x => x + 10;
const pipedFunction = pipe(double, square, addTen);
const result = pipedFunction(5);
console.log(result); // 110
在这个例子中,pipe(double, square, addTen)
的执行顺序是:
double(5)
得到 10square(10)
得到 100addTen(100)
得到 110
最终 pipedFunction(5)
的结果也是 110。
pipe
的一些注意事项
- 参数类型: 和
compose
一样,pipe
组合的函数也需要满足一个条件:前一个函数的返回值类型必须是后一个函数可以接受的参数类型。 - 执行顺序:
pipe
的执行顺序是从左向右的,这对于理解函数组合至关重要。 - 参数数量:
pipe
组合的函数可以是单参数函数,也可以是多参数函数,但只有最左边的函数可以接收多个参数。 比如,pipe(h, g, f)
,h
可以接收多个参数,但g
和f
只能接收一个参数。
4. compose
vs pipe
:选哪个?
既然 compose
和 pipe
都能实现函数组合,那么我们应该选择哪个呢?
其实,这取决于个人偏好和代码的可读性。
- 如果你习惯从右向左阅读代码,或者你的代码逻辑更适合从右向左思考,那么
compose
可能更适合你。 - 如果你习惯从左向右阅读代码,或者你的代码逻辑更符合数据流动的模式,那么
pipe
可能更适合你。
一般来说,pipe
更符合我们日常的阅读习惯,更容易理解代码的执行流程。 因此,在大多数情况下,我个人更倾向于使用 pipe
。
5. 实际应用:来点更复杂的例子
理论讲完了,咱们来几个更实际的例子,看看 compose
和 pipe
在实际开发中是如何使用的。
5.1 数据转换
假设我们需要从一个包含用户信息的数组中提取所有用户的姓名,并将姓名转换为大写。 可以这样实现:
const users = [
{ id: 1, name: 'john doe', age: 30 },
{ id: 2, name: 'jane smith', age: 25 },
{ id: 3, name: 'peter jones', age: 40 },
];
const getName = user => user.name;
const toUpperCase = str => str.toUpperCase();
const extractNames = users => users.map(getName);
const processNames = pipe(
extractNames,
names => names.map(toUpperCase)
);
const result = processNames(users);
console.log(result); // ["JOHN DOE", "JANE SMITH", "PETER JONES"]
在这个例子中,我们使用了 pipe
函数将三个函数 extractNames
、names => names.map(toUpperCase)
组合起来,形成一个新的函数 processNames
。 processNames
函数可以一次性完成提取姓名和转换为大写的功能。
5.2 验证表单
假设我们需要验证一个表单,需要验证用户名、密码和邮箱是否符合规范。 可以这样实现:
const isNotEmpty = str => str.length > 0;
const isEmail = str => /^[^s@]+@[^s@]+.[^s@]+$/.test(str);
const isPasswordValid = str => str.length >= 8;
const validateUsername = username => {
if (!isNotEmpty(username)) {
return '用户名不能为空';
}
return '';
};
const validatePassword = password => {
if (!isNotEmpty(password)) {
return '密码不能为空';
}
if (!isPasswordValid(password)) {
return '密码长度必须大于等于8位';
}
return '';
};
const validateEmail = email => {
if (!isNotEmpty(email)) {
return '邮箱不能为空';
}
if (!isEmail(email)) {
return '邮箱格式不正确';
}
return '';
};
const validateForm = (username, password, email) => {
return {
username: validateUsername(username),
password: validatePassword(password),
email: validateEmail(email),
};
};
const form = {
username: 'john',
password: 'password123',
email: '[email protected]',
};
const errors = validateForm(form.username, form.password, form.email);
console.log(errors);
如果使用 compose
或者 pipe
,可以把验证逻辑封装得更简洁:
const isNotEmpty = str => str.length > 0;
const isEmail = str => /^[^s@]+@[^s@]+.[^s@]+$/.test(str);
const isPasswordValid = str => str.length >= 8;
const validate = (validator, errorMessage) => value => {
if (!validator(value)) {
return errorMessage;
}
return '';
};
const validateUsername = validate(isNotEmpty, '用户名不能为空');
const validatePassword = validate(password => isNotEmpty(password) && isPasswordValid(password), '密码不能为空且长度必须大于等于8位');
const validateEmail = validate(isEmail, '邮箱格式不正确');
const form = {
username: 'john',
password: 'password123',
email: '[email protected]',
};
const errors = {
username: validateUsername(form.username),
password: validatePassword(form.password),
email: validateEmail(form.email)
};
console.log(errors);
这个例子展示了如何使用高阶函数来简化验证逻辑。虽然没有直接使用 compose
或 pipe
,但这种函数式的思维方式同样可以提高代码的可读性和可维护性。
5.3 Redux Middleware
在 Redux 中,middleware 也是一种函数组合的应用。 Redux middleware 可以拦截和处理 dispatch 的 action,例如:
const logger = store => next => action => {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};
const thunk = store => next => action => {
if (typeof action === 'function') {
return action(store.dispatch, store.getState);
}
return next(action);
};
applyMiddleware
函数就是用来组合这些 middleware 的:
import { applyMiddleware, createStore } from 'redux';
const store = createStore(reducer, applyMiddleware(thunk, logger));
applyMiddleware
的内部实现其实就是使用 compose
将多个 middleware 组合起来,形成一个新的函数,这个函数可以增强 dispatch
的功能。
6. 小结
今天我们学习了 JavaScript 中 compose
和 pipe
这两个非常有用的函数组合工具。 它们可以帮助我们编写更简洁、可复用、易于测试的代码。 掌握 compose
和 pipe
是掌握函数式编程的关键一步。
最后,给大家留个小作业:
- 尝试自己实现一个
compose
和pipe
函数。 - 在自己的项目中找到一些可以使用
compose
或pipe
的场景,并尝试用它们来优化代码。
希望今天的讲座对大家有所帮助! 咱们下次再见!