JavaScript:理解執行環境、作用域鏈和活動對象


作用域的原理,對JS將如何解析標識符做出了解答。而作用域的形成與執行環境和活動對象緊密相關。

我們對於JS標識符解析的判斷,存在一個常見誤區

首先,看一個關於JS標識符解析的問題 ,源於風雪之隅提出的問題

var name = 'globalName';
function funcA() {
     console.log(name);
     var name = 'funAName';
     console.log(name);
     console.log(age);
}
 
funcA();

這段代碼的運行結果是怎樣的?

相信會有人跟我最初遇到這個問題時一樣,以為結果會是這樣:

globalName
funAName
[腳本錯誤: ReferenceError]

​ 我們認為:在funA中, 第一次console.log的時候,會取到全局變量name的值'globalName', 而第二次值被局部變量name覆蓋, 所以第二次console.log是'funAName'。 而age屬性沒有定義, 所以腳本會出錯。

但是實際上,運行結果是這樣的:

undefined
funAName
[腳本錯誤: ReferenceError]

​ 為什么會這樣呢?在JS中,標識符解析是沿作用域鏈一級一級地搜索標識符的過程,搜索過程始終從作用域鏈的前端開始,然后逐級地向后回溯,直到找到標識符為止(如果找不到標識符,通常會導致錯誤發生)。所以為了正確地判斷標識符的解析結果,我們必須把作用域鏈原理弄清楚。

深入理解作用域原理能夠為我們解密這個誤區

​ 作用域鏈(scope chain)的生成跟執行環境(execution context)、函數對象(function object)和活動對象(activation object)緊密相關。

執行環境

​ 執行環境是JavaScript中最為重要的一個概念。執行函數定義了變量或函數有權訪問的其它數據,決定了它們各自的行為。每個執行環境都有一個與之關聯的變量對象(variable object)和一個作用域鏈(scope chain),環境中定義的所以變量和函數都保存在其變量對象中。執行環境分為兩種,一種是全局執行環境,一種是函數執行環境。

  1. 全局執行環境

    ​ 全局執行環境是最外圍的一個執行環境,其變量對象就是全局活動對象(window activation object),全局執行環境直到應用程序退出——例如關閉網頁或瀏覽器——時才會被銷毀。

  2. 函數執行環境

    ​ 每個函數都有自己的執行環境。當執行流進入一個函數時,函數環境就會被推入一個環境棧中。當函數執行完之后,棧將其環境彈出,把控制權返回給之前的執行環境。函數執行環境的變量對象是該函數的活動對象(activation object)。

作用域鏈

​ 對於每一個執行環境,都會創建一個與之關聯的作用域鏈。每個執行環境的作用域鏈的前端,始終都是該執行環境的變量對象,對於全局執行環境就相當於window對象,對於函數執行環境就相當於該函數的活動對象;對於全局執行環境,已經是根部,沒有后續,對於函數執行環境,其作用域鏈的后續是該函數對象的[[scope]]屬性里的作用域鏈。

函數對象

​ 在一個函數定義的時候, 會創建一個這個函數對象的[[scope]]屬性(內部屬性,只有JS引擎可以訪問, 但FireFox的幾個引擎(SpiderMonkey和Rhino)提供了私有屬性__parent__來訪問它),並將這個[[scope]]屬性指向定義它的作用域鏈上。 在這里的問題中,因為funcA定義在全局環境, 所以此時的[[scope]]只是指向全局活動對象window active object。

活動對象

​ 在一個函數對象被調用的時候,會創建一個活動對象,首先將該函數的每個形參和實參,都添加為該活動對象的屬性和值;將該函數體內顯示聲明的變量和函數,也添加為該活動的的屬性(在剛進入該函數執行環境時,未賦值,所以值為undefined,這個是JS的提前聲明機制)。

​ 然后將這個活動對象做為該函數執行環境的作用域鏈的最前端,並將這個函數對象的[[scope]]屬性里作用域鏈接入到該函數執行環境作用域鏈的后端。

現在讓我們回到最初的問題

var name = 'globalName';
function funcA() {
	 //當funcA()被調用時,剛進入funcA的執行環境,其作用域鏈最前端的funA activation object里有name屬性,值為undefined。
     console.log(name);
     var name = 'funAName';
     console.log(name);
     console.log(age);
}
 
funcA();

​ 雖然感覺這個例子的誤區跟JS提前聲明機制的關系也很大,但是理解funA.[[scope]]屬性的作用域鏈是funA定義時的作用域鏈,而不是被調用時的作用域鏈,對於看懂多層函數嵌套的情況下作用域鏈的坑,也是非常關鍵的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM