什么是柯里化
柯里化(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 的特点,如果每次一传参都是一个流程的执行,那么实现一个柯里化方法,在调用时就很清晰的显示的流程执行的顺序。
根据柯里化的特点,被总结出了如下几个主要作用/优点:
- 参数复用。
- 延迟计算/运行。
参数复用
利用柯里化,我们可以固定住其中的部分参数,在调用时这个参数就不需要再次传递,这也就是我们这里说的参数复用。示例:
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/