这些高阶的函数技术,你掌握了么

在 JavaScript 中,函数为一等公民(First Class),所谓的 “一等公民”,指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或作为其它函数的返回值

接下来阿宝哥将介绍与函数相关的一些技术,阅读完本文,你将了解高阶函数、函数组合、柯里化、偏函数、惰性函数和缓存函数的相关知识。

一、高阶函数

在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数:

  • 接受一个或多个函数作为输入;
  • 输出一个函数。

接收一个或多个函数作为输入,即函数作为参数传递。这种应用场景,相信很多人都不会陌生。比如常用的 Array.prototype.map()Array.prototype.filter() 高阶函数:

// Array.prototype.map 高阶函数
const array = [1, 2, 3, 4];
const map = array.map(x => x * 2); // [2, 4, 6, 8]

// Array.prototype.filter 高阶函数
const words = ['semlinker', 'kakuqo', 'lolo', 'abao'];
const result = words.filter(word => word.length > 5); // ['semlinker', 'kakuqo']

而输出一个函数,即调用高阶函数之后,会返回一个新的函数。我们日常工作中,常见的 debouncethrottle 函数就满足这个条件,因此它们也可以被称为高阶函数。

二、函数组合

函数组合就是将两个或两个以上的函数组合生成一个新函数的过程:

const composeFn = function (f, g) {  return function (x) {    return f(g(x));  };};

在以上代码中,fg 都是函数,而 x 是组合生成新函数的参数。

2.1 函数组合的作用

在项目开发过程中,为了实现函数的复用,我们通常会尽量保证函数的职责单一,比如我们定义了以下功能函数:

在拥有以上功能函数的基础上,我们就可以自由地对函数进行组合,来实现特定的功能:

function lowerCase(input) {
  return input && typeof input === 'string' ? input.toLowerCase() : input;
}

function upperCase(input) {
  return input && typeof input === 'string' ? input.toUpperCase() : input;
}

function trim(input) {
  return typeof input === 'string' ? input.trim() : input;
}

function split(input, delimiter = ',') {
  return typeof input === 'string' ? input.split(delimiter) : input;
}

const trimLowerCaseAndSplit = compose(trim, lowerCase, split); // 参考下面compose的实现
trimLowerCaseAndSplit(' a,B,C '); // ['a', 'b', 'c']

在以上的代码中,我们通过 compose 函数实现了一个 trimLowerCaseAndSplit 函数,该函数会对输入的字符串,先执行去空格处理,然后在把字符串中包含的字母统一转换为小写,最后在使用 , 分号对字符串进行拆分。利用函数组合的技术,我们就可以很方便的实现一个 trimUpperCaseAndSplit 函数。

2.2 组合函数的实现

function compose(...funcs) {  return function (x) {    return funcs.reduce(function (arg, fn) {      return fn(arg);    }, x);  };}

在以上的代码中,我们通过 Array.prototype.reduce 方法来实现组合函数的调度,对应的执行顺序是从左到右。这个执行顺序与 Linux 管道或过滤器的执行顺序是一致的。

不过如果你想从右往左开始执行的话,这时你就可以使用 Array.prototype.reduceRight 方法来实现。

其实每当看到 compose 函数,阿宝哥就情不自禁想到 “如何更好地理解中间件和洋葱模型” 这篇文章中介绍的 compose 函数:

