什么是函数式编程
函数式编程是一种编程思想或者说编程泛型,经典的命令式编程模型强调指令的执行,而函数式强调函数计算本身,主要思想是把运算过程尽量编写为一系列嵌套的函数调用。
举个例子:
1 | (1 + 2 - 3) \* 4 / 5 |
可以看到命令式编程的特点是把每一个运算当做一个步骤来执行,通过步骤的组合最后得到结果。而函数式编程则把运算用函数来体现,通过函数的组合来得到运算结果。
函数式编程的特征
一等公民的函数
函数与其他的数据类型一样,是平等的,可以赋值给其他变量或者作为参数传递,以及作为其他函数的返回值,如下例中的 increase
1 | const increase = (val) => val + 1; |
纯函数
纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。副作用是指函数的内部与外部互动,比如说修改全局变量的值,产生运算外的其他结果。函数式编程强调没有副作用,函数要保持独立,所有的功能就是返回一个新的值而没有其他的行为。
比如 slice 和 splice,slice 对于相同的输入能保证返回相同的输出,因此它是纯函数。而 splice 会改变调用它的数组,这就会产生可观察的副作用,数组永久的改变了。
1 | const arr = [1, 2, 3, 4, 5]; |
用表达式代替语句
表达式是一个单纯的运算并且总是有返回值,语句是执行某一操作,没有返回值。函数式编程要求只使用表达式不用语句,也就是每一步都是单纯运算并且有返回值。
不修改状态
函数式编程只是返回新的值,不修改系统的变量。
在一般的语言中,变量用来保存状态,不修改变量意味着状态不能被保存在变量中。 函数式编程用参数来保存状态。
1 | const reverse = (string, reverseStr) => { |
上面是一个用来反转字符串的递归函数,注意到它并没有使用变量来保存状态,而是使用参数来进行传递。
并且它的最后一步是调用自身函数,这就是尾递归。尾调用在函数式编程中非常重要。因为递归在本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令,所有的循环都用递归实现,使用递归最好使用尾递归。(在浏览器层面目前仅 safari 支持,chrome 的 V8 默认关闭尾递归优化功能,因为尾调用优化有隐式优化和调用栈丢失的问题)。
为什么要使用函数式编程
开发效率
函数式编程使用了大量的函数,减少了代码的重复,开发效率快。更容易和代码的模块化结合起来,方便管理和扩展。函数式编程不依赖和改变外界的状态,只要给定输入参数必然会返回相同的结果。因此每个函数都可以看做是独立的单元,有利于编写单元测试和调试。扩展时也只需要增加对应的运算函数即可。
可读性高
函数式编程可以使用链式调用的写法
1 | (1 + 2 - 3) \* 4 / 5 |
易读性强的代码就是最好的注释,能非常直观的表达含义,配合类型签名(type signatures)能够表露函数的行为和目的。
并发编程
函数式编程是不会产生死锁的,因为它不会修改变量,所以不存在锁线程的问题。不用担心一个线程的数据被另一个线程修改,所以可以很安全的在多个线程上运行不同的任务,部署并发编程。
1 | let action1 = doSth1(); |
action1 和 action2 互不干扰,执行的先后顺序并不会影响最终结果,所以可以把它们分配在两个线程上完成。而在一般的编程模式中,action1 可能会修改系统状态,而 action2 可能会用到这些状态,所以必须保证 action2 在 action1 后面执行,也就不能在其他线程上运行了。
参考资料
- 函数式编程离我们有多远
- 什么是函数式编程思维
- JS 函数式编程指南