摘要:之前一篇文章我們?cè)敿?xì)說明了變量對(duì)象,而這里,我們將詳細(xì)說明作用域鏈。而的作用域鏈,則同時(shí)包含了這三個(gè)變量對(duì)象,所以的執(zhí)行上下文可如下表示。下圖展示了閉包的作用域鏈。其中為當(dāng)前的函數(shù)調(diào)用棧,為當(dāng)前正在被執(zhí)行的函數(shù)的作用域鏈,為當(dāng)前的局部變量。
初學(xué)JavaScript的時(shí)候,我在學(xué)習(xí)閉包上,走了很多彎路。而這次重新回過頭來對(duì)基礎(chǔ)知識(shí)進(jìn)行梳理,要講清楚閉包,也是一個(gè)非常大的挑戰(zhàn)。
閉包有多重要?如果你是初入前端的朋友,我沒有辦法直觀的告訴你閉包在實(shí)際開發(fā)中的無處不在,但是我可以告訴你,前端面試,必問閉包。面試官們常常用對(duì)閉包的了解程度來判定面試者的基礎(chǔ)水平,保守估計(jì),10個(gè)前端面試者,至少5個(gè)都死在閉包上。
可是為什么,閉包如此重要,還是有那么多人沒有搞清楚呢?是因?yàn)榇蠹也辉敢鈱W(xué)習(xí)嗎?還真不是,而是我們通過搜索找到的大部分講解閉包的中文文章,都沒有清晰明了的把閉包講解清楚。要么淺嘗輒止,要么高深莫測(cè),要么干脆就直接亂說一通。包括我自己曾經(jīng)也寫過一篇關(guān)于閉包的總結(jié),回頭一看,不忍直視[捂臉]。
因此本文的目的就在于,能夠清晰明了得把閉包說清楚,讓讀者朋友們看了之后,就把閉包給徹底學(xué)會(huì)了,而不是似懂非懂。
在詳細(xì)講解作用域鏈之前,我默認(rèn)你已經(jīng)大概明白了JavaScript中的下面這些重要概念。這些概念將會(huì)非常有幫助。
基礎(chǔ)數(shù)據(jù)類型與引用數(shù)據(jù)類型
內(nèi)存空間
垃圾回收機(jī)制
執(zhí)行上下文
變量對(duì)象與活動(dòng)對(duì)象
如果你暫時(shí)還沒有明白,可以去看本系列的前三篇文章,本文文末有目錄鏈接。為了講解閉包,已經(jīng)為大家做好了基礎(chǔ)知識(shí)的鋪墊哦。
作用域
在JavaScript中,我們可以將作用域定義為一套規(guī)則,這套規(guī)則用來管理引擎如何在當(dāng)前作用域以及嵌套的子作用域中根據(jù)標(biāo)識(shí)符名稱進(jìn)行變量查找。
這里的標(biāo)識(shí)符,指的是變量名或者函數(shù)名
JavaScript中只有全局作用域與函數(shù)作用域(因?yàn)閑val我們平時(shí)開發(fā)中幾乎不會(huì)用到它,這里不討論)。
作用域與執(zhí)行上下文是完全不同的兩個(gè)概念。我知道很多人會(huì)混淆他們,但是一定要仔細(xì)區(qū)分。
JavaScript代碼的整個(gè)執(zhí)行過程,分為兩個(gè)階段,代碼編譯階段與代碼執(zhí)行階段。編譯階段由編譯器完成,將代碼翻譯成可執(zhí)行代碼,這個(gè)階段作用域規(guī)則會(huì)確定。執(zhí)行階段由引擎完成,主要任務(wù)是執(zhí)行可執(zhí)行代碼,執(zhí)行上下文在這個(gè)階段創(chuàng)建。
作用域鏈
回顧一下上一篇文章我們分析的執(zhí)行上下文的生命周期,如下圖。
我們知道函數(shù)在調(diào)用激活時(shí),會(huì)開始創(chuàng)建對(duì)應(yīng)的執(zhí)行上下文,在執(zhí)行上下文生成的過程中,變量對(duì)象,作用域鏈,以及this的值會(huì)分別被確定。之前一篇文章我們?cè)敿?xì)說明了變量對(duì)象,而這里,我們將詳細(xì)說明作用域鏈。
作用域鏈,是由當(dāng)前環(huán)境與上層環(huán)境的一系列變量對(duì)象組成,它保證了當(dāng)前執(zhí)行環(huán)境對(duì)符合訪問權(quán)限的變量和函數(shù)的有序訪問。
為了幫助大家理解作用域鏈,我我們先結(jié)合一個(gè)例子,以及相應(yīng)的圖示來說明。
var a = 20; function test() { var b = a + 10; function innerTest() { var c = 10; return b + c; } return innerTest(); } test();
在上面的例子中,全局,函數(shù)test,函數(shù)innerTest的執(zhí)行上下文先后創(chuàng)建。我們?cè)O(shè)定他們的變量對(duì)象分別為VO(global),VO(test), VO(innerTest)。而innerTest的作用域鏈,則同時(shí)包含了這三個(gè)變量對(duì)象,所以innerTest的執(zhí)行上下文可如下表示。
innerTestEC = { VO: {...}, // 變量對(duì)象 scopeChain: [VO(innerTest), VO(test), VO(global)], // 作用域鏈 }
我們可以直接用一個(gè)數(shù)組來表示作用域鏈,數(shù)組的第一項(xiàng)scopeChain[0]為作用域鏈的最前端,而數(shù)組的最后一項(xiàng),為作用域鏈的最末端,所有的最末端都為全局變量對(duì)象。
很多人會(huì)誤解為當(dāng)前作用域與上層作用域?yàn)榘P(guān)系,但其實(shí)并不是。以最前端為起點(diǎn),最末端為終點(diǎn)的單方向通道我認(rèn)為是更加貼切的形容。如圖。
注意,因?yàn)樽兞繉?duì)象在執(zhí)行上下文進(jìn)入執(zhí)行階段時(shí),就變成了活動(dòng)對(duì)象,這一點(diǎn)在上一篇文章中已經(jīng)講過,因此圖中使用了AO來表示。Active Object
是的,作用域鏈?zhǔn)怯梢幌盗凶兞繉?duì)象組成,我們可以在這個(gè)單向通道中,查詢變量對(duì)象中的標(biāo)識(shí)符,這樣就可以訪問到上一層作用域中的變量了。
對(duì)于那些有一點(diǎn) JavaScript 使用經(jīng)驗(yàn)但從未真正理解閉包概念的人來說,理解閉包可以看作是某種意義上的重生,突破閉包的瓶頸可以使你功力大增。
閉包是一種特殊的對(duì)象。
它由兩部分組成。執(zhí)行上下文(代號(hào)A),以及在該執(zhí)行上下文中創(chuàng)建的函數(shù)(代號(hào)B)。
當(dāng)B執(zhí)行時(shí),如果訪問了A中變量對(duì)象中的值,那么閉包就會(huì)產(chǎn)生。
在大多數(shù)理解中,包括許多著名的書籍,文章里都以函數(shù)B的名字代指這里生成的閉包。而在chrome中,則以執(zhí)行上下文A的函數(shù)名代指閉包。
因此我們只需要知道,一個(gè)閉包對(duì)象,由A、B共同組成,在以后的篇幅中,我將以chrome的標(biāo)準(zhǔn)來稱呼。
// demo01 function foo() { var a = 20; var b = 30; function bar() { return a + b; } return bar; } var bar = foo(); bar();
上面的例子,首先有執(zhí)行上下文foo,在foo中定義了函數(shù)bar,而通過對(duì)外返回bar的方式讓bar得以執(zhí)行。當(dāng)bar執(zhí)行時(shí),訪問了foo內(nèi)部的變量a,b。因此這個(gè)時(shí)候閉包產(chǎn)生。
在基礎(chǔ)進(jìn)階(一)中,我總結(jié)了JavaScript的垃圾回收機(jī)制。JavaScript擁有自動(dòng)的垃圾回收機(jī)制,關(guān)于垃圾回收機(jī)制,有一個(gè)重要的行為,那就是,當(dāng)一個(gè)值,在內(nèi)存中失去引用時(shí),垃圾回收機(jī)制會(huì)根據(jù)特殊的算法找到它,并將其回收,釋放內(nèi)存。
而我們知道,函數(shù)的執(zhí)行上下文,在執(zhí)行完畢之后,生命周期結(jié)束,那么該函數(shù)的執(zhí)行上下文就會(huì)失去引用。其占用的內(nèi)存空間很快就會(huì)被垃圾回收器釋放。可是閉包的存在,會(huì)阻止這一過程。
先來一個(gè)簡(jiǎn)單的例子。
var fn = null; function foo() { var a = 2; function innnerFoo() { console.log(a); } fn = innnerFoo; // 將 innnerFoo的引用,賦值給全局變量中的fn } function bar() { fn(); // 此處的保留的innerFoo的引用 } foo(); bar(); // 2
在上面的例子中,foo()執(zhí)行完畢之后,按照常理,其執(zhí)行環(huán)境生命周期會(huì)結(jié)束,所占內(nèi)存被垃圾收集器釋放。但是通過fn = innerFoo,函數(shù)innerFoo的引用被保留了下來,復(fù)制給了全局變量fn。這個(gè)行為,導(dǎo)致了foo的變量對(duì)象,也被保留了下來。于是,函數(shù)fn在函數(shù)bar內(nèi)部執(zhí)行時(shí),依然可以訪問這個(gè)被保留下來的變量對(duì)象。所以此刻仍然能夠訪問到變量a的值。
這樣,我們就可以稱foo為閉包。
下圖展示了閉包foo的作用域鏈。
我們可以在chrome瀏覽器的開發(fā)者工具中查看這段代碼運(yùn)行時(shí)產(chǎn)生的函數(shù)調(diào)用棧與作用域鏈的生成情況。如下圖。
關(guān)于如何在chrome中觀察閉包,以及更多閉包的例子,請(qǐng)閱讀基礎(chǔ)系列(六)
在上面的圖中,紅色箭頭所指的正是閉包。其中Call Stack為當(dāng)前的函數(shù)調(diào)用棧,Scope為當(dāng)前正在被執(zhí)行的函數(shù)的作用域鏈,Local為當(dāng)前的局部變量。
所以,通過閉包,我們可以在其他的執(zhí)行上下文中,訪問到函數(shù)的內(nèi)部變量。比如在上面的例子中,我們?cè)诤瘮?shù)bar的執(zhí)行環(huán)境中訪問到了函數(shù)foo的a變量。個(gè)人認(rèn)為,從應(yīng)用層面,這是閉包最重要的特性。利用這個(gè)特性,我們可以實(shí)現(xiàn)很多有意思的東西。
不過讀者朋友們需要注意的是,雖然例子中的閉包被保存在了全局變量中,但是閉包的作用域鏈并不會(huì)發(fā)生任何改變。在閉包中,能訪問到的變量,仍然是作用域鏈上能夠查詢到的變量。
對(duì)上面的例子稍作修改,如果我們?cè)诤瘮?shù)bar中聲明一個(gè)變量c,并在閉包fn中試圖訪問該變量,運(yùn)行結(jié)果會(huì)拋出錯(cuò)誤。
var fn = null; function foo() { var a = 2; function innnerFoo() { console.log(c); // 在這里,試圖訪問函數(shù)bar中的c變量,會(huì)拋出錯(cuò)誤 console.log(a); } fn = innnerFoo; // 將 innnerFoo的引用,賦值給全局變量中的fn } function bar() { var c = 100; fn(); // 此處的保留的innerFoo的引用 } foo(); bar();
關(guān)于這一點(diǎn),很多同學(xué)把函數(shù)調(diào)用棧與作用域鏈沒有分清楚,所以有的大神看了我關(guān)于介紹執(zhí)行上下文的文章時(shí)就義正言辭的說我的例子有問題,而這些評(píng)論有很大的誤導(dǎo)作用,為了幫助大家自己擁有能夠辨別的能力,所以我寫了基礎(chǔ)(六),教大家如何在chrome中觀察閉包,作用域鏈,this等。當(dāng)然我也不敢100%保證我文中的例子就一定正確,所以教大家如何去辨認(rèn)我認(rèn)為才是最重要的。
閉包的應(yīng)用場(chǎng)景
除了面試,在實(shí)踐中,閉包有兩個(gè)非常重要的應(yīng)用場(chǎng)景。分別是模塊化與柯里化。
柯里化
在函數(shù)式編程中,利用閉包能夠?qū)崿F(xiàn)很多炫酷的功能,柯里化便是其中很重要的一種。點(diǎn)擊了解更多關(guān)于柯里化的知識(shí)
模塊
在我看來,模塊是閉包最強(qiáng)大的一個(gè)應(yīng)用場(chǎng)景。如果你是初學(xué)者,對(duì)于模塊的了解可以暫時(shí)不用放在心上,因?yàn)槔斫饽K需要更多的基礎(chǔ)知識(shí)。但是如果你已經(jīng)有了很多JavaScript的使用經(jīng)驗(yàn),在徹底了解了閉包之后,不妨借助本文介紹的作用域鏈與閉包的思路,重新理一理關(guān)于模塊的知識(shí)。這對(duì)于我們理解各種各樣的設(shè)計(jì)模式具有莫大的幫助。
(function () { var a = 10; var b = 20; function add(num1, num2) { var num1 = !!num1 ? num1 : a; var num2 = !!num2 ? num2 : b; return num1 + num2; } window.add = add; })(); add(10, 20);
在上面的例子中,我使用函數(shù)自執(zhí)行的方式,創(chuàng)建了一個(gè)模塊。add是模塊對(duì)外暴露的一個(gè)公共方法。而變量a,b被作為私有變量。在面向?qū)ο蟮拈_發(fā)中,我們常常需要考慮是將變量作為私有變量,還是放在構(gòu)造函數(shù)中的this中,因此理解閉包,以及原型鏈?zhǔn)且粋€(gè)非常重要的事情。模塊十分重要,因此我會(huì)在以后的文章專門介紹,這里就暫時(shí)不多說啦。
為了驗(yàn)證自己有沒有搞懂作用域鏈與閉包,這里留下一個(gè)經(jīng)典的思考題,常常也會(huì)在面試中被問到。
利用閉包,修改下面的代碼,讓循環(huán)輸出的結(jié)果依次為1, 2, 3, 4, 5
for (var i=1; i<=5; i++) { setTimeout( function timer() { console.log(i); }, i*1000 ); }
點(diǎn)此查看關(guān)于此題的詳細(xì)解讀
關(guān)于作用域鏈的與閉包我就總結(jié)完了,雖然我自認(rèn)為我是說得非常清晰了,但是我知道理解閉包并不是一件簡(jiǎn)單的事情,所以如果你有什么問題,可以在評(píng)論中問我。你也可以帶著從別的地方?jīng)]有看懂的例子在評(píng)論中留言。大家一起學(xué)習(xí)進(jìn)步。
前端基礎(chǔ)進(jìn)階系列目錄
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/90521.html
摘要:不過其實(shí)簡(jiǎn)書文章評(píng)論里有很多大家的問題以及解答,對(duì)于進(jìn)一步理解文中知識(shí)幫助很大的,算是有點(diǎn)可惜吧。不過也希望能夠?qū)φ趯W(xué)習(xí)前端的你有一些小幫助。如果在閱讀中發(fā)現(xiàn)了一些錯(cuò)誤,請(qǐng)?jiān)谠u(píng)論里告訴我,我會(huì)及時(shí)更改。 前端基礎(chǔ)進(jìn)階(一):內(nèi)存空間詳細(xì)圖解 前端基礎(chǔ)進(jìn)階(二):執(zhí)行上下文詳細(xì)圖解 前端基礎(chǔ)進(jìn)階(三):變量對(duì)象詳解 前端基礎(chǔ)進(jìn)階(四):詳細(xì)圖解作用域鏈與閉包 前端基礎(chǔ)進(jìn)階(五):全方位...
摘要:文章分享持續(xù)更新更多資源請(qǐng)文章轉(zhuǎn)自一前端文章基礎(chǔ)篇,,前端基礎(chǔ)進(jìn)階一內(nèi)存空間詳細(xì)圖解前端基礎(chǔ)進(jìn)階二執(zhí)行上下文詳細(xì)圖解前端基礎(chǔ)進(jìn)階三變量對(duì)象詳解前端基礎(chǔ)進(jìn)階四詳細(xì)圖解作用域鏈與閉包前端基礎(chǔ)進(jìn)階五全方位解讀前端基礎(chǔ)進(jìn)階六在開發(fā)者工具中觀察函數(shù)調(diào) 文章分享(持續(xù)更新) 更多資源請(qǐng)Star:https://github.com/maidishike... 文章轉(zhuǎn)自:https://gith...
摘要:在的開發(fā)者工具中,通過斷點(diǎn)調(diào)試,我們能夠非常方便的一步一步的觀察的執(zhí)行過程,直觀感知函數(shù)調(diào)用棧,作用域鏈,變量對(duì)象,閉包,等關(guān)鍵信息的變化。其中表示當(dāng)前的局部變量對(duì)象,表示當(dāng)前作用域鏈中的閉包。 showImg(https://segmentfault.com/img/remote/1460000008404321); 在前端開發(fā)中,有一個(gè)非常重要的技能,叫做斷點(diǎn)調(diào)試。 在chrome...
摘要:本期推薦文章從作用域鏈談閉包,由于微信不能訪問外鏈,點(diǎn)擊閱讀原文就可以啦。推薦理由這是一篇譯文,深入淺出圖解作用域鏈,一步步深入介紹閉包。作用域鏈的頂端是全局對(duì)象,在全局環(huán)境中定義的變量就會(huì)綁定到全局對(duì)象中。 (關(guān)注福利,關(guān)注本公眾號(hào)回復(fù)[資料]領(lǐng)取優(yōu)質(zhì)前端視頻,包括Vue、React、Node源碼和實(shí)戰(zhàn)、面試指導(dǎo)) 本周開始前端進(jìn)階的第二期,本周的主題是作用域閉包,今天是第6天。 本...
摘要:使用上一篇文章的例子來說明下自由變量進(jìn)階期深入淺出圖解作用域鏈和閉包訪問外部的今天是今天是其中既不是參數(shù),也不是局部變量,所以是自由變量。 (關(guān)注福利,關(guān)注本公眾號(hào)回復(fù)[資料]領(lǐng)取優(yōu)質(zhì)前端視頻,包括Vue、React、Node源碼和實(shí)戰(zhàn)、面試指導(dǎo)) 本周正式開始前端進(jìn)階的第二期,本周的主題是作用域閉包,今天是第7天。 本計(jì)劃一共28期,每期重點(diǎn)攻克一個(gè)面試重難點(diǎn),如果你還不了解本進(jìn)階計(jì)...
閱讀 2101·2023-04-26 00:09
閱讀 3129·2021-09-26 10:12
閱讀 3497·2019-08-30 15:44
閱讀 2869·2019-08-30 13:47
閱讀 927·2019-08-23 17:56
閱讀 3234·2019-08-23 15:31
閱讀 480·2019-08-23 13:47
閱讀 2516·2019-08-23 11:56