JavaScript執行環境與執行棧

neuyu發表於2021-09-09

執行環境

執行環境 ( 也稱"執行上下文" ) 可以說是 JavaScript 最重要的一個概念。那麼執行環境到底是什麼呢?一句話就可以概括:程式碼 ( 包括函式 ) 執行時所需要的所有資訊就是執行環境。由於 ES 歷經多個版本,所以執行環境的標準也一直在變,下面列出了三個主要的版本內容:

ES3 標準中的執行環境

scope:作用域,如果有作用域巢狀的情況就稱作"作用域鏈"。
variable object:變數物件,用於儲存識別符號的特殊物件。
this value:this 值。

*識別符號:包括變數、函式名、屬性名和函式的引數。

ES5 標準中的執行環境

variable environment:變數環境,當宣告變數時使用。
lexical environment:詞法環境,當獲取識別符號值時使用。
this value:this 值。

ES6 標準中的執行環境

variable environment:變數環境,當宣告變數時使用。
lexical environment:詞法環境,當獲取識別符號值或者 this 值時使用。

*在 ES6 中,執行環境中實際增加了不少內容,我們這裡只介紹了普通函式執行時所需要的內容。

執行棧

當開啟網頁或瀏覽器時,宿主環境(1)會將程式碼傳遞給引擎(2)去執行,引擎首先會建立一個全域性執行環境。全域性環境中的程式碼自上而下有順序的執行,當遇到一個函式時,函式的環境被建立,函式中的程式碼開始執行;而在函式執行之後,控制權又返還給之前的環境。ES 這種類似於" "(3)的控制機制,稱為執行棧。

(1) 宿主環境:瀏覽器或者 Node 環境。
(2) 引擎:從頭到尾負責整個 JavaScript 程式碼的編譯及執行過程。
(3) 棧:一種遵循" 後進先出 "原則的有序資料集合,可以簡單理解為使用 push() 和 pop() 運算元組。

例子:

console.log(1);

function pFn() {
    console.log(2);
    (function cFn() {
        console.log(3);
    }());
    console.log(4);
}
pFn();

console.log(5);
//輸出:1 2 3 4 5

示意圖:
圖片描述

我們可以透過瀏覽器,直觀的看一下執行棧的形式:

圖片描述

編譯原理

我們知道,執行環境中有很多非常有用的" 工具 “,這些” 工具 “會協助引擎完成整個函式的執行工作。例如,ES3 標準中的作用域,它會協助引擎查詢當前環境中所有識別符號的定義的位置;變數物件,幫助引擎儲存環境中的變數和函式。當然,這些工作大部分情況下發生在程式碼執行前的幾微秒之內,稱之為” 編譯階段 "。JavaScript 的整個編譯階段比較複雜,一般會經歷詞法分析、語法分析、程式碼生成、效能最佳化等步驟,這裡不做深入討論。

下面我們舉例說明,看看當函式 fn 執行的時候,引擎是如何工作的:

var b=1;
function fn(){
    var a = 1;
    return a+b;
}
fn();

1、首先,遇到 var a,引擎會詢問作用域是否已經有一個該名稱的變數存在於同一個作用域中。如果存在,引擎會忽略該宣告,繼續進行編譯;很顯然不存在,所以引擎會在當前作用域中宣告一個新的變數,並命名為 a ( 此時還沒有賦值,預設為 undefined )。

2、第二步,又遇到 a,引擎會首先詢問作用域,在當前的作用域中是否存在一個叫作 a 的變數,很顯然存在,所以引擎就會使用這個變數;遇到 b,引擎對作用域做出同樣的詢問,很顯然不存在,所以引擎會到外層巢狀的作用域中繼續查詢,在全域性作用域找到了該變數,引擎就會將 1 賦值給變數 b 。

3、經過以上兩步,函式 fn 環境中出現的所有識別符號的值已經基本鎖定,那麼引擎就會立即自上而下開始執行程式碼。為變數 a 賦值 1,計算 1+1 的值並返回它。

4、最後一步,函式 fn 的環境銷燬,退出執行棧,將控制權返還給全域性環境。

變數提升的原因

在編譯階段,引擎會宣告變數和函式,但不會對變數進行賦值,這主要是出於對效能的考慮。變數被宣告,但是不一定會在後面使用到,如果沒有使用卻賦了值,只是白白浪費記憶體而已。上面例子中的全域性變數 b ,在函式 fn 沒有執行之前,也不會賦值,直到函式中使用了這個變數,才不得不去載入數字 1。簡單的說,var a 這段程式碼發生在編譯階段,而 =1 這段程式碼會根據實際情況,發生在執行階段,這也就是" 變數提升 "的原因。另外需要注意的是,函式宣告的是整個函式體( 因為函式宣告不存在賦值操作),而且優先順序高於同名的變數。

例子1:

console.log(fn()); //輸出:1
console.log(n); //輸出:undefined

function fn() {
    return 1;
}
var n = 2;

由於宣告發生在賦值的前面,上面例子1的程式碼可以理解為下面的形式:

function fn() {
    return 1;
}
var n;

console.log(fn()); //輸出:1
console.log(n); //輸出:undefined

n = 2;

例子2:

fn(); //輸出:1

var fn = function() {
    console.log(2);
}

function fn() {
    console.log(1);
}

由於函式宣告優先順序高,因此同名變數宣告會被忽略,上面例子2的程式碼可以理解為下面的形式:

function fn() {
    console.log(1);
}

//由於函式宣告優先順序高,因此這個變數宣告會被忽略
//var fn;

fn(); //輸出:1

fn = function() {
    console.log(2);
}

*變數提升並非物理意義上的順序改變,程式碼執行的順序還是按照你書寫程式碼時的順序在執行。只是由於,變數宣告發生在程式碼的編譯階段,而變數賦值卻發生在程式碼的執行階段,時間上的差異導致了這種現象。


執行時流程圖

綜合以上的內容,JavaScript 的執行時流程圖如下:
圖片描述

*這張圖會根據內容的增加不斷進行補充。


如有錯誤,歡迎指正,本人不勝感激。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4550/viewspace-2822452/,如需轉載,請註明出處,否則將追究法律責任。

相關文章