學習做用域中的「名詞」

先有雞仍是先有蛋

經過以前的文章,咱們熟悉了做用域的基本概念。可是做用域中的變量,函數聲明在什麼地方查找,引用它們的時候又發生了什麼。正是咱們將要討論的內容。前端

在咱們的認知中JavaScript代碼在執行的時候是由上到下一行一行執行的。但實際上並不徹底正確。例如:編程

a = 1;
var a;
console.log(a);
複製代碼

按照咱們以前的認知由上到下,最後a輸出undefined,由於var a聲明在a = 1後面,但最後輸出的結果是1數組

考慮另一段代碼:函數

console.log(a);
var a = 1;
複製代碼

鑑於上一個代碼片斷所表現的特色,可能認爲這個代碼片斷也會輸出1,或者可能拋出異常錯誤。實際上輸出的是undefinedui

那麼究竟是聲明在前,仍是賦值在前?this

回顧JavaScript引擎

爲了弄明白這個問題,咱們須要再次回顧JavaScript引擎,引擎會在解釋JavaScript代碼以前首先對其進行編譯。編譯階段中的一個很重要的工做就是找到全部的聲明,並在合適的做用域中將它們關聯起來。spa

執行環境

執行環境也能夠叫執行上下文,每當JavaScript編譯器工做時,都會建立一個執行環境或者說進入一個執行上下文中。它們定義了變量或函數訪問其餘數據的權限,決定了它們各自的行爲。它們在邏輯上組成一個堆棧,堆棧底部永遠是全局環境,而頂部就是當前環境。設計

例如:咱們能夠定義執行環境是一個數組:3d

stack = [];
複製代碼

在初始化階段,stack是這樣的:指針

stack = [
    globalContext
];
複製代碼

每次函數執行,進入function的時候,這個堆棧都會被壓入。

function foo(){
    return 'hello';
}
foo();
複製代碼

那麼,stack將會發生改變:

stack = [
    <foo> functionContext globalContext ]; 複製代碼

每一個函數都有本身的執行環境,每次函數退出也就是執行到return的時候,都會退出當前的執行環境,相應的stack就會彈出,棧中的指針會移動位置。相關代碼執行完畢後,stack只會包含全局環境,一直到整個程序結束。

變量對象

在進行JavaScript編程是總避免不了聲明函數和變量,在每一個執行環境中有一個變量對象,咱們定義的全部變量和函數都保存在這個對象中。

變量對象(VO)存儲一下內容:

函數聲明(function)

變量聲明(var)

咱們能夠用一個JavaScript對象來表示一個變量對象例如:

VO = {};
複製代碼

如前面所說執行環境中有一個變量對象,它是執行環境的一個屬性,例如:

context = {
    VO = {};
}
複製代碼

當咱們聲明一個變量或一個函數的時候,例如:

var a = 1;
 
function foo() {
  var b = 20;
};
 
test();
複製代碼

對應的變量對象是:

//全局環境的變量對象
globalContext: {
    vo: {
        a: 1,
        foo: function } } //foo函數環境的變量對象 fooContext: {
    vo: {
        b: 20
    }
}
複製代碼

不一樣執行環境中的變量對象

抽象變量對象VO

全局執行環境變量對象GlobalContextVO (VO === this === global)

函數執行環境變量對象FunctionContextVO (VO === AO, 而且添加了arguments)

全局執行環境的變量對象

全局對象是在進入任何執行環境以前就已經建立了的對象;這個對象只存在一份,它的屬性在程序中任何地方均可以訪問,全局對象的生命週期終止於程序退出那一刻。

global = {
  Math: <...>, String: <...> ... ... window: global //引用自身 }; var a = 1; console.log(a); // 直接訪問,在VO(globalContext)裏找到:a console.log(window['a']); // 間接經過global訪問:global === VO(globalContext): a console.log(a === this.a); // true var b = 'b'; console.log(window[b]); // 間接經過動態屬性名稱訪問:b 複製代碼

函數執行環境的變量對象

在函數執行環境中,變量對象是不能直接訪問的,此時由活動對象(AO)代替變量對象。活動對象是在進入函數執行環境時被建立的,它經過函數的arguments屬性初始化。

VO(functionContext) === AO;

AO = {
  arguments: <Args> }; // Arguments對象是活動對象,屬性以下: // callee — 指向當前函數的引用 // length — 真正傳遞的參數個數 function foo(a, b, c) { // 聲明的函數參數數量arguments (a, b, c) console.log(foo.length); // 3 // 真正傳進來的參數個數(only x, y) console.log(arguments.length); // 2 // 參數的callee是函數自身 console.log(arguments.callee === foo); // true // 參數共享 console.log(a === arguments[0]); // true console.log(a); // 10 arguments[0] = 20; console.log(a); // 20 a = 30; console.log(arguments[0]); // 30 // 不過,沒有傳進來的參數c,和參數的第3個索引值是不共享的 c = 40; console.log(arguments[2]); // undefined arguments[2] = 50; console.log(c); // 40 } foo(10, 20); 複製代碼

做用域鏈

當代碼在一個執行環境中,引擎會建立變量對象的一個做用域鏈。做用域鏈的用途是保證對執行環境有權限的變量和函數的有序訪問。做用域鏈的前端,始終都是當前執行的代碼所在環境的變量對象。若是這個環境是函數,則活動對象(VO)做爲變量對象(即arguments對象)。做用域鏈中的下一個變量對象來自包含環境,而再下一個變量對象則來自下一個包含環境,一直延續到全局執行環境;全局執行環境的變量對象始終都是做用域鏈中的最後一個對象。

做用域鏈與一個執行環境相關,變量對象的做用域鏈用於在標識符解析中變量查找。

函數執行環境的做用域鏈在函數調用時建立的,包含活動對象和這個函數內部的[[scope]]屬性,例如:

var a = 1;
 
function foo() {
  var b = 2;
  console.log(a + b);
}
 
foo(); // 3
複製代碼

函數建立時:

fooContext.AO = {
  b: undefined // undefined – 進入上下文的時候是2 – 活動對象
};

複製代碼

函數foo如何訪問到變量a?理論上函數應該能訪問一個更高一層執行環境的變量對象。實際上它正是這樣,這種機制是經過函數內部的[[scope]]屬性來實現的。[[scope]]是全部父變量對象的層級鏈,處於當前函數執行環境之上,在函數建立時存於其中,直至函數銷燬。函數foo的[[scope]]以下:

foo.[[Scope]] = [
  globalContext.VO // === Global
];
複製代碼

函數調用時:

Scope = AO|VO + foo.[[Scope]]
複製代碼

進入foo執行環境建立AO/VO以後,在執行環境中建立一個做用域鏈(Scope屬性)。這樣foo函數就能訪問全局執行環境中的變量a

變量提高

經過前面對於引擎的瞭解,變量提高是Javascript中執行環境和變量對象的工做方式的一種認知。它處於代碼的編譯階段,JavaScript僅提高聲明,而不提高初始化。

函數聲明和變量聲明會被提高

當咱們的代碼運行時,首先在執行環境的變量對象中聲明變量和函數,而後纔是代碼執行階段。當咱們看到var a = 1時,實際上JavaScript會將其當作兩部分:var = aa = 1

var a;
a = 1;

//全局環境的變量對象
globalContext: {
    vo: {
        a: 1
    }
}
複製代碼

函數優先

函數聲明會首先被提高,而後纔是變量,例如:

foo(); // 1
var foo;
function foo() { 
    console.log( 1 );
}
foo = function() { 
    console.log( 2 );
};
複製代碼

參考

相關文章
相關標籤/搜索