javascript之詞法範圍及函數的運行過程

來源:互聯網
上載者:User
文章目錄
  • 經典案列重現
  • 6、重釋經典案例

詞法範圍:變數的範圍是在定義時決定而不是執行時決定,也就是說詞法範圍取決於源碼,通過靜態分析就能確定,因此詞法範圍也叫做靜態範圍。 with和eval除外,所以只能說JS的範圍機制非常接近詞法範圍(Lexical scope)。

下面通過幾個小小的案例,開始深入的瞭解對理解詞法範圍和閉包必不可少的,JS執行時底層的一些概念和理論知識。

經典案列重現1、經典案例一
1 /*全域(window)域下的一段代碼*/2 function a(i) {3     var i;4     alert(i);5 };6 a(10);

疑問:上面的代碼會輸出什麼呢?
答案:沒錯,就是彈出10。具體執行過程應該是這樣的

  1. a 函數有一個形參 i,調用 a 函數時傳入實參 10,形參 i=10
  2. 接著定義一個同名的局部變數 i,未賦值
  3. alert 輸出 10
  4. 思考:局部變數 i 和形參 i 是同一個儲存空間嗎?
  5. 按照定義來理解:局部變數 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,猜對了… ,下面簡單說一下具體執行過程

  1. a 函數有一個形參i,調用 a 函數時傳入實參 10,形參 i=10
  2. 第一個 alert 把形參 i 的值 10 輸出
  3. 第二個 alert 把 arguments[0] 輸出,應該也是 i
  4. 接著定義個局部變數 i 並賦值為2,這時候局部變數 i=2
  5. 第三個 alert 就把局部變數 i 的值 2 輸出
  6. 第四個alert再次把 arguments[0] 輸出
  7. 思考:這裡能說明局部變數 i 和形參 i 的值相同嗎?
3、經典案例三
1 /*全域(window)域下的一段代碼*/2 function a(i) {3     var i = i;4     alert(i);5 };6 a(10);

疑問:上面的代碼又又會輸出什麼呢?(10 )
答案:在FireBug中的運行結果是 10,下面簡單說一下具體執行過程

  1. 第一句聲明一個與形參 i 同名的局部變數 i,根據結果我們知道,後一個 i 是指向了
  2. 形參 i,所以這裡就等於把形參 i 的值 10 賦了局部變數 i
  3. 第二個 alert 當然就輸出 10
  4. 思考:結合案列二,這裡基本能說明局部變數 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,下面簡單說一下具體執行過程

  1. 第一個alert輸出undefined
  2. 第二個alert輸出 2
  3. 思考:到底怎麼回事兒?           

 

 

看到上面的幾個例子,你可能會弄錯。原因是:我們能很快的寫出一個方法,但到底方法內部是怎麼執行的呢?執行的細節又是怎麼樣的呢?你可能沒有進行過深入的學習和瞭解。要瞭解這些細節,那就需要瞭解 JS 引擎的工作方式,所以下面我們就把 JS 引擎對一個方法的解析過程進行一個稍微深入一些的介紹

解析過程

1、執行順序

  1. 編譯型語言,編譯步驟分為:詞法分析、文法分析、語義檢查、代碼最佳化和位元組產生。
  2. 解釋型語言,通過詞法分析和文法分析得到文法分析樹後,就可以開始解釋執行了。這裡是一個簡單原始的關於解析過程的原理,僅作為參考,詳細的解析過程(各種JS引擎還有不同)還需要更深一步的研究

  JavaScript執行過程,如果一個文檔流中包含多個script程式碼片段(用script標籤分隔的js代碼或引入的js檔案),它們的運行順序是:

  步驟1. 讀入第一個程式碼片段(js執行引擎並非一行一行地執行程式,而是一段一段地分析執行的)

  步驟2. 做詞法分析和文法分析,有錯則報語法錯誤(比如括弧不匹配等),並跳轉到步驟5

  步驟3. 對【var】變數和【function】定義做“預解析“(永遠不會報錯的,因為只解析正確的聲明)

  步驟4. 執行程式碼片段,有錯則報錯(比如變數未定義)

  步驟5. 如果還有下一個程式碼片段,則讀入下一個程式碼片段,重複步驟2

  步驟6. 結束

