[译] JavaScript 函数式编程指引

525 阅读5分钟

原文链接 Introduction to Functional Programming

本文旨在对比命令式编程与函数式编程两种不同的解决问题的方式。目的并不是专门教大家函数式编程,而是介绍给大家一种区别于传统方法(循环、变换等)的不同的思考方式。在日后遇到问题时能从不同的角度去思考,同时给自己的技能树添加更多的工具。

函数式编程的基础可以分为如下三个要点来阐述:

  • 不可变数据结构(Immutable Data Structures)
  • 纯函数(Pure Functions)
  • 头等函数(First Class Functions)

让我们分别看一下每个要点吧:

不可变数据结构(Immutable Data Structures)

我们在使用像 JavaScript 这样的编程语言时,我们可以这样给变量进行赋值let myVariable = 5;,但是不存在任何一种机制来阻止我们在后期继续对该变量进行赋值操作,比如myVariable = "Now I'm a string."。这样的操作实际上很危险,如果有其他函数依赖myVariable这个number类型变量,或者是某个异步函数同时也要用到myVariable,这时就会遇到一些问题。

纯函数(Pure Functions)

纯函数是无副作用的。啥叫无副作用? 首先,如果一个函数的输出仅仅依赖于其输入,那么该函数就被认为是一个纯函数。如果我们的函数拿到输入后执行了数据库的更新操作,然后返回了一个值,这样的操作就称之为具有副作用,即更新了数据库。也就是说多次调用同一个函数不总是得到相同的结果(内存不足、数据库被锁等情况)。使用纯函数对于我们书写少 bug、易测试的代码很有帮助。

头等函数(First Class Functions)

「头等」这个词出现在这里可能看起来比较陌生,但是其意思是指函数可以被用做参数或者可以像其他数据类型一样使用。比如字符串类型、整型和浮点型等。支持头等函数的编程语言允许将函数作为参数传给其他函数,可以把这种方式看做依赖注入。

命令式编程和函数式编程对比

这里用下面的例子对两者进行比较,功能是求得数组 [1, 2, 3, 4]中的数字之和。

命令式编程写法:

要将上面的代码改为函数式编程形式的话,这里有一个问题需要解决,那就是使用不可变数据结构。但是我们在每次迭代中都给sum赋了不同的值。

为了将代码改为函数式编程思想,让我们来分解一下累加和的计算过程。

首先,我们以某个值作为初始值,在我们的例子中该值为let sum = 0;,接下来,我们从数组中取出第一项1并将其累加到sum上。这一步我们得到了0 + 1 = 1。然后重复这个步骤,取出2将其累加到sum上,即1 + 2 = 3,这一过程遍历到数组尾部为止。

可视化该过程:

我们可以将该算法视作两个单独的函数,首先我们需要某种方式来进行数字的相加操作:

接下来我们需要以某种方式来循环遍历数组,由于大多数函数式编程通常都使用递归来替代循环,那我们就创建一个递归函数来遍历我们的数组。来看一下该函数可能的样子:

在这个函数中,list是我们想要遍历的数组,index作为当前要遍历的起点位置,如果我们遍历到达list尾部,或者是给出的list无效,那么循环结束。否则再次调用loopindex加 1。试着在return loop(list, index + 1)之前添加console.log(list[index]),我们应该可以看到控制台输出了1 2 3 4

为了最终实现求得数组累加和,我们需要将loopadd函数结合起来,在看下面的例子时要回忆我们上面提到的算法:

这里改变了传入loop函数中的参数,增加了accu,用来保存list的累加和。我们直接用add函数计算accu与与list中当前项的和。如果我们console.log(loop(list));会发现控制台会输出结果 10。

现在,不如我们更进一步吧。要是我们不想求数组累加和了,改为将它们相乘呢?那现在我们要复制一份loop的代码,把add函数改成其他东西(可能是multiply?)?太麻烦了吧。还记得头等函数吗,我们要利用这种思想将我们的函数变的更为通用。

上面例子中唯一不同之处在于我们给loop函数新增了一个参数,来接收一个函数。这回我们不传add给它了,取而代之的是传一个函数进来,调用该函数获取最终结果。这样就能轻松的实现针对listadd, multiply, subtract等一系列操作了。

我们不再只是简单的遍历数组了,而是将数组像纸一样折起来,直到我们得到一个结果。在 JavaScript 中,我们把这种函数称之为 reduce

结语

我们对函数式编程进行了一些基础的概览,同时看到针对同一问题的不同拆解方式给与我们不同的解法。reduce可以说是其他类似mapfilter这样的操作的基础。