
英文 | https://medium.com/@MeowMeow-afk/execution-context-in-javascript-164435701192
今天這篇文章,我們将了解 JavaScript 提供的黑盒,讓我們的代碼神奇地運作“執行上下文”。
這是迄今為止最重要的主題之一,它可以使你對其他關鍵主題一目了然,例如,作用域、詞法作用域、閉包和提升,而且學習JavaScript的真正工作原理很有趣。
到目前為止,在代碼編輯器(Vs code )中編寫的每一行混亂代碼都在我們現在将讨論的這個執行上下文中運作。
坐下來,放松一下,收拾好你的美食,因為我會讓你明白的。
在 JavaScript 中,一切都發生在執行上下文中,我的意思是一切。你可以将其視為評估和執行 JavaScript 代碼的環境。
每當你的浏覽器與任何 JavaScript 代碼交叉路徑時,浏覽器的 JavaScript 引擎就會建立一個特殊的環境來處理此 JavaScript 代碼的轉換和執行。這個環境被稱為執行上下文。
執行上下文包含目前正在運作的代碼以及有助于其執行的所有内容。
執行上下文的類型
當你在浏覽器中運作腳本時,javascript 引擎會建立不同類型的執行上下文。
全局執行上下文 (GEC)
當你第一次運作腳本或你的代碼不在任何函數中時,它會被放置在全局執行上下文 (GEC) 中。
在這裡,每當 JavaScript 引擎接收到一個腳本檔案時,它首先會建立一個預設執行上下文,這就是我們所說的全局執行上下文 (GEC)。它是一個基本/預設執行上下文,所有不在函數内部的代碼都會在其中執行。
注意:每個 JavaScript檔案隻有一個 GEC
函數執行上下文 (FEC)
每當你的 JavaScript 引擎遇到函數調用時,它都會在全局執行上下文中建立一種稱為函數執行上下文的不同類型的 EC,以評估和執行該函數内部編寫的代碼。
每個函數調用都有自己的 FEC(即使你多次調用同一個函數),是以,在腳本運作時可以有多個 FEC。
它們是如何建立的?
現在執行上下文的建立分兩個階段進行:
- 建立階段
- 執行階段
1、建立階段
在此階段,将建立一個執行上下文對象 (ECO),其中包含我們的代碼在其運作時(執行階段)使用的重要資訊/資料。
屬性在此對象 (ECO) 中分三個不同階段進行設定和定義。
- 建立變量對象 (VO)。
- 建立範圍鍊。
- 賦予此關鍵字價值。
階段 1:變量對象的建立
變量對象就像一個在執行上下文中建立的容器,它将變量和函數聲明存儲在鍵:值對(不是函數表達式)中。
在 GEC 中,使用 var 關鍵字聲明的每個變量都會向指向該變量的變量對象添加一個屬性,并将其值設定為未定義,使用 let 或 const 聲明的變量擷取未初始化的值,而在函數聲明中,一個屬性被添加到指向該函數的變量對象中,所有的函數聲明都将被存儲并可以在 VO 中通路,甚至在代碼開始運作之前。
在 FEC 中,不會建立此變量對象,而是構造了一個名為“argument”的類似數組的對象,其中包括提供給該函數的所有參數。
這種甚至在代碼執行之前就将變量和函數(聲明)存儲在記憶體中,這就是我們所說的提升。
第 2 階段:建立範圍鍊
在 JavaScript 中,作用域是一種了解一段代碼對腳本其他域的可通路性的方法。
每個函數執行上下文都會建立它的作用域,可以将其視為一個環境或空間,它定義的變量和函數可以通過一個稱為作用域的程序來通路。
現在,當一個函數(比如 X() )在另一個函數(比如 Y() )中定義時,這個内部函數 X() 将可以通路變量,并且在外部函數 Y() 中定義的其他函數也将具有通路外部函數的代碼,但事情并不止于此,它還可以通路其父元素的代碼等等,直到 GCE,這種行為就是我們所說的詞法作用域,但反過來不是真的。
這個作用域的概念在JavaScript 中引發了一個被稱為閉包的相關現象,即使在外部函數執行完成之後,内部函數也可以通路與外部函數關聯的代碼……它已經死了,消失了,很久了走了。
讓我們再看一個例子來了解作用域鍊。
let a = 10;
function first() {
let b = 20;
second();
function second() {
let c = 30;
console.log(a + b + c);
}
}
first(); // output is 60
這裡變量 a 和 b 沒有在函數 second() 中定義,它隻能通路在其自己的範圍(本地範圍)中定義的變量 c,但是由于詞法範圍,它可以通路它所在的函數以及它的父母。是以,當你運作此代碼時,JavaScript 引擎将無法找到變量 a 或 b,是以,它将沿着執行上下文鍊首先找到 b 并解析它,因為它已在函數 first() 範圍内成功找到它,解析後繼續查找變量 a ,JavaScript 引擎為該變量一直到全局執行上下文并解析它。
這個 JavaScript 引擎沿着不同執行上下文鍊向上的過程,或者我們可以說周遊執行上下文的範圍以解析變量或函數調用/調用,稱為 Scope Chaining。
第 3 階段:設定“this”關鍵字的值
在 javascript 中,this 關鍵字是指執行上下文所屬的範圍。
在 GEC 中,這指的是一個全局對象,在浏覽器的情況下是一個視窗對象。是以,在函數聲明中,使用“var”關鍵字初始化的變量分别作為方法和屬性配置設定給這個全局對象。
是以
var x = "hello"
function y() {
console.log("hello")
}
與以下内容相同
window.x = "hello"
window.y = () => {
console.log("hello")
}
但在 FEC 的情況下,它不會建立“this”關鍵字,而是可以通路定義它的環境的關鍵字。
執行階段
在執行上下文的這個階段,我們的代碼開始執行,執行後從執行堆棧或調用堆棧彈出,我們将在本文後面介紹。
到目前為止,Variable 對象包含值為 undefined 和 uninitialized 的變量,具體取決于變量是分别使用 var 關鍵字還是使用 let/const 聲明的。
這裡 JavaScript引擎再次讀取 EC 中的代碼,用它們的實際值更新這些變量。然後代碼被解析,被轉譯,最後被執行
執行堆棧(調用堆棧)
你有沒有想過 JavaScript 引擎如何跟蹤它在腳本運作時建立的各種 EC 的所有這些建立和删除?答案是執行堆棧或簡單的調用堆棧。
“JavaScript 是一種同步的單線程語言”
單線程是指它隻能夠一次執行一個任務,一次是一行代碼,而同步是指這些任務的執行以特定的順序發生。是以,當 JavaScript 引擎讀取腳本時,它會建立不同的執行上下文并将它們存儲在稱為調用堆棧或執行堆棧的堆棧資料結構中。
var name = "Victor";
function firstFunc() {
var a = "Hi!";
secondFunc();
console.log(`${a} ${name}`);
}
function secondFunc(){
var b = "Hey!";
third();
console.log(`${b} ${name}`);
}
function thirdFunc() {
var c = "Hello!";
console.log(`${c} ${name}`);
}
first();
當腳本在浏覽器中加載時,浏覽器的 JS 引擎首先會建立一個我們在上面詳細介紹過的預設特殊環境,即全局執行上下文,并将其推送到此執行堆棧。
之後,當 JS 引擎發現函數調用時執行檔案時,它會為其建立一個單獨的函數執行上下文,如下圖所示(步驟 2),并将其推送到現有預設 GEC 之上的堆棧中。
在執行 firstFunc() 時,它遇到對 secondFunc() 的調用,它暫停 firstFunc() 的執行并建立另一個 FEC 并推送到 firstFunc() FEC 頂部的堆棧,然後再次為 thirdFunc() 建立一個單獨的 FEC 稱呼。
頂部的 EC 将首先由 JS 引擎執行,執行完成後,它會從堆棧中彈出,并開始執行上一個活動 EC 下面的 EC,如上圖所示,直到到達 GEC。
結論
執行上下文是 JavaScript 的核心,了解它很重要,因為它可以幫助你正确了解其他主要概念。
希望這篇文章對你有所幫助,如果你覺得有用的話,請将它分享給你的朋友,最後,感謝你的閱讀,祝程式設計愉快!