javaScript 中的函数柯里化(Currying)简述

目录
[隐藏]

什么是柯里化

柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

简单来说,柯里化即是将多参函数转换成一系列的单参函数,其最大特点即只有一个参数,不达到最终目的时返回值都是函数。下面我们举一个简单的加法实现的例子来说明柯里化的特点。

javaScript 实现柯里化函数

对于实现一个加法函数,多参数方式的实现示例:

function add(a, b, c) {
    return a + b + c;
}
// 执行
add(1, 2, 3); // 6

柯里化要求只能有一个参数,其实现出来的执行效果应该是这样的:

// 简单实现加法柯里化方法
function add (a) {
    var total = a;
    return function (b) {
        total += b;
        return function (c) {
            return total += c;
        }
    }
}
add(1)(2)(3); // 6

这个实现比较简单直接,这就是柯里化函数要求的效果。

ES6 箭头函数实现柯里化

ES6 的箭头函数有一个特点是,单行执行的结果即作为返回值,那么使用箭头函数实现柯里化就变得简洁多了:

const add = a => b => c => a + b + c;
add(1)(2)(3); // 6

参数任意的柯里化函数实现

对于前文加法函数,如果要求参数可以任意多,那么在 JavaScript 中该如何实现呢?

对于这个需求,从执行效果来看,当后续还有参数时,函数返回值应当是一个函数;当后续没有了参数时,函数返回值应当是一个值。那么如果能够让函数返回值既可以作为函数继续传参执行,又可以在取值时输出为值,这个问题就解决了。

回想一下,在 JavaScript 中对象到原始值的转换规则:

当一个对象转换成原始值时,先查看对象是否有 valueOf 方法,如果有并且返回值是一个原始值,那么直接返回这个值,否则没有 valueOf 或返回的不是原始值,那么调用 toString 方法,返回字符串表示。

简单来说,如果对一个对象类型取值,会调用它的 valueOf 或 toString 方法,返回其执行结果。每一个对象都默认有这两个方法的实现。示例:

// 定义函数 fn
function fn() {}

fn.toString(); // 输出:"function fn() {}"
fn.valueOf();  // 输出 function fn() {}

那么,只需要重写这两个方法中的一个,即可实现取值时得到的是我们期望的结果值。我们尝试一下:

function add(a) {
    var total = a;
    var _fn = function (b) {
        total += b;
        return _fn;
    };
    _fn.toString = _fn.valueOf = function () {
        return total;
    };
    return _fn;
}
add(1)(2); // 3
add(1)(2)(3); // 6
add(1)(2)(3)(4); // 10

通用的柯里化实现

function currying(fn) {
    // 缓存队列
    var _args = [];

    return function cb() {
        // 不传参数时返回方法的执行
        if (arguments.length === 0) {
            return fn.apply(this, _args);
        }

        Array.prototype.push.apply(_args, arguments);

        return cb;
    }
}

另一种通用实现:

function currying(fn) {
    var outerArgs = Array.prototype.slice.call(arguments, 1);
    return function() {
        var innerArgs = Array.prototype.slice.call(arguments),
            finalArgs = outerArgs.concat(innerArgs);
        return fn.apply(null, finalArgs);
    };
}

使用示例:

var gd = currying(loc,'广东');
var zh = currying(zj,'珠海');

zh('香洲区');          // 广东-珠海-香洲区
zh('高新区');          // 广东-珠海-高新区
gd('广州','越秀区');   // 广东-越秀-越秀区

柯里化函数的作用

柯里化函数在使用上更为简洁,可以帮助你更好的处理和抽象代码的逻辑。参考加法函数 add 的特点,如果每次一传参都是一个流程的执行,那么实现一个柯里化方法,在调用时就很清晰的显示的流程执行的顺序。

根据柯里化的特点,被总结出了如下几个主要作用/优点:

  1. 参数复用。
  2. 延迟计算/运行。

参数复用

利用柯里化,我们可以固定住其中的部分参数,在调用时这个参数就不需要再次传递,这也就是我们这里说的参数复用。示例:

const local = country => province => city => `${country}-${province}-${city}!`;
const cn = say('China');
const cnHn = say('河南');
const cnGd = sayToMr('广东');

cnHn('信阳'); // China-河南-信阳
greetMiss('珠海'); // China-广东-珠海

利用复用的参数,可以让柯里化的方法具有更高的适用性。

延迟计算

使用通用的实现 currying 方法来实现加法函数,即可看到延迟计算的效果:

// 原始方法
function add() {
  var sum = 0, i, len;
    for (i = 0, len = arguments.length; i < len; i++) {
    sum += arguments[i];
  }
  return sum;
}
// 将 add 柯里化
var addCurrying = currying(add);
var add123 = addCurrying(1)(2)(3); // 此时只生成了三个方法存于队列中,并未执行
add123(); // 此时才真正的执行计算

柯里化函数的副作用

柯里化会创建大量的嵌套和闭包,在未执行前都得不到释放,这会导致内存占用变大。但这一般情况下都不会有什么问题,大多数时候 DOM 操作是影响性能的主要因素。关于 js 柯里化函数的性能:

  • 存取 arguments 对象通常要比存取命名参数要慢
  • 使用 fn.apply() 和 fn.call() 要比直接调用 fn() 要慢
  • 创建大量嵌套作用域和闭包会带来内存和速度上的开销
  • 大多数时候 DOM 操作是影响性能的主要因素

相关参考

http://zh.wikipedia.org/wiki/Currying
https://segmentfault.com/a/1190000004589338
http://www.zhangxinxu.com/wordpress/2013/02/js-currying/

点赞 (1)

发表评论

电子邮件地址不会被公开。 必填项已用*标注