執行環境express
做用域鏈的造成與執行環境(Execution Environment)相關,在JavaScript當中,產生執行環境有以下3中情形:閉包
1 進入全局環境函數
2 調用eval函數this
3 調用functionspa
在一個執行環境A上能夠建立執行環境B,執行環境B又能夠建立執行環境C...,這一系列的執行環境構成執行環境棧,最新建立的執行環境位於棧頂(棧底永遠是全局執行環境),當棧頂執行環境結束以後(與之相關的代碼執行結束)就會被彈出站外,底下的執行環境就會成爲新的棧頂。以下圖所示:指針
一個執行環境由3部分組成:LexicalEnvironment,VariableEnvironment,ThisBinding。其中的ThisBinding就是this值,而LexicalEnvironment和VariableEnvironent至關於兩個指針,它們指向Lexical Environment(注意不是LexicalEnvironment),Lexical Environment裏面包含的就是標識符。code
Lexical Environment又由2部分組成:Environment Record和一個outer指針,其中outer指針指向當前執行環境下面執行環境的Lexical Environment,而Environment Record裏面就是綁定的程序中各類標示符。對象
Environment Record又能夠細分爲2種類型:一種叫Declaration Environment Record,另外一種是Object Environment Record。這兩種類型的Environment Record都記錄程序中使用到的標識符,不一樣之處在於,Declaration Environment Record主要綁定的是變量的聲明和函數的聲明,而Object Environment Record會關聯一個Object,Object Environment Record裏面綁定的都是和這個變量相關的屬性(好比with語法),這樣在JavaScript代碼裏面訪問這個變量的屬性時就能夠不用顯示指定該Object。blog
整個關係以下圖所示:ip
須要注意的是,執行環境棧最底層的全局執行環境的outer指針指向null。當查找某一個標示符時,就從最頂端執行環境的Lexical Environment開始查找,若是找不到,就進入到outer指針指向的下一個Lexical Environment裏面進行查找(上面的圖示是爲了演示,實際中outer指針指向的Lexical Environment有可能不是相鄰執行環境的Lexical Environment),直到查找到該變量或者outer指針指向的是null,所以,由outer指針鏈接的Lexical Environment就是當前函數運行時的做用域鏈。
在執行環境剛創建的過程當中,LexicalEnvironment和VariableEnvironment指向的同一個Lexical Environment,可是若是在該執行環境下遇到了特殊的語法,好比with,那麼LexicalEnvironment就會指向新的Lexical Record,當with運行結束,LexicalEnvironment又會指向VariableEnvironment所指的Lexical Environment,以下面代碼所示:
function f() { var i = 1; with(document) { write("Hello"); } var j = 2; }
在執行with語句以前的執行環境以下圖所示:
執行with語句時執行環境以下圖所示(注意沒有建立新的執行環境,只是增長了一個Lexical Environment):
執行with語句後執行環境以下圖所示:
Environment Record的構成
無論Environment Record是Declaration Environment Record,仍是Object Environment Record,都綁定的是當前執行環境下的標示符的信息,即Key-Value,Key就是標示符名,Value就是對應的值,以下圖所示:
進入函數造成執行環境
假設有以下代碼:
function f(a, b, c) { var i = 1; var j = 2; //函數聲明(function declaration) function inner(k, l, m) { var i = 1; var p = 2; var q = 3; } //函數表達式(function expression) var fVar = function(x, y, z) { var i = 1; var r = 2; var s = 3; } }
當在JavaScript當中以f(1, 2, 3)調用這個函數時,就會爲這個函數建立新執行環境,在執行該函數內部任何代碼以前,會先將函數裏面的標示符信息放到Environment Record當中。Environment Record當中的綁定標示符能夠分紅4部分(這4個部分的順序在Environment Record當中固定,而且每一部份內部的順序依據標識符出現的前後):函數參數,函數聲明,arguments,變量聲明,通過綁定後,函數f的執行環境以下:
參數標示符直接綁定的是傳給函數的實參值;
函數聲明標示符綁定的是函數對象(這也是函數聲明標示符能夠先使用,後聲明緣由,由於在代碼運行以前,函數聲明標示符就已經加入到了Environment Record中,而且綁定了函數對象);
arguments綁定的是arguments對象;
而變量聲明(包括函數表達式,也能夠當作是變量聲明),綁定的都是undefined,只有當代碼真正運行到賦值給變量的語句,變量纔會持有會變成咱們賦予變量的值(這也是在變量聲明以前可使用該變量,可是變量值老是undefined的緣由)。
上圖中Environment Record裏面的變量i和j是函數f裏面的變量,與函數聲明inner以及函數表達式fVar裏面的變量無關,由於此時只是對它們進行了定義,尚未進行調用。
須要注意的是:
1) Environment Record會綁定arguments標示符的條件是參數名和函數聲明的標識符沒有命名爲arguments的;
2) 若是多個函數使用同一個標識符,Environment Record也只會有一條記錄,而且該記錄綁定的是最後聲明的函數對象,即後聲明的函數對象覆蓋以前的函數對象,而且若是函數聲明標識符合參數標識符重名,那麼函數標識符覆蓋參數標識符,即若是有以下代碼:
function f(a, b, c) { alert(a); function a() { ... } alert(a); }
當使用f(1, 2, 3)調用時,兩處alert都會顯示function a,而不是參數a。
3)若是變量聲明標識符與以前參數,函數聲明,arguments標識符重名,那麼變量標識符不會被綁定,即若是有下面代碼:
function f(a, b, c) { function i() { ... } alert(i); var i = 1; alert(i); }
當使用f(1, 2, 3)調用時,第一處的alert會顯示function i,而不是undefined值,由於變量聲明i與函數i重名,不會被綁定;第二個alert會顯示1,由於運行了var i =1;以後,Environment Record裏面標識符i對應的值被賦予了1。
同時,若是屢次聲明同一個變量,Environment Record裏面也只會有一條記錄。
剩下的一個問題是,在調用函數的時候,新的執行環境是如何與外層的執行環境聯繫到一塊兒的,即新執行環境的outer指針是如何知道該指向哪個執行環境的。答案就是,在定義函數時,函數對象的內部屬性[[scope]]會引用定義本身時所在的Lexical Environment,當發生調用時,outer指針就指向[[scope]]所引用的Lexical Environment。假設有以下代碼:
function f1() { var f = f2(); f(); } function f2() { var i = 1; return function() { i = 2; } }
當調用f2時,執行環境以下圖所示:
當調用f2結束時,執行環境以下圖所示:
f2調用結束,f2相關的執行環境從棧頂彈出,而f2執行環境引用的Lexical Environment因爲有函數對象f的[[scope]]屬性引用,所以不會隨着f2的執行環境彈出棧頂而被垃圾回收。
當調用函數f時,執行環境以下圖所示:
從圖上能夠看到,從最頂端f的Lexical Environment,沿着outer指針,函數f能夠訪問到函數f2裏面的局部變量i,這就是閉包的原理。
參考資料
ECMA-262