JavaScript 函数式编程
在 ES6 的支持下(特别是箭头函数),JavaScript 函数式编程的实践性得到了很大的提升。再加上 Babel 等神器,目前已经可以自己构建函数式编程的一些小套路了。
函数式编程
彻底的函数式编程包括以下特点:
- 禁用 var/let,所有东西都用 const 定义,无变量,强制 immutable
- 禁用 if/else,允许使用条件表达式
condition ? a : b
- 禁用 for/while/do-while
- 禁用 prototype 和 this 来解除面向对象编程范式
- 禁用 function 和 return 关键字,仅试用 lambda 表达式来编程
- 禁用多参数函数,只允许使用单个参数,相当于强行 curry 化
在实践过程中,有时候还是不要强行遵照上面的特点,要灵活应变,写出又有逼格又实用的代码,才是真正追求的。
累加器
累加器在编程过程中是很常见的,一般情况下,我们会借助一个临时变量还有循环来进行累加
// 最基本的求和累加器
let sum = 0;
for (let value of array) {
sum += value;
}
很明显的,上述代码是过程式编程的典型。我们可以用上更高端的 reduce
函数
array.reduce((previous, current) => {
previous + current;
});
逼格瞬间高了很多。如果我们需要给一个初始值,则有
const initValue = 10;
array.reduce((previous, current) => {
previous + current;
}, initValue);
倒序遍历
reduce
函数已经让我们可以使用一个函数实现累加器的功能了,那么我们能否将多参数转化为单参数呢?且看下面定义的函数
const foldr = f => accumulator => ([x, ...xs]) =>
x === undefined ? accumulator : f(x)(foldr(f)(accumulator)(xs));
该函数接受三个参数
- f:对数组中的每一项和累加器的操作,该参数是一个
函数
(函数式编程的特点之一) - accumulator:累加器
- [x, …xs]:要操作的数组
函数采用递归的方式,算到最后是这样的形式(以数组 [1, 2] 为例)
f(1)(f(2)(accumulator))
不难看出,最终是将数组倒序遍历,计算出 f(x)(accumulator) 的值, 并将结果作为新的 accumulator 传递回去,直到遍历完毕。因为是将数组倒序遍历,所以我们将函数起名叫 foldr
,这是个常见的函数
sum 函数
不妨来尝试使用一下这个函数。还是以求和为例,则有
const f = x => accumulator => x + accumulator; // 箭头函数默认返回
let initValue = 10;
const array = [1, 2, 3, 4];
let sumValue = foldr(f)(initValue)(array); // 20
逼格满满啊有木有?!我们可以定义一个属于自己的 sum 函数
const sum = initValue => foldr(x => accumulator => x + accumulator)(initValue);
let sumValue = sum(10)([1, 2, 3, 4]); // 20
一个属于自己的 sum 函数就这样诞生了,单参数,无变量,单语句,干脆利落。自己的函数库至此多了一个高逼格函数!(差不多就行,不能太不要脸了)
map 函数
我们再来看看熟悉的 map
函数
let squareArray = [1, 2, 3, 4].map(item => item * item); // [1, 4, 9, 16]
使用 map
函数已经相当优雅了,不过,既然我们已经有遍历函数了,那么我们能否用函数式编程实现 map
函数呢?map
函数返回的是一个处理后的数组,那么我们只需让累加器返回数组即可
const map = f => foldr(x => accumulator => [f(x), ...accumulator])([]);
相当于 f(x)(accumulator)
返回了一个数组 [f(x), ...accumulator]
, 遍历结束后刚好就是 map
函数返回的结果,调用方式如下
let squareArray = map(item => item * item)([1, 2, 3, 4]); // [1, 4, 9, 16]
又一个函数入库了!
正序遍历
倒序遍历我们已经玩腻了,那有没有正序遍历呢?
const foldl = f => accumulator => ([x, ...xs]) =>
x === undefined ? accumulator : foldl(f)(f(accumulator)(x))(xs);
函数采用递归的方式,算到最后是这样的形式(以数组 [1, 2] 为例)
f(f(a)(1))(2)
不难看出,最终是将数组正序遍历,计算出 f(accumulator)(x) 的值, 并将结果作为新的 accumulator 传递下去,直到遍历完毕。
因为是将数组正序遍历,所以我们将函数起名叫
foldl
,与前面的foldr
函数对应
loopOnArray 函数
正序遍历的一个典型用处就是循环。我们利用 foldl
循环打印数组的每一个值
foldl(item => console.log(item))()([1, 2, 3, 4]);
不难发现,我们是不需要累加的,即不需要存储 accumulator 这个状态,每一步操作都和 accumulator 无关,但还是需要传这个参数
const loopOnArray = f => foldl(_ => x => f(x))(undefined);
我们这里用 _
来代表空参数,因为这里不需要用到 accumulator, 用一个空参数来替代,调用范式如下
loopOnArray(item => console.log(item))([1, 2, 3, 4])h
一个函数也能实现循环了,亦可赛艇!