在《高级程序设计》中,对于闭包一直没有很好的解释,在stackoverflow上翻出了一篇很老的《JavaScript closure for dummies》(2016)~
出处:http://stackoverflow.com/questions/111102/how-do-javascript-closures-work
本文旨在用JavaScript代码让程序员理解闭包,函数式编程的程序员或者导师请绕行。
只要理解了闭包的核心理念,闭包并不难学。但是通过学习一些相关的学术论文或者学术方面的信息很难理解闭包。
本文是写给有主流语言编程经验的程序员的,至少应该能够看懂如下JavaScript函数:
1 function sayHello(name) { 2 var text = 'Hello ' + name; 3 var say = function() { console.log(text); } 4 say(); 5 }
两个一句话总结:
以下代码给函数返回了一个引用:
1 function sayHello2(name) { 2 var text = 'Hello ' + name; // Local variable 3 var say = function() { console.log(text); } 4 return say; 5 } 6 var say2 = sayHello2('Bob'); 7 say2(); // logs "Hello Bob"
大多数的JavaScript程序员能够理解,上述代码是如何返回给变量(say2)一个函数的引用的,如果你不理解的话,请在学习闭包前先弄懂它。C语言程序员可能认为这个函数是返回了一个函数的指针,并认为变量say和say2是两个指向了函数的指针。
C的函数指针和JavaScript的函数引用之间有关键性的区别:在JavaScript中你可以认为,一个函数引用变量既是一个指向了函数的指针,也是一个指向了闭包的隐性指针。
因为匿名函数 function() { console.log(text); } 是在另外一个函数( sayHello2() )里面声明的,所以以上代码包含一个闭包。在JavaScript中,如果你在另外一个函数里面使用 function 关键字,那么你就创造了一个闭包。
在C和大多数其他常见语言中,当一个函数return后,因为堆栈已经被清理了,所以所有的局部变量都无法再被访问。
在JavaScript中,如果你在另外一个函数中声明一个函数,那么当你调用一个函数并return后,这个局部变量仍旧是可访问的,上述代码已经证实过这一点:我们在sayHello2()函数return后调用了函数say()。
function() { console.log(text); } // Output of say2.toString();
通过say2.toString()的输出结果可以看到,代码指向了变量text。因为sayHello()的局部变量text被保存在一个闭包里,所以匿名函数可以引用值为'Hello Bob'的text。
魔法在于,在JavaScript中,一个函数引用会有一个隐式的、指向该函数被创建时所在的闭包的引用,就像是方法指针加上对一个对象的隐式引用一样。
出于某些原因闭包有时看起来的确难以理解,但是当你学习一些例子的时候你就能够明白他们是怎样工作起来的了。我建议认真逐一的研究以下举例,直到你理解了他们是如何运作的。如果你在不完全理解闭包是怎样运作之前就使用他们,很快你就会创造一些非常诡异的bug~
这个举例展示了:局部变量并没有被赋值,他们是通过引用被保持的。这有点像当外部函数存在时,保持了一个堆栈结构在内存里。
function say667() { // Local variable that ends up within closure var num = 666; var say = function() { console.log(num); } num++; return say; } var sayNumber = say667(); sayNumber(); // alerts 667
因为三个全局函数在同一个调用setupSomeGlobals()的闭包中,所以他们有指向相同闭包的同一个引用。
var gLogNumber, gIncreaseNumber, gSetNumber; function setupSomeGlobals() { // Local variable that ends up within closure var num = 666; // Store some references to functions as global variables gLogNumber = function() { console.log(num); } gIncreaseNumber = function() { num++; } gSetNumber = function(x) { num = x; } } setupSomeGlobals(); gIncreaseNumber(); gLogNumber(); // 667 gSetNumber(5); gLogNumber(); // 5 var oldLog = gLogNumber; setupSomeGlobals(); gLogNumber(); // 666 oldLog() // 5
当三个函数被定义的时候,三个函数共享了对于相同闭包的访问,这个闭包是setupSomGlobals()的局部变量。
注意在以上距离中,如果你再次调用setupSomeGlobals()
,会创建一个新的闭包(堆栈空间)。旧的变量gAlertNumber
, gIncreaseNumber
, gSetNumber值会被有着新闭包的新函数覆盖。(在JavaScript中,无论什么时候在其他函数中声明了一个新函数,当外部函数每次被调用时,内部函数都会被创新创建)。
对于很多人来说在本例都会犯错,所以你一定要理解闭包才行。当你在循环里面定义函数的时候一定要非常小心,因为闭包里的局部变量的表现通常不像你想的那样。
function buildList(list) { var result = []; for (var i = 0; i < list.length; i++) { var item = 'item' + i; result.push( function() {console.log(item + ' ' + list[i])} ); } return result; } function testList() { var fnlist = buildList([1,2,3]); // Using j only to help prevent confusion -- could use i. for (var j = 0; j < fnlist.length; j++) { fnlist[j](); } }
行 result.push( function() {console.log(item + ' ' + list[i])} 三次向结果数组添加了一个指向匿名函数的引用,如果你不是很熟悉匿名函数的话,你可以认为这里匿名函数的作用类似如下代码:
pointer = function() {console.log(item + ' ' + list[i])}; result.push(pointer);
注意当你执行这个例子的时候,"item2 undefined"会被alert三次!这是因为如前例所述,这三次都是指向buildList的本地变量的同一个闭包。当行 fnlist[j](); 被执行时调用了匿名函数,这些匿名函数都使用了一个相同的闭包,他们都使用这个闭包的当前值i和item(i的值是3是因为循环已经结束了,也因此item的值是item2)。注意因为索引是从0开始的,因此item的值是item2,而i++会使i的值加到3。
本例说明了,闭包中会包含任何在闭包存在前的、该闭包的外部函数内声明的局部变量。注意变量alice实际上是在匿名函数之后声明的,即匿名函数先被声明了。而因为alice与闭包在同一个作用域(JavaScript做了变量提升),所以调用的函数可以存取到alice变量。同样的,sayAlice()()直接调用了sayAlice()返回的引用,这与之前的例子其实是一样的,只是没有临时变量而已。
function sayAlice() { var say = function() { console.log(alice); } // Local variable that ends up within closure var alice = 'Hello Alice'; return say; } sayAlice()();
注意:变量say也在闭包之中,并且可以被任何在sayAlice()中声明的函数存取,也可以被函数内部的递归存取。
最后的例子说明,每一次调用都会给局部变量创建一个隔离的闭包,每个函数声明都不是同一个闭包,每个函数的调用都有一个闭包。
function newClosure(someNum, someRef) { // Local variables that end up within closure var num = someNum; var anArray = [1,2,3]; var ref = someRef; return function(x) { num += x; anArray.push(num); console.log('num: ' + num + '\nanArray ' + anArray.toString() + '\nref.someVar ' + ref.someVar); } } obj = {someVar: 4}; fn1 = newClosure(4, obj); fn2 = newClosure(5, obj); fn1(1); // num: 5; anArray: 1,2,3,5; ref.someVar: 4; fn2(1); // num: 6; anArray: 1,2,3,6; ref.someVar: 4; obj.someVar++; fn1(2); // num: 7; anArray: 1,2,3,5,7; ref.someVar: 5; fn2(2); // num: 8; anArray: 1,2,3,6,8; ref.someVar: 5;
如果一切都似乎不是很清晰的话那么最好通过这些举例去详细理解他们。比起理解举例来说,读懂其中的说明更加困难。我对闭包和堆栈结构等的解释在技术上来说并不严格正确,但是他们能够更有效简单的帮助我们理解闭包。当完全掌握这些基础理念后再着眼于细节会更有帮助。