讀古今文學網 > 編寫高質量代碼:改善JavaScript程序的188個建議 > 建議78:正確理解執行上下文和作用域鏈 >

建議78:正確理解執行上下文和作用域鏈

執行上下文(execution context)是ECMAScript規範中用來描述JavaScript代碼執行的抽像概念。所有的JavaScript代碼都是在某個執行上下文中運行的。在當前執行上下文中調用function會進入一個新的執行上下文。該function調用結束後會返回到原來的執行上下文中。如果function在調用過程中拋出異常,並且沒有將其捕獲,有可能從多個執行上下文中退出。在function調用過程中,也可能調用其他的function,從而進入新的執行上下文,由此形成一個執行上下文棧。

每個執行上下文都與一個作用域鏈(scope chain)關聯起來。該作用域鏈用來在function執行時求出標識符(identifier)的值。該鏈中包含多個對象,在對標識符進行求值的過程中,會從鏈首的對象開始,然後依次查找後面的對象,直到在某個對象中找到與標識符名稱相同的屬性。在每個對象中進行屬性查找時,會使用該對象的prototype鏈。在一個執行上下文中,與其關聯的作用域鏈只會被with語句和catch子句影響。

在進入一個新的執行上下文時,會按順序執行下面的操作:

(1)創建激活(activation)對像

激活對象是在進入新的執行上下文時創建出來的,並且與新的執行上下文關聯起來。在初始化構造函數時,該對像包含一個名為arguments的屬性。激活對像在變量初始化時也會被用到。JavaScript代碼不能直接訪問該對象,但可以訪問該對象的成員(如arguments)。

(2)創建作用域鏈

接下來的操作是創建作用域鏈。每個function都有一個內部屬性[[scope]],它的值是一個包含多個對象的鏈。該屬性的具體值與function的創建方式和在代碼中的位置有很大關係(見本建議後面介紹的「function對象的創建方式」內容)。此時的主要操作是將上一步創建的激活對像添加到function的[[scope]]屬性對應的鏈的前面。

(3)變量初始化

這一步對function中需要使用的變量進行初始化。初始化時使用的對象是創建激活對像過程中所創建的激活對象,不過此時稱做變量對象。會被初始化的變量包括function調用時的實際參數、內部function和局部變量。在這一步中,對於局部變量,只是在變量對像中創建了同名的屬性,其屬性值為undefined,只有在function執行過程中才會被真正賦值。全局JavaScript代碼是在全局執行上下文中運行的,該上下文的作用域鏈只包含一個全局對象。

函數總是在自己的上下文環境中運行,如讀/寫局部變量、函數參數,以及運行內部邏輯結構等。在創建上下文環境的過程中,JavaScript會遵循一定的運行規則,並按照代碼順序完成一系列操作。這個操作過程如下:

第1步,根據調用時傳遞的參數創建調用對象。

第2步,創建參數對象,存儲參數變量。

第3步,創建對像屬性,存儲函數定義的局部變量。

第4步,把調用對像放在作用域鏈的頭部,以便檢索。

第5步,執行函數結構體內語句。

第6步,返回函數返回值。

針對上面的操作過程,下面進行詳細描述。

首先,在函數上下文環境中創建一個調用對象。調用對象與上下文環境是兩個不同的概念,也是另一種運行機制。對象可以定義和訪問自己的屬性或方法,不過這裡的對象不是完整意義上的對象,它沒有原型,並且不能夠被引用,這與Arguments對象的arguments數組不是真正意義上的數組一樣。

調用對像會根據傳遞的參數創建自己的Arguments對象,這是一個結構類似數組的對象,該對像內部存儲著調用函數時所傳遞的參數。接著,創建名為arguments的屬性,該屬性引用剛創建的Arguments對象。

然後,為上下文環境分配作用域。作用域由對像列表或對像鏈組成。每個函數對象都有一個內部屬性(scope),這個屬性值也是由對像列表或對像鏈組成的。scope屬性值構成了函數調用上下文環境的作用域,同時,調用對像被添加到作用域鏈的頭部,即該對像列表的頂部(作用域鏈的前端)。

