RxJS 系列之一 - Functional Programming 简介

1,937 阅读7分钟

RxJS 系列目录

什么是函数式编程

简单说,"函数式编程"是一种 "编程范式"(programming paradigm),也就是如何编写程序的方法论。

它属于 "结构化编程" 的一种,主要思想是把运算过程尽量写成一系列嵌套的函数调用。举例来说,现在有这样一个数学表达式:

(5+6) - 1 * 3

传统的过程式编程,可能这样写:

var a = 5 + 6;
var b = 1 * 3;
var c = a - b;

函数式编程要求使用函数,我们可以把运算定义成不同的函数:

const add = (a, b) => a + b;
const mul = (a, b) => a * b;
const sub = (a,b) => a - b;

sub(add(5,6), mul(1,3));

我们把每个运算包成一个个不同的函数,并且根据这些函数组合出我们要的结果,这就是最简单的函数式编程。

函数式编程基础条件

函数为一等公民 (First Class)

所谓 "一等公民"(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为其它函数的返回值。

函数赋值给变量:

const greet = function(msg) { console.log(`Hello ${msg}`); }
greet('Semlinker'); // Output: 'Hello Semlinker'

函数作为参数:

const logger = function(msg) { console.log(`Hello ${msg}`); };
const greet = function(msg, print) { print(msg); };
greet('Semlinker', logger);

函数作为返回值:

const a = function(a) {
  return function(b) {
    return a + b;
  };
};
const add5 = a(5);
add5(10); // Output: 15

函数式编程重要特性

只用表达式,不用语句

"表达式"(expression)是一个单纯的运算过程,总是有返回值;"语句"(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。

原因是函数式编程的开发动机,一开始就是为了处理运算(computation),不考虑系统的读写(I/O)。"语句"属于对系统的读写操作,所以就被排斥在外。

Pure Function

Pure Function (纯函数) 的特点:

  • 给定相同的输入参数,总是返回相同的结果
  • 没有产生任何副作用
  • 没有依赖外部变量的值

所谓 "副作用")(side effect),是指函数内做了与本身运算无关的事,比如修改某个全局变量的值,或发送 HTTP 请求,甚至函数体内执行 console.log 都算是副作用。函数式编程强调函数不能有副作用,也就是函数要保持纯粹,只执行相关运算并返回值,没有其他额外的行为。

前端中常见的产生副作用的场景:

  • 发送 HTTP 请求
  • 函数内调用 logger 函数,如 console.log、console.dir 等
  • 修改外部变量的值
  • 函数内执行 DOM 操作

接下来我们看一下纯函数与非纯函数的具体示例:

纯函数示例:

const double = (number) => number * 2;
double(5);

非纯函数示例:

Math.random(); // => 0.3384159509502669
Math.random(); // => 0.9498302571942787
Math.random(); // => 0.9860841663478281

不修改状态 - 利用参数保存状态

函数式编程只是返回新的值,不修改系统变量。因此,不修改变量,也是它的一个重要特点。

在其他类型的语言中,变量往往用来保存"状态"(state)。不修改变量,意味着状态不能保存在变量中。函数式编程使用参数保存状态,最好的例子就是递归,具体示例如下:

function findIndex(arr, predicate, start = 0) {
    if (0 <= start && start < arr.length) {
        if (predicate(arr[start])) {
            return start;
        }
        return findIndex(arr, predicate, start+1);
    }
}
findIndex(['a', 'b'], x => x === 'b'); // 查找数组中'b'的索引值

示例中的 findIndex 函数用于查找数组中某个元素的索引值,我们通过 start 参数来保存当前的索引值,这就是利用参数保存状态。

引用透明

引用透明(Referential transparency),指的是函数的运行不依赖于外部变量或 "状态",只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。

非引用透明的示例:

const FIVE = 5;
const addFive = (num) => num + FIVE;
addFive(10);

函数式编程的优势

1.代码简洁,开发快速

函数式编程大量使用函数,减少了代码的重复,因此程序比较短,开发速度较快。

2.接近自然语言,易于理解,可读性高

函数式编程的自由度很高,可以写出很接近自然语言的代码。我们可以通过一系列的函数,封装数据的处理过程,代码会变得非常简洁且可读性高,具体参考以下示例:

[1,2,3,4,5].map(x => x * 2).filter(x => x > 5).reduce((p,n) => p + n);

3.可维护性高、方便代码管理

函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。因此,每一个函数都可以被看做独立单元,很有利于进行单元测试(unit testing)和除错(debugging),以及模块化组合。

4.易于"并发编程"

函数式编程不需要考虑"死锁"(deadlock),因为它不修改变量,所以根本不存在"锁"线程的问题。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程,部署"并发编程"(concurrency)。

函数式编程中常用方法

forEach

在 ES 5 版本之前,我们只能通过 for 循环遍历数组:

var heroes = ['Windstorm', 'Bombasto', 'Magneta', 'Tornado'];
for (var i =0, len = heroes.length; i < len; i++) {
  console.log(heroes[i]);
}

在 ES 5 版本之后,我们可以使用 forEach 方法,实现上面的功能:

forEach 方法签名:

array.forEach(callback[, thisArg])

参数说明:

  • callback - 对数组中每一项,进行处理的函数
    • currentValue - 数组中正在处理的当前元素
    • index - 数组中正在处理的当前元素的索引
    • array - 处理的数组
  • thisArg (可选的) - 设置执行 callback 函数时,this 的值

以上示例 forEach 方法实现:

var heroes = ['Windstorm', 'Bombasto', 'Magneta', 'Tornado'];
heroes.forEach(name => console.log(name));

map

在 ES 5 版本之前,对于上面的示例,如果我们想给每个英雄的名字添加一个前缀,但不改变原来的数组,我们可以这样实现:

var heroes = ['Windstorm', 'Bombasto', 'Magneta', 'Tornado'];
var prefixedHeroes = [];
for (var i =0, len = heroes.length; i < len; i++) {
  prefixedHeroes.push('Super_' + heroes[i]);
}

在 ES 5 版本之后,我们可以使用 map 方法,方便地实现上面的功能。

map 方法签名:

const new_array = arr.map(callback[, thisArg])

参数说明:

  • callback - 对数组中每一项,进行映射处理的函数
    • currentValue - 数组中正在处理的当前元素
    • index - 数组中正在处理的当前元素的索引
    • array - 处理的数组
  • thisArg (可选的) - 设置执行 callback 函数时,this 的值

以上示例 map 方法实现:

var heroes = ['Windstorm', 'Bombasto', 'Magneta', 'Tornado'];
var prefixedHeroes = heroes.map(name => 'Super_' + name);

filter

在 ES 5 版本之前,对于 heroes 数组,我们想获取名字中包含 m 字母的英雄,我们可以这样实现:

var heroes = ['Windstorm', 'Bombasto', 'Magneta', 'Tornado'];
var filterHeroes = [];
for (var i =0, len = heroes.length; i < len; i++) {
  if(/m/i.test(heroes[i])) {
    filterHeroes.push(heroes[i]);
  }
}

在 ES 5 版本之后,我们可以使用 filter 方法,方便地实现上面的功能。

filter 方法签名:

var new_array = arr.filter(callback[, thisArg])

参数说明:

  • callback - 用来测试数组的每个元素的函数。调用时使用参数 (element, index, array)。返回true表示保留该元素(通过测试),false则不保留。
  • thisArg (可选的) - 设置执行 callback 函数时,this 的值

以上示例 filter 方法实现:

var heroes = ['Windstorm', 'Bombasto', 'Magneta', 'Tornado'];
var filterRe = /m/i;
var filterHeroes = heroes.filter(name => filterRe.test(name));

参考资源