一文带你简单了解函数式编程

142 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

在JavaScript中,函数是第一公民,因此JavaScript符合函数式编程(Functional Programming)的范式,在函数式编程中,有很多概念,我们先从纯函数说起。

纯函数(pure function)

对于纯函数来说,维基百科是这样定义的:

在程序设计中,若一个函数符合以下条件,那么这个函数被称为纯函数:

此函数在相同的输入值时,需产生相同的输出。

函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关。

该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等

简单理解来说,对于一个函数,当它的确定的输入产生了确定的输出,并且在函数执行的过程中,没有产生副作用,那么这个函数就是纯函数。

什么又是副作用呢?

在计算机科学中,副作用表示在执行一个函数的时候,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改了参数等等...

在了解了纯函数的概念后,我们来看看下面这几个函数是否是纯函数:

function sum(num1, num2){
    return num1 + num2
}

let foo = 1

function add(num){
    // 函数依赖了全局变量
    return foo + num
}

function bar(info){
    // 函数修改了全局变量,产生了副作用
    info.name = 'codertzm'
}

很明显,函数sum是纯函数,而add函数和bar函数都不是纯函数,因为add函数依赖了全局变量导致了不确定的输出bar函数修改了全局变量导致了副作用

纯函数的优势

为什么说纯函数对于函数式编程如此重要呢?

  1. 可以让程序员安心的使用这个函数
  2. 只关心业务逻辑,不需要关心传入的内容是如何获得的或者是依赖于其他外部的变量

像React这样的推崇函数式编程的前端框架,就要求我们在封装组件的时候,这个组件必须像纯函数一样,不让它们的props被修改。

函数柯里化(curry)

函数柯里化是函数式编程的一个重要概念,它可以提高函数使用的灵活度,增加参数的复用

函数柯里化的定义是一种将使用接收多个参数的一个函数转换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数,而且返回结果的是新函数的技术。

或者可以说成:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

var add = function(x) {
  return function(y) {
    return x + y;
  };
};

var addOne = add(1);
var addTen = add(10);

addOne(2); // 3

addTen(2); // 10

这里我们定义了一个 add函数,它接受一个参数并返回一个新的函数。调用 add之后,返回的函数就通过闭包的方式记住了 add 的第一个参数。

单一职责(函数柯里化的作用)

我们了解了函数柯里化是什么东西后,可能会有这样的疑问:函数为什么需要柯里化?

在设计模式里,有一种原则叫单一职责原则(Single Responsibility Principle),它具体表现为一个对象(方法)只做一件事情。

而函数柯里化其实就是将每次传入的参数在单一的函数中进行处理,处理完后在下一个函数中再使用处理后的结果。

比如在上面的例子中,我们先实现了一个加1的方法,再将想要加1的那个数字传递给下一个函数作为参数。

并且,函数柯里化还可以帮助我们复用参数逻辑

同样在上面的例子中,我们实现了加1这个方法后,可以无限的复用这个逻辑。

函数柯里化的实现

接下来我们来实现函数柯里化。

通过上面的例子其实我们能够想到,柯里化函数一定是有递归调用的,且返回值一定是个函数,因此我们可以通过判断实参与形参的个数来判断是否继续递归调用

function myCurry(fn) {
  function curry(...args) {
    // 判断接收的参数的个数是否大于需要传入的参数
    if (args.length >= fn.length) {
      return fn.apply(this, args)
    } else {
      // 没有达到个数时,返回一个新的函数,继续接受参数,其实就是收集参数的过程
      return function (...restArgs) {
        // 防止this出现问题
        return curry.apply(this, [...args,...restArgs])
      }
    }
  }
  return curry
}

实现柯里化函数后,我们来看看结果

function foo(a, b, c, d) {
  return a + b + c + d
}

var curryFoo = myCurry(foo)
curryFoo(1,3)(4)(5) // 13

至此,我们就实现了函数柯里化。

代码组合(compose)

var compose = function(f,g) {
  return function(x) {
    return f(g(x));
  };
};

这其实就是一个简单的组合函数,fg都是函数,x 是在它们之间通过“管道”传输的值。

代码组合其实就是一种对函数使用的技巧,它可以将两个依次执行的函数通过代码组合起来,自动依次调用。

接下来我们来实现一个比较复杂的组合函数

function myCompose(...fns) {
  var length = fns.length
  // 判断传入的参数是否都是函数
  for (var i = 0; i < length; i++) {
    if (typeof fns[i] !== 'function') {
      throw new TypeError('Expected arguments are functions')
    }
  }

  function compose(...args) {
    var index = 0
    // 第一个参数的返回值
    var result = length ? fns[index].apply(this, args) : args
    while (++index < length) {
      // 将上一次的结果作为这次的参数传入
      result = fns[index].call(this, result)
    }
    return result
  }
    
  return compose
}

至此,我们可以使用这个myCompose函数进行将多个函数进行组合调用。

声明式的代码

在函数式编程中,由于函数是第一公民,因此我们需要使用声明式的代码而非命令式的代码。

与命令式代码不同,声明式代码要求更多的使用表达式。

来看看声明式代码和命令式代码的比较:

var makes = [];
for (i = 0; i < cars.length; i++) {
  makes.push(cars[i].make);
} 

var makes = cars.map(function(car){ return car.make; });

很明显的是:

命令式的循环要求必须先实例化一个数组,而且执行完这个实例化语句之后,解释器才继续执行后面的代码。

声明式的代码使用 map 函数,它是一个表达式,它对执行顺序没有要求。而且,map 函数如何进行迭代,返回的数组如何收集,都有很大的自由度。它指明的是做什么,不是怎么做

声明式的代码除了更加简洁和清晰以外,map函数还可以进一步被优化,这就是函数式编程的优势。

总结

关于函数式编程还有其他可以说的内容,比如范畴学、函子等,本篇文章只是作为函数式编程的入门,关于函数式编程的库也有很多,如:lodash、rxjs等,大家可以去使用并阅读它们的源码去学习。