0. 什么是函数式编程?
函数式编程(Functional programming)是一种编程范式,利用函数把整个程序的执行过程封装起来,通过组合各种函数来获得程序的执行结果。
举个例子,这个例子是将一段英文文本转换为每个单词的首字母均大写(对应Python中的.title()方法):
let str = "My name is UltraMar1ne, I've 18 years old."
str = str.split(" ")
.map(word => word.slice(0, 1).toUpperCase() + word.slice(1))
.join(" ");
console.log(str)
这段代码的执行结果是:
My Name Is UltraMar1ne, I’ve 18 Years Old.
解读一下上面的代码:
先通过.split() 方法将文本分割成单词数组;然后通过.map() 方法返回一个新的数组,这个新数组的每个单词通过.toUpperCase() 方法将切片后的第一个字符大写;最后再将其通过.join() 方法用空格将其拼接
通过上面的例子,我们仔细观察一下,可以发现函数式编程有以下特点:
1. 声明式编程和命令式编程
首先来解释一下这两个概念:
- 声明式编程:告诉程序我们想干什么,而不是通过自己手写操作来一步步实现。这种程序一般是表达式,是一些函数调用的复合、一些值和操作符,用来计算结果
- 命令式编程:编写指令,来完成自己所想的内容。会涉及很多复杂的细节,比如
if , while , for 等
举个例子,现有一系列自定义对象,其中一个的定义如下:
let student = {
name,
age,
studentId,
score,
...
};
然后我想取出所有对象的studentId 属性,两种编程方式分别如何去做呢?(这里将以返回值的形式展现)
let getStudentId = (arr) => {
return arr.map(student => student.studentId);
}
let getStudentId = (arr) => {
let res = [];
arr.forEach(student => {
res.push(student.studentId);
});
return res;
}
在规模较小、较为简单的程序中,两者其实并没有什么太大的差别。
但是随着程序规模的增长,函数式编程采用表达式的思想将会贯穿代码始终,这也是函数式编程的基础。
2. 函数式编程的特性
2.1 无副作用
调用函数时不会修改外部状态
即,无论调用多少次,其返回值状态不可能会发生改变
let a = 1;
function add() {
return ++a;
}
function add(a) {
return a+1;
}
2.2 透明引用
函数只会用到传递给它的变量以及自己内部创建的变量,不会使用到其他变量
let a = 1, b = 2;
function add() {
return a+b;
}
function add(a) {
return a+1;
}
2.3 不可变变量
一个变量一旦创建后,就不能再进行修改,任何修改都会生成一个新的变量。使用不可变变量最大的好处是线程安全。多个线程可以同时访问同一个不可变变量,让并行变得更容易实现。 由于 JavaScript 原生不支持不可变变量,需要通过第三方库来实现。 (如 Immutable.js ,Mori 等等)
let obj = Immutable({ a: 1 });
let obj2 = obj.set('a', 2);
console.log(obj);
console.log(obj2);
2.4 函数是一等公民
在JavaScript中,函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值
3. 常见的函数式编程模型
3.1 闭包
如果一个函数引用了自由变量,那么该函数就是一个闭包
自由变量是指不属于该函数作用域的变量(所有全局变量都是自由变量,严格来说引用了全局变量的函数都是闭包,但这种闭包并没有什么用,通常情况下我们说的闭包是指函数内部的函数)
闭包形成需要什么条件呢?
- 存在内、外层函数嵌套
- 内层函数对外层函数的局部变量进行引用
闭包有什么用呢?
闭包可以定义一些作用域局限的持久化变量,这些变量可以用来做缓存或者计算的中间量
举个例子:
const cache = (function() {
const store = {};
return {
get(key) {
return store[key];
},
set(key, val) {
store[key] = val;
}
}
}());
console.log(cache)
cache.set('a', 1);
cache.get('a');
上述例子中,创建了一个存储对象store ,由于在返回时形成了闭包,这样使得store 可以一直被引用,不会被回收
由此也可见闭包的弊端:持久化变量不会被回收,导致内存占用,很容易造成资源浪费
3.2 高阶函数
高阶函数这个概念自我从Python学起时,就一直有被提及,当时还觉得就是很高级的函数,其实不然
高阶函数是指一个函数以函数为参数,或以函数为返回值,或同时存在上述两种情况
高阶函数经常用于:
- 抽象或隔离行为、作用,异步控制流程作为回调函数,promises,monads等;
- 创建可以泛用于各种数据类型的功能;
- 部分应用于函数参数(偏函数应用)或创建一个柯里化的函数,用于复用或函数复合;
- 接受一个函数列表并返回一些由这个列表中的函数组成的复合函数。
JavaScript是支持高阶函数的,而且也自带了一些我们熟知的高阶函数如:Array.prototype.map , Array.prototype.filter , Array.prototype.reduce 等,具体用法请移步红宝书(《JavaScript高级程序设计(第4版)》)
3.3 函数柯里化
函数柯里化又称部分求值,柯里化函数会接收一些参数,但是不会立即求值,而是继续返回一个新函数,将传入的参数通过闭包的形式保存,等到被真正求值的时候,再一次性把所有传入的参数进行求值
举个例子:
function add(x,y){
return x + y;
}
add(1,2);
let add = function(x) {
return function(y) {
return x + y;
};
};
let increment = add(1);
increment(2);
let increment = add(1)(2);
3.4 函数组合(Composition)
上文有提到过,可以通过函数的链式调用来进行表达式的求取。但是,随着链式调用的规模不断增大,代码的可读性也会随之降低,有没有一种方法可以解决上述问题?
有,就是函数组合
例如,假设有一个 compose 函数,它可以接受多个函数作为参数,然后返回一个新的函数。当我们为这个新函数传递参数时,该参数就会「流」过其中的函数,最后返回结果:
let compose = function(f, g) {
return function(x) {
return f(g(x));
};
};
let compose = (f, g) => (x => f(g(x)));
let add1 = x => x + 1;
let mul5 = x => x * 5;
compose(mul5, add1)(2);
|