引子
最近发现一个问题:一部分写JS的人,其实对于函数式编程的概念并不是太了解。如下的代码片断,常常让他们觉得不可思议:
1 2 3 4 5 6 7 |
OAuth2Server.prototype.authCodeGrant = function (check) { var self = this; return function (req, res, next) { new AuthCodeGrant(self, req, res, next, check); }; }; |
上述片断来自开源项目node-oauth2-server,这个authCodeGrant原型函数涉及到JS编程中经常用到的两个概念:闭包 和 高阶函数(check变量在这个函数中被闭包,authCodeGrant能返回函数,因此是一个高阶函数。
闭包
闭包就是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。
如何来理解这个自由变量呢?
自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量
什么样的变量是自由变量呢?如下片断中的freeVar对inner()
来说就是个自由变量:
1 2 3 4 5 6 7 |
function wrapper() { var freeVar = 42; function inner() { return 2 * freeVar; } return inner; } |
自由变量在闭包生成之前,并不是函数的一部分。在函数被调用时,闭包才会形成,函数将这个自由变量纳入自己的作用域,也就是说,自由变量从此与定义它的容器无关,以函数被调用那一刻为时间点,成为函数Context中的成员。
来看一个困惑前端的示例,循环添加事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<button>第1条记录</button> <button>第2条记录</button> <button>第3条记录</button> <button>第4条记录</button> <button>第5条记录</button> <button>第6条记录</button> <script type="text/javascript"> var buttonst_obj = document.getElementsByTagName("button"); for (var i = 0, len = buttonst_obj.length; i < len; i++) { buttonst_obj[i].onclick = function() { alert(i); }; } </script> |
上述片断的结果是:每个Button弹出的都是6。因为没有形成有效的闭包,因为闭包是有延迟求值特性的,所以在函数得到执行时,i === 6。
如果我们将它改成这样,i 做为外层函数的参数而被内层函数闭包,结果也是我们想要的:
1 2 3 4 5 6 7 8 9 |
var buttonst_obj = document.getElementsByTagName("button"); for (var i = 0, len = buttonst_obj.length; i < len; i++) { buttonst_obj[i].onclick = clickEvent(i); } function clickEvent(i){ return function () { console.log(i); } } |
Why? 因为这个clickEvent(i)
高阶函数,它将 i 作为自由变量(注意:i 并不是内函数的参数,也不是内函数的一部分)传递,在 click 时闭包已经形成并被传递。
闭包的作用域
虽然自由变量从闭包时起 “将和这个函数一同存在,即使已经离开了创造它的环境也不例外”,但我们必须搞清楚,闭包产生时的作用域,看个例子:
1 2 3 4 5 6 7 8 9 10 |
var scope = 'global'; function echo(){ console.log(scope); } function wrapper(){ var scope = 'inner'; echo(); } echo(); // 输出global wrapper(); // 输出global |
为什么在wrapper内部的echo()调用,会输出全局scope?因为:echo定义的位置,只能闭包到全局的scope,它的外层作用域就是全局空间,即便是延迟求值也如此。
把这段代码稍加改造,就能看得更清楚:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var scope = 'global'; function echo(){ console.log(scope); } function wrapper(){ var scope = 'inner'; function echo(){ console.log(scope); } echo(); } echo(); //输出global wrapper(); //输出inner |
闭包的自由变量来自何处,和它的外层作用域(被定义的位置)也是有关系的。
高阶函数
上述循环事件片断中的 clickEvent(i)
即为一个高阶函数。
高阶函数满足:要么接受一个或多个函数作为输入;要么输出一个函数
为什么会用到高阶函数?粗糙的说,就是为了闭包。
接受函数作为输入的高阶函数
这种高阶函数可作为一种模式的构造器,比如:我有快速排序/堆排序/希尔排序 等若干个排序函数,那么我只需要提供一个高阶函数,就能生成基于这若干种排序函数的排序器:
1 2 3 4 5 6 7 8 9 10 11 12 |
//排序器 var sortingGenerator = function(sortFunc){ return function(args){ var arguments = [].slice.call(args); return sortFunc(arguments); } }; //引入排序算法 var heapSort = require('heapSort'); var heapSorter = sortingGenerator(heapSort); //使用算法 heapSorter(4, 22, 44, 66, 77); |
当然,其实这个高阶函数也输出了函数
输出函数的高阶函数
和上例一样,高阶函数输出一个函数也很好理解:先闭包自由变量,根据它在将来调用时产生不一样的输出。
比如,我需要一个函数,既可以算平方,也可以算立方,最好什么方都能算,这时我就需要一个如下片断的高阶函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
//计算m的N次方 var powerOfN = function(n){ return function(m){ var res = 1; for(var i = 0; i < n; ++i){ res *= m; } return res; } ; }; //按需生成 var powerOf2 = powerOfN(2); var powerOf3 = powerOfN(3); //调用传参 console.log(powerOf2(3)); console.log(powerOf3(2)); |
小结
通过闭包和高阶函数的组合运用,我们可以提炼出这样一种编程模式:通过分离>=2次的参数传递,以最少的代码实现动态的算法生成器。
做为一个老程序员,在学函数式编程时,遇到了点转不过弯的情况。
非常感谢莫邪,从此文开始学习闭包和高阶函数的知识。
对于有个地方,跟莫邪探讨一下:
第1条记录
第2条记录
第3条记录
第4条记录
第5条记录
第6条记录
var buttonst_obj = document.getElementsByTagName(“button”);
for (var i = 0, len = buttonst_obj.length; i < len; i++) {
buttonst_obj[i].onclick = function() {
alert(i);
};
}
在点击按钮的视乎,应该就形成了闭包,只是仅仅形成了一个闭包的实例?内存中只有一个i,与匿名函数的代码段形成的闭包。
能够访问i变量的应该只有for循环,以及匿名函数里的代码。
由于闭包是运行期的概念,在真正执行点击之后,匿名函数真正绑定引用的i这个变量,所以变成 i = 6 了。
var buttonst_obj = document.getElementsByTagName(“button”);
for (var i = 0, len = buttonst_obj.length; i < len; i++) {
buttonst_obj[i].onclick = clickEvent(i);
}
function clickEvent(i){
return function () {
console.log(i);
}
}
这个例子里,clickEvent函数的return那段代码与clickEvent函数被调用时,所产生的栈桢上的变量i形成了闭包。
所以,应该形成了 buttonst_obj.length 6 个闭包的实例。
是不是可以这样理解?
需要闭包的场景:
因为循环中有异步函数(事件),在这个包裹循环量i的函数(事件)被触发时,其实循环已经结束,所以这个函数(事件)包裹的i总是最后一次的值。
闭包传递的变量:既不是函数参数也不是函数的局部变量,这样在循环时它的值就被锁定在了异步函数的栈桢上。
嗯,理解了,非常感谢莫邪~~
刚刚做了个实验, 发现循环生成闭包函数的需求可以用let来实现, 代码量还少了很多.
https://gist.github.com/mingyang91/a56b56c3a7ed3b432e32
但是, 不知道为什么let和var会产生这样的区别.
运行结果中, for作用域内的let值就像被替换成了常量.
有些不解, 还望三哥解答一下
在MDN上看到有
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures#Creating_closures_in_loops_A_common_mistake
这样一段描述 “在 JavaScript 1.7 引入 let 关键字 之前,闭包的一个常见的问题发生于在循环中创建闭包”. 也只是一提, 没有做深入讲解
ES6的let可以很好的解决循环变量闭包问题,他不用延迟求值