原文(作者张洋)

JavaScript: The core

前言

例子

在多數程式語言中,變數是有塊級作用域的。如C

// C
int sum = 0;

for(int i = 1; i <=10; i++){
    sum += i;
}

int j = i; //會出錯,因為i僅在block中可見

但在JavaScript中則沒有塊級作用域

// JavaScript
int sum = 0;

for (var i=1; i<=10; i++){
    sum += i;
}

console.log(i); // 11

結論:ECMAScript中的作用域分為全局作用域和函數作用域,全局作用域是全局可見的,在函數中沒有使用var的變量也是全局變量,只有使用var的變數才是函數作用域變量,函數作用域變量在函數作用域中會覆蓋同名的全局作用域變量。全局作用域變量在整個腳本中可見,僅在被覆蓋的函數作用域中被覆蓋,而函數作用域變量只在函數內可見,除了被內嵌套函數中同名函數覆蓋的區域外。

ECMAScirpt作用域模型

執行上下文棧

由執行上下文堆疊而成,每次調用新的上下文,就會把新的上下文壓到棧頂。

執行上下文 Execution context

可執行代碼分成三種:Global code, Eval code, Function code

當執行控制流進入Global code或Function code時,都會暫停當前的執行上下文,創建一個新執行上下文(此時調用新上下文的叫caller,被調用的叫callee),callee執行其內容,直到返回為止。而執行一函數時,該執行上下文會在棧頂,離開時,會銷毀該環境。

執行上下文(EC)可以被當作一個簡單的對象,其屬性包含一個variable object,一個this,和scope chain。

變量對象 Variable Object

每一個EC都有一個與其相關的變量對象(varaible object),它存放了此上下文中定義過的變量和函數(只有函數宣告,不包含函數表達式)。這個對象在不同的EC是不同的,如在global環境就是global object本身。而函數會產生新的作用愈域,eval的variable object使用global或者caller的varaible object。

一段程式碼如下

var name = "Microsoft";

function funcA(){
    var name = "Google";
    alert(name);
}

funcA(); //Google

alert(name); //Microsoft

當執行到funcA時,建立一個環境並壓到棧頂,而兩個執行環境各自指向funcA的變數對象和全局變數對象,每個variable object如同Hash table,保存所有函數和變量。

當函數被caller調用,會產生一個特殊的activation object,它會有正式參數和arguments對象。這個activation object就會作為函數的varaible object。

作用域鏈 Scope Chain

上面提到函數中的var會覆蓋全域變量,這個模型怎麽知道何時該用函數域變量,何時該用全局變量?這就涉及到作用域鏈。

作用域鏈包含其調用鏈的variable object和當前的variable object。

假設另一個例子

var name = "Microsoft";

function funcA(){
    // var name = "Google";
    alert(name);
}

funcA(); //Microsoft
alert(name); //Microsoft

ECMAScript執行模型中每個環境都會關聯一個作用域鏈。可以把作用域鏈想成是一個pointer stack,每個元素都是指向某個varaible object的指針,從棧頂到棧底,指向的對象分別是當前環境的variable object,父EC的variable object,依此類推,最後一個指向全局varaible object。下圖是對上圖增加作用域後的樣子。

funcA內部有個pointer,指向funcA的作用域鏈,funcA的作用域鏈分別指向兩個變量對象。當ECMAScript解釋器尋找一個變量時,會先到scope0指向的變量對象中找,找不到再往scope1指向的變量對象中找。

這張圖有點誤導,真正的且實現在ES5上的,是在每個activation object上都有一個屬性[[scope]]指向其caller EC 的 varaible object。不過以array來表現是合理且容易理解的。另外,catch clausewith object可以動態的增添作用域鏈。

結論:ECMAScript開始執行時,創建一個全域EC和全域variable object,之後每次進入一個函數,就將該函數的EC壓入棧頂,同時創建EC的variable object和作用域鏈,作用域鏈的規則是複製上一層環境的作用域鏈,並將指向當前環境變量對象的指針放到棧頂。尋找變量時,先在當前的activation object找,找不到就會從其scope chain中開始找。跳出函數時,執行環境從棧頂彈出,該變量對象和作用域鏈也同時銷毀。

解讀閉包和匿名函數

functional arguments & functional value

JavaScript的函數可以當作變量傳遞,可以傳入當函數參數,也可以當函數返回值。把函數當參數代入另一個函數時,稱作functional arguments,把函數當返回值時,稱作functional value。

為了在函數內的activation variable找不到變量時,為了使其可以向外找,而且即使在外層已經結束了還能找到,所以在產生函數時會在[scope]屬性存放祖先的作用域鏈。注意,是在產生函數的時候執行上述動作的。

Scope chain = Activation object + [scope]

不能理解閉包關鍵是沒有理解ECMAScript模型

結合上面的執行模型,想像一下將一個函數內嵌套另一個函數,並將此函數作為返回值,執行模型為何?

var name = "Microsoft";

function funcA(){
    var name = "Google";
    alert(name);
    return function(){
        name = "Facebook";
        alert(name);
    };
}

var o = funcA(); //Google
alert(name); //Microsoft
o(); //Facebook

匿名函數引用的name是哪一個變量?

匿名函數的作用域鏈應該有三個pointer,第一個指向匿名函數的變量對象,第二個指向funcA的變量對象,第三個是全局變量對象。根據前述的搜尋順序,因為匿名函數沒有name的函數變量,它的name變量來自funcA(而不是全局變量)。

但是

var o = funcA();執行完後,funcA的變量對象應該就銷毀了,為什麽還可以被訪問到?

關鍵已經提過:作用域鏈的規則是複製上一層環境的作用域鏈,並將指向當前環境變量對象的指針放到棧頂

因此,匿名函數的作用域鏈有三個元素,引用了三個變量對象。返回的匿名函數賦值給變量o,變量o是全局變量,在funcA執行完後不會被銷毀。賦值的時候,匿名函數的作用域鏈已經建立,並用此作用域鏈引用funcA的變量對象,因此,當funcA執行完畢後,其執行環境和作用域鏈確實被銷毀了,但是其變量對象沒有被銷毀,因為匿名函數的作用域鏈對其有引用,無法被垃圾回收機制銷毀。下圖為funcA執行完後的樣子

funcA執行完成後作用域鏈被銷毀,但其變量對象(紅色)因為被匿名函數的作用域鏈引用了,所以沒有銷毀。因此匿名函數可以訪問其成員,當匿名函數完成後,就會連匿名函數的作用域鏈一起被銷毀。

閉包,就是該匿名函數,正式一點說,閉包就是能訪問另一個函數作用域中變量的函數。閉包優點是可以訪問另一個函數域中的變量,缺點就是耗記憶體。

關於this

this和作用域沒什麽關係。在使用閉包時常會誤用this

var name = "Global name";

function funcA(){
    var name = "FuncA name";
    return function(){
        alert(this.name);
    };
}

funcA()(); //Global name

上面我們在閉包中引用this,本來以為this指向funcA,結果卻是"Global name",說明this指向全局對象window。因為this指向調用自己的對象,上面的代碼可以寫成window.funcA()(),即是window。在全局定義的域中定義的變量和函數都是window的成員。

關於this,可以讀

http://www.ruanyifeng.com/blog/2010/04/using_this_keyword_in_javascript.html

http://www.zcfy.cc/article/901

https://segmentfault.com/a/1190000006076637

results matching ""

    No results matching ""