• 函数式编程
    • 柯里化
    • 函数合成
    • 参数倒置
    • 执行边界
    • 队列操作
    • 合并操作
    • 配对操作
    • 参考链接

    函数式编程

    JavaScript 语言从一诞生,就具有函数式编程的烙印。它将函数作为一种独立的数据类型,与其他数据类型处于完全平等的地位。在 JavaScript 语言中,你可以采用面向对象编程,也可以采用函数式编程。有人甚至说,JavaScript 是有史以来第一种被大规模采用的函数式编程语言。

    ES6 的种种新增功能,使得函数式编程变得更方便、更强大。本章介绍 ES6 如何进行函数式编程。

    柯里化

    柯里化(currying)指的是将一个多参数的函数拆分成一系列函数,每个拆分后的函数都只接受一个参数(unary)。

    1. function add (a, b) {
    2. return a + b;
    3. }
    4. add(1, 1) // 2

    上面代码中,函数add接受两个参数ab

    柯里化就是将上面的函数拆分成两个函数,每个函数都只接受一个参数。

    1. function add (a) {
    2. return function (b) {
    3. return a + b;
    4. }
    5. }
    6. // 或者采用箭头函数写法
    7. const add = x => y => x + y;
    8. const f = add(1);
    9. f(1) // 2

    上面代码中,函数add只接受一个参数a,返回一个函数f。函数f也只接受一个参数b

    函数合成

    函数合成(function composition)指的是,将多个函数合成一个函数。

    1. const compose = f => g => x => f(g(x));
    2. const f = compose (x => x * 4) (x => x + 3);
    3. f(2) // 20

    上面代码中,compose就是一个函数合成器,用于将两个函数合成一个函数。

    可以发现,柯里化与函数合成有着密切的联系。前者用于将一个函数拆成多个函数,后者用于将多个函数合并成一个函数。

    参数倒置

    参数倒置(flip)指的是改变函数前两个参数的顺序。

    1. var divide = (a, b) => a / b;
    2. var flip = f.flip(divide);
    3. flip(10, 5) // 0.5
    4. flip(1, 10) // 10
    5. var three = (a, b, c) => [a, b, c];
    6. var flip = f.flip(three);
    7. flip(1, 2, 3); // => [2, 1, 3]

    上面代码中,如果按照正常的参数顺序,10 除以 5 等于 2。但是,参数倒置以后得到的新函数,结果就是 5 除以 10,结果得到 0.5。如果原函数有 3 个参数,则只颠倒前两个参数的位置。

    参数倒置的代码非常简单。

    1. let f = {};
    2. f.flip =
    3. fn =>
    4. (a, b, ...args) => fn(b, a, ...args.reverse());

    执行边界

    执行边界(until)指的是函数执行到满足条件为止。

    1. let condition = x => x > 100;
    2. let inc = x => x + 1;
    3. let until = f.until(condition, inc);
    4. until(0) // 101
    5. condition = x => x === 5;
    6. until = f.until(condition, inc);
    7. until(3) // 5

    上面代码中,第一段的条件是执行到x大于 100 为止,所以x初值为 0 时,会一直执行到 101。第二段的条件是执行到等于 5 为止,所以x最后的值是 5。

    执行边界的实现如下。

    1. let f = {};
    2. f.until = (condition, f) =>
    3. (...args) => {
    4. var r = f.apply(null, args);
    5. return condition(r) ? r : f.until(condition, f)(r);
    6. };

    上面代码的关键就是,如果满足条件就返回结果,否则不断递归执行。

    队列操作

    队列(list)操作包括以下几种。

    • head: 取出队列的第一个非空成员。
    • last: 取出有限队列的最后一个非空成员。
    • tail: 取出除了“队列头”以外的其他非空成员。
    • init: 取出除了“队列尾”以外的其他非空成员。

    下面是例子。

    1. f.head(5, 27, 3, 1) // 5
    2. f.last(5, 27, 3, 1) // 1
    3. f.tail(5, 27, 3, 1) // [27, 3, 1]
    4. f.init(5, 27, 3, 1) // [5, 27, 3]

    这些方法的实现如下。

    1. let f = {};
    2. f.head = (...xs) => xs[0];
    3. f.last = (...xs) => xs.slice(-1);
    4. f.tail = (...xs) => Array.prototype.slice.call(xs, 1);
    5. f.init = (...xs) => xs.slice(0, -1);

    合并操作

    合并操作分为concatconcatMap两种。前者就是将多个数组合成一个,后者则是先处理一下参数,然后再将处理结果合成一个数组。

    1. f.concat([5], [27], [3]) // [5, 27, 3]
    2. f.concatMap(x => 'hi ' + x, 1, [[2]], 3) // ['hi 1', 'hi 2', 'hi 3']

    这两种方法的实现代码如下。

    1. let f = {};
    2. f.concat =
    3. (...xs) => xs.reduce((a, b) => a.concat(b));
    4. f.concatMap =
    5. (f, ...xs) => f.concat(xs.map(f));

    配对操作

    配对操作分为zipzipWith两种方法。zip操作将两个队列的成员,一一配对,合成一个新的队列。如果两个队列不等长,较长的那个队列多出来的成员,会被忽略。zipWith操作的第一个参数是一个函数,然后会将后面的队列成员一一配对,输入该函数,返回值就组成一个新的队列。

    下面是例子。

    1. let a = [0, 1, 2];
    2. let b = [3, 4, 5];
    3. let c = [6, 7, 8];
    4. f.zip(a, b) // [[0, 3], [1, 4], [2, 5]]
    5. f.zipWith((a, b) => a + b, a, b, c) // [9, 12, 15]

    上面代码中,zipWith方法的第一个参数是一个求和函数,它将后面三个队列的成员,一一配对进行相加。

    这两个方法的实现如下。

    1. let f = {};
    2. f.zip = (...xs) => {
    3. let r = [];
    4. let nple = [];
    5. let length = Math.min.apply(null, xs.map(x => x.length));
    6. for (var i = 0; i < length; i++) {
    7. xs.forEach(
    8. x => nple.push(x[i])
    9. );
    10. r.push(nple);
    11. nple = [];
    12. }
    13. return r;
    14. };
    15. f.zipWith = (op, ...xs) =>
    16. f.zip.apply(null, xs).map(
    17. (x) => x.reduce(op)
    18. );

    参考链接

    • Mateo Gianolio, Haskell in ES6: Part 1