阮一峰: https://ruanyifeng.com/blog/2017/02/fp-tutorial.html
原文: https://zhuanlan.zhihu.com/p/363757919
上一篇文章我们讲解了闭包的机制 从编译原理角度认识 javascript中的「闭包」,提到闭包可以理解为 定义在一个函数内部的函数,将内部函数作为返回值。这正体现了函数是一等公民的特点。而这正是我们要说的函数式编程的两个基本特征之。

到底什么是函数式编程?

其实,函数式编程是一种编程范式,除了函数式编程之外还有 命令式编程,声明式编程 等编程范式。
命令式编程

命令式编程 是面向计算机硬件的抽象,有变量、赋值语句、表达式、控制语句等,可以理解为 命令式编程就是冯诺伊曼的指令序列。 它的主要思想是关注计算机执行的步骤,即一步一步告诉计算机先做什么再做什么。

比如,我们要查找数组 numList 中大于5的所有数字,需要这样告诉计算机:

  1. 创建一个存储结果的集合变量 results
  2. 遍历这个数字集合 numList;
  3. 一个一个地判断每个数字是不是大于 5,如果是就将这个数字添加到结果集合变量 results 中。
    1. let results = [];
    2. for(let i = 0; i < numList.length; i++){
    3. if(numList[i] > 5){
    4. results.push(numList[i])
    5. }
    6. }
    声明式编程

    声明式编程 是以数据结构的形式来表达程序执行的逻辑。它的主要思想是告诉计算机应该做什么,但不指定具体要怎么做。SQL 语句就是最明显的一种声明式编程的例子,例如:

  1. SELECT * FROM collection WHERE num > 5

除了 SQL,网页编程中用到的 HTML 和 CSS 也都属于声明式编程。它的特点:

  • 它不需要创建变量用来存储数据
  • 另一个特点是它不包含循环控制的代码如 for, while

函数式编程
而函数式编程和声明式编程是有所关联的,因为他们思想是一致的:即只关注做什么而不是怎么做。但函数式编程不仅仅局限于声明式编程

函数式编程是面向数学的抽象,将计算描述为一种表达式求值,其实,函数式程序就是一个表达式。

函数式编程本质

函数式编程中函数并部署指计算机中的函数,而是指数学中的函数,即自变量的映射。函数的值取决于函数的参数的值,不依赖于其他状态,比如abs(x)函数计算x的绝对值,只要x不变,无论何时调用、调用次数,最终的值都是一样。

函数式编程的特点

  • 函数是第一等公民
  • 函数是纯函数

接下来我们分别介绍下函数式编程的这两个特点
函数是第一等公民
函数是第一等公民:是指函数跟其它的数据类型一样处于平等地位,可以赋值给其他变量,可以作为参数传入另一个函数,也可以作为别的函数的返回值。正如我们开头提到的闭包的实现好体现了这个特点,例如如下代码:

  1. // 赋值
  2. var func1 = function func1() { }
  3. // 函数作为参数
  4. function func2(fn) {
  5. fn()
  6. }
  7. // 函数作为返回值
  8. function func3() {
  9. return function() {}
  10. }

函数是纯函数

纯函数是指相同的输入总会得到相同的输出,并且不会产生副作用的函数。纯函数的两个特点:

  • 相同的输入必有同输出
  • 没有副作用

无副作用 指的是函数内部的操作不会对外部产生影响(如修改全局变量的值、修改 dom 节点等)。

  1. // 是纯函数
  2. function sum(x,y){
  3. return x + y
  4. }
  5. // 输出不确定,不是纯函数
  6. function random(x){
  7. return Math.random() * x
  8. }
  9. // 有副作用,不是纯函数
  10. function setFontSize(el,fontsize){
  11. el.style.fontsize = fontsize ;
  12. }
  13. // 输出不确定、有副作用,不是纯函数
  14. let count = 0;
  15. function addCount(x){
  16. count+=x;
  17. return count;
  18. }

函数式编程的基本运算
函数合成(compose)

指的是将代表各个动作的多个函数合并成一个函数。

上面讲到,函数式编程是对过程的抽象,关注的是动作。看下下面的例子

  1. function add(x) {
  2. return x + 10
  3. }
  4. function multiply(x) {
  5. return x * 10
  6. }
  7. console.log(multiply(add(2))) // 120

将合成的动作抽象为一个函数 compose如下:

  1. function compose(f,g) {
  2. return function(x) {
  3. return f(g(x));
  4. };
  5. }
  6. // 这样我们我们可以通过如下的方式得到合成函数
  7. // 执行动作的顺序是从右往左
  8. let calculate=compose(multiply,add);
  9. console.log(calculate(2)) // 120

只要往 compose 函数中传入代表各个动作的函数,我们便能得到最终的合成函数。但上述 compose 函数的局限性是只能够合成两个函数,如果需要合成的函数不止两个呢,所以需要一个通用的 compose 函数。

  1. function compose() {
  2. let args = arguments;
  3. console.log(args)
  4. let start = args.length - 1;
  5. return function () {
  6. let i = start - 1;
  7. let result = args[start].apply(this, arguments);
  8. while (i >= 0){
  9. // 函数作为参数,倒着运算,上一次运算的输出作为下一次运算的输入
  10. result = args[i].call(this, result);
  11. i--;
  12. }
  13. return result;
  14. };
  15. }
  16. // 使用
  17. function add(x){
  18. return x + 10
  19. }
  20. function multiply(x) {
  21. return x * 10
  22. }
  23. function minus(x) {
  24. return x - 10
  25. }
  26. let composeFun = compose(minus, multiply, add);
  27. composeFun(2) // 110

