什么是函数式编程
简单说,"函数式编程"是一种 "编程范式"(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));
参考资源
函数式编程初探
30天精通RxJS(02) 函数式编程基本概念