Js 作用域與作用域鏈與執行上下文不得不說的故事 ⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄


最近在研究Js,發現自己對作用域,作用域鏈,活動對象這幾個概念,理解得不是很清楚,所以拜讀了@田小計划大神的博客與其他文章,受益匪淺,寫這篇隨筆算是自己的讀書筆記吧~。

作用域

首先明確一個概念,js只有函數作用域(function-based),沒有塊級作用域,也就是只有函數會有自己的作用域,其他都沒有。

接着,作用域分為全局作用域與局部作用域。

全局作用域中的對象可以在代碼的任何地方訪問,一般來說,下面情況的對象會在全局作用域中:

  • 最外層函數和在最外層函數外面定義的變量
  • 沒有通過關鍵字"var"聲明的變量
  • 瀏覽器中,window對象的屬性

局部作用域又被稱為函數作用域(Function scope),所有的變量和函數只能在作用域內部使用。

復制代碼
var foo = 1;
window.bar = 2;

function baz(){
    a = 3;
    var b = 4;
}
// Global scope: foo, bar, baz, a 
// Local scope: b

在創建這個函數的時候,這個函數的作用域與作用域鏈(函數的作用域鏈將會在運行時用到)就已經決定了,而是不是在調用的時候,這句話至管重要。

作用域鏈

每一個Javascript函數都被表示為對象,它是一個函數實例。它包含我們編程定義的可訪問屬性,和一系列不能被程序訪問,僅供Javascript引擎使用的內部屬性,其中一個內部屬性是[[Scope]],由ECMA-262標准第三版定義。

  內部[[Scope]]屬性包含一個函數被創建的作用域中對象的集合。此集合被稱為函數的作用域鏈,它決定哪些數據可以由函數訪問。此函數中作用域鏈中每個對象被稱為一個可變對象,以“鍵值對”表示。當一個函數創建以后,它的作用域鏈被填充以這些對象,它們代表創建此函數的環境中可訪問的數據:

1 function add(num1, num2){ 
2   var sum = num1 + num2; 
3   return sum;
4 }

  當add()函數創建以后,它的作用域鏈中填入了一個單獨可變對象,此全局對象代表了所有全局范圍定義的變量。此全局對象包含諸如窗口、瀏覽器和文檔之類的訪問接口。如下圖所示:(add()函數的作用域鏈,注意這里只畫出全局變量中很少的一部分)

  add函數的作用域鏈將會在運行時用到,假設運行了如下代碼:

1 var total = add(5,10);

  運行此add函數時會建立一個內部對象,稱作“運行期上下文”(execution context),一個運行期上下文定義了一個函數運行時的環境。且對於單獨的每次運行而言,每個運行期上下文都是獨立的,多次調用就會產生多此創建。而當函數執行完畢,運行期上下文被銷毀。

  一個運行期上下文有自己的作用域鏈,用於解析標識符。當運行期上下文被創建的時,它的作用域被初始化,連同運行函數的作用域鏈[[Scope]]屬性所包含的對象。這些值按照它們出現在函數中的順序,被復制到運行期上下文的作用域鏈中。這項工作一旦執行完畢,一個被稱作“激活對象”的新對象就創建好了。此激活對象作為函數執行期一個可變對象,包含了訪問所有局部變量,命名參數,參數集合和this的接口。然后,此對象被推入到作用域鏈的最前端。當作用域鏈被銷毀時,激活對象也一同被銷毀。如下所示:(運行add()時的作用域鏈)

  在函數運行的過程中,每遇到一個變量,就要進行標識符識別。標識符識別這個過程要決定從哪里獲得數據或者存取數據。此過程搜索運行期上下文的作用域鏈,查找同名的標識符。搜索工作從運行函數的激活目標的作用域前端開始。如果找到了,就使用這個具有指定標識符的變量;如果沒找到,搜索工作將進入作用域鏈的下一個對象,此過程持續運行,直到標識符被找到或者沒有更多可用對象可用於搜索,這種情況視為標識符未定義。正是這種搜索過程影響了性能。如果想了解如何編寫高性能的js,建議看看這篇blog,這個小結也是摘自於它——http://www.cnblogs.com/coco1s/p/4017544.html

