Declan's Blog

Hi, nice to meet you.

  1. 1. 什么是函数式编程
  2. 2. 函数式编程的特征
    1. 2.1. 一等公民的函数
    2. 2.2. 纯函数
    3. 2.3. 用表达式代替语句
    4. 2.4. 不修改状态
  3. 3. 为什么要使用函数式编程
    1. 3.1. 开发效率
    2. 3.2. 可读性高
    3. 3.3. 并发编程

什么是函数式编程

函数式编程是一种编程思想或者说编程泛型,经典的命令式编程模型强调指令的执行,而函数式强调函数计算本身,主要思想是把运算过程尽量编写为一系列嵌套的函数调用。

举个例子:

1
2
3
4
5
6
7
8
9
10
(1 + 2 - 3) \* 4 / 5

// 命令式
const a = 1 + 2;
const b = a - 3;
const c = b \* 4;
const d = c / 5;

// 函数式
const result = divide(mutiply(subtract(add(1, 2), 3), 4), 5);

可以看到命令式编程的特点是把每一个运算当做一个步骤来执行,通过步骤的组合最后得到结果。而函数式编程则把运算用函数来体现,通过函数的组合来得到运算结果。

函数式编程的特征

一等公民的函数

函数与其他的数据类型一样,是平等的,可以赋值给其他变量或者作为参数传递,以及作为其他函数的返回值,如下例中的 increase

1
2
const increase = (val) => val + 1;
const arr = [1, 2, 3, 4].map(increase);

纯函数

纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。副作用是指函数的内部与外部互动,比如说修改全局变量的值,产生运算外的其他结果。函数式编程强调没有副作用,函数要保持独立,所有的功能就是返回一个新的值而没有其他的行为。

比如 slice 和 splice,slice 对于相同的输入能保证返回相同的输出,因此它是纯函数。而 splice 会改变调用它的数组,这就会产生可观察的副作用,数组永久的改变了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const arr = [1, 2, 3, 4, 5];

// 纯的
arr.slice(0, 3);
// => [1,2,3]
arr.slice(0, 3);
// => [1,2,3]
arr.slice(0, 3);
// => [1,2,3]

//不纯的
arr.splice(0, 3);
// => [1,2,3]
arr.splice(0, 3);
// => [4,5]
arr.splice(0, 3);
// => []

用表达式代替语句

表达式是一个单纯的运算并且总是有返回值,语句是执行某一操作,没有返回值。函数式编程要求只使用表达式不用语句,也就是每一步都是单纯运算并且有返回值。

不修改状态

函数式编程只是返回新的值,不修改系统的变量。

在一般的语言中,变量用来保存状态,不修改变量意味着状态不能被保存在变量中。 函数式编程用参数来保存状态。

1
2
3
4
const reverse = (string, reverseStr) => {
if (!string.length) return reverseStr;
return reverse(string.substring(1, string.length), string.substring(0, 1) + reverseStr);
};

上面是一个用来反转字符串的递归函数,注意到它并没有使用变量来保存状态,而是使用参数来进行传递。

并且它的最后一步是调用自身函数,这就是尾递归。尾调用在函数式编程中非常重要。因为递归在本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令,所有的循环都用递归实现,使用递归最好使用尾递归。(在浏览器层面目前仅 safari 支持,chrome 的 V8 默认关闭尾递归优化功能,因为尾调用优化有隐式优化和调用栈丢失的问题)。

为什么要使用函数式编程

开发效率

函数式编程使用了大量的函数,减少了代码的重复,开发效率快。更容易和代码的模块化结合起来,方便管理和扩展。函数式编程不依赖和改变外界的状态,只要给定输入参数必然会返回相同的结果。因此每个函数都可以看做是独立的单元,有利于编写单元测试和调试。扩展时也只需要增加对应的运算函数即可。

可读性高

函数式编程可以使用链式调用的写法

1
2
3
(1 + 2 - 3) \* 4 / 5

add(1, 2).subtract(3).mutiply(4).divide(5);

易读性强的代码就是最好的注释,能非常直观的表达含义,配合类型签名(type signatures)能够表露函数的行为和目的。

并发编程

函数式编程是不会产生死锁的,因为它不会修改变量,所以不存在锁线程的问题。不用担心一个线程的数据被另一个线程修改,所以可以很安全的在多个线程上运行不同的任务,部署并发编程。

1
2
3
let action1 = doSth1();
let action2 = doSth2();
let action3 = doSth3();

action1 和 action2 互不干扰,执行的先后顺序并不会影响最终结果,所以可以把它们分配在两个线程上完成。而在一般的编程模式中,action1 可能会修改系统状态,而 action2 可能会用到这些状态,所以必须保证 action2 在 action1 后面执行,也就不能在其他线程上运行了。

参考资料

  • 函数式编程离我们有多远
  • 什么是函数式编程思维
  • JS 函数式编程指南
This article was last updated on days ago, and the information described in the article may have changed.