JavaScript 函数式编程

Author Avatar
Splendour 3月 20, 2016

在 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

一个函数也能实现循环了,亦可赛艇!