函数式编程的目标是使用函数来抽象作用在数据之上的控制流与操作,从而在系统中消除副作用并减少对状态的改变。
基本概念
声明式编程
命令式编程很具体地告诉计算机如何执行某个任务,而声明式编程是将程序的描述和求值分离开。
举个例子,假设需要计算一个数组中所有数字的平方,命令式的程序如下:
var array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
for(let i = 0; i < array.length; i++) {
array[i] = Math.pow(array[i], 2);
}
array // [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
函数式编程的程序如下:
var array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];cy
array.map(num => Math.pow(num, 2))
纯函数
函数式编程基于一个前提,即使用纯函数构建具有不变性的程序。纯函数具有以下两个性质:
- 仅取决于输入,而不依赖于任何在函数求值期间、或调用间隔时可能变化的隐藏状态和外部状态。
- 不会造成超出其作用域的变化,例如修改全局对象或引用传递的参数。
常见的会带来副作用的例子:
- 改变一个全局的变量、属性或数据结构
- 改变函数参数的原始值
- 处理用户输入
- 抛出一个异常,除非它又被当前函数捕获了
- 屏幕打印或记录日志
- 查询 HTML 文档、浏览器的 cookie 或访问数据库
引用透明
如果一个函数对于相同的输入始终产生相同的结果,那么就说它是引用透明的。引用透明不仅可以使代码更容易测试,还可以让我们更容易推理整个程序。
不可变性
不可变数据是指那些被创建后不能更改的数据。
函数式编程是指为创建不可变的程序,通过消除外部可见的副作用,来对纯函数的声明式的求值过程。
优点
先宏观地了解一下函数式可以给程序代码带来的好处:
- 促使将任务分解成简单的函数
- 使用流式的调用链来处理数据
- 通过响应式范式降低时间驱动代码的复杂性
鼓励复杂任务的分解
从宏观上看,函数式编程实际上是分解和组合之间的相互作用。分解是将程序拆分为小片段,组合是将小片段连接到一起。
函数式思维的学习通常始于将特定任务分解为逻辑子任务的过程。如果需要,子任务可以继续分解,直到分解成为一个个简单的、相互独立的纯函数功能单元。(这里涉及到模块化、单一职责的思想)
两个函数的组合是一个新函数,它拿到一个函数的输出,传给另一个函数。假设有两个函数 f 和 g,形式上,其组合可以如下描述:
f * g = f(g(x))
使用流式链来处理数据
链指的是一连串函数的调用,它们共享一个通用的对象返回值(如 jQuery 对象)。就像组合一样,链有助于写出简明扼要的代码,而且他们通常多用于函数式和相应式的 JavaScript 类库。
例如以下代码,就是使用函数链编程:
_.chain(enrollment)
.filter(student => student.enrolled > 1)
.pluck('grade')
.average()
.value();
复杂异步应用中的响应
异步操作包括请求远程数据、处理用户输入或与本地存储交互的情况等。在写这种代码的时候,一般都伴随着回调,这种模式会打破线性的代码流,使代码变得难以阅读。
响应式范式的主要好处是,它能够提高代码的抽象级别,使你忘记与异步和事件驱动程序创建的相关样板代码,从而更专注于具体的业务逻辑。此外,这宗新兴范式能够充分利用函数式编程中函数链和组合的优势。
对比以下两种编程方式:
// 读取并验证学生的 SSN 的命令式代码
var valid = false;
var elem = ducument.querySelector('#student-ssn');
elem.onkeyup = function(event) {
var val = elem.value;
if(val !== null && val.length !== 0) {
val = val.replace(/^\s*|\s*$|\-s/g, '');
if(val.length === 9) {
console.log(`Valid SSN: ${val}`);
val = true;
}
} else {
console.log(`Invalid SSN: ${val}`);
}
}
// 读取并验证学生SSN 的函数式代码
Rx.Observable.fromEvent(document.querySelector('#student-ssn'), 'keyup')
.map(input => input.srcElement.value)
.filter(ssn => ssn !== null && ssn.length !== 0)
.map(ssn => ssn.replace(/^\s*|\s*$|\-s/g, ''))
.skipWhile(ssn => ssn.length !== 9)
.subscribe(
validSsn => console.log(`Valid SSN: ${validSsn}`)
);
这里使用到了 RxJS 这个库,这个库使用到了一个叫做 Observable 的概念。Observable 能够订阅一个数据流,让开发者可以通过使用组合喝链式操作来优雅地处理数据。
小结
- 使用函数式的代码绝不会更改或破坏全局状态,有助于提高代码的可测试性和可维护性。
- 函数式编程采用声明式的风格,易于推理。提高了应用程序的整体可读性,通过采用组合和 lambda 表达式使代码更加精简。
- 集合中的数据元素处理可以通过链接如 map 和 reduce 这样的函数来实现。
- 函数式编程将函数视为积木,通过一等高阶函数来提高代码的模块化和复用性。
- 可以利用响应式编程组合各个函数来降低事件驱动程序的复杂性。