函数闭包&柯里化
闭包
有这么一道题:
要实现一个函数,主要功能是对参数进行加法运算,但是参数传递方式略有不同:
CISDI_Cal(0).toString() //输出0
CISDI_Cal(0)(1).toString() //输出1
CISDI_Cal(0)(1)(2).toString() //输出3
var v = CISDI_Cal(0)(1); v(2).toString() //输出3
这个题考察的是javascript里面常用的函数闭包,从示例可以看到:
- 从第二个示例
CISDI_Cal(n)
返回的是一个函数对象,从第三和四示例可以看到CISDI_Cal(n)(n)
返回的也是一个函数对象;
CISDI_Cal(0).toString()
返回了累加的结果,说明每个返回的函数对象都有toString
方法;
- 多次函数调用累加得到结果,故前一函数获得参数累加后的值保留到了后一函数进行累加。
由上面分析可知,CISDI_Cal
内部有一个函数,这个函数执行后对输入进行了累加并返回了一个同样的函数,这个函数有toString
方法输出累加结果:
1 2 3 4 5 6 7 8 9 10
| function CISDI_Cal(n) { function add(a) { n += a; return add; } add.toString = function() { return n; } return add; }
|
上面这个例子讲的是函数闭包,过上面这种特殊的函数写法,可以让一个函数读取一个与自己不同作用域的局部变量,上面的n
是函数CISDI_Cal
内的局部变量,对add
是可见的,但是反过来就不行,add
内部的局部变量,对CISDI_Cal
就是不可见的,既然add
可以读取CISDI_Cal
中的局部变量,那么只要把add
作为返回值,我们就可以在CISDI_Cal
外部读取它的内部变量,简单一句话来说函数闭包就是 函数内包含子函数,并最终return子函数 ,
闭包函数的最大价值在于:我们可以在函数的外部(即子函数),直接读取该函数的局部变量。再仔细研究,就会发现CISDI_Cal
函数就如同一个“类”,而其定义的局部变量就如同该“类”的全局变量;而子函数add
函数,则如同这个“类”的方法,可以直接使用这个“类”的全局变量n。
闭包函数的主要作用
- 缓存:可以实现数据缓存,我们可以把一个需要长期用到的变量设为闭包函数的局部变量,在子函数里面直接使用它。因此局部变量只定义初始化一次,但我们可以多次调用子函数并使用该变量。这比起我们在子函数中定义初始化变量,多次调用则多次初始化的做法,效率更高。闭包函数常见的一种用途就是,我们可以通过此实现计数功能。在闭包函数定义一个计数变量,而在子函数中对其进行++的操作。这样每次调用闭包函数,计数变量就会加1。
- 实现封装:如同前面所说,闭包函数就如同一个“类”,只有在该闭包函数里的方法才可以使用其局部变量,闭包函数之外的方法是不能读取其局部变量的。这就实现了面向对象的封装性,更安全更可靠。
那什么是柯里化呢?
额,这么说吧…利用柯里化机制的函数function就是闭包函数。
柯里化(Currying),又称部分求值(Partial Evaluation),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
百科上的定义是针对众多函数式语言而言的,按照Stoyan Stefanov(《JavaScript Pattern》作者)的说法,所谓“柯里化”就是使函数理解并处理部分应用.举个栗子的话,就是下面这个(来自张鑫旭):
柯南身子虽小,但是里面住的却是大柯南,也就是一个function里面还有个function。不同柯南处理不同情况,例如,小柯南可以和…稍等,他女朋友叫什么的忘了,我查查…哦,毛利兰一起洗澡澡;但是大柯南就不行。小柯南不能当面指正犯人,需借助小五郎;但是,大柯南就可以直接质问指出凶手。就类似于,内外function处理不同的参数。如果代码表示就是(小柯南=smallKenan; 大柯南=bigKenan; 小柯南嗑药会变大柯南):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| var smallKenan = function(action) { var bigKenan = function(doing) { var result = ""; if (action === "take drugs") { if (doing === "bathWithGirlFriend") { result = "尖叫,新一,你这个色狼,然后一巴掌,脸煮熟了~"; } else if (doing === "pointOutKiller") { result = "新一,这个案子就交给你的,快点找出谁是凶手吧~"; } } else { if (doing === "bathWithGirlFriend") { result = "来吧,柯南,一起洗澡吧~"; } else if (doing === "pointOutKiller") { result = "小孩子家,滚一边去!"; } } console.log(result); return arguments.callee; }; return bigKenan; }; smallKenan("take drugs")("bathWithGirlFriend")("pointOutKiller");
|
结果如下:
尖叫,新一,你这个色狼,然后一巴掌,脸煮熟了~
新一,这个案子就交给你的,快点找出谁是凶手吧~
“吃药”、“洗澡”、“指出凶手”就可以看成三个参数,其中,“吃药”确实是小柯南使用的,而后面的是“洗澡”、“指出凶手”虽然跟在smallKenan()后面,实际上是大柯南使用的。这个就是柯里化,参数部分使用。外部函数处理部分应用,剩下的由外部函数的返回函数处理。
柯里化有3个常见作用:1. 参数复用;2. 提前返回;3. 延迟计算/运行。
1.参数复用
前面第一个栗子就是,每次add
都需要一个n参与计算,并保存计算结果,通过柯里化过程,add
无需添加这个多余的参数。
2.提前返回
很常见的一个例子,兼容现代浏览器以及IE浏览器的事件添加方法。我们正常情况可能会这样写:
1 2 3 4 5 6 7 8 9 10 11
| var addEvent = function(el, type, fn, capture) { if (window.addEventListener) { el.addEventListener(type, function(e) { fn.call(el, e); }, capture); } else if (window.attachEvent) { el.attachEvent("on" + type, function(e) { fn.call(el, e); }); } };
|
上面的方法有什么问题呢?很显然,我们每次使用addEvent为元素添加事件的时候,(eg. IE6/IE7)都会走一遍if…else if …,其实只要一次判定就可以了,怎么做?–柯里化。改为下面这样子的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| var addEvent = (function(){ if (window.addEventListener) { return function(el, sType, fn, capture) { el.addEventListener(sType, function(e) { fn.call(el, e); }, (capture)); }; } else if (window.attachEvent) { return function(el, sType, fn, capture) { el.attachEvent("on" + sType, function(e) { fn.call(el, e); }); }; } })();
|
初始addEvent的执行其实值实现了部分的应用(只有一次的if…else if…判定),而剩余的参数应用都是其返回函数实现的,典型的柯里化。
3.延迟计算
一般而言,延迟计算或运行是没有必要的,因为一天花10块钱和月末花300块钱没什么本质区别——只是心里好受点(温水炖青蛙)。嘛,毕竟只是个人看法,您可能会不这么认为。举个例子,我每周末都要去钓鱼,我想知道我12月份4个周末总共钓了几斤鱼,把一些所谓的模式、概念抛开,我们可能就会下面这样实现:
1 2 3 4 5 6 7 8 9 10 11
| var fishWeight = 0; var addWeight = function(weight) { fishWeight += weight; }; addWeight(2.3); addWeight(6.5); addWeight(1.2); addWeight(2.5); console.log(fishWeight);
|
每次addWeight都会累加鱼的总重量。
若是有柯里化实现,则会是下面这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| var curryWeight = function(fn) { var _fishWeight = []; return function() { if (arguments.length === 0) { return fn.apply(null, _fishWeight); } else { _fishWeight = _fishWeight.concat([].slice.call(arguments)); } } }; var fishWeight = 0; var addWeight = curryWeight(function() { var i=0; len = arguments.length; for (i; i<len; i+=1) { fishWeight += arguments[i]; } }); addWeight(2.3); addWeight(6.5); addWeight(1.2); addWeight(2.5); addWeight(); console.log(fishWeight);
|
部分内容转载自张鑫旭-鑫空间-鑫生活