知识点总结
- 了解柯里化
- 闭包的应用
柯里化的简单实现
一个参数的函数柯里化实现
function _curry1(fn) {
return function f1(a) {
if (arguments.length === 0) {
return f1;
} else {
return fn.apply(this, arguments);
}
};
}
const curryMathAbs = _curry1(Math.abs);
console.log(curryMathAbs(-3)); // 3
console.log(curryMathAbs()(-3)); // 3
Math.abs是个只有一个入参的函数。将其柯里化,得到函数curryMathAbs,只需判断调用curryMathAbs给它传递了一个参数还是两个参数,传递一个参数时,就返回f1,传递两个参数时,调用fn,也就是原函数Math.abs。
两个参数的函数柯里化实现
function _curry2(fn) {
return function f2(a, b) {
switch (arguments.length) {
case 0:
return f2;
case 1:
return _curry1(function(_b) { return fn(a, _b); });
default:
return fn(a, b);
}
};
}
const curryMathatan2 = _curry2(Math.atan2);
console.log(curryMathatan2(90)(15)); // 1.4056476493802699
Math.atan2是个有两个入参的求正切值的函数。将其柯里化,得到函数curryMathatan2,这时候要判断给其传入的时0个参数,1个参数,还是2个参数。传入0个参数,返回f2,传入1个参数,返回_curry1的一个匿名函数,用来等待接受下一个参数,传入两个参数,直接调用原函数Math.atan2。
多个参数的函数柯里化实现
显然我们一直这样写下去真的很呆,需要一个通用的柯里化方法。
function _curryN(length, args, fn){
return function(){
const newArgs = args.concat(Array.prototype.slice.call(arguments));
return newArgs.length < length
? _curryN.call(this, fn.length, newArgs, fn)
: fn.apply(this,newArgs);
}
}
function curry(fn){
return _curryN(fn.length, [], fn);
}
const curryMathatan2 = curry(Math.atan2);
console.log(curryMathatan2(90)(15)); // 1.4056476493802699
通过如果传入的参数个数,小于原函数的参数个数,就返回_curryN,并缓存住已经传入的参数。实现了任意个参数的函数柯里化。
可以传入占位符的柯里化函数实现
加入我们有一个函数f,拥有3个入参,即f(1,2,3)。将其柯里化,g = curry(f)。那么我们可以这样使用g。g(1)(2)(3)、g(1)(2, 3)、g(1, 2)(3)等。我们有这样一个需求,先传入第二个参数,再传入第一个参数,再传入第三个参数,可以吗?
可以,我们可以使用一个占位符对象,告诉g这个参数暂时不传值。
g = curry(f)
g(1)(2)(3)
也可以
g2 = g(_)(2)(3)
g2(1)
这里的_就是我们的一个占位符对象,告诉g函数先不传第一个参数,后面再传。
那我们就先实现一个占位符对象,和一个判断它是占位符的方法。
const _ = {'@@functional/placeholder': true};
function _isPlaceholder(a) {
return a != null &&
typeof a === 'object' &&
a['@@functional/placeholder'] === true;
}
支持占位符的一个参数的函数柯里化实现
function _curry1(fn) {
return function f1(a) {
if (arguments.length === 0 || _isPlaceholder(a)) {
return f1;
} else {
return fn.apply(this, arguments);
}
};
}
逻辑里多了一个含占位符的判断。
支持占位符的两个参数的函数柯里化实现
function _curry2(fn) {
return function f2(a, b) {
switch (arguments.length) {
case 0:
return f2;
case 1:
return _isPlaceholder(a)
? f2
: _curry1(function(_b) { return fn(a, _b); });
default:
return _isPlaceholder(a) && _isPlaceholder(b)
? f2
: _isPlaceholder(a)
? _curry1(function(_a) { return fn(_a, b); })
: _isPlaceholder(b)
? _curry1(function(_b) { return fn(a, _b); })
: fn(a, b);
}
};
}
可以看到因为要判断占位符的逻辑,使代码的逻辑判断多了些。
支持占位符的多个参数的函数柯里化实现
_curryN(length, received, fn) {
return function() {
var combined = [];
var argsIdx = 0;
var left = length;
var combinedIdx = 0;
while (combinedIdx < received.length || argsIdx < arguments.length) {
var result;
if (combinedIdx < received.length &&
(!_isPlaceholder(received[combinedIdx]) ||
argsIdx >= arguments.length)) {
result = received[combinedIdx];
} else {
result = arguments[argsIdx];
argsIdx += 1;
}
combined[combinedIdx] = result;
if (!_isPlaceholder(result)) {
left -= 1;
}
combinedIdx += 1;
}
return left <= 0
? fn.apply(this, combined)
: _arity(left, _curryN(length, combined, fn));
};
}
function curry(fn){
return _curryN(fn.length, [], fn);
}
因为占位符,导致判断入参个数及缓存它们的逻辑复杂了许多,需要花一点时间去理解了。
以上就是Ramda源码中对于柯里化的实现了。
一些细节
Ramda对所有自己支持的方法,都进行了柯里化
我们看一下add方法的源码:
var add = _curry2(function add(a, b) {
return Number(a) + Number(b);
});
export default add;
add方法是一个有两个参数的方法,所以使用_curry2进行柯里化,这里没有使用curryN,我理解是在细节上对性能的追求吧。
而且Ramda的大部分方法,都是三个参数以内的,很少使用curryN来对方法进行柯里化,所以在Ramda的内部方法中,我们可以看到_curry1, _curry2, _curry3, _curryN都存在。

