天天看點

JS程式設計建議——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(){

}

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來擷取的。

繼續閱讀