- 简述
- 无副作用(No Side Effects)
- 高阶函数(High-Order Function)
- 柯里化(Currying)
- 闭包(Closure)
- 不可变(Immutable)
- 惰性计算(Lazy Evaluation)
- Monad
简述
函数式编程的概念来自于数学中的函数,即自变量映射。中心思想是指:一个函数的返回值,仅仅依赖于参数的值,而不会因为其他外部的状态而不同。比如有一个求幂的函数 pow(base, exponent)
,它的计算结果仅仅依赖于 base (基数)
和 exponent (指数)
的不同而不同。这个函数无论我们调用多少次,只要参数一致,那么返回值绝对一致。
在代码构建的过程中,我们很难将所有函数都构建成符合函数式编程思维的范式,但如果应用函数式编程,则它的好处主要体现于:
- 引用透明(Referential Transparency)
- 无副作用(No Side Effect)
- 无竞争态 (No Race Condition)
- 惰性求值 (Lazy Evaluation)
本文的目的是希望以简单易懂的方式,表述函数式编程思维的范式和应用。本文中示例使用的编程语言为:JavaScript
。
无副作用(no side effects)
函数在表现方式上,我们可以将其区分为 纯函数
和 非纯函数
。他们有以下区分:
纯函数:返回值仅依赖于参数,输入相同的值,便会得到相同的值的函数。
let seed = 0; // 定义一个外部变量
// 一个用于求和的函数
const sum = (x, y) => x + y;
sum(10, 2); // 结果: 12
sum(10, 2); // 结果: 12
sum(3, 6); // 结果: 9
sum(3, 6); // 结果: 9
console.log( seed ); // 输出: 0
非纯函数:在参数一致的情况下,返回值可能不一致的函数。
let seed = 0; // 定义一个外部变量
// 另一个用于求和的函数
const sum = (x, y) => x + y + (++seed);
sum(10, 2); // 结果: 13
sum(10, 2); // 结果: 14
sum(3, 6); // 结果: 12
sum(3, 6); // 结果: 13
console.log( seed ); // 输出: 4
纯函数
和 非纯函数
最大的两个不同的表现在于:副作用性
和 引用透明性
。
副作用性
是指,该函数的调用过程中,是否对主函数(调用者)产生了附加影响,例如修改了函数外的变量或参数,我们就认为该函数是 有副作用
的函数。
而 引用透明
则是指,函数的运行不依赖于外部的变量或者状态,仅仅依赖于输入的参数。如果程序中任意两处具有相同输入值的函数调用能够互相置换,而不影响程序的动作,那么该函数就具有引用透明性。
由上述示例可见,非纯函数造成的最大的问题,就是其 不可预知性
。如果代码比较复杂时,会为我们梳理程序运行逻辑造成一定的困难。因此,在函数式编程思维中,我们应尽可能的确保我们编写的函数是 纯函数
。
JavaScript内置对象中的 非纯函数
在理想中,我们的函数都应该是 纯函数
。但理想往往需要屈从于现实状况。在 JavaScript
的内置对象中,就有非常多的 非纯函数
,也提供着必要的功能实现,如我们常见的:
- Math.random()
- console.log()
- element.addEventListener()
- Date.now()
- Array.prototype.sort()
- ajax操作等
副作用主要表现于:
- I/O 操作:其结果本身就是无法预估的,因此无法判断给定了的参数,是否能给予我们预期的返回结果;比如接收输入、或者将结果输出。
- 改变全局变量原有值,或者改变参数对象值及其属性:其执行结果也是带有副作用的。
- 抛出异常或以错误中止:函数除了返回一个值之外,还可能发生不太确定的执行结果。
Array
是一个专门用于操作数组的对象,是所有数组的父级对象,设定了所有数组可调用的方法有哪些。我们常用 slice()
和 splice()
来作为纯函数和非纯函数的典型示例:
函数 | 作用 |
---|---|
slice( [begin, end] ) | 方法返回一个新的数组对象,这一对象是一个由 begin和 end(不包括end)决定的原数组的浅拷贝。原始数组不会被改变。 参数: begin:操作起始位置索引; end:操作结束位置索引(不包含); |
splice(start[, deleteCount]) | 方法通过删除现有元素和/或添加新元素来修改数组,并以数组返回原数组中被修改的内容。 参数: start:操作起始位置索引; deleteCount:删除元素的数量; |
假设我们现在有一个数组 [1,3,5,7,9]
,当我们希望从 begin
位置开始检索,获取到 end
位置时,我们会这么去做:
const arr = [1, 3, 5, 7, 9];
// slice() 是一个纯函数!
arr.slice(1, 3); // 结果:[3, 5]
arr.slice(1, 3); // 结果:[3, 5]
arr.slice(1, 3); // 结果:[3, 5]
而假设我们要在数组 [1,3,5,7,9]
中删除相应数据时:
const arr = [1, 3, 5, 7, 9];
// splice() 不是一个纯函数!
arr.splice(1, 3); // 结果:[3, 5, 7]
arr.splice(1, 3); // 结果:[9]
arr.splice(1, 3); // 结果:[]
在一段程序中,我们无法保证所有的函数都是纯函数。但纯函数的覆盖面越大,对于调试、缓存数据及线程安全都会提供越多的便利。有一种说法是,保证80%的函数是纯函数即可。