参数固定
我注意到curryN方法的源码是这样的
function curryN(length, fn) {
if (length === 1) {
return _curry1(fn);
}
return _arity(length, _curryN(length, [], fn));
}
这里面调用了_arity, _arity又是什么样呢?
function _arity(n, fn) {
/* eslint-disable no-unused-vars */
switch (n) {
case 0: return function() { return fn.apply(this, arguments); };
case 1: return function(a0) { return fn.apply(this, arguments); };
case 2: return function(a0, a1) { return fn.apply(this, arguments); };
case 3: return function(a0, a1, a2) { return fn.apply(this, arguments); };
case 4: return function(a0, a1, a2, a3) { return fn.apply(this, arguments); };
case 5: return function(a0, a1, a2, a3, a4) { return fn.apply(this, arguments); };
case 6: return function(a0, a1, a2, a3, a4, a5) { return fn.apply(this, arguments); };
case 7: return function(a0, a1, a2, a3, a4, a5, a6) { return fn.apply(this, arguments); };
case 8: return function(a0, a1, a2, a3, a4, a5, a6, a7) { return fn.apply(this, arguments); };
case 9: return function(a0, a1, a2, a3, a4, a5, a6, a7, a8) { return fn.apply(this, arguments); };
case 10: return function(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9) { return fn.apply(this, arguments); };
default: throw new Error('First argument to _arity must be a non-negative integer no greater than ten');
}
}
arity是一个让参数固定个数的函数。它的好处在于,我们能知道执行了部分参数的函数,还需要几个参数。
const sumArgs = (...args) => R.sum(args);
const curriedAddFourNumbers = R.curryN(4, sumArgs);
const f = curriedAddFourNumbers(1, 2);
console.log('curriedAddFourNumbers', curriedAddFourNumbers);
// curriedAddFourNumbers ƒ (a0, a1, a2, a3) {
// return fn.apply(this, arguments);
// }
console.log('f', f);
// f ƒ (a0, a1) {
// return fn.apply(this, arguments);
// }
注意f这个函数的打印结果,是一个还需要两个参数的函数,这就是arity函数的功劳,让我们知道执行了部分参数的curriedAddFourNumbers,还需要2个参数,就可以得出结果。