JavaScript闭包模型
----- [原创翻译]2016-09-01 09:32:22
《 一》 闭包并不神秘
本文利用JavaScript代码来阐述闭包,目的是为了使普通开发者能很好的理解闭包,并不面向专家或函数式编程开发者。
一旦领悟了闭包的核心思想,它就不再难于理解;然而,只看一些理论上的文档或是以理论为中心的文档只会让你南辕北辙。
本文适用于那些在主流编程语言中有一定编程经历的开发者们,且能看懂如下的JavaScript函数:
1 function sayHello(name) { 2 var text = 'Hello ' + name; 3 var say = function() { console.log(text); } 4 say(); 5 }
《二》 闭包实例
两句话总结:
- 闭包是支持 头等函数(first-class functions)的一种函数;它是一个能在其作用域类引用变量(变量先被声明),赋值给另外一个变量,作为一个参数传递给其他函数,或是作为一个函数的结果返回。又或者--
- 闭包是函数开始执行时分配出来的一个栈帧,且当函数返回后不会被释放(类似于‘栈帧’是在堆上被分配而不是在栈上被分配!)。
下面的代码返回给另一个函数一个引用。
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"
运行结果:(--PS:译者添加--)
大部分的开发者都够能理解上面的代码中一个函数的引用是如何返回给一个变量(say2)。如果你这还不能看明白的话,那么你需要去学习基础的知识而不应该是闭包了。一个C 语言开发者会理解为一个函数返回了一个指针给另外一个函数,变量say 与say2分别是两个函数的指针。
而C语言中指针指向一个函数与JavaScript里一个函数的引用有一个很关键的区别。那就是,在JavaScript中,你可以理解为一个函数引用变量是一个指针指向一个函数,同时又是一个隐式的指针指向闭包。
上面的代码中存在一个闭包,因为匿名函数 function() {console.log(text);}在其他函数里面有被定义了,实例中在sayHeelo2()函数中定义的。在JavaScript中,如果你在另一个函数中使用function关键字,那么你就正在创建一个闭包。
在C与其他类似的编程言语里,当一个函数返回后,所有的局部变量将不会再可利用,因为栈帧已经被销毁。
在JavaScript中,如果你在一个函数中声明另一个函数,那么局部变量在从你调用的函数返回后依然可以使用。正像上面演示的那样,因为我们从sayHello2()返回之后再调用的say2()注意到我们调用的代码里面有text这个变量,而这个变量是sayHello2()函数里的一个局部变量。
仔细观察say2.toString()的输出,我们不难发现这段代码引用了text变量。匿名函数能够引用包含值为'Hello Bob'的text,原因就在于sayHello2()的局部变量被保存在闭包中。
神秘就在于JavaScript里的函数引用也是对函数中创建的闭包的一个隐式引用---类似于委托是加在某一个对象上面的隐式引用的一个方法指针。
《三》 更多实例
出于某些原因,当你只看相关文档时,发现闭包好像确实难于理解,但是当你结合一些实例就能够了解到它们到底是如何工作的(可能会花费我们一些时间)。建议在实践中认真学习直到你理解它们是如何工作的。如果你在没有完全理解闭包是如何工作的就使用她们,估计你可能搞出很多非常糟糕的bug。
实例1:
本例演示了局部变量不被复制—它们被保存在引用中。这就好像当存在着一个函数的时候在内存中存一个相应的栈帧。
实例1运行结果:(--PS:译者添加--)
实例2:
三个全局函数都是对同一个闭包的引用,因为它们都被定义成单独调用setupSomeGlobals()的函数。
1 var gLogNumber, gIncreaseNumber, gSetNumber; 2 function setupSomeGlobals() { 3 // Local variable that ends up within closure 4 var num = 42; 5 // Store some references to functions as global variables 6 gLogNumber = function() { console.log(num); } 7 gIncreaseNumber = function() { num++; } 8 gSetNumber = function(x) { num = x; } 9 } 10 11 setupSomeGlobals(); 12 gIncreaseNumber(); 13 gLogNumber(); // 43 14 gSetNumber(5); 15 gLogNumber(); // 5 16 17 var oldLog = gLogNumber; 18 19 setupSomeGlobals(); 20 gLogNumber(); // 42 21 22 oldLog() // 5
实例2运行结果:(--PS:译者添加--)
当三个函数被定义的时候,它们共享同一个闭包的入口——setupSomeGlobals()函数的局部变量。
在上面的例子中,如果再次调用setuoSomeGlobals()函数,那么一个全新的闭包(栈帧!)将创建。原来的三个变量:gLoNumber, gIncreaseNumberm fSetNumber 将被产生新的闭包的函数重写;(在JavaScript中,无论何时你在一个函数里面声明了另一个函数,当外部的函数被调用的时候里面的函数都会被再创建一次。)
实例3:
下面的实例对开发者来说并不陌生,因此需要很好地理解一下。当在一个循环中定义一个函数的时候要特别地小心:闭包里的局部变量可能并不会像你一开始想象那样执行。
1 function buildList(list) { 2 var result = []; 3 for (var i = 0; i < list.length; i++) { 4 var item = 'item' + i; 5 result.push( function() {console.log(item + ' ' + list[i])} ); 6 } 7 return result; 8 } 9 10 function testList() { 11 var fnlist = buildList([1,2,3]); 12 // Using j only to help prevent confusion -- could use i. 13 for (var j = 0; j < fnlist.length; j++) { 14 fnlist[j](); 15 } 16 } 17 18 testList() //logs "item2 undefined" 3 times
实例3运行结果:(--PS:译者添加--)
这一行代码:result.push(function() {console.log(item + ' ' + list[i])} ;result数组对匿名函数加引用了三次。如果不是很理解匿名函数可以思考下面的例子:
1 pointer = function() {console.log(item + ' ' + list[i])}; 2 result.push(pointer);
注意到当运行这段代码的时候,结果输出三次"item2 undefined"!这是因为就如前面的例子所示,bulidList中的局部变量只存在一个闭包。当fnlist[j]调用匿名函数的时候;他们共用一个闭包,且在闭包中使用i与item的当前值(这时循环结束 i 的值为3,item的值为‘item2’)。下标从0开始的,所以是item2,而i++将使得i的值为3。
实例4:
本实例演示了闭包包含任何在其终止之前函数内声明的局部变量,变量alice 在匿名函数之后被声明。匿名函数首先被声明,当它被调用的时候,他可以传递alice变量,因为alice变量是在其相同的作用域内(JavaScript允许变量挂起),sayAlice()()直接调用函数引用并从sayAlice()返回—其正好如之前做的一样,只是没有临时变量。
1 function sayAlice() { 2 var say = function() { console.log(alice); } 3 // Local variable that ends up within closure 4 var alice = 'Hello Alice'; 5 return say; 6 } 7 sayAlice()();// logs "Hello Alice"
实例4运行结果:(--PS:译者添加--)
巧妙性:注意到变量say也在闭包之中,其可以被任何函数在其sayAlice()函数中存取,或者可以在函数内被递归地存取。
实例4:
最后一个实例表明:局部变量每一次调用都会创建一个独立的闭包,在每一个函数声明中都没有单一的闭包存在。下面是每一次调用函数的闭包。
1 function newClosure(someNum, someRef) { 2 // Local variables that end up within closure 3 var num = someNum; 4 var anArray = [1,2,3]; 5 var ref = someRef; 6 return function(x) { 7 num += x; 8 anArray.push(num); 9 console.log('num: ' + num + 10 '\nanArray ' + anArray.toString() + 11 '\nref.someVar ' + ref.someVar); 12 } 13 } 14 obj = {someVar: 4}; 15 fn1 = newClosure(4, obj); 16 fn2 = newClosure(5, obj); 17 fn1(1); // num: 5; anArray: 1,2,3,5; ref.someVar: 4; 18 fn2(1); // num: 6; anArray: 1,2,3,6; ref.someVar: 4; 19 obj.someVar++; 20 fn1(2); // num: 7; anArray: 1,2,3,5,7; ref.someVar: 5; 21 fn2(2); // num: 8; anArray: 1,2,3,6,8; ref.someVar: 5;
实例4运行结果:(--PS:译者添加--)
总结:
如果看到这里还是觉得不够清晰,那最好的方式就是把每一个例子实现一遍,阅读一段文档相比于理解实例困难太多了。关于闭包和栈帧的说明,不能保证绝对技术无误——都有简化以助于理解。
归纳如下几点:
- 只要在一个函数里面使用其他函数,那么闭包就被使用了。
- 只要在函数中使用了eval()函数,那么闭包就被使用了,eval能引用该函数的局部变量,且在eval中甚至还可以使用eval('var foo = ...' )创建新的局部变量。
- 只要在一个函数中使用了 new Function(...)(函数构造子),那么就不会创建闭包。(new出来的函数不能引用外部函数的局部变量)
- JavaScript中的闭包相当于保存所有局部变量的一个拷贝,当一个函数退出,就变成它自己本身。
- 可能这样想是最好的,认为闭包总是在一个函数的入口被创建,它的局部变量被加到闭包中。
- 每一次含有闭包的函数被调用,那么局部变量的集合就会被保存一次。(给定函数里面包含函数的声明,且对里面的函数存在一个引用被返回或是外部的引用也以某种形式保存)。
- 两个函数就好像他们有相同的源文本,却因‘隐形’的闭包表现得完全不一样。我认为JavaScript代码不能绝对地找出是否一个函数引用存在一个闭包。
- 如果你试图做出一些动态源码的改变(例如:myFunction = Function(myFunction.toString().replace(/Hello/.'Hola'));),如果myFunction是闭包的话,那么它就不会有效果。
- 有可能在函数里面声明的函数中获取函数的声明。且能够在获得超过一层的闭包。
- JavaScript中的闭包跟其他函数式编程语言有明显差异。
链接:
感谢:
如果你学习过闭包(在这或是其他任何地方),无论是任何形式的改变的提议的反馈,只要能使本文变得更加清晰明了,我都非常感兴趣。
(翻译自stackoverflow社区;原文信息:作者:Morris 发布时间:2006-02-21)