08月07, 2022

JS进阶(7)--高阶函数(5)--函数管道

骚操作:函数管道

有的时候,我们可以面临这样一种场景:

  1. 需要连续调用多个函数才能得到结果
  2. 前一个函数的返回结果,将作为参数传递给下一个函数

遇到这种情况,我们就可以使用函数管道,将要依次调用的函数组装起来,形成一个完整的调用通道。

这里有一个例子。

考虑这么一种场景:我有一个字符串,需要对它依次进行下面的处理:

  1. 将字符串的每一个单词(第一个单词除外)首字母大写
  2. 将字符串中每一个单词除首字母外小写
  3. 去掉字符串的所有空白字符
  4. 若字符串的长度超过 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

本文链接:http://www.yanhongzhi.com/post/js_ap_17.html

-- EOF --

Comments