文章目錄
詞法範圍:變數的範圍是在定義時決定而不是執行時決定,也就是說詞法範圍取決於源碼,通過靜態分析就能確定,因此詞法範圍也叫做靜態範圍。 with和eval除外,所以只能說JS的範圍機制非常接近詞法範圍(Lexical scope)。
下面通過幾個小小的案例,開始深入的瞭解對理解詞法範圍和閉包必不可少的,JS執行時底層的一些概念和理論知識。
經典案列重現1、經典案例一
1 /*全域(window)域下的一段代碼*/2 function a(i) {3 var i;4 alert(i);5 };6 a(10);
疑問:上面的代碼會輸出什麼呢?
答案:沒錯,就是彈出10。具體執行過程應該是這樣的
- a 函數有一個形參 i,調用 a 函數時傳入實參 10,形參 i=10
- 接著定義一個同名的局部變數 i,未賦值
- alert 輸出 10
- 思考:局部變數 i 和形參 i 是同一個儲存空間嗎?
- 按照定義來理解:局部變數 i 和形參 i 是同一個儲存空間(引用同一個記憶體位址)。ECMAScript中,函數執行時,傳入函數的實際參數會在函數內部用一個數組來表示,可以通過arguments對象來訪問這個參數數組。命名的形參僅是提供便利,但不是必需的。javascript權威指南裡說道:Arguments對象有一個非同尋常的特性。當函數具有了命名了的參數時,Arguments對象的數組元素是存放函數參數的局部變數的同義字。arguments[]數組和命名了的參數是引用同一變數的兩種不同方法。用參數名改變一個參數的值時同時會改變通過arguments[]數組獲得的值,反之亦然。所以可以把函數的參數想象成一早就聲明了的局部變數並已賦值(如果傳入參數的話),而且此變數不管寫入的值是基本類型還是參考型別,都會改變arguments[]數組對應的值,所以上面案列第3行代碼定義了一個同名的局部變數i且未賦值是會被忽略的,因為ECMAScript規定在同一範圍裡,如果重複聲明一個變數並賦予初始值,那麼它擔當的不過是一個指派陳述式的角色;如果重複聲明一個變數但沒有初始值,那麼它不會對原來存在的變數有任何的影響。如:
從很明顯看出,在語句var i;未執行時,i的值已經是10了。另一種理解是:對【var】變數做“預解析“,也就是說在函數執行之前,【var】變數就已經聲明了但未賦值,當執行到var語句時僅僅是賦值而已。所以在函式宣告局部變數時,一般都寫在函數體的開頭,以免影響理解,如經典案例四。
2、經典案例二
1 /*全域(window)域下的一段代碼*/2 function a(i) {3 alert(i);4 alert(arguments[0]); //arguments[0]應該就是形參 i5 var i = 2;6 alert(i);7 alert(arguments[0]);8 };9 a(10);
疑問:上面的代碼又會輸出什麼呢?(10,10,2,2 )
答案:在FireBug中的運行結果是第二個10,10,2,2,猜對了… ,下面簡單說一下具體執行過程
- a 函數有一個形參i,調用 a 函數時傳入實參 10,形參 i=10
- 第一個 alert 把形參 i 的值 10 輸出
- 第二個 alert 把 arguments[0] 輸出,應該也是 i
- 接著定義個局部變數 i 並賦值為2,這時候局部變數 i=2
- 第三個 alert 就把局部變數 i 的值 2 輸出
- 第四個alert再次把 arguments[0] 輸出
- 思考:這裡能說明局部變數 i 和形參 i 的值相同嗎?
3、經典案例三
1 /*全域(window)域下的一段代碼*/2 function a(i) {3 var i = i;4 alert(i);5 };6 a(10);
疑問:上面的代碼又又會輸出什麼呢?(10 )
答案:在FireBug中的運行結果是 10,下面簡單說一下具體執行過程
- 第一句聲明一個與形參 i 同名的局部變數 i,根據結果我們知道,後一個 i 是指向了
- 形參 i,所以這裡就等於把形參 i 的值 10 賦了局部變數 i
- 第二個 alert 當然就輸出 10
- 思考:結合案列二,這裡基本能說明局部變數 i 和形參 i 指向了同一個儲存地址!
4、經典案例四
1 /*全域(window)域下的一段代碼*/2 var i=10;3 function a() {4 alert(i);5 var i = 2;6 alert(i);7 };8 a();
疑問:上面的代碼又會輸出什麼呢?
答案:在FireBug中的運行結果是 undefined, 2,下面簡單說一下具體執行過程
- 第一個alert輸出undefined
- 第二個alert輸出 2
- 思考:到底怎麼回事兒?
看到上面的幾個例子,你可能會弄錯。原因是:我們能很快的寫出一個方法,但到底方法內部是怎麼執行的呢?執行的細節又是怎麼樣的呢?你可能沒有進行過深入的學習和瞭解。要瞭解這些細節,那就需要瞭解 JS 引擎的工作方式,所以下面我們就把 JS 引擎對一個方法的解析過程進行一個稍微深入一些的介紹
解析過程
1、執行順序
- 編譯型語言,編譯步驟分為:詞法分析、文法分析、語義檢查、代碼最佳化和位元組產生。
- 解釋型語言,通過詞法分析和文法分析得到文法分析樹後,就可以開始解釋執行了。這裡是一個簡單原始的關於解析過程的原理,僅作為參考,詳細的解析過程(各種JS引擎還有不同)還需要更深一步的研究
JavaScript執行過程,如果一個文檔流中包含多個script程式碼片段(用script標籤分隔的js代碼或引入的js檔案),它們的運行順序是:
步驟1. 讀入第一個程式碼片段(js執行引擎並非一行一行地執行程式,而是一段一段地分析執行的)
步驟2. 做詞法分析和文法分析,有錯則報語法錯誤(比如括弧不匹配等),並跳轉到步驟5
步驟3. 對【var】變數和【function】定義做“預解析“(永遠不會報錯的,因為只解析正確的聲明)
步驟4. 執行程式碼片段,有錯則報錯(比如變數未定義)
步驟5. 如果還有下一個程式碼片段,則讀入下一個程式碼片段,重複步驟2
步驟6. 結束
2、特殊說明
全域域(window)域下所有JS代碼可以被看成是一個“匿名方法“,它會被自動執行,而此“匿名方法“內的其它方法則是在被顯示調用的時候才被執行
3、關鍵步驟
上面的過程,我們主要是分成兩個階段
- 解析:就是通過文法分析和預解析構造合法的文法分析樹。
- 執行:執行具體的某個function,JS引擎在執行每個函數執行個體時,都會建立一個執行環境(ExecutionContext)和使用中的物件(activeObject)(它們屬於宿主對象,與函數執行個體的生命週期保持一致)
3、關鍵概念
到這裡,我們再更強調以下一些概念,這些概念都會在下面用一個一個的實體來表示,便於大家理解
- 文法分析樹(SyntaxTree)可以直觀地表示出這段代碼的相關資訊,具體的實現就是JS引擎建立了一些表,用來記錄每個方法內的變數集(variables),方法集(functions)和範圍(scope)等
- 執行環境(ExecutionContext)可理解為一個記錄當前執行的方法【外部描述資訊】的對象,記錄所執行方法的類型,名稱,參數和使用中的物件(activeObject)
- 使用中的物件(activeObject)可理解為一個記錄當前執行的方法【內部執行資訊】的對象,記錄內部變數集(variables)、內嵌函數集(functions)、實參(arguments)、範圍鏈(scopeChain)等執行所需資訊,其中內部變數集(variables)、內嵌函數集(functions)是直接從第一步建立的文法分析樹複製過來的
- 詞法範圍:變數的範圍是在定義時決定而不是執行時決定,也就是說詞法範圍取決於源碼,通過靜態分析就能確定,因此詞法範圍也叫做靜態範圍。 with和eval除外,所以只能說JS的範圍機制非常接近詞法範圍(Lexical scope)
- 範圍鏈:詞法範圍的實現機制就是範圍鏈(scopeChain)。範圍鏈是一套按名稱尋找(Name Lookup)的機制,首先在當前執行環境的 ActiveObject 中尋找,沒找到,則順著範圍鏈到父 ActiveObject 中尋找,一直找到全域調用對象(Global Object)
4、實體表示
5、函數的運行過程
- 建立執行環境(execution context)的階段,函數將初始化各種變數,並將它們記錄在一個內部的變數對象(variable object)中。記錄在該變數對象中的變數依次有下面三種:(a)函數的實際參數;(b)內部的函式宣告;(c)內部變數集。此時前面兩種變數有了具體的值,內部變數集的值未undefined。
- 建立實參(arguments)對象,同名的實參,形參和變數之間是【引用】關係
- 執行方法內的指派陳述式,這才會對變數集中的變數進行賦值處理
- 變數尋找規則是首先在當前執行環境的 ActiveObject 中尋找,沒找到,則順著執行環境中屬性 ScopeChain 指向的 ActiveObject 中尋找,一直到 Global Object(window)
- 方法執行完成後,內部變數值不會被重設,至於變數什麼時候被銷毀,請參考下面一條
- 方法內變數的生存周期取決於方法執行個體是否存在活動引用,如沒有就銷毀使用中的物件
- 6和7 是使閉包能訪問到外部變數的根本原因
6、重釋經典案例
案列一二三:根據【在一個方法中,同名的實參,形參和變數之間是參考關聯性,也就是JS引擎的處理是同名變數和形參都引用同一個記憶體位址】,所以才會有案例二中的修改arguments會影響到局部變數的情況出現
案例四:根據【JS引擎變數尋找規則,首先在當前執行環境的 ActiveObject 中尋找,沒找到,則順著執行環境中屬性 ScopeChain 指向的 ActiveObject 中尋找,一直到 Global Object(window)】,所以在案例四中,因為在當前的ActiveObject中找到了有變數 i 的定義,只是值為 “undefined”,所以直接輸出 “undefined” 了