本文旨在对比命令式编程与函数式编程两种不同的解决问题的方式。目的并不是专门教大家函数式编程,而是介绍给大家一种区别于传统方法(循环、变换等)的不同的思考方式。在日后遇到问题时能从不同的角度去思考,同时给自己的技能树添加更多的工具。
函数式编程的基础可以分为如下三个要点来阐述:
- 不可变数据结构(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
无效,那么循环结束。否则再次调用loop
,index
加 1。试着在return loop(list, index + 1)
之前添加console.log(list[index])
,我们应该可以看到控制台输出了1 2 3 4
。
为了最终实现求得数组累加和,我们需要将loop
和add
函数结合起来,在看下面的例子时要回忆我们上面提到的算法:

loop
函数中的参数,增加了accu
,用来保存list
的累加和。我们直接用add
函数计算accu
与与list
中当前项的和。如果我们console.log(loop(list));
会发现控制台会输出结果 10。
现在,不如我们更进一步吧。要是我们不想求数组累加和了,改为将它们相乘呢?那现在我们要复制一份loop
的代码,把add
函数改成其他东西(可能是multiply
?)?太麻烦了吧。还记得头等函数吗,我们要利用这种思想将我们的函数变的更为通用。

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

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

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