番外:透過JS程式碼看本質 - 04 從執行上下文物件入手看宣告提前
從執行上下文物件入手看宣告提前
資料
- js 中的活動物件 與 變數物件 什麼區別? 答主:貘吃饃香 的回答
- 湯姆大叔的部落格
涉及到的關鍵詞
- 執行上下文(執行環境)
- 活動物件、變數物件
- arguments、宣告提前
- 作用域鏈
一、執行上下文(Execution Contexts)
(1) 為什麼要有執行上下文
- 每個函式都有對應的執行環境,它定義了變數或者函式有權訪問的資料,決定他們各自的行為
- 執行上下文(執行環境)是存在的物件,是規範中的存在和引擎上實現,JS無法訪問,無法操作,只不過大家都這麼叫,上一篇講的是函式環境,是記憶體上的實現,要注意區分
- 每個函式有自己的函式環境,函式環境都有對應的執行上下文(執行環境),函式環境是棧資料結構,那麼執行上下文也應該是棧資料結構的,因為當恢復函式環境時,自然也要恢復執行上下文(執行環境)
(2) 什麼時候開始建立執行上下文
每次執行函式前,會對函式進行預編譯,這個時間段就在建立執行上下文。
執行上下文包含:
- 變數物件(Variable Objecct),儲存宣告的變數和函式的具體位置
- this value,確定this的值
- 作用域鏈,確定能夠訪問的變數和函式
執行上下文也分:
- 全域性執行上下文
- 函式執行上下文
(3) 建立執行上下文的過程
宣告變數 var0 函式 A() { 宣告變數 var1 函式 B() 宣告變數 var2 } 函式 B() { 宣告變數 var3 函式 C() 宣告變數 var4 } 函式 C() { 宣告變數 var5 } //執行函式 A 函式A()
- 程式碼一開始,先是全域性執行上下文物件
globalExecutionContent = {
VO: {
arguments: undefined //預設是 undefined
},
Scope: [globalContent.VO],
this: window
}
- 呼叫函式A,建立A函式的執行上下文物件
AExecutionContent = {
VO: {
arguments: undefined //預設是 undefined
},
Scope: [AExecutionContent.VO, globalExecutionContent.VO],
this: window
}
- 其它函式的執行上下文物件也是如此,注意作用域鏈的最左端是當前執行上下文物件的變數物件,其後依次的是上層的執行上下文物件的變數物件
(4) 變數物件和活動物件
前面說過每個執行上下文都有一個對應的變數物件,但是注意執行上下文也是棧新式的結構,當前函式呼叫另外一個函式時,肯定也要儲存當前的執行上下文,並建立新的上下文及其變數物件
此時,最上層的執行上下文和它的變數物件都是被 “啟用的”,因為要執行當前環境程式碼必須訪問執行上下文和它的變數物件
這樣看其它的下層的執行上下文和它的變數物件都是 “休息狀態”,在當前環境不需要訪問它們
二、變數物件怎麼建立的
這一小節涉及宣告提前
進入函式並建立執行上下文物件時,對函式進行一掃描
-
掃描函式的所有形參,並將形參名稱 和對應值組成的鍵值對作為變數物件VO的屬性。如果沒有傳遞對應的實參,將undefined作為對應值。如果形參名為arguments,將覆蓋arguments(這裡和上面執行環境對應)
-
掃描函式程式碼中所有的函式宣告(注意是函式宣告,函式表示式不算)
將函式名和對應值(指向記憶體中該函式的引用指標)組成組成的鍵值對作為變數物件VO的屬性
如果變數物件VO已經存在同名的屬性,則覆蓋這個屬性 -
掃描函式程式碼中所有的變數宣告
由變數名和對應值(此時為undefined) 組成,作為變數物件的屬性
如果變數名與已經宣告的形參或函式相同,此時什麼都不會發生,變數宣告不會干擾已經存在的這個同名屬性。
三、作用域和作用域鏈
(1) 作用域
- 變數(或者說標識)的作用域是程式原始碼中定義這個作用域的區域
- 作用域是描述這個變數起作用的程式碼範圍
(2) 作用域鏈
JS是靜態作用域,這意味這一旦程式碼確定,作用域確定,並且不會改變
this不是作用域的一部分,可以從上一小節的執行上下文的作用域鏈可以看到 this和變數物件和作用域鏈是同級的
JS函式內部還能定義函式,內部函式還能訪問上層函式的變數和函式(注意這個上層,是定義函式時的程式碼的上層環境或者說上層函式)
作用域鏈就像一個數組一樣,它的最前端表示當前函式的作用域,往後就是上層函式的作用域,一直到全域性環境,當需要訪問變數或者函式時,查詢方式就是按照這樣的路徑查詢