前言
柯里化,可以理解为提前接收部分参数,延迟执行,不立即输出结果,而是返回一个接受剩余参数的函数。因为这样的特性,也被称为部分计算函数。柯里化,是一个逐步接收参数的过程。在接下来的剖析中,你会深刻体会到这一点。
反柯里化,是一个泛型化的过程。它使得被反柯里化的函数,可以接收更多参数。目的是创建一个更普适性的函数,可以被不同的对象使用。有鸠占鹊巢的效果。
一、柯里化
1.1 例子
实现 add(1)(2, 3)(4)() = 10
的效果
依题意,有两个关键点要注意:
- 传入参数时,代码不执行输出结果,而是先记忆起来
- 当传入空的参数时,代表可以进行真正的运算
完整代码如下:
1 function currying(fn){ 2 var allArgs = []; 3 4 return function next(){ 5 var args = [].slice.call(arguments); 6 7 if(args.length > 0){ 8 allArgs = allArgs.concat(args); 9 return next; 10 }else{ 11 return fn.apply(null, allArgs); 12 } 13 } 14 } 15 var add = currying(function(){ 16 var sum = 0; 17 for(var i = 0; i < arguments.length; i++){ 18 sum += arguments[i]; 19 } 20 return sum; 21 });
1.2 记忆传入参数
由于是延迟计算结果,所以要对参数进行记忆。
这里的实现方式是采用闭包。
1 function currying(fn){ 2 var allArgs = []; 3 4 return function next(){ 5 var args = [].slice.call(arguments); 6 7 if(args.length > 0){ 8 allArgs = allArgs.concat(args); 9 return next; 10 } 11 } 12 }
当执行var add = currying(...)
时,add
变量已经指向了next
方法。此时,allArgs
在next
方法内部有引用到,所以不能被GC回收。也就是说,allArgs
在该赋值语句执行后,一直存在,形成了闭包。
依靠这个特性,只要把接收的参数,不断放入allArgs
变量进行存储即可。
所以,当arguments.length > 0
时,就可以将接收的新参数,放到allArgs
中。
最后返回next
函数指针,形成链式调用。
1.3 判断触发函数执行条件
题意是,空参数时,输出结果。所以,只要判断arguments.length == 0
即可执行。
另外,由于计算结果的方法,是作为参数传入currying
函数,所以要利用apply
进行执行。
综合上述思考,就可以得到以下完整的柯里化函数。
1 function currying(fn){ 2 var allArgs = []; // 用来接收参数 3 4 return function next(){ 5 var args = [].slice.call(arguments); 6 7 // 判断是否执行计算 8 if(args.length > 0){ 9 allArgs = allArgs.concat(args); // 收集传入的参数,进行缓存 10 return next; 11 }else{ 12 return fn.apply(null, allArgs); // 符合执行条件,执行计算 13 } 14 } 15 }
1.4 总结
柯里化,在这个例子中可以看出很明显的行为规范:
- 逐步接收参数,并缓存供后期计算使用
- 不立即计算,延后执行
- 符合计算的条件,将缓存的参数,统一传递给执行方法
1.5 扩展
实现 add(1)(2, 3)(4)(5) = 15
的效果。
很多人这里就犯嘀咕了:我怎么知道执行的时机?
其实,这里有个忍者技艺:valueOf
和toString
。
js在获取当前变量值的时候,会根据语境,隐式调用valueOf
和toString
方法进行获取需要的值。
那么,实现起来就很简单了。
1 function currying(fn){ 2 var allArgs = []; 3 4 function next(){ 5 var args = [].slice.call(arguments); 6 allArgs = allArgs.concat(args); 7 return next; 8 } 9 // 字符类型 10 next.toString = function(){ 11 return fn.apply(null, allArgs); 12 }; 13 // 数值类型 14 next.valueOf = function(){ 15 return fn.apply(null, allArgs); 16 } 17 18 return next; 19 } 20 var add = currying(function(){ 21 var sum = 0; 22 for(var i = 0; i < arguments.length; i++){ 23 sum += arguments[i]; 24 } 25 return sum; 26 });
二、反柯里化
2.1 例子
有以下轻提示类。现在想要单独使用其show
方法,输出新对象obj
中的内容。
1 // 轻提示 2 function Toast(option){ 3 this.prompt = ''; 4 } 5 Toast.prototype = { 6 constructor: Toast, 7 // 输出提示 8 show: function(){ 9 console.log(this.prompt); 10 } 11 }; 12 13 // 新对象 14 var obj = { 15 prompt: '新对象' 16 };
用反柯里化的方式,可以这么做
1 function unCurrying(fn){ 2 return function(){ 3 var args = [].slice.call(arguments); 4 var that = args.shift(); 5 return fn.apply(that, args); 6 } 7 } 8 9 var objShow = unCurrying(Toast.prototype.show); 10 11 objShow(obj); // 输出"新对象"
2.2 反柯里化的行为
- 非我之物,为我所用
- 增加被反柯里化方法接收的参数
在上面的例子中,Toast.prototype.show
方法,本来是Toast
类的私有方法。跟新对象obj
没有半毛钱关系。
经过反柯里化后,却可以为obj
对象所用。
为什么能被obj
所用,是因为内部将Toast.prototype.show
的上下文重新定义为obj
。也就是用apply
改变了this
指向。
而实现这一步骤的过程,就需要增加反柯里化后的objShow
方法参数。
2.3 另一种反柯里化的实现
1 Function.prototype.unCurrying = function(){ 2 var self = this; 3 return function(){ 4 return Function.prototype.call.apply(self, arguments); 5 } 6 } 7 8 // 使用 9 var objShow = Toast.prototype.show.unCurrying(); 10 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
方法,会报错。
三、实战
3.1 判断变量类型(反柯里化)
1 var fn = function(){}; 2 var val = 1; 3 4 if(Object.prototype.toString.call(fn) == '[object Function]'){ 5 console.log(`${fn} is function.`); 6 } 7 8 if(Object.prototype.toString.call(val) == '[object Number]'){ 9 console.log(`${val} is number.`); 10 }
上述代码,用反柯里化,可以这么写:
1 var fn = function(){}; 2 var val = 1; 3 var toString = Object.prototype.toString.unCurrying(); 4 5 if(toString(fn) == '[object Function]'){ 6 console.log(`${fn} is function.`); 7 } 8 9 if(toString(val) == '[object Number]'){ 10 console.log(`${val} is number.`); 11 }
3.2 监听事件(柯里化)
1 function nodeListen(node, eventName){ 2 return function(fn){ 3 node.addEventListener(eventName, function(){ 4 fn.apply(this, Array.prototype.slice.call(arguments)); 5 }, false); 6 } 7 } 8 9 var bodyClickListen = nodeListen(document.body, 'click'); 10 bodyClickListen(function(){ 11 console.log('first listen'); 12 }); 13 14 bodyClickListen(function(){ 15 console.log('second listen'); 16 });
使用柯里化,优化监听DOM节点事件。addEventListener
三个参数不用每次都写。
后记
其实,反柯里化和泛型方法一样,只是理念上有一些不同而已。理解这种思维即可。
转载:https://www.imooc.com/article/46624