通过 compose 将上述三个动作代表的函数合并成了一个,并最终输出了正确的结果。
函数柯里化(Currying)

函数柯里化又称部分求值。一个柯里化的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值

柯里化函数有如下两个特性:

  • 接受一个单一参数
  • 返回接受余下的参数而且返回结果的新函数

    1. function sum(a, b) {
    2. return a + b;
    3. }
    4. console.log(sum(2, 2)) // 4

    假设函数 sum 的柯里化函数是 sumCurry,那么从上述定义可知,sumCurry(2)(2) 应该实现与上述代码相同的效果,输出 4 。这里我们可以比较容易的知道,sumCurry 的代码如下

    1. // sumCurry 是 sum 的柯里化函数
    2. function sumCurry(a) {
    3. return function(b) {
    4. return a + b;
    5. }
    6. }
    7. console.log(sumCurry(2)(2)); // 4

    如果有一个函数 createCurry 能够实现柯里化,那么我们便可以通过下述的方式来得出相同的结果

    1. // sumCurry 返回一个柯里化函数
    2. var sumCurry=createCurry(sum);
    3. console.log(sumCurry(2)(2)); // 4

    可以看到,函数 createCurry 传入一个函数 sum 作为参数,返回了一个柯里化函数 sumCurry,函数 sumCurry 能够处理 sum 中的剩余参数。这个过程就称为函数柯里化,我们称 sumCurry 是 add 的柯里化函数。
    怎么得到实现柯里化的函数 createCurry 呢?这里我直接给出 createCurry 的代码 ```javascript // 参数只能从左到右传递 function createCurry(func, arrArgs) { var args=arguments; var funcLength = func.length; var arrArgs = arrArgs || [];

    return function() {

    1. var _arrArgs = Array.prototype.slice.call(arguments);
    2. var allArrArgs=arrArgs.concat(_arrArgs)
    3. // 如果参数个数小于最初的func.length,则递归调用,继续收集参数
    4. if (allArrArgs.length < funcLength) {
    5. return args.callee.call(this, func, allArrArgs);
    6. }
    7. // 参数收集完毕,则执行func
    8. return func.apply(this, allArrArgs);

    } }

// createCurry 返回一个柯里化函数 var sumCurry=createCurry(function(a, b, c) { return a + b + c; }); sumCurry(1)(2)(3) // 6 sumCurry(1, 2, 3) // 6 sumCurry(1)(2,3) // 6 sumCurry(1,2)(3) // 6

  1. 柯里化实际上是把简答的问题复杂化了,但是复杂化的同时在使用函数时拥有了更加多的自由度。<br />**柯里化用途**<br />现在需要实现一个功能,将一个全是数字的数组中的数字转换成百分数的形式。按照正常的逻辑,我们可以按如下代码实现
  2. ```javascript
  3. function getPercentList(array) {
  4. return array.map(function(item) {
  5. return item * 100 + '%'
  6. })
  7. }
  8. console.log(getPercentList([1, 0.2, 3, 0.4]));
  9. // 结果:['100%', '20%', '300%', '40%']

如果通过柯里化的方式来实现

  1. function map(func, array) {
  2. return array.map(func);
  3. }
  4. var mapCurry = createCurry(map);
  5. var getNewArray = mapCurry(function(item) {
  6. return item * 100 + '%'
  7. })
  8. console.log(getNewArray([1, 0.2, 3, 0.4]));
  9. // 结果:['100%', '20%', '300%', '40%']

高阶函数
满足下列条件之一的函数就可以称为高阶函数:

  1. 函数作为参数被传递

把函数当作参数传递,这代表我们可以抽离出一部分容易变化的业务逻辑,把这部分业务逻辑放在函数参数中,这样一来可以分离业务代码中变化与不变的部分。其中一个重要应用场景就是常见的回调函数。
下面例子中js的函数都是对高阶函数的利用:

  1. [1, 4, 2, 5, 0].sort((a, b) => a - b);
  2. // [0, 1, 2, 4, 5]
  3. [0, 1, 2, 3, 4].map(v => v + 1);
  4. // [1, 2, 3, 4, 5]
  5. [0, 1, 2, 3, 4].every(v => v < 5);
  6. // true

2.函数作为返回值输出

让函数继续返回一个可执行的函数,意味着运算过程是可延续的

  1. const fn = (() => {
  2. let students = [];
  3. return {
  4. addStudent(name) {
  5. if (students.includes(name)) {
  6. return false;
  7. }
  8. students.push(name);
  9. },
  10. showStudent(name) {
  11. if (Object.is(students.length, 0)) {
  12. return false;
  13. }
  14. return students.join(",");
  15. }
  16. }
  17. })();
  18. fn.addStudent("liming");
  19. fn.addStudent("zhangsan");
  20. fn.showStudent(); //输出:liming,zhangsan

同时满足两个条件的高阶函数

  1. const plus = (...args) => {
  2. let n = 0;
  3. for (let i = 0; i < args.length; i++) {
  4. n += args[i];
  5. }
  6. return n;
  7. }
  8. const mult = (...args) => {
  9. let n = 1;
  10. for (let i = 0; i < args.length; i++) {
  11. n *= args[i];
  12. }
  13. return n;
  14. }
  15. const createFn = (fn) => {
  16. let obj = {};
  17. return (...args) => {
  18. let keyName = args.join("");
  19. if (keyName in obj) {
  20. return obj[keyName];
  21. }
  22. obj[keyName] = fn.apply(null, args);
  23. return obj[keyName];
  24. }
  25. }
  26. let fun1 = createFn(plus);
  27. console.log(fun1(2, 2, 2)); //输出:6
  28. let fun2 = createFn(mult);
  29. console.log(fun2(2, 2, 2)); //输出:8