摘要:并且作用域鏈也確定了在當前上下文中查找標識符后返回的值。為了具象化分析問題,我們可以假設作用域鏈是一個數組,數組成員有一系列變量對象組成。注意,所有作用域鏈的最末端都為全局變量對象。所以作用域作用域鏈都是在當前運行環境內代碼執行前就確定了。
什么是作用域(Scope)?
作用域產生于程序源代碼中定義變量的區域,在程序編碼階段就確定了。javascript 中分為全局作用域(Global context: window/global )和局部作用域(Local Scope , 又稱為函數作用域 Function context)。簡單講作用域就是當前函數的生成環境或者上下文,包含了當前函數內定義的變量以及對外層作用域的引用。
作用域:
作用域(Scope) | - |
---|---|
window/global Scope | 全局作用域 |
function Scope | 函數作用域 |
Block Scope | 塊作用域(ES6) |
eval Scope | eval作用域 |
作用域定義了一套規則,這套規則定義了引擎如何在當前作用域或嵌套作用域根,據標識符來查詢變量。反過來說N個作用域組成的作用域鏈決定了函數作用域內標識符查找后返回的值。
所以作用域確定了當前上下文內定義的變量的可見性,即子作用域可以訪問到當前作用域內屬性、函數。并且作用域鏈(Scope Chain)也確定了在當前上下文中查找標識符后返回的值。
Scope分為Lexical Scope和Dynamic Scope。Lexical Scope正如字面意思,即詞法階段定義的Scope。換種說法,作用域是根據源代碼中變量和塊的位置,在詞法分析器(lexer)處理源代碼時設置。javascript 采用的就是詞法作用域。作用域規則
作用域限制了函數內變量、函數的可訪問性。在函數內部申明的屬性、函數屬于該函數的私有屬性,不對函數外部代碼暴露,同時函數內部申明的嵌套函數繼承了對當前函數內屬性、函數的訪問權。具體規則如下:
如果變量 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),則全局作用域下的局部作用域內的執行代碼或者說是表達式都可以訪問到變量 a 的值。局部變量里的同名變量(a)會截斷對全局變量 a 的訪問。(這里的變量 a 就相當于是飼養員,候飼養員會在合適的時候給雞兒們投食。但是農場主為了節約成本,規定飼養員要就近給雞投食,當飼養員1離雞寶寶更近時其他飼養員就不能千里迢迢跨過鴨綠江去喂雞了。)
let a = 1 let b = 2 function foo () { let b = 3 function too () { console.log(a) // 1 console.log(b) // 3 } too() } foo()
再次強調 javascript 作用域會嚴格限制變量的可訪問范圍: 即根據源代碼中代碼和塊的位置,嵌套作用域擁有對被嵌套作用域(外層作用域)的訪問權限。(這一條規則說明整個農場是有規則的,不能反向的投食。)
作用域鏈(Scope Chain)作用域鏈,是由當前環境與上層環境的一系列作用域共同組成,它保證了當前執行環境對符合訪問權限的變量和函數的有序訪問。
上面解釋的稍微有些晦澀,對于我這樣大腦不好使的就需要在大腦里重復的"讀"幾次才能明白。那么作用域鏈是干嘛的? 簡單的說作用域鏈就是管理函數申明是形成的作用域嵌套(依賴)關系,并在函數運行階段解析函數訪問標識符的值。
再簡單點解釋作用域鏈是干嘛的:作用域鏈就是用來查找變量的,作用域鏈是由一系列作用域串聯起來的。
作用域鏈的訪問在函數執行過程中,每遇到一個變量,都會經歷一次標識符解析過程,以決定從哪里獲取和存儲數據。該過程從作用域鏈頭部,也就是當前執行函數的作用域開始(下圖中從左向右),查找同名的標識符,如果找到了就返回這個標識符對應的值,如果沒找到繼續搜索作用域鏈中的下一個作用域,如果搜索完所有作用域都未找到,則認為該標識符未定義。函數執行過程中,每個標識符值得解析都要經歷這樣的搜索過程。
為了具象化分析問題,我們可以假設作用域鏈是一個數組(Scope Array),數組成員有一系列變量對象組成。我們可以在數組這個單向通道中,也就是上圖模擬從左向右查詢變量對象中的標識符,這樣就可以訪問到上一層作用域中的變量了。直到最頂層(全局作用域),并且一旦找到,即停止查找。所以內層的變量可以屏蔽外層的同名變量。想象一下如果變量不是按從內向外的查找,那整個語言設計會變得N復雜了(我們需要設計一套復雜的雞寶寶找食物的規則)
還是上面的栗子:
let a = 1 let b = 2 function foo () { let b = 3 function too () { console.log(a) // 1 console.log(b) // 3 } too() } foo()
作用域嵌套結構是這樣的:
栗子中,當 javascript 引擎執行到函數 too 時, 全局、函數 foo、函數 too 的上下文分別會被創建。上下文內包含它們各自的變量對象和作用域鏈(注意: 作用域鏈包含可訪問到的上層作用域的變量對象,在上下文創建階段根據作用域規則被收集起來形成一個可訪問鏈),我們設定他們的變量對象分別為VO(global),VO(foo), VO(too)。而 too 的作用域鏈,則同時包含了這三個變量對象,所以 too 的執行上下文可如下表示:
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() 在執行的時候先在當前作用域內查找變量 a 。然后根據函數定義時的作用域關系會在當前作用域的上層作用域里查找變量標識符 a,所以最后查到的是全局作用域的 a 而不是 foo函數里面的 a 。
變量對象、執行上下文會在后面介紹。閉包
在JavaScript中,函數和函數聲明時的詞法作用域形成閉包?;蛘吒ㄋ椎睦斫鉃殚]包就是能夠讀取其他函數內部變量的函數,這里把閉包理解為函數內部定義的函數。
我們來看個閉包的例子
let a = 1 function foo() { let a = 2 function too() { console.log(a) } return too } foo()() // 2
這是一個閉包的栗子,一個函數執行后返回另一個可執行函數,被返回的函數保留有對它定義時外層函數作用域的訪問權。foo()() 調用時依次執行了 foo、too 函數。too 雖然是在全局作用域里執行的,但是too定義在 foo 作用域里面,根據作用域鏈規則取最近的嵌套作用域的屬性 a = 2。
再拿農場的故事做比如。農場主發現還有一種方法會更節約成本,就是讓每個雞媽媽作為家庭成員的‘飼養員’, 從而改變了之前的‘飼養結構’。
從作用域鏈的結構可以發現,javascript引擎在查找變量標識符時是依據作用域鏈依次向上查找的。當標識符所在的作用域位于作用域鏈的更深的位置,讀寫的時候相對就慢一些。所以在編寫代碼的時候應盡量少使用全局代碼,盡可能的將全局的變量緩存在局部作用域中。
不加強記憶很容記錯作用域與執行上下文的區別。代碼的執行過程分為編譯階段和解釋執行階段。始終應該記住javascript作用域在源代碼的編碼階段就確定了,而作用域鏈是在編譯階段被收集到執行上下文的變量對象里的。所以作用域、作用域鏈都是在當前運行環境內代碼執行前就確定了。這里暫且不過多的展開執行上下文的概念,可以關注后續文章。
閉包的一些優缺點
閉包的用處:
用于保存私有屬性:將不需要對外暴露的屬性、函數保存在閉包函數的父函數里,避免外部操作對值的干擾
避免局部屬性污染全局變量空間導致的命名空間混亂
模塊化封裝,將對立的功能模塊通過閉包進去封裝,只暴露較少的 API 供外部應用使用
閉包的缺點:
內存消耗:由于閉包會使得函數中的變量都被保存在內存中,內存消耗很大,所以不能濫用閉包,否則會造成網頁的性能問題。
導致內存泄露:由于IE的 js 對象和 DOM 對象使用不同的垃圾收集方法,因此閉包在IE中會導致內存泄露問題,也就是無法銷毀駐留在內存中的元素。解決方法是,在退出函數之前,將不使用的局部變量全部刪除)。
編譯階段和解釋執行階段會在變量對象一節詳細介紹。
關于閉包會的一些其他知識點在后面的章節里也會有提及,盡請關注。
思考最后,再來看一個面試題:
for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 1000); } // 5 5 5 5 5
要求對上面的代碼進行修改,使其輸出"0 1 2 3 4"
這里也涉及到作用域鏈的概念,當然跟 javascript 的執行機制也有關。修改方式有很多種,下面給出一種:
for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }(i), 1000); } // 0 1 2 3 4
詳細原理分析會在javascript 執行機制一節詳細介紹。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/95192.html
摘要:中的繼承并不是明確規定的,而是通過模仿實現的。繼承中的繼承又稱模擬類繼承。將函數抽離到全局對象中,函數內部直接通過作用域鏈查找函數。這種范式編程是基于作用域鏈,與前面講的繼承是基于原型鏈的本質區別是屬性查找方式的不同。 這一節梳理對象的繼承。 我們主要使用繼承來實現代碼的抽象和代碼的復用,在應用層實現功能的封裝。 javascript 的對象繼承方式真的是百花齊放,屬性繼承、原型繼承、...
摘要:中函數是一等公民。小明小明調用函數時,傳遞給函數的值被稱為函數的實參值傳遞,對應位置的函數參數名叫作形參。所以不推薦使用構造函數創建函數因為它需要的函數體作為字符串可能會阻止一些引擎優化也會引起瀏覽器資源回收等問題。 函數 之前幾節中圍繞著函數梳理了 this、原型鏈、作用域鏈、閉包等內容,這一節梳理一下函數本身的一些特點。 javascript 中函數是一等公民。 并且函數也是對象,...
閱讀 2056·2021-11-22 13:52
閱讀 980·2021-11-17 09:33
閱讀 2710·2021-09-01 10:49
閱讀 2846·2019-08-30 15:53
閱讀 2662·2019-08-29 16:10
閱讀 2436·2019-08-29 11:31
閱讀 1354·2019-08-26 11:40
閱讀 1867·2019-08-26 10:59