概述
函数式编程的一个思想是:每个函数只做好一件事情。每个函数就像是一个个乐高积木,当我们在构建复杂系统的时候,我们应该清晰的了解我们接下来需要哪个“部件”,以及在哪里获取这个部件。
但有些时候,你把蓝色 2x2 的方块和灰色 4x1 的方块以某种形式组装到一起,然后意识到:“这是个有用的部件,我可能会常用到它”。
那么你现在想到了一种新的“部件”,它是两种其他部件的组合,在需要的时候能触手可及。这时候,将这个蓝黑色 L 形状的方块组合体放到需要使用的地方,比每次分开考虑两种独立方块的组合要有效的多。
所以我们可以通过定义某种组合方式,让他们称为一种新的函数组合,并在程序的不同部分使用,这种方式称为函数组合。
输出到输入
函数组合的核心在于:一个函数的输出是另一个函数的输入。把数据的流向想像成一个流水线的传送带,在传送带的各个环节负责对物品的不同处理。举个例子,下面是两个常用的函数:
function words(str) {
return String( str )
.toLowerCase()
.split( /\s|\b/ )
.filter( function alpha(v){
return /^[\w]+$/.test( v );
} );
}
function unique(list) {
var uniqList = [];
for (let i = 0; i < list.length; i++) {
// value not yet in the new list?
if (uniqList.indexOf( list[i] ) === -1 ) {
uniqList.push( list[i] );
}
}
return uniqList;
}
当我们应用它时我们会这样使用:
var wordsFound = words( text );
var wordsUsed = unique( wordsFound );
然而我们在构建程序的过程中经常需要将这两种函数一起使用,让它更像是一个具备了两种功能的函数套件,并节省代码量,我们可以这样调用:
var wordsUsed = unique( words( text ) );
但是这样看起来比较凌乱,我们可以写一个组合函数的函数:
function compose(fn2,fn1) {
return function composed(origValue){
return fn2( fn1( origValue ) );
};
}
用箭头函数表示
var compose =
(fn2,fn1) =>
origValue =>
fn2( fn1( origValue ) );
最后 我们可以利用这个函数来组合我们之前的两个函数了:
var uniqueWords = compose2( unique, words );
var wordsUsed = uniqueWords(text);
更进一步
当然,在更多时候我们需要一个更加通用的函数组合方式,而不仅仅是只能组合两个函数,而是可以组合任意数量的函数。思路也是非常简单,我们通过一个while循环,不断将需要组合的函数拿出来执行即可。
function compose(...fns) {
return function composed(result){
// 拷贝一份保存函数的数组
var list = fns.slice();
while (list.length > 0) {
// 将最后一个函数从列表尾部拿出
// 并执行它
result = list.pop()( result );
}
return result;
};
}
我们在之前的两个函数的基础上增加一个函数:
function skipShortWords(list) {
var filteredList = [];
for (let i = 0; i < list.length; i++) {
if (list[i].length > 4) {
filteredList.push( list[i] );
}
}
return filteredList;
}
然后我们就可以将三个函数组合起来使用:
var biggerWords = compose( skipShortWords, unique, words );
var wordsUsed = biggerWords( text );
其它实现
当然,你可能不会在实际的开发中自己去实现一个compose函数用来组合函数,你一般会选择使用一个三方库,但是了解函数组合的实现有助于我们理解函数式编程的思想。compose函数的原始实现方式与我们这个有一些不同:
let compose = (...fns) => {
return (...args) => {
return fns.reverse().reduce((fn1, fn2) => {
return fn2(fn1(...args));
})
}
}
核心是使用reduce函数来实现循环,这种实现的好处在于代码更加简洁,且使用了reduce这样的函数式编程的方法实现,在性能上跟for循环实现差不多
同样,我们可以使用递归来实现compose函数:
var compose = (...fns) => {
// 拿出最后两个参数
var [ fn1, fn2, ...rest ] = fns.reverse();
var composedFn = (...args) => {
fn2( fn1( ...args ) );
}
if (rest.length == 0) return composedFn;
return compose( ...rest.reverse(), composedFn );
};
总结
函数组合是一种定义函数的模式,它能将一个函数调用的输出路由到另一个函数的调用上,然后一直进行下去。
因为 JS 函数只能返回单个值,这个模式本质上要求所有组合中的函数(可能第一个调用的函数除外)是一元的,当前函数从上一个函数输出中只接收一个输入。
相较于在我们的代码里详细列出每个调用,函数组合使用 compose(..)
实用函数来提取出实现细节,让代码变得更可读,让我们更关注组合完成的是什么,而不是它具体做什么。