變量的訪問規(guī)則:
如果變量 a 在函數內部定義, 則函數內部其他變量具有訪問變量 a 的權限,但是函數外部代碼沒有訪問變量 a 的權限。所以同一作用域內變量可以相互訪問,即 a、b、c 在同一個作用域他們就可以相互訪問。這就像雞媽媽有寶寶,雞寶寶可以相互打鬧,其他雞就不能跟他們打鬧了,為什么? 因為雞媽媽不容許~ 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),則全局作用域下的局部作用域內的執(zhí)行代碼或者說是表達式都可以訪問到變量 a 的值。局部變量里的同名變量(a)會截斷對全局變量 a 的訪問。(這里的變量 a 就相當于是飼養(yǎng)員,候飼養(yǎng)員會在合適的時候給雞兒們投食。但是農場主為了節(jié)約成本,規(guī)定飼養(yǎng)員要就近給雞投食,當飼養(yǎng)員1離雞寶寶更近時其他飼養(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()
再次強調 javascript 作用域會嚴格限制變量的可訪問范圍: 即根據源代碼中代碼和塊的位置,被嵌套作用域擁有對嵌套作用域的訪問權限。(這一條規(guī)則說明整個農場是有規(guī)則的,不能反向的投食。)
作用域鏈,是由當前環(huán)境與上層環(huán)境的一系列作用域共同組成,它保證了當前執(zhí)行環(huán)境對符合訪問權限的變量和函數的有序訪問。
上面解釋的稍微有些晦澀,對于我這樣大腦不好使的就需要在大腦里重復的'讀'幾次才能明白。那么作用域鏈是干嘛的? 簡單的說作用域鏈就是解析標識符的,負責返回表達式執(zhí)行時所依賴變量的值。再簡單點回答:作用域鏈就是用來查找變量的,作用域鏈是由一系列作用域串聯(lián)起來的。
在函數執(zhí)行過程中,每遇到一個變量,都會經歷一次標識符解析過程以決定從哪里獲取和存儲數據。該過程從作用域鏈頭部,也就是當前執(zhí)行函數的作用域開始(下圖中從左向右),查找同名的標識符,如果找到了就返回這個標識符對應的值,如果沒找到繼續(xù)搜索作用域鏈中的下一個作用域,如果搜索完所有作用域都未找到,則認為該標識符未定義。函數執(zhí)行過程中,每個標識符值得解析都要經歷這樣的搜索過程。
為了具象化分析問題,我們可以假設作用域鏈是一個數組(Scope Array),數組成員有一系列變量對象組成。我們可以在數組這個單向通道中,也就是上圖模擬從左向右查詢變量對象中的標識符,這樣就可以訪問到上一層作用域中的變量了。直到最頂層(全局作用域),并且一旦找到,即停止查找。所以內層的變量可以屏蔽外層的同名變量。想想一下如果變量不是按從內向外的查找,那整個語言設計會變得N復雜了(我們需要設計一套復雜的雞寶寶找食物的規(guī)則)
還是上面的栗子:
let a = 1 let b = 2 function foo () { let b = 3 function too () { console.log(a) // 1 console.log(b) // 3 } too() } foo()
作用域嵌套結構是這樣的:
栗子中,當 javascript 引擎執(zhí)行到函數 too 時, 全局、函數 foo、函數 too 的上下文分別會被創(chuàng)建。上下文內包含它們各自的變量對象和作用域鏈(注意: 作用域鏈包含可訪問到的上層作用域的變量對象,在上下文創(chuàng)建階段根據作用域規(guī)則被收集起來形成一個可訪問鏈),我們設定他們的變量對象分別為VO(global),VO(foo), VO(too)。而 too 的作用域鏈,則同時包含了這三個變量對象,所以 too 的執(zhí)行上下文可如下表示:
too = { VO: {...}, // 變量對象 scopeChain: [VO(too), VO(foo), VO(global)], // 作用域鏈 }
我們可以直接用scopeChain
來表示作用域鏈數組,數組的第一項scopeChain[0]為作用域鏈的最前端(當前函數的變量對象),而數組的最后一項,為作用域鏈的最末端(全局變量對象 window )。所有作用域鏈的最末端都為全局變量對象。
再舉個栗子:
let a = 1 function foo() { console.log(a) } function too() { let a = 2 foo() } too() // 1
這個栗子如果對作用域的特點理解不透徹很容易以為輸出是2。但其實最終輸出的是 1。 foo() 在執(zhí)行的時候先在當前作用域內查找變量 a 。然后根據函數定義時的作用域關系會在當前作用域的上層作用域里查找變量標識符 a,所以最后查到的是全局作用域的 a 而不是 foo函數里面的 a 。
變量對象、執(zhí)行上下文會在后面介紹。在 JavaScript 中,函數和函數聲明時的詞法作用域形成閉包。我們來看個閉包的例子
let a = 1 function foo() { let a = 2 function too() { console.log(a) } return too } foo()() // 2
這是一個閉包的栗子,一個函數執(zhí)行后返回另一個可執(zhí)行函數,被返回的函數保留有對它定義時外層函數作用域的訪問權。foo()()
調用時依次執(zhí)行了 foo、too 函數。too 雖然是在全局作用域里執(zhí)行的,但是too定義在 foo 作用域里面,根據作用域鏈規(guī)則取最近的嵌套作用域的屬性 a = 2。
再拿農場的故事做比如。農場主發(fā)現(xiàn)還有一種方法會更節(jié)約成本,就是讓每個雞媽媽作為家庭成員的‘飼養(yǎng)員’, 從而改變了之前的‘飼養(yǎng)結構’。
關于閉包會在后面的章節(jié)里也會有介紹。
從作用域鏈的結構可以發(fā)現(xiàn),javascript
引擎在查找變量標識符時是依據作用域鏈依次向上查找的。當標識符所在的作用域位于作用域鏈的更深的位置,讀寫的時候相對就慢一些。所以在編寫代碼的時候應盡量少使用全局代碼,盡可能的將全局的變量緩存在局部作用域中。
不加強記憶很容記錯作用域與后面將要介紹的執(zhí)行上下文的區(qū)別。代碼的執(zhí)行過程分為編譯階段和解釋執(zhí)行階段。始終應該記住javascript
作用域在源代碼的編碼階段就確定了,而作用域鏈是在編譯階段被收集到執(zhí)行上下文的變量對象里的。所以作用域、作用域鏈都是在當前運行環(huán)境內代碼執(zhí)行前就確定了。這里暫且不過多的展開執(zhí)行上下文的概念,可以關注后續(xù)文章。
相信看了本文案例你已經掌握了方法,更多精彩請關注Gxl網其它相關文章!
推薦閱讀:
PromiseA+的實現(xiàn)步驟詳解
EasyCanvas繪圖庫在Pixeler項目開發(fā)中使用實戰(zhàn)總結
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯(lián)系,我們將在第一時間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com