實際上,這個頭部是針對該函數的作用域鏈而言的,把調用對像添加到作用域的頭部就是把調用對像排在函數作用域鏈的最上面。例如,在下面這個示例中,當調用函數e時,將創建函數e的調用對像和函數e的作用域,但在調用函數e之前,會先調用函數g,並且生成調用函數g的對象。而調用函數e的對象會在函數e的作用域範圍內處於頭部位置,即排在最前面。代碼如下:


function f{

return e;

function e{

return g;

function g{

return 1;

}

}

}

alert(f);//1


接著,正式執行函數體內代碼,此時JavaScript會對函數體內創建的變量執行變量實例化操作(即轉換為調用對象的屬性)。下面進行具體說明。

將函數的形參也創建為調用對象的命名屬性,如果調用函數時傳遞的參數與形參一致,則將相應參數的值賦給這些命名屬性,否則會將命名屬性賦值為undefined。

對於內部定義函數(注意其與嵌套函數的區分,兩者語義不完全重合),會以其聲明時所用名稱為調用對像創建同名屬性,對應的函數則被創建為函數對象,並將其賦值給該屬性。

將在函數內部聲明的所有局部變量創建為調用對象的命名屬性。注意,在執行函數體內的代碼並計算相應的賦值表達式之前不會對局部變量進行真正的實例化。

由於arguments屬性與函數局部變量對應的命名屬性都屬於同一個調用對象,因此可以將arguments作為函數的局部變量來看待。

最後,創建this對象並對其進行賦值。如果賦值為一個對象,則this將指向該對像引用。如果賦值為null,則this就指向全局對象。

創建全局上下文環境的過程與上面的描述稍微不同,因為全局上下文環境沒有參數,所以不需要通過定義調用對像來引用這些參數。全局上下文環境會有一個作用域,即全局作用域,它的作用域鏈實際上只由一個對像組成,即全局對像(window)。全局上下文環境也會有變量實例化的過程,它的內部函數就是涉及大部分JavaScript代碼的、常規的頂級函數聲明。全局上下文環境也會使用this對像來引用全局對象。

JavaScript作用域可以細分為詞法作用域和動態作用域。詞法作用域又稱為定義作用域,這是從靜態角度來說的。在函數沒有被調用之前,根據函數結構的嵌套關係來確定函數的作用域。因此詞法作用域取決於源代碼,通常編譯器可以進行靜態分析來確定每個標識符實際的引用。

動態作用域也稱為執行作用域,這是從動態角度來說的。當函數被調用之後,其作用域會因為調用而發生變化,此時作用域鏈也會隨之調整。

定義作用域就是用來說明函數在定義時存在的嵌套關係。當函數被執行時,作用域可能會發生變化。JavaScript函數運行在它們被定義的作用域中,而不是它們被執行的作用域中。

在JavaScript中,function對象的創建方式有3種:function聲明、function表達式和使用Function構造器。


function a{}

var a=function{}

var a=new Function


通過這3種方法創建出來的function對象的scope屬性的值有所不同,從而影響function執行過程中的作用域鏈,具體說明如下:

❑使用function語句聲明的function對象是在進入執行上下文時的變量初始化過程中創建的。該對象的scope屬性的值是它被創建時的執行上下文對應的作用域鏈。

❑使用function表達式的function對象是在該表達式被執行的時候創建的。該對象的scope屬性的值與使用function聲明創建的對象一樣。

❑使用Function構造器聲明一個function通常有兩種方式,常用格式是var funcName=new Function(p1,p2,...,pn,body),其中p1,p2,…,pn表示的是該function的形式參數,body是function的內容,使用該方式的function對象是在構造器被調用的時候創建的。該對象的scope屬性的值總是一個只包含全局對象的作用域鏈。

function對象的length屬性可以用來獲取聲明function時指定的形式參數的個數,而function對像被調用時的實際參數是通過arguments來獲取的。