JavaScript中的執(zhí)行上下文和變量提升
本文轉(zhuǎn)載自微信公眾號「新鈦云服」,作者林泓輝 翻譯。轉(zhuǎn)載本文請聯(lián)系新鈦云服公眾號。
與許多同類語言相比,JavaScript 是一種易于學(xué)習(xí)的編程語言。但是,如果您想理解、調(diào)試和編寫更好的代碼,則需要多加注意一些基本概念。
在本文中,我們將了解兩個這樣的概念:
- 執(zhí)行上下文
- 變量提升
作為一個初學(xué)者的JavaScript,了解這些概念將有助于您了解this關(guān)鍵字,作用域和閉包。
JavaScript 中的執(zhí)行上下文
一般來說,一個 JavaScript 源文件會有多行代碼。作為開發(fā)人員,我們將代碼組織成變量、函數(shù)、數(shù)據(jù)結(jié)構(gòu)(如對象和數(shù)組)等等。
語法環(huán)境決定了我們?nèi)绾我约霸诤翁幘帉懘a。看看下面的代碼:
- function doSomething() {
- var age= 7;
- // Some more code
- }
在上面的代碼中,變量age在語法上位于函數(shù)內(nèi)部doSomething。
請注意,我們的代碼不會按原樣運(yùn)行。它必須由編譯器翻譯成計算機(jī)可理解的字節(jié)碼。通常,語法環(huán)境在您的代碼中會有多個。然而,并不是所有的環(huán)境都會同時執(zhí)行。
幫助代碼執(zhí)行的環(huán)境稱為執(zhí)行上下文。它是當(dāng)前正在運(yùn)行的代碼,以及有助于運(yùn)行它的一切??梢杂泻芏嗾Z法環(huán)境,但當(dāng)前運(yùn)行的代碼只能有一個執(zhí)行上下文。
查看下圖以了解語法環(huán)境和執(zhí)行上下文之間的區(qū)別:
語法環(huán)境與執(zhí)行上下文
那么在執(zhí)行上下文中到底發(fā)生了什么?代碼被逐行解析,生成可執(zhí)行的字節(jié)碼,分配內(nèi)存并執(zhí)行。
讓我們采用我們在上面看到的相同函數(shù)。您認(rèn)為執(zhí)行以下行時可能會發(fā)生什么?
- var age = 7;
這段源代碼在最終執(zhí)行之前經(jīng)歷了以下階段:
- 標(biāo)記:在此階段,源代碼字符串分解為多個有意義的塊,稱為Tokens. 例如,代碼var age = 7;標(biāo)記為var , age , = , 7和, ; .
- 解析:下一個階段是解析,在這個階段,一個標(biāo)記數(shù)組變成一個由語言語法理解的嵌套元素樹。這棵樹被稱為AST(抽象語法樹)。
- 代碼生成:在這個階段,在解析階段創(chuàng)建的 AST 變成可執(zhí)行的字節(jié)碼。該可執(zhí)行字節(jié)碼隨后由 JIT(即時)編譯器進(jìn)一步優(yōu)化。
下面的動畫圖片顯示了源代碼到可執(zhí)行字節(jié)碼的轉(zhuǎn)換。
可執(zhí)行字節(jié)碼的源代碼
所有這些事情都發(fā)生在一個執(zhí)行上下文中。所以執(zhí)行上下文是代碼的特定部分的執(zhí)行環(huán)境。
有兩種類型的執(zhí)行上下文:
- 全局執(zhí)行上下文 (GEC)
- 函數(shù)執(zhí)行上下文 (FEC)
每個執(zhí)行上下文都有兩個階段:
- 創(chuàng)建階段
- 執(zhí)行階段
讓我們詳細(xì)看看它們中的每一個,并更好地理解它們。
JavaScript 中的全局執(zhí)行上下文 (GEC)
每當(dāng)我們執(zhí)行 JavaScript碼時,它都會創(chuàng)建一個全局執(zhí)行上下文(也稱為基本執(zhí)行上下文)。全局執(zhí)行上下文有兩個階段。
創(chuàng)建階段
在創(chuàng)建階段,創(chuàng)建了兩個獨(dú)特的東西:
- 調(diào)用的全局對象window(用于客戶端 JavaScript)。
- 一個名為this的變量。
如果代碼中聲明了任何變量,則會為該變量分配內(nèi)存。該變量使用唯一key進(jìn)行初始化,并賦值為undefined。
如果代碼中有function ,它會被直接放入內(nèi)存中。我們將在Hoisting后面的部分中詳細(xì)了解這部分。
執(zhí)行階段
代碼執(zhí)行在這個階段開始。在這里進(jìn)行全局變量的賦值。請注意,這里沒有調(diào)用函數(shù),因為它發(fā)生在函數(shù)執(zhí)行上下文中。我們將在后面討論這個問題。
讓我們通過幾個例子來理解這兩個階段。
示例 1:加載空腳本
創(chuàng)建一個名為index.js的空 JavaScript 文件及一個包含以下內(nèi)容的 HTML 文件:
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Document</title>
- <script src='./index.js'></script>
- </head>
- <body>
- I'm loading an empty script
- </body>
- </html>
我們使用<script>標(biāo)簽將空腳本文件導(dǎo)入到 HTML 文件中。
在瀏覽器中加載 HTML 文件并打開 Chrome DevTools(快捷鍵通常為F12)或其他瀏覽器也是可以的。選擇console選項卡,鍵入window并按回車鍵。您可以看到瀏覽器的Window對象。
windows對象
現(xiàn)在,輸入this并按回車鍵。您可以看到和Window對象一樣的this對象。
'this' 的值
如果您輸入window === this則會得到返回值true
好的,那么我們學(xué)到了什么?
- 當(dāng)我們加載 JavaScript 文件時,即使它是空的,也會創(chuàng)建全局執(zhí)行上下文。
- 它在創(chuàng)建階段為我們創(chuàng)建了兩個特殊的東西,即window對象和this。
- 在全局執(zhí)行上下文中,window對象 和this是相等的。
- 因為腳本文件是空白的,所以沒有什么可以執(zhí)行的。所以在執(zhí)行階段什么也不會發(fā)生。
示例 2:使用變量和函數(shù)
現(xiàn)在讓我們看一個在 JavaScript 文件中包含一些代碼的示例。我們將添加一個變量blog,并為其分配一個值。我們還將定義一個名為logBlog的函數(shù)。
- var blog = 'freeCodeCamp';
- function logBlog() {
- console.log(this.blog);
- }
在創(chuàng)建階段:
- 全局對象window和變量this被創(chuàng)建。
- 內(nèi)存被分配給變量blog和函數(shù)logBlog。
- 該變量blog由一個特殊值undefined初始化。該函數(shù)logBlog直接放置在內(nèi)存中。
在執(zhí)行階段:
- 值freeCodeCamp被分配給變量blog。
- 由于我們已經(jīng)定義了函數(shù)但還沒有調(diào)用它,因此函數(shù)執(zhí)行不會發(fā)生。我們將調(diào)用該函數(shù),看看當(dāng)我們了解函數(shù)執(zhí)行上下文時會發(fā)生什么。
JavaScript 中的函數(shù)執(zhí)行上下文 (FEC)
當(dāng)我們調(diào)用一個函數(shù)時,會創(chuàng)建一個函數(shù)執(zhí)行上下文。讓我們擴(kuò)展上面使用的相同示例,但這次我們將調(diào)用該函數(shù)。
- var blog = 'freeCodeCamp';
- function logBlog() {
- console.log(this.blog);
- }
- // Let us call the function
- logBlog();
函數(shù)執(zhí)行上下文經(jīng)歷相同的階段,即創(chuàng)建和執(zhí)行。
函數(shù)執(zhí)行階段可以訪問一個名為arguments的特殊值。它是傳遞給函數(shù)的參數(shù)。但在我們的示例中,沒有傳遞任何參數(shù)。
請注意,在全局執(zhí)行上下文中創(chuàng)建的window對象和this變量仍然可以在此上下文中訪問。
當(dāng)一個函數(shù)調(diào)用另一個函數(shù)時,會為新的函數(shù)調(diào)用創(chuàng)建一個新的函數(shù)執(zhí)行上下文。每個函數(shù)中相應(yīng)的變量只能在對應(yīng)的執(zhí)行上下文中使用。
在 JavaScript 中的變量提升
讓我們轉(zhuǎn)到另一個基本概念Hoisting。當(dāng)我第一次聽說Hoisting時,花了一些時間才理解這個意思。
在英語中,hoisting 的意思是使用繩索和滑輪提升某物。這可能會誤導(dǎo)您認(rèn)為 JavaScript 引擎會在特定代碼執(zhí)行階段拉取變量和函數(shù)。接下來,讓我們理解Hoisting的意思。
JavaScript 中的變量提升
請看下面的例子并猜測輸出:
- console.log(name);
- var name; // undefined
然而,為什么是undefined?如果我們在其他編程語言中使用類似的代碼。在這種情況下,我們將在控制臺得到報錯,指出該變量name未聲明,而我們正試圖在此之前訪問它。但是在JavaScript的執(zhí)行上下文里:
在創(chuàng)建階段,
- 內(nèi)存被分配給變量name,并且
- 一個特殊的值undefined被分配給變量。
在執(zhí)行階段,
該console.log(name)語句將執(zhí)行。
這種為變量分配內(nèi)存并賦值為undefined在執(zhí)行上下文的創(chuàng)建階段使用值進(jìn)行初始化的機(jī)制稱為Variable Hoisting(變量提升)。
特殊值undefined意味著聲明了一個變量但沒有賦值。
如果我們?yōu)樽兞糠峙湟粋€這樣的值:
- name = 'freeCodeCamp';
執(zhí)行階段會將這個值賦給變量。
JavaScript 中的函數(shù)提升
現(xiàn)在讓我們談?wù)凢unction Hoisting(函數(shù)提升)。它與Variable Hoisting的模式相同。
執(zhí)行上下文的創(chuàng)建階段將函數(shù)聲明放入內(nèi)存,并在執(zhí)行階段執(zhí)行。請看下面的例子:
- // Invoke the function functionA
- functionA();
- // Declare the function functionA
- function functionA() {
- console.log('Function A');
- // Invoke the function FunctionB
- functionB();
- }
- // Declare the function FunctionB
- function functionB() {
- console.log('Function B');
- }
輸出如下:
- Function A
- Function B
執(zhí)行上下文為函數(shù)創(chuàng)建內(nèi)存并將整個函數(shù)聲明functionA放入其中。
函數(shù)創(chuàng)建自己的執(zhí)行上下文。所以類似的事情也發(fā)生了functionB。
接下來,函數(shù)分別在它們的執(zhí)行上下文中執(zhí)行。
在創(chuàng)建階段將整個函數(shù)聲明提前放入內(nèi)存稱為Function Hoisting。
一些基本規(guī)則
既然我們了解了變量提升的概念,那么讓我們了解一些基本規(guī)則:
- 在代碼中使用變量和函數(shù)之前,務(wù)必先定義它們。這將減少意外的錯誤,為您的調(diào)試減少麻煩。
- 提升僅用于函數(shù)聲明,而不用于初始化。這是一個函數(shù)初始化的例子,代碼執(zhí)行會中斷。
- logMe();
- var logMe = function() {
- console.log('Logging...');
- }
代碼執(zhí)行將中斷,因為在函數(shù)初始化時,變量logMe將作為變量而不是函數(shù)被提升。因此,對于變量提升,內(nèi)存分配將在初始化時發(fā)生undefined。這就是我們會得到錯誤的原因:
函數(shù)初始化時出錯
假設(shè)我們嘗試在聲明之前訪問一個變量,然后使用letandconst關(guān)鍵字來聲明它。在這種情況下,它們將被提升但不會被分配默認(rèn)值undefined。訪問此類變量將導(dǎo)致ReferenceError. 下面是一個例子:
- console.log(name);
- let name;
它會拋出錯誤:
使用 let 和 const 關(guān)鍵字聲明的提升變量時出錯
如果我們使用var代替let和,相同的代碼將毫無問題地運(yùn)行const。這個錯誤是因為新的JavaScript 語言的保護(hù)機(jī)制,防止意外提升可能會導(dǎo)致不必要的麻煩。
感謝您能看到最后,我希望這篇文章能幫助您更好的理解JavaScript中的執(zhí)行上下文與變量提升的機(jī)制。
原文:https://www.freecodecamp.org/news/javascript-execution-context-and-hoisting/