实现 compose 方法
基础知识
什么是函数组合 (Function Composition)? 函数组合是一种将多个简单的、单一职责的函数,合并成一个更复杂的新函数的技术。其核心思想是,将一个函数的输出作为另一个函数的输入。
在数学上,如果我们有两个函数
f(x)和g(x),那么它们的组合函数(f ∘ g)(x)就等同于f(g(x))。执行顺序:从右到左 这是
compose函数的一个关键约定。当我们写compose(f, g, h)时,它生成的函数在执行时,函数的调用顺序是从右到左的。也就是说,它等价于(...args) => f(g(h(...args)))。- 数据首先被传入最右边的函数
h。 h的执行结果被传入中间的函数g。g的执行结果被传入最左边的函数f。f的最终结果是整个组合函数的返回结果。
注意:也有一种从左到右执行的组合方式,通常被称为
pipe。compose和pipe功能相同,只是执行顺序相反。- 数据首先被传入最右边的函数
为什么使用
compose?提高可读性: 它能让我们避免丑陋的函数嵌套地狱。对比一下:
// 嵌套方式 let result = fn1(fn2(fn3(fn4(value)))); // compose 方式 const composedFn = compose(fn1, fn2, fn3, fn4); let result = composedFn(value);compose的方式清晰地描述了一个从右到左的数据处理“管道”,更具声明性。促进函数复用: 它鼓励开发者编写小而美的、功能单一的“纯函数”,这些函数可以像乐高积木一样,在不同的组合中被复用。
核心思路
我们的目标是创建一个高阶函数 compose(...funcs),它接收任意数量的函数作为参数,并返回一个组合后的新函数。
收集函数: 使用 rest 参数
...funcs将所有传入的函数收集到一个数组中。处理边界情况:
- 如果没有传入任何函数,
compose()应该返回一个“恒等函数”,即一个接收什么就返回什么的函数 (arg => arg)。 - 如果只传入一个函数,
compose(fn)直接返回该函数fn即可。
- 如果没有传入任何函数,
创建函数链条: 核心思路是利用数组的
reduce方法,将函数数组“折叠”成一个单一的函数。- 我们可以将
[f, g, h]这个数组,通过reduce变为(...args) => f(g(h(...args)))。 reduce的初始累加值是第一个函数f。- 在第一次迭代中,
a是f,b是g。我们返回一个新函数(...args) => a(b(...args)),即(...args) => f(g(...args))。 - 在第二次迭代中,
a是上一步返回的新函数,b是h。我们再次返回(...args) => a(b(...args)),它展开后就是(...args) => ( (...innerArgs) => f(g(...innerArgs)) )(h(...args)),最终效果等同于(...args) => f(g(h(...args)))。
- 我们可以将
代码实现
下面是一个非常简洁且强大的 compose 函数实现。
/**
* 函数组合,将多个函数组合成一个新函数,从右到左执行。
* @param {...Function} funcs - 需要组合的函数列表
* @returns {Function} - 返回一个组合后的新函数
*/
function compose(...funcs) {
// 1. 处理没有传入函数的边界情况
if (funcs.length === 0) {
// 返回一个恒等函数
return (arg) => arg;
}
// 2. 处理只传入一个函数的边界情况
if (funcs.length === 1) {
return funcs[0];
}
// 3. 使用 Array.prototype.reduce 将函数串联起来
return funcs.reduce(
(a, b) =>
(...args) =>
a(b(...args))
);
}
代码解析
边界情况处理:
- 空函数数组:当
funcs.length === 0时,返回恒等函数(arg) => arg。该函数接收任意参数并直接返回,确保无函数组合时调用安全(如避免TypeError)。 - 单个函数:当
funcs.length === 1时,直接返回唯一的函数,无需额外处理,符合“组合单个函数即自身”的逻辑。
- 空函数数组:当
核心实现:数组 reduce 方法:
- reduce 作用:通过
funcs.reduce(...)从左到右遍历函数数组,逐步组合函数。 - 累加器逻辑:每次迭代返回新函数作为下一次的累加器,实现函数嵌套调用。
- 参数说明:
a:已组合好的函数(累加器)。b:当前处理的函数(数组元素)。
- reduce 作用:通过
函数组合逻辑分解:
- 迭代函数定义:
(a, b) => (...args) => a(b(...args))。- 外层
(...args)接收最终调用时的参数。 - 内层先执行
b(...args)(当前函数处理参数),再将结果传入a(已组合函数)。
- 外层
- 手动模拟 compose(f, g, h):
- 第一次迭代:
a = f,b = g。- 返回函数:
(...args) => f(g(...args))。
- 第二次迭代:
a = (...args) => f(g(...args)),b = h。- 返回函数:
(...args) => [f(g(...args))](h(...args)),等价于f(g(h(...args)))。
- 第一次迭代:
- 最终结果:reduce 完成后返回嵌套所有函数的调用链,参数从右至左传递(如
compose(f, g, h)(x)等价于f(g(h(x))))。
- 迭代函数定义:
执行顺序关键点:
- 组合函数调用时,参数先传入最右侧函数(如
h),结果依次传递给左侧函数(g→f),形成“右到左”的执行链。 - reduce 的累加器机制确保每次迭代都将新函数“包裹”在已组合函数外层,最终形成完整的函数调用嵌套。
- 组合函数调用时,参数先传入最右侧函数(如
使用示例
// 准备一些单一功能的函数
const toUpperCase = (str) => str.toUpperCase();
const addExclamation = (str) => `${str}!`;
const reverse = (str) => str.split('').reverse().join('');
// 使用 compose 将它们组合起来
// 执行顺序:reverse -> addExclamation -> toUpperCase
const composedFn = compose(toUpperCase, addExclamation, reverse);
const result = composedFn('hello');
console.log(result); // 输出: "!OLLEH"
const resultShouldBe = toUpperCase(addExclamation(reverse('hello')));
console.log(resultShouldBe); // OLLEH!
// 啊,是我最开始写的示例输出错了,现在更正。
console.log('实际结果:', result); // 应该是 OLLEH!