javascript
JS闭包—你不知道的JavaScript上卷读书笔记(二)
關(guān)于閉包,初學(xué)者會(huì)被繞的暈頭轉(zhuǎn)向,在學(xué)習(xí)的路上也付出了很多精力來(lái)理解。 讓我們一起來(lái)揭開(kāi)閉包神秘的面紗。
閉包晦澀的定義
看過(guò)很多關(guān)于閉包的定義,很多講的云里霧里,晦澀難懂。讓不少人以為閉包是多么玄乎的東西。在我看過(guò)的所有書(shū)籍中,我更喜歡《你不知道的javascript(上卷)》的定義:
當(dāng)函數(shù)可以記住并訪(fǎng)問(wèn)所在的詞法作用域時(shí),就產(chǎn)生了閉包,或者說(shuō)函數(shù)在創(chuàng)建時(shí)的詞法作域之外執(zhí)行。
通俗的來(lái)說(shuō)(不嚴(yán)謹(jǐn)): 就是函數(shù)套函數(shù),子函數(shù)可以有權(quán)訪(fǎng)問(wèn)父函數(shù)的變量、父函數(shù)的父函數(shù)的變量、一直到全局變量。子函數(shù)如果不被銷(xiāo)毀,整條作用域鏈上的變量仍然保存在內(nèi)存中。
關(guān)于詞法作用域,這里不做過(guò)多的解釋,詳情參考:http://www.cnblogs.com/ylweb/p/7531259.html.
下面用一些代碼來(lái)解釋這個(gè)定義:
function foo() {var a = 2;function bar() {console.log( a ); // 2}bar(); }foo();嚴(yán)格來(lái)說(shuō)這段代碼并沒(méi)有形成閉包,因?yàn)閎ar是在創(chuàng)建時(shí)所在的詞法作用域執(zhí)行。bar() 對(duì)a 的引用的方法是詞法作用域的查找規(guī)則,而這些規(guī)則只是閉包的一部分。從學(xué)術(shù)的角度說(shuō):,在上面的代碼片段中,函數(shù)bar() 具有一個(gè)涵蓋foo() 作用域的閉包
(事實(shí)上,涵蓋了它能訪(fǎng)問(wèn)的所有作用域,比如全局作用域)。也可以認(rèn)為bar() 被封閉在
了foo() 的作用域中。為什么呢?原因簡(jiǎn)單明了,因?yàn)閎ar() 嵌套在foo() 內(nèi)部。
下面我們來(lái)看一段代碼,清晰地展示了閉包:
function foo() {var a = 2;function bar() {console.log( a );}return bar; }var baz = foo(); baz(); // 2 —— 朋友,這就是閉包的效果。函數(shù)bar() 的詞法作用域能夠訪(fǎng)問(wèn)foo() 的內(nèi)部作用域。然后我們將bar() 函數(shù)本身當(dāng)作一個(gè)值類(lèi)型進(jìn)行傳遞。在這個(gè)例子中,我們將bar 所引用的函數(shù)對(duì)象本身當(dāng)作返回值。
在foo() 執(zhí)行后,其返回值(也就是內(nèi)部的bar() 函數(shù))賦值給變量baz 并調(diào)用baz(),實(shí)際上只是通過(guò)不同的標(biāo)識(shí)符引用調(diào)用了內(nèi)部的函數(shù)bar()。
bar() 顯然可以被正常執(zhí)行。但是在這個(gè)例子中,它在自己定義的詞法作用域以外的地方執(zhí)行。
在foo() 執(zhí)行后,通常會(huì)期待foo() 的整個(gè)內(nèi)部作用域都被銷(xiāo)毀,因?yàn)槲覀冎酪嬗欣厥掌饔脕?lái)釋放不再使用的內(nèi)存空間。由于看上去foo() 的內(nèi)容不會(huì)再被使用,所以很自然地會(huì)考慮對(duì)其進(jìn)行回收。
而閉包的“神奇”之處正是可以阻止這件事情的發(fā)生。事實(shí)上內(nèi)部作用域依然存在,因此沒(méi)有被回收。誰(shuí)在使用這個(gè)內(nèi)部作用域?原來(lái)是bar() 本身在使用。
拜bar() 所聲明的位置所賜,它擁有涵蓋foo() 內(nèi)部作用域的閉包,使得該作用域能夠一直存活,以供bar() 在之后任何時(shí)間進(jìn)行引用。
bar() 依然持有對(duì)該作用域的引用,而這個(gè)引用就叫作閉包。
循環(huán)與閉包
以下是一個(gè)最常見(jiàn)的for循環(huán)例子:
for (var i=1; i<=5; i++) {setTimeout( function timer() {console.log( i );}, i*1000 ); }正常情況下,我們對(duì)這段代碼行為的預(yù)期是分別輸出數(shù)字1~5,但實(shí)際上,這段代碼在運(yùn)行時(shí)輸出五次6。 Why?
首先解釋6 是從哪里來(lái)的。這個(gè)循環(huán)的終止條件是i 不再<=5。條件首次成立時(shí)i 的值是6。因此,輸出顯示的是循環(huán)結(jié)束時(shí)i 的最終值。
仔細(xì)想一下,這好像又是顯而易見(jiàn)的,延遲函數(shù)的回調(diào)會(huì)在循環(huán)結(jié)束時(shí)才執(zhí)行。事實(shí)上,當(dāng)定時(shí)器運(yùn)行時(shí)即使每個(gè)迭代中執(zhí)行的是setTimeout(.., 0),所有的回調(diào)函數(shù)依然是在循環(huán)結(jié)束后才會(huì)被執(zhí)行,因此會(huì)每次輸出一個(gè)6 出來(lái)。
問(wèn)題實(shí)質(zhì):
我們?cè)噲D假設(shè)循環(huán)中的每個(gè)迭代在運(yùn)行時(shí)都會(huì)給自己“捕獲”一個(gè)i 的副本。但是根據(jù)作用域的工作原理,實(shí)際情況是盡管循環(huán)中的五個(gè)函數(shù)是在各個(gè)迭代中分別定義的,但是它們都被封閉在一個(gè)共享的全局作用域中,因此實(shí)際上只有一個(gè)i。
改進(jìn)方案:
for (var i=1; i<=5; i++) {(function(j) {setTimeout( function timer() {console.log( j );}, j*1000 );})( i ); }ES6改進(jìn):
for (let i=1; i<=5; i++) {setTimeout( function timer() {console.log( i );}, i*1000 ); }轉(zhuǎn)載于:https://www.cnblogs.com/ylweb/p/7804309.html
總結(jié)
以上是生活随笔為你收集整理的JS闭包—你不知道的JavaScript上卷读书笔记(二)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 复习……方法的重载
- 下一篇: javascript总for of和fo