骚操作:函数管道
有的时候,我们可以面临这样一种场景:
- 需要连续调用多个函数才能得到结果
- 前一个函数的返回结果,将作为参数传递给下一个函数
遇到这种情况,我们就可以使用函数管道,将要依次调用的函数组装起来,形成一个完整的调用通道。
这里有一个例子。
考虑这么一种场景:我有一个字符串,需要对它依次进行下面的处理:
- 将字符串的每一个单词(第一个单词除外)首字母大写
- 将字符串中每一个单词除首字母外小写
- 去掉字符串的所有空白字符
- 若字符串的长度超过 15 个字符,去掉后面的字符
按照这样的要求,我需要编写 4 个函数:
/**
* 将字符串的每一个单词(第一个单词除外)首字母大写
* @param {string} str 待处理的字符串
*/
function firstUpper(str) {
return str.split(" ").filter(s => s.length > 0).map((s, index) => {
if (s.length > 0 ) {
return s[0].toUpperCase() + s.substring(1);
}
return s;
}).join(" ");
}
/**
* 将字符串中每一个单词除首字母外小写
* @param {string} str 待处理的字符串
*/
function otherLower(str) {
return str.split(" ").map(s => {
if (s.length > 0) {
return s[0] + s.substring(1).toLowerCase();
}
}).join(" ");
}
/**
* 去掉字符串的所有空白字符
* @param {string} str 待处理的字符串
*/
function removeEmpty(str) {
return str.replace(/\s*/g, "");
}
/**
* 若字符串的长度超过指定字符数,去掉后面的字符
* @param {number} number 要保留的字符数
* @param {string} str 待处理的字符串
*/
function cutString(str,number){
return str.substring(0, number);
}
有了这四个函数的帮助,我们只需要依次调用它们就可以完成功能了,下面是一个调用过程:
const str = " my firST nAme ";
const upper = firstUpper(str); //首字母大写
const lower = otherLower(upper); //其他字母小写
const words = removeEmpty(lower); //去掉空白
const result = cutString(words,15); //保留15个字符
console.log(result); //输出:myFirstName
你觉得上面的调用过程合适吗?它至少有以下的问题:
出现大量的中间变量,它们仅仅在计算过程中使用,计算完成后就变得毫无意义了。它们的出现除了增加 JS 垃圾回收的负担外毫无意义。
调用过程代码臃肿,不易阅读。
有的时候,我们想切换调用顺序(比如第一步和第二步调换顺序),却要作出不少的改动。
如果在不同的地方,有多个字符串需要我们这样处理,我们不得不反复的重复这个调用过程,非常的麻烦 那如何解决这些问题呢?
仔细观察上面的函数调用过程,它们每一次调用后产生的返回结果,将作为下一次调用的参数,于是,我们可以将这些函数连起来,形成一个管道。
假设我们已经实现了将函数连成管道的函数,姑且叫它pipe
,有了这个函数,我们就可以用下面的代码来操作了:
//连接函数管道
const camel = pipe(firstUpper, otherLower, removeEmpty, cutString);
console.log(camel(" my firST nAme "));//输出:myFirstName
console.log(camel(" user nick name "));//输出:userNickName
这样的代码是不是清爽多了呢?而且,我们可以根据需要,任意调换管道中的函数顺序,同时,可以将连接而成的管道反复使用。
接下来,就是如何实现管道函数了。
其实,管道函数就是一个高阶函数,它返回一个新的函数,这个新的函数在调用时,会将之前传入的函数循环调用,把每次调用的结果,作为参数放入到下一个函数。
管道函数实现如下:
/**
* 函数管道
* @param {Array} functions 要连接的函数数组
*/
function pipe(...functions){
return function(data){
let midData = data; //midData用于保存每次调用的结果
for(const func of functions){
midData = func(midData);
}
return midData;
}
}
如果你能够活用之前学习过的数组的累计函数 reduce
,就可以将上面的代码进一步简化为:
/**
* 函数管道
* @param {Array} functions 要连接的函数数组
*/
function pipe(...functions){
return function(data){
return functions.reduce((result, func)=>{
return func(result);
}, data);
}
}
到目前为止,我们完成了函数管道,今后凡是需要调用多个函数,并且前一个函数的结果是后一个函数的参数,遇到这种情况,直接用管道把它们连接起来,形成一个新的函数,之后调用新的函数即可。
由于函数管道强烈依赖一个前提条件,即管道中的函数必须只能有一个参数,因此,如果遇到了管道中需要用到多参函数的场景,我们可以利用上一节讲解的柯里化来固定已知参数。
面对这种情况,柯里化就可以登堂入室了:
//连接函数管道
const camel = pipe(firstUpper, otherLower, removeEmpty, curry(cutString, 15));
console.log(camel(" my firST nAme "));//输出:myFirstName
console.log(camel(" user nick name "));//输出:userNickName
Comments