JS執行上下文和活動物件

2020-07-16 10:05:04
在《JS變數》一節中我們曾介紹過變數的作用域,JavaScript 支援全域性作用域和區域性作用域。這個區域性作用域也就是函數作用域,區域性變數在函數內可見,也稱為私有變數。

作用域

作用域(Scope)表示變數的作用範圍、可見區域,包括詞法作用域和執行作用域。
  • 詞法作用域:根據程式碼的結構關係來確定作用域。詞法作用域是一種靜態的詞法結構,JavaScript 解析器主要根據詞法結構確定每個變數的可見性和有效區域。
  • 執行作用域:當程式碼被執行時,才能夠確定變數的作用範圍和可見性。與詞法作用域相對,它是一種動態作用域,函數的作用域會因為呼叫物件不同而發生變化。

JavaScript 支援詞法作用域,JavaScript 函數只能執行在被預先定義好的詞法作用域裡,而不是被執行的作用域裡。

執行上下文和活動物件

JavaScript 程式碼是按順序從上到下被解析的,當然 JavaScript 引擎並非逐行的分析和執行程式碼,而是逐段的去分析和執行。當執行一段程式碼時,先進行預處理,如變數提升、函數提升等。

JavaScript 可執行程式碼包括 3 中型別:全域性程式碼、函數程式碼、eval 程式碼。每執行一段可執行程式碼,都會建立對應的執行上下文。在指令碼中可能存在大量的可執行程式碼段,所以 JavaScript 引擎先建立執行上下文棧,來管理指令碼中所有執行上下文。

執行上下文是一個專業術語,比較抽象,實際上就是在記憶體中開闢的一塊獨立執行的空間。執行上下文棧相當於一個陣列,陣列元素就是一個個獨立的執行上下文區域。

當 JavaScript 開始解釋程式時,最先遇到的是全域性程式碼,因此在初始化程式的時候,首先向執行上下文棧壓入一個全域性執行上下文,並且只有當整個應用程式結束的時候,全域性執行上下文才被清空。

當執行一個函數的時候,會建立一個函數的執行上下文,並且壓入到執行上下文棧,當函數執行完畢,會將函數的執行上下文棧中彈出。

每個執行上下文都有 3 個基本屬性:變數物件、作用域鏈和 this。

變數物件是與執行上下文相關的資料作用域,儲存了在上下文中定義的變數和函數宣告。JavaScript 程式碼不能直接存取該物件,但是可以存取該物件的成員(如 arguments)。不同程式碼段中的變數物件也不相同,簡單說明如下。、

1. 全域性上下文的變數物件

全域性上下文的變數物件,初始化是全域性物件。

全域性物件是預定義物件,作為 JavaScript 的全域性函數和全域性屬性的預留位置。通過全域性物件,可以存取其他所有預定義的物件、函數和屬性。

在用戶端 JavaScript 中,全域性物件是 window 物件,通過 window 物件的 window 屬性指向自身。

【範例1】下面程式碼演示了在全域性作用域中宣告變數 b,並賦值,然後通過 window 物件的屬性 b 來讀取這個全域性變數值。同時演示了使用 this 存取 window 物件,使用 this.window 同樣可以存取 window 物件。
var b = true;
console.log(window.b);  //true
this.window.b = false;
console.log(this.b);  //false

2. 函數上下文的變數物件

變數物件是 ECMAScript 規範術語。在一個執行上下文中,變數物件才被啟用。只有啟用的變數物件,其各種屬性才能被存取。

在函數執行上下文中,變數物件常常被稱為活動物件,兩者意思相同。活動物件是在進入函數上下文時被建立,初始化時只包括 Arguments 物件。它通過函數的 arguments 屬性存取,arguments 屬性值為 Arguments 物件。

函數執行上下文的程式碼處理可以分成兩個階段:分析和執行,簡單說明如下。

1) 進入執行上下文。當進入執行上下文時,不會執行程式碼,只進行分析。此時變數物件包括:
  • 函數的所有形參(如果是函數上下文)——由名稱和對應值組成的一個變數物件的屬性被建立。如果沒有實參,屬性值設為 undefined。
  • 函數宣告——由名稱和對應值(函數物件)組成一個變數物件的屬性被建立。如果變數物件已經存在相同名稱的屬性,則會完全替換這個屬性。
  • 變數宣告——由名稱和對應值(undefined)組成的一個變數物件的屬性被建立。如果變數名稱與已經宣告的形參或函數相同,則變數宣告不會覆蓋已經存在的這類屬性。

【範例2】在進入函數執行上下文時,會給變數物件新增形參、函數宣告、變數宣告等初始的屬性值。下面程式碼簡單演示了這個階段的處理過程。
function f(a) {  //宣告外部函數
    var b = 1;  //宣告區域性變數,並賦值1
    function c() {}  //宣告內部函數
    var d = function () {};  //宣告區域性變數,並賦值為匿名函數
    b = 2;  //修改變數b的值為2
}
f(3);  //呼叫函數,並傳入實參值3
在進入函數執行上下文後,活動物件的結構模擬如下。
AO = {
    arguments : {
        0 : 3,  //實參值
        length : 1  //實參長度
    },
    a : 3,  //實參值
    b : undefined,  //宣告區域性變數b
    c : function c() {}  //宣告函數c,參照function c() {}
    d : undefined  //宣告區域性變數d
}

2) 執行程式碼。在程式碼執行階段會按順序執行程式碼,這時可能會修改變數物件的值。

【範例3】在程式碼執行階段,可能會修改變數物件的屬性值。針對上面範例,當程式碼執行完後,活動物件的結構模擬如下。
AO = {
    arguments : {
        0 : 3,  //實參值
        length : 1  //實參長度
    }, 
    a : 3,  //實參值
    b : 1,  //初始化賦值
    c : function () {},  //參照宣告的函數c
    d : function () {}  //參照函數表示式"d"
}