摘要:并且作用域鏈也確定了在當(dāng)前上下文中查找標(biāo)識符后返回的值。為了具象化分析問題,我們可以假設(shè)作用域鏈?zhǔn)且粋€(gè)數(shù)組,數(shù)組成員有一系列變量對象組成。注意,所有作用域鏈的最末端都為全局變量對象。所以作用域作用域鏈都是在當(dāng)前運(yùn)行環(huán)境內(nèi)代碼執(zhí)行前就確定了。
什么是作用域(Scope)?
作用域產(chǎn)生于程序源代碼中定義變量的區(qū)域,在程序編碼階段就確定了。javascript 中分為全局作用域(Global context: window/global )和局部作用域(Local Scope , 又稱為函數(shù)作用域 Function context)。簡單講作用域就是當(dāng)前函數(shù)的生成環(huán)境或者上下文,包含了當(dāng)前函數(shù)內(nèi)定義的變量以及對外層作用域的引用。
作用域:
作用域(Scope) | - |
---|---|
window/global Scope | 全局作用域 |
function Scope | 函數(shù)作用域 |
Block Scope | 塊作用域(ES6) |
eval Scope | eval作用域 |
作用域定義了一套規(guī)則,這套規(guī)則定義了引擎如何在當(dāng)前作用域或嵌套作用域根,據(jù)標(biāo)識符來查詢變量。反過來說N個(gè)作用域組成的作用域鏈決定了函數(shù)作用域內(nèi)標(biāo)識符查找后返回的值。
所以作用域確定了當(dāng)前上下文內(nèi)定義的變量的可見性,即子作用域可以訪問到當(dāng)前作用域內(nèi)屬性、函數(shù)。并且作用域鏈(Scope Chain)也確定了在當(dāng)前上下文中查找標(biāo)識符后返回的值。
Scope分為Lexical Scope和Dynamic Scope。Lexical Scope正如字面意思,即詞法階段定義的Scope。換種說法,作用域是根據(jù)源代碼中變量和塊的位置,在詞法分析器(lexer)處理源代碼時(shí)設(shè)置。javascript 采用的就是詞法作用域。作用域規(guī)則
作用域限制了函數(shù)內(nèi)變量、函數(shù)的可訪問性。在函數(shù)內(nèi)部申明的屬性、函數(shù)屬于該函數(shù)的私有屬性,不對函數(shù)外部代碼暴露,同時(shí)函數(shù)內(nèi)部申明的嵌套函數(shù)繼承了對當(dāng)前函數(shù)內(nèi)屬性、函數(shù)的訪問權(quán)。具體規(guī)則如下:
如果變量 a 在函數(shù)內(nèi)部定義, 則函數(shù)內(nèi)部其他變量具有訪問變量 a 的權(quán)限,但是函數(shù)外部代碼沒有訪問變量 a 的權(quán)限。所以同一作用域內(nèi)變量可以相互訪問,即 a、b、c 在同一個(gè)作用域他們就可以相互訪問。這就像雞媽媽有寶寶,雞寶寶可以相互打鬧,其他雞就不能跟他們打鬧了,為什么? 因?yàn)殡u媽媽不容許~ o(^?^)o 。
let a = 1 function foo () { let b = 1 + a let c = 2 console.log(b) // 2 } console.log(c) // error 全局作用無法訪問到 c foo()
如果變量 a 在全局作用域下定義(window/global),則全局作用域下的局部作用域內(nèi)的執(zhí)行代碼或者說是表達(dá)式都可以訪問到變量 a 的值。局部變量里的同名變量(a)會截?cái)鄬θ肿兞?a 的訪問。(這里的變量 a 就相當(dāng)于是飼養(yǎng)員,候飼養(yǎng)員會在合適的時(shí)候給雞兒們投食。但是農(nóng)場主為了節(jié)約成本,規(guī)定飼養(yǎng)員要就近給雞投食,當(dāng)飼養(yǎng)員1離雞寶寶更近時(shí)其他飼養(yǎng)員就不能千里迢迢跨過鴨綠江去喂雞了。)
let a = 1 let b = 2 function foo () { let b = 3 function too () { console.log(a) // 1 console.log(b) // 3 } too() } foo()
再次強(qiáng)調(diào) javascript 作用域會嚴(yán)格限制變量的可訪問范圍: 即根據(jù)源代碼中代碼和塊的位置,嵌套作用域擁有對被嵌套作用域(外層作用域)的訪問權(quán)限。(這一條規(guī)則說明整個(gè)農(nóng)場是有規(guī)則的,不能反向的投食。)
作用域鏈(Scope Chain)作用域鏈,是由當(dāng)前環(huán)境與上層環(huán)境的一系列作用域共同組成,它保證了當(dāng)前執(zhí)行環(huán)境對符合訪問權(quán)限的變量和函數(shù)的有序訪問。
上面解釋的稍微有些晦澀,對于我這樣大腦不好使的就需要在大腦里重復(fù)的"讀"幾次才能明白。那么作用域鏈?zhǔn)歉陕锏模?簡單的說作用域鏈就是管理函數(shù)申明是形成的作用域嵌套(依賴)關(guān)系,并在函數(shù)運(yùn)行階段解析函數(shù)訪問標(biāo)識符的值。
再簡單點(diǎn)解釋作用域鏈?zhǔn)歉陕锏模鹤饔糜蜴溇褪怯脕聿檎易兞康模饔糜蜴準(zhǔn)怯梢幌盗凶饔糜虼?lián)起來的。
作用域鏈的訪問在函數(shù)執(zhí)行過程中,每遇到一個(gè)變量,都會經(jīng)歷一次標(biāo)識符解析過程,以決定從哪里獲取和存儲數(shù)據(jù)。該過程從作用域鏈頭部,也就是當(dāng)前執(zhí)行函數(shù)的作用域開始(下圖中從左向右),查找同名的標(biāo)識符,如果找到了就返回這個(gè)標(biāo)識符對應(yīng)的值,如果沒找到繼續(xù)搜索作用域鏈中的下一個(gè)作用域,如果搜索完所有作用域都未找到,則認(rèn)為該標(biāo)識符未定義。函數(shù)執(zhí)行過程中,每個(gè)標(biāo)識符值得解析都要經(jīng)歷這樣的搜索過程。
為了具象化分析問題,我們可以假設(shè)作用域鏈?zhǔn)且粋€(gè)數(shù)組(Scope Array),數(shù)組成員有一系列變量對象組成。我們可以在數(shù)組這個(gè)單向通道中,也就是上圖模擬從左向右查詢變量對象中的標(biāo)識符,這樣就可以訪問到上一層作用域中的變量了。直到最頂層(全局作用域),并且一旦找到,即停止查找。所以內(nèi)層的變量可以屏蔽外層的同名變量。想象一下如果變量不是按從內(nèi)向外的查找,那整個(gè)語言設(shè)計(jì)會變得N復(fù)雜了(我們需要設(shè)計(jì)一套復(fù)雜的雞寶寶找食物的規(guī)則)
還是上面的栗子:
let a = 1 let b = 2 function foo () { let b = 3 function too () { console.log(a) // 1 console.log(b) // 3 } too() } foo()
作用域嵌套結(jié)構(gòu)是這樣的:
栗子中,當(dāng) javascript 引擎執(zhí)行到函數(shù) too 時(shí), 全局、函數(shù) foo、函數(shù) too 的上下文分別會被創(chuàng)建。上下文內(nèi)包含它們各自的變量對象和作用域鏈(注意: 作用域鏈包含可訪問到的上層作用域的變量對象,在上下文創(chuàng)建階段根據(jù)作用域規(guī)則被收集起來形成一個(gè)可訪問鏈),我們設(shè)定他們的變量對象分別為VO(global),VO(foo), VO(too)。而 too 的作用域鏈,則同時(shí)包含了這三個(gè)變量對象,所以 too 的執(zhí)行上下文可如下表示:
too = { VO: {...}, // 變量對象 scopeChain: [VO(too), VO(foo), VO(global)], // 作用域鏈 }
我們直接用scopeChain來表示作用域鏈數(shù)組,數(shù)組的第一項(xiàng)scopeChain[0]為作用域鏈的最前端(當(dāng)前函數(shù)的變量對象),而數(shù)組的最后一項(xiàng),為作用域鏈的最末端(全局變量對象 window )。注意,所有作用域鏈的最末端都為全局變量對象。
再舉個(gè)栗子:
let a = 1 function foo() { console.log(a) } function too() { let a = 2 foo() } too() // 1
這個(gè)栗子如果對作用域的特點(diǎn)理解不透徹很容易以為輸出是2。但其實(shí)最終輸出的是 1。 foo() 在執(zhí)行的時(shí)候先在當(dāng)前作用域內(nèi)查找變量 a 。然后根據(jù)函數(shù)定義時(shí)的作用域關(guān)系會在當(dāng)前作用域的上層作用域里查找變量標(biāo)識符 a,所以最后查到的是全局作用域的 a 而不是 foo函數(shù)里面的 a 。
變量對象、執(zhí)行上下文會在后面介紹。閉包
在JavaScript中,函數(shù)和函數(shù)聲明時(shí)的詞法作用域形成閉包。或者更通俗的理解為閉包就是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù),這里把閉包理解為函數(shù)內(nèi)部定義的函數(shù)。
我們來看個(gè)閉包的例子
let a = 1 function foo() { let a = 2 function too() { console.log(a) } return too } foo()() // 2
這是一個(gè)閉包的栗子,一個(gè)函數(shù)執(zhí)行后返回另一個(gè)可執(zhí)行函數(shù),被返回的函數(shù)保留有對它定義時(shí)外層函數(shù)作用域的訪問權(quán)。foo()() 調(diào)用時(shí)依次執(zhí)行了 foo、too 函數(shù)。too 雖然是在全局作用域里執(zhí)行的,但是too定義在 foo 作用域里面,根據(jù)作用域鏈規(guī)則取最近的嵌套作用域的屬性 a = 2。
再拿農(nóng)場的故事做比如。農(nóng)場主發(fā)現(xiàn)還有一種方法會更節(jié)約成本,就是讓每個(gè)雞媽媽作為家庭成員的‘飼養(yǎng)員’, 從而改變了之前的‘飼養(yǎng)結(jié)構(gòu)’。
從作用域鏈的結(jié)構(gòu)可以發(fā)現(xiàn),javascript引擎在查找變量標(biāo)識符時(shí)是依據(jù)作用域鏈依次向上查找的。當(dāng)標(biāo)識符所在的作用域位于作用域鏈的更深的位置,讀寫的時(shí)候相對就慢一些。所以在編寫代碼的時(shí)候應(yīng)盡量少使用全局代碼,盡可能的將全局的變量緩存在局部作用域中。
不加強(qiáng)記憶很容記錯(cuò)作用域與執(zhí)行上下文的區(qū)別。代碼的執(zhí)行過程分為編譯階段和解釋執(zhí)行階段。始終應(yīng)該記住javascript作用域在源代碼的編碼階段就確定了,而作用域鏈?zhǔn)窃诰幾g階段被收集到執(zhí)行上下文的變量對象里的。所以作用域、作用域鏈都是在當(dāng)前運(yùn)行環(huán)境內(nèi)代碼執(zhí)行前就確定了。這里暫且不過多的展開執(zhí)行上下文的概念,可以關(guān)注后續(xù)文章。
閉包的一些優(yōu)缺點(diǎn)
閉包的用處:
用于保存私有屬性:將不需要對外暴露的屬性、函數(shù)保存在閉包函數(shù)的父函數(shù)里,避免外部操作對值的干擾
避免局部屬性污染全局變量空間導(dǎo)致的命名空間混亂
模塊化封裝,將對立的功能模塊通過閉包進(jìn)去封裝,只暴露較少的 API 供外部應(yīng)用使用
閉包的缺點(diǎn):
內(nèi)存消耗:由于閉包會使得函數(shù)中的變量都被保存在內(nèi)存中,內(nèi)存消耗很大,所以不能濫用閉包,否則會造成網(wǎng)頁的性能問題。
導(dǎo)致內(nèi)存泄露:由于IE的 js 對象和 DOM 對象使用不同的垃圾收集方法,因此閉包在IE中會導(dǎo)致內(nèi)存泄露問題,也就是無法銷毀駐留在內(nèi)存中的元素。解決方法是,在退出函數(shù)之前,將不使用的局部變量全部刪除)。
編譯階段和解釋執(zhí)行階段會在變量對象一節(jié)詳細(xì)介紹。
關(guān)于閉包會的一些其他知識點(diǎn)在后面的章節(jié)里也會有提及,盡請關(guān)注。
思考最后,再來看一個(gè)面試題:
for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 1000); } // 5 5 5 5 5
要求對上面的代碼進(jìn)行修改,使其輸出"0 1 2 3 4"
這里也涉及到作用域鏈的概念,當(dāng)然跟 javascript 的執(zhí)行機(jī)制也有關(guān)。修改方式有很多種,下面給出一種:
for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }(i), 1000); } // 0 1 2 3 4
詳細(xì)原理分析會在javascript 執(zhí)行機(jī)制一節(jié)詳細(xì)介紹。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/95192.html
摘要:棧底為全局上下文,棧頂為當(dāng)前正在執(zhí)行的上下文。位于棧頂?shù)纳舷挛膱?zhí)行完畢后會自動出棧,依次向下直至所有上下文運(yùn)行完畢,最后瀏覽器關(guān)閉時(shí)全局上下文被銷毀。 講清楚之執(zhí)行上下文 標(biāo)簽 : javascript 什么是執(zhí)行上下文? 當(dāng) JavaScript 代碼執(zhí)行一段可執(zhí)行代碼時(shí),會創(chuàng)建對應(yīng)的上下文(execution context)并將該上下文壓入上下文棧(context stack...
摘要:中的繼承并不是明確規(guī)定的,而是通過模仿實(shí)現(xiàn)的。繼承中的繼承又稱模擬類繼承。將函數(shù)抽離到全局對象中,函數(shù)內(nèi)部直接通過作用域鏈查找函數(shù)。這種范式編程是基于作用域鏈,與前面講的繼承是基于原型鏈的本質(zhì)區(qū)別是屬性查找方式的不同。 這一節(jié)梳理對象的繼承。 我們主要使用繼承來實(shí)現(xiàn)代碼的抽象和代碼的復(fù)用,在應(yīng)用層實(shí)現(xiàn)功能的封裝。 javascript 的對象繼承方式真的是百花齊放,屬性繼承、原型繼承、...
摘要:中函數(shù)是一等公民。小明小明調(diào)用函數(shù)時(shí),傳遞給函數(shù)的值被稱為函數(shù)的實(shí)參值傳遞,對應(yīng)位置的函數(shù)參數(shù)名叫作形參。所以不推薦使用構(gòu)造函數(shù)創(chuàng)建函數(shù)因?yàn)樗枰暮瘮?shù)體作為字符串可能會阻止一些引擎優(yōu)化也會引起瀏覽器資源回收等問題。 函數(shù) 之前幾節(jié)中圍繞著函數(shù)梳理了 this、原型鏈、作用域鏈、閉包等內(nèi)容,這一節(jié)梳理一下函數(shù)本身的一些特點(diǎn)。 javascript 中函數(shù)是一等公民。 并且函數(shù)也是對象,...
閱讀 2060·2021-11-22 13:52
閱讀 984·2021-11-17 09:33
閱讀 2716·2021-09-01 10:49
閱讀 2851·2019-08-30 15:53
閱讀 2663·2019-08-29 16:10
閱讀 2437·2019-08-29 11:31
閱讀 1356·2019-08-26 11:40
閱讀 1872·2019-08-26 10:59