function compose(middleware) {
  // 省略部分代码
  return function (context, next) {
    let index = -1;
    return dispatch(0);
    function dispatch(i) {
      if (i <= index)
        return Promise.reject(new Error('next() called multiple times'));
      index = i;
      let fn = middleware[i];
      if (i === middleware.length) fn = next;
      if (!fn) return Promise.resolve();
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}

利用上述的 compose 函数,我们就可以实现以下通用的任务处理流程:

三、柯里化

柯里化(Currying)是一种处理函数中含有多个参数的方法,并在只允许单一参数的框架中使用这些函数。这种转变是现在被称为 “柯里化” 的过程,在这个过程中我们能把一个带有多个参数的函数转换成一系列的嵌套函数。它返回一个新函数,这个新函数期望传入下一个参数。当接收足够的参数后,会自动执行原函数。

在理论计算机科学中,柯里化提供了简单的理论模型,比如:在只接受一个单一参数的 lambda 演算中,研究带有多个参数的函数的方式。与柯里化相反的是 Uncurrying,一种使用匿名单参数函数来实现多参数函数的方法。比如:

const func = function(a) {  return function(b) {    return a * a + b * b;  }}

func(3)(4); // 25

Uncurrying 不是本文的重点,接下来我们使用 Lodash 提供的 curry 函数来直观感受一下,对函数进行 “柯里化” 处理之后产生的变化:

const abc = function(a, b, c) {
  return [a, b, c];
};
 
const curried = _.curry(abc);
 
curried(1)(2)(3); // => [1, 2, 3]
curried(1, 2)(3); // => [1, 2, 3]
curried(1, 2, 3); // => [1, 2, 3]

_.curry(func, [arity=func.length])

创建一个函数,该函数接收 func 的参数,要么调用func返回的结果,如果 func 所需参数已经提供,则直接返回 func 所执行的结果。或返回一个函数,接受余下的func 参数的函数,可以使用 func.length 设置需要累积的参数个数。

来源:https://www.lodashjs.com/docs/lodash.curry

这里需要特别注意的是,在数学和理论计算机科学中的柯里化函数,一次只能传递一个参数。而对于 JavaScript 语言来说,在实际应用中的柯里化函数,可以传递一个或多个参数。好的,介绍完柯里化的相关知识,接下来我们来介绍柯里化的作用。

3.1 柯里化的作用

3.1.1 参数复用
function buildUri(scheme, domain, path) {  return `${scheme}://${domain}/${path}`;}

const profilePath = buildUri('https', 'github.com', 'semlinker/semlinker');const awesomeTsPath = buildUri('https', 'github.com', 'semlinker/awesome-typescript');

在以上代码中,首先我们定义了一个 buildUri 函数,该函数可用于构建 uri 地址。接着我们使用 buildUri 函数构建了阿宝哥 Github 个人主页 和 awesome-typescript 项目的地址。对于上述的 uri 地址,我们发现 httpsgithub.com 这两个参数值是一样的。

假如我们需要继续构建阿宝哥其他项目的地址,我们就需要重复设置相同的参数值。那么有没有办法简化这个流程呢?答案是有的,就是对 buildUri 函数执行柯里化处理,具体处理方式如下:

const _ = require('lodash');

const buildUriCurry = _.curry(buildUri);
const myGithubPath = buildUriCurry('https', 'github.com');
const profilePath = myGithubPath('semlinker/semlinker');
const awesomeTsPath = myGithubPath('semlinker/awesome-typescript');

3.1.2 延迟计算/运行
const add = function (a, b) {  return a + b;};

const curried = _.curry(add);const plusOne = curried(1);

在以上代码中,通过对 add 函数执行 “柯里化” 处理,我们可以实现延迟计算。好的,简单介绍完柯里化的作用,我们来动手实现一个柯里化函数。

3.2 柯里化的实现

现在我们已经知道了,当柯里化后的函数接收到足够的参数后,就会开始执行原函数。而如果接收到的参数不足的话,就会返回一个新的函数,用来接收余下的参数。基于上述的特点,我们就可以自己实现一个 curry 函数:

function curry(func) {
  return function curried(...args) {
    if (args.length >= func.length) { // 通过函数的length属性,来获取函数的形参个数
      return func.apply(this, args);
    } else {
      return function (...args2) {
        return curried.apply(this, args.concat(args2));
      };
    }
  }
}

四、偏函数应用

在计算机科学中,偏函数应用(Partial Application)是指固定一个函数的某些参数,然后产生另一个更小元的函数。而所谓的元是指函数参数的个数,比如含有一个参数的函数被称为一元函数。

偏函数应用(Partial Application)很容易与函数柯里化混淆,它们之间的区别是:

  • 偏函数应用是固定一个函数的一个或多个参数,并返回一个可以接收剩余参数的函数;
  • 柯里化是将函数转化为多个嵌套的一元函数,也就是每个函数只接收一个参数。

了解完偏函数与柯里化的区别之后,我们来使用 Lodash 提供的 partial 函数来了解一下它如何使用。

4.1 偏函数的使用

function buildUri(scheme, domain, path) {  return `${scheme}://${domain}/${path}`;}

const myGithubPath = _.partial(buildUri, 'https', 'github.com');const profilePath = myGithubPath('semlinker/semlinker');const awesomeTsPath = myGithubPath('semlinker/awesome-typescript');

_.partial(func, [partials])

创建一个函数。该函数调用 func,并传入预设的 partials 参数。

来源:https://www.lodashjs.com/docs/lodash.partial

4.2 偏函数的实现

偏函数用于固定一个函数的一个或多个参数,并返回一个可以接收剩余参数的函数。基于上述的特点,我们就可以自己实现一个 partial 函数:

function partial(fn) {
  let args = [].slice.call(arguments, 1);
  return function () {
    const newArgs = args.concat([].slice.call(arguments));
    return fn.apply(this, newArgs);
  };
}

4.3 偏函数实现 vs 柯里化实现

五、惰性函数

由于不同浏览器之间存在一些兼容性问题,这导致了我们在使用一些 Web API 时,需要进行判断,比如:

function addHandler(element, type, handler) {  if (element.addEventListener) {    element.addEventListener(type, handler, false);  } else if (element.attachEvent) {    element.attachEvent('on' + type, handler);  } else {    element['on' + type] = handler;  }}

在以上代码中,我们实现了不同浏览器 添加事件监听 的处理。代码实现起来也很简单,但存在一个问题,即每次调用的时候都需要进行判断,很明显这是不合理的。对于上述这个问题,我们可以通过惰性载入函数来解决。

5.1 惰性载入函数

所谓的惰性载入就是当第 1 次根据条件执行函数后,在第 2 次调用函数时,就不再检测条件,直接执行函数。要实现这个功能,我们可以在第 1 次条件判断的时候,在满足判断条件的分支中覆盖掉所调用的函数,具体的实现方式如下所示:

function addHandler(element, type, handler) {
  if (element.addEventListener) {
    addHandler = function (element, type, handler) {
      element.addEventListener(type, handler, false);
    };
  } else if (element.attachEvent) {
    addHandler = function (element, type, handler) {
      element.attachEvent('on' + type, handler);
    };
  } else {
    addHandler = function (element, type, handler) {
      element['on' + type] = handler;
    };
  }
  // 保证首次调用能正常执行监听
  return addHandler(element, type, handler);
}

除了使用以上的方式,我们也可以利用自执行函数来实现惰性载入:

const addHandler = (function () {  if (document.addEventListener) {    return function (element, type, handler) {      element.addEventListener(type, handler, false);    };  } else if (document.attachEvent) {    return function (element, type, handler) {      element.attachEvent('on' + type, handler);    };  } else {    return function (element, type, handler) {      element['on' + type] = handler;    };  }})();

通过自执行函数,在代码加载阶段就会执行一次条件判断,然后在对应的条件分支中返回一个新的函数,用来实现对应的处理逻辑。

六、缓存函数

缓存函数是将函数的计算结果缓存起来,当下次以同样的参数调用该函数时,直接返回已缓存的结果,而无需再次执行函数。这是一种常见的以空间换时间的性能优化手段。

要实现缓存函数的功能,我们可以把经过序列化的参数作为 key,在把第 1 次调用后的结果作为  value 存储到对象中。在每次执行函数调用前,都先判断缓存中是否含有对应的 key,如果有的话,直接返回该 key 对应的值。分析完缓存函数的实现思路之后,接下来我们来看一下具体如何实现:

function memorize(fn) {
  const cache = Object.create(null); // 存储缓存数据的对象
  return function (...args) {
    const _args = JSON.stringify(args);
    return cache[_args] || (cache[_args] = fn.apply(fn, args));
  };
};

定义完 memorize 缓存函数之后,我们就可以这样来使用它:

let complexCalc = (a, b) => {  // 执行复杂的计算};

let memoCalc = memorize(complexCalc);memoCalc(666, 888);memoCalc(666, 888); // 从缓存中获取

七、参考资源

  • 维基百科 - 高阶函数
  • 维基百科 - 柯里化
  • javascript-functional-programming-explained-partial-application-and-currying
(0)

相关推荐

  • C++雾中风景16:std::make_index_sequence, 来试一试新的黑魔法吧

    C++14在标准库里添加了一个很有意思的元函数: std::integer_sequence.并且通过它衍生出了一系列的帮助模板: std::make_integer_sequence, std::m ...

  • 高端知识点:折叠表达式(我也是第一次知道)

    不知道大家以前了解折叠表达式不,这里介绍一下! 我也是最近才知道这个知识点,在C++中通过折叠表达式可以更容易的在可变参数模板中处理可变参数. 先看一下传统的使用递归方式来处理可变参数的代码: #in ...

  • ​高阶的思考技术

    高阶的思考技术:以孙子兵法为例来谈思考,说白了就是表征.计算 认知框架 2020-08-24优质创作者 已关注 所属专栏:高阶的思考技术! 心智是由各个器官组成的一个系统,这个系统执行的功能就是计算, ...

  • 没有学不会的python--函数式编程以及高阶函数

    没有学不会的python 函数式编程 到现在为止,我们的没有学不会python系列文章已经讲了很多知识点了,如果是第一次刷到这篇文章的朋友可以去我主页看一下以前写的文章.前面讲了很多知识点,每个知识点 ...

  • Python高阶函数

    该篇中主要介绍什么是高阶函数,高阶函数的用法以及几个常见的内置的高阶函数. 什么是高阶函数? 高阶函数:一个函数可以作为参数传给另外一个函数,或者一个函数的返回值为另外一个函数(若返回值为该函数本身, ...

  • 安利5个Python高阶函数:lambda,Map,Filter,Itertools,Generat...

    任何编程语言的高级特征通常都是通过大量的使用经验才发现的.比如你在编写一个复杂的项目,并在 stackoverflow 上寻找某个问题的答案.然后你突然发现了一个非常优雅的解决方案,它使用了你从不知道 ...

  • 第 14 天:Python 高阶函数

    函数式编程现在逐渐被广大开发群体接受,越来越多的开发者们开始使用这种优雅的开发模式,而我们使用函数式编程最主要的是需要清楚: 什么是高阶函数(Higher-order Functions)? Pyth ...

  • 4个vlookup函数的高阶用法

    一.提取固定长度的数字 如下图,我们想要提取工号,只需要将函数设置为:=VLOOKUP(0,{0,1}*MID(A2,ROW($1:$30),6),2,0), 因为这个一个数组公式所以我们要按Ctrl ...

  • 高阶牛仔课!【双人技术】摆荡与脊柱关系

    主题:[双人牛仔] 骨盆技巧,脚掌摆荡与脊柱关系 双人对抗+力量传导原理与技巧 星光高阶组合,慢动作分解 15分钟单人技巧+45分钟双人技巧 +30分钟组合分解 适合教学+高级选手 视频共90分钟(中 ...

  • 【图解】长胶“挤”,很隐蔽很刁专的高阶技术!

    长胶大联盟 2019-06-14 "快拉过去被长胶一挤,那球比拱的还恶心.弧线低平,速度更快,还是下旋,接到拍子上都是重的.根本来不及发力拉,只来得及用拍子一挡,好吧,下网-" 长 ...

  • 「MACD循环理论」:高阶技术,更高的是思维体系 转载

    第一部分:要用周期循环的视角,思维看待过去,当下和未来 以人为本位思考,过去犯的错误,当下未必不会犯,当下犯的错误,未来未必不会犯: 以人为本位思考,过去能做到的事情,当下及未来未必一定做得好: 以人 ...