2、特殊說明
  全域域(window)域下所有JS代碼可以被看成是一個“匿名方法“,它會被自動執行,而此“匿名方法“內的其它方法則是在被顯示調用的時候才被執行
3、關鍵步驟
  上面的過程,我們主要是分成兩個階段

  1. 解析:就是通過文法分析和預解析構造合法的文法分析樹。
  2. 執行:執行具體的某個function,JS引擎在執行每個函數執行個體時,都會建立一個執行環境(ExecutionContext)和使用中的物件(activeObject)(它們屬於宿主對象,與函數執行個體的生命週期保持一致)

3、關鍵概念
  到這裡,我們再更強調以下一些概念,這些概念都會在下面用一個一個的實體來表示,便於大家理解

  1. 文法分析樹(SyntaxTree)可以直觀地表示出這段代碼的相關資訊,具體的實現就是JS引擎建立了一些表,用來記錄每個方法內的變數集(variables),方法集(functions)和範圍(scope)等
  2. 執行環境(ExecutionContext)可理解為一個記錄當前執行的方法【外部描述資訊】的對象,記錄所執行方法的類型,名稱,參數和使用中的物件(activeObject)
  3. 使用中的物件(activeObject)可理解為一個記錄當前執行的方法【內部執行資訊】的對象,記錄內部變數集(variables)、內嵌函數集(functions)、實參(arguments)、範圍鏈(scopeChain)等執行所需資訊,其中內部變數集(variables)、內嵌函數集(functions)是直接從第一步建立的文法分析樹複製過來的
  4. 詞法範圍:變數的範圍是在定義時決定而不是執行時決定,也就是說詞法範圍取決於源碼,通過靜態分析就能確定,因此詞法範圍也叫做靜態範圍。 with和eval除外,所以只能說JS的範圍機制非常接近詞法範圍(Lexical scope)
  5. 範圍鏈:詞法範圍的實現機制就是範圍鏈(scopeChain)。範圍鏈是一套按名稱尋找(Name Lookup)的機制,首先在當前執行環境的 ActiveObject 中尋找,沒找到,則順著範圍鏈到父 ActiveObject 中尋找,一直找到全域調用對象(Global Object)

4、實體表示

  

5、函數的運行過程

  1. 建立執行環境(execution context)的階段,函數將初始化各種變數,並將它們記錄在一個內部的變數對象(variable object)中。記錄在該變數對象中的變數依次有下面三種:(a)函數的實際參數;(b)內部的函式宣告;(c)內部變數集。此時前面兩種變數有了具體的值,內部變數集的值未undefined。
  2. 建立實參(arguments)對象,同名的實參,形參和變數之間是【引用】關係
  3. 執行方法內的指派陳述式,這才會對變數集中的變數進行賦值處理
  4. 變數尋找規則是首先在當前執行環境的 ActiveObject 中尋找,沒找到,則順著執行環境中屬性 ScopeChain 指向的 ActiveObject 中尋找,一直到 Global Object(window)
  5. 方法執行完成後,內部變數值不會被重設,至於變數什麼時候被銷毀,請參考下面一條
  6. 方法內變數的生存周期取決於方法執行個體是否存在活動引用,如沒有就銷毀使用中的物件
  7. 6和7 是使閉包能訪問到外部變數的根本原因
6、重釋經典案例

  案列一二三:根據【在一個方法中,同名的實參,形參和變數之間是參考關聯性,也就是JS引擎的處理是同名變數和形參都引用同一個記憶體位址】,所以才會有案例二中的修改arguments會影響到局部變數的情況出現

  案例四:根據【JS引擎變數尋找規則,首先在當前執行環境的 ActiveObject 中尋找,沒找到,則順著執行環境中屬性 ScopeChain 指向的 ActiveObject 中尋找,一直到 Global Object(window)】,所以在案例四中,因為在當前的ActiveObject中找到了有變數 i 的定義,只是值為 “undefined”,所以直接輸出 “undefined” 了 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.