实现 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!