執行上下文

在JavaScript中有三種代碼運行環境:

  • Global Code
    • JavaScript代碼開始運行的默認環境
  • Function Code
    • 代碼進入一個JavaScript函數
  • Eval Code
    • 使用eval()執行代碼

為了表示不同的運行環境,JavaScript中有一個執行上下文(Execution context,EC)的概念。也就是說,當JavaScript代碼執行的時候,會進入不同的執行上下文,這些執行上下文就構成了一個執行上下文棧(Execution context stack,ECS)

執行上下文包含三個重要的概念,彼此聯系且不好理解,導致了新手很難做到一次理解清楚,如下圖所示:

每個執行上下文都有三個重要的屬性,變量對象(Variable object,VO),作用域鏈(Scope chain)和this,當然還有一些附加的屬性。

 

當一段JavaScript代碼執行的時候,JavaScript解釋器會創建Execution Context,其實這里會有兩個階段:

  • 創建階段(當函數被調用,但是開始執行函數內部代碼之前)
    • 創建Scope chain
    • 創建VO/AO(variables, functions and arguments)
    • 設置this的值
  • 激活/代碼執行階段
    • 設置變量的值、函數的引用,然后解釋/執行代碼

這里想要詳細介紹一下"創建VO/AO"中的一些細節,因為這些內容將直接影響代碼運行的行為。

對於"創建VO/AO"這一步,JavaScript解釋器主要做了下面的事情:

  • 根據函數的參數,創建並初始化arguments object
  • 掃描函數內部代碼,查找函數聲明(Function declaration)
    • 對於所有找到的函數聲明,將函數名和函數引用存入VO/AO中
    • 如果VO/AO中已經有同名的函數,那么就進行覆蓋
  • 掃描函數內部代碼,查找變量聲明(Variable declaration)
    • 對於所有找到的變量聲明,將變量名存入VO/AO中,並初始化為"undefined"
    • 如果變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會干擾已經存在的這類屬性

看下面的例子:

復制代碼
function foo(i) {
    var a = 'hello';
    var b = function privateB() {

    };
    function c() {

    }
}

foo(22);
復制代碼

對於上面的代碼,在"創建階段",可以得到下面的Execution Context object:

復制代碼
fooExecutionContext = {
    scopeChain: { ... },
    variableObject: {
        arguments: {
            0: 22,
            length: 1
        },
        i: 22,
        c: pointer to function c()
        a: undefined,
        b: undefined
    },
    this: { ... }
}
復制代碼

在"激活/代碼執行階段",Execution Context object就被更新為:

復制代碼
fooExecutionContext = {
    scopeChain: { ... },
    variableObject: {
        arguments: {
            0: 22,
            length: 1
        },
        i: 22,
        c: pointer to function c()
        a: 'hello',
        b: pointer to function privateB()
    },
    this: { ... }
}

總結

函數在定義時就會確定他的作用域與作用域鏈(靜態),只有調用的時候才會創建一個執行上下文,其中包含了調用時的形參,其中的函數聲明與變量(VO), 同時創建活動對象(AO),並將AO壓入執行上下文的作用域鏈的最前端並且包含了this的屬性,執行上下文的作用域鏈是通過正在被調用函數的作用域鏈得到的(動態)。

以上的理解如有錯誤,敬請指正,敬禮~

 

參考

http://www.cnblogs.com/coco1s/p/4017544.html

http://www.cnblogs.com/wilber2013/p/4909459.html

http://www.cnblogs.com/wilber2013/p/4909430.html#_nav_3

 


免責聲明!

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



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