点击上方“IT平头哥联盟”,选择“置顶或者星标”
公众号回复 [加群 ] 加入我们一起进步~
作者 leon
柯里化,可以理解为提前接收部分参数,延迟执行,不立即输出结果,而是返回一个接受剩余参数的函数。因为这样的特性,也被称为部分计算函数。柯里化,是一个逐步接收参数的过程。在接下来的剖析中,你会深刻体会到这一点。 反柯里化,是一个泛型化的过程。它使得被反柯里化的函数,可以接收更多参数。目的是创建一个更普适性的函数,可以被不同的对象使用。有鸠占鹊巢的效果。
实现 add(1)(2, 3)(4)() = 10 的效果
依题意,有两个关键点要注意: - 传入参数时,代码不执行输出结果,而是先记忆起来 - 当传入空的参数时,代表可以进行真正的运算
完整代码如下:
function currying(fn){
var allArgs = [];
return function next(){
var args = [].slice.call(arguments);
if(args.length > 0){
allArgs = allArgs.concat(args);
return next;
}else{
return fn.apply(null, allArgs);
}
}
}
var add = currying(function(){
var sum = 0;
for(var i = 0; i < arguments.length; i++){
sum += arguments[i];
}
return sum;
});
由于是延迟计算结果,所以要对参数进行记忆。 这里的实现方式是采用闭包。
function currying(fn){
var allArgs = [];
return function next(){
var args = [].slice.call(arguments);
if(args.length > 0){
allArgs = allArgs.concat(args);
return next;
}
}
}
当执行var add = currying(...)
时,add变量已经指向了next方法。此时,allArgs在next方法内部有引用到,所以不能被GC回收。也就是说,allArgs在该赋值语句执行后,一直存在,形成了闭包。
依靠这个特性,只要把接收的参数,不断放入allArgs变量进行存储即可。
所以,当arguments.length > 0时,就可以将接收的新参数,放到allArgs中。
最后返回next函数指针,形成链式调用。
题意是,空参数时,输出结果。所以,只要判断arguments.length == 0
即可执行。
另外,由于计算结果的方法,是作为参数传入currying
函数,所以要利用apply进行执行。
综合上述思考,就可以得到以下完整的柯里化函数。
function currying(fn){
var allArgs = []; // 用来接收参数
return function next(){
var args = [].slice.call(arguments);
// 判断是否执行计算
if(args.length > 0){
allArgs = allArgs.concat(args); // 收集传入的参数,进行缓存
return next;
}else{
return fn.apply(null, allArgs); // 符合执行条件,执行计算
}
}
}
柯里化,在这个例子中可以看出很明显的行为规范:
实现 add(1)(2, 3)(4)(5) = 15 的效果。
很多人这里就犯嘀咕了:我怎么知道执行的时机?
其实,这里有个忍者技艺:valueOf和toString。
js在获取当前变量值的时候,会根据语境,隐式调用valueOf和toString方法进行获取需要的值。 那么,实现起来就很简单了。
function currying(fn){
var allArgs = [];
function next(){
var args = [].slice.call(arguments);
allArgs = allArgs.concat(args);
return next;
}
// 字符类型
next.toString = function(){
return fn.apply(null, allArgs);
};
// 数值类型
next.valueOf = function(){
return fn.apply(null, allArgs);
}
return next;
}
var add = currying(function(){
var sum = 0;
for(var i = 0; i < arguments.length; i++){
sum += arguments[i];
}
return sum;
});
有以下轻提示类。现在想要单独使用其show方法,输出新对象obj中的内容。
// 轻提示
function Toast(option){
this.prompt = '';
}
Toast.prototype = {
constructor: Toast,
// 输出提示
show: function(){
console.log(this.prompt);
}
};
// 新对象
var obj = {
prompt: '新对象'
};
用反柯里化的方式,可以这么做
function unCurrying(fn){
return function(){
var args = [].slice.call(arguments);
var that = args.shift();
return fn.apply(that, args);
}
}
var objShow = unCurrying(Toast.prototype.show);
objShow(obj); // 输出"新对象"
在上面的例子中,Toast.prototype.show
方法,本来是Toast类的私有方法。跟新对象obj没有半毛钱关系。
经过反柯里化后,却可以为obj对象所用。
为什么能被obj所用,是因为内部将Toast.prototype.show
的上下文重新定义为obj。也就是用apply改变了this指向。
而实现这一步骤的过程,就需要增加反柯里化后的objShow方法参数。
Function.prototype.unCurrying = function(){
var self = this;
return function(){
return Function.prototype.call.apply(self, arguments);
}
}
// 使用
var objShow = Toast.prototype.show.unCurrying();
objShow(obj);
这里的难点,在于理解Function.prototype.call.apply(self, arguments);
。
可以分拆为两步:
1)Function.prototype.call.apply(…)的解析
可以看成是callFunction.apply(...)
。这样,就清晰很多。
callFunction
的this指针,被apply修改为self。
然后执行callFunction -> callFunction(arguments)
2)callFunction(arguments)的解析
call
方法,第一个参数,是用来指定this的。所以callFunction(arguments) -> callFunction(arguments[0], arguments[1-n])
。
由此可以得出,反柯里化后,第一个参数,是用来指定this
指向的。
3)为什么要用apply(self, arguments)
如果使用apply(null, arguments)
,因为null
对象没有call方法,会报错。
var fn = function(){};
var val = 1;
if(Object.prototype.toString.call(fn) == '[object Function]'){
console.log(`${fn} is function.`);
}
if(Object.prototype.toString.call(val) == '[object Number]'){
console.log(`${val} is number.`);
}
上述代码,用反柯里化,可以这么写:
var fn = function(){};
var val = 1;
var toString = Object.prototype.toString.unCurrying();
if(toString(fn) == '[object Function]'){
console.log(`${fn} is function.`);
}
if(toString(val) == '[object Number]'){
console.log(`${val} is number.`);
}
function nodeListen(node, eventName){
return function(fn){
node.addEventListener(eventName, function(){
fn.apply(this, Array.prototype.slice.call(arguments));
}, false);
}
}
var bodyClickListen = nodeListen(document.body, 'click');
bodyClickListen(function(){
console.log('first listen');
});
bodyClickListen(function(){
console.log('second listen');
});
使用柯里化,优化监听DOM节点事件。addEventListener
三个参数不用每次都写。
其实,反柯里化和泛型方法一样,只是理念上有一些不同而已。理解这种思维即可。
PS:福利,转发本公众号任意一篇文章到朋友圈集20个赞,即可获取掘金小册5折码哦!
- end -
用心分享 一起成长 做有温度的攻城狮
每天记得对自己说:你是最棒的!