JavaScript 中的執(zhí)行上下文和執(zhí)行棧實(shí)例講解
JavaScript - 原理系列
在日常開(kāi)發(fā)中,每當(dāng)我們接手一個(gè)現(xiàn)有項(xiàng)目后,我們總喜歡先去看看別人寫(xiě)的代碼。每當(dāng)我們看到別人寫(xiě)出很酷的代碼的時(shí)候,我們總會(huì)感慨!寫(xiě)出這么優(yōu)美而又簡(jiǎn)潔的代碼的兄弟到底是怎么養(yǎng)成的呢?
我要怎樣才能達(dá)到和大佬一樣的水平呢!好了,廢話(huà)不多說(shuō),讓我們切入今天的主題。
一、執(zhí)行上下文
簡(jiǎn)而言之,【執(zhí)行上下文】就是JavaScript 代碼被解析和執(zhí)行時(shí)所在環(huán)境的抽象概念, 在JavaScript 中運(yùn)行任何的代碼都是在它的執(zhí)行上下文中運(yùn)行。
在運(yùn)行JavaScript代碼時(shí),每當(dāng)需要執(zhí)行代碼時(shí),執(zhí)行代碼會(huì)先進(jìn)入一個(gè)環(huán)境(瀏覽器、Node客戶(hù)端),這時(shí)就會(huì)為該環(huán)境創(chuàng)建一個(gè)執(zhí)行上下文,它會(huì)在你運(yùn)行代碼前做一些準(zhǔn)備工作,如確定作用域,創(chuàng)建全局、局部變量對(duì)象等。
執(zhí)行上下文的分類(lèi)
- 全局執(zhí)行上下文:
這是默認(rèn)的、最基礎(chǔ)的執(zhí)行上下文。不在任何函數(shù)中的代碼都位于全局執(zhí)行上下文中。
它做了兩件事:
- 創(chuàng)建一個(gè)全局對(duì)象,在瀏覽器中這個(gè)全局對(duì)象就是 window 對(duì)象。
將 this
指針指向這個(gè)全局對(duì)象。一個(gè)程序中只能存在一個(gè)全局執(zhí)行上下文。
- 函數(shù)執(zhí)行上下文:
每次調(diào)用函數(shù)時(shí),都會(huì)為該函數(shù)創(chuàng)建一個(gè)新的執(zhí)行上下文。每個(gè)函數(shù)都擁有自己的執(zhí)行上下文,但是只有在函數(shù)被調(diào)用的時(shí)候才會(huì)被創(chuàng)建。一個(gè)程序中可以存在任意數(shù)量的函數(shù)執(zhí)行上下文。每當(dāng)一個(gè)新的執(zhí)行上下文被創(chuàng)建,它都會(huì)按照特定的順序執(zhí)行一系列步驟,具體過(guò)程將在本文后面討論。
- Eval 函數(shù)執(zhí)行上下文:
運(yùn)行在 eval
函數(shù)中的代碼也獲得了自己的執(zhí)行上下文,但由于 Javascript 開(kāi)發(fā)人員不常用 eval 函數(shù),所以在這里不再討論。
執(zhí)行上下文的數(shù)量限制(堆棧溢出)
執(zhí)行上下文可存在多個(gè),雖然沒(méi)有明確的數(shù)量限制,但如果超出棧分配的空間,會(huì)造成堆棧溢出。常見(jiàn)于遞歸調(diào)用,沒(méi)有終止條件造成死循環(huán)的場(chǎng)景。
下面是示例代碼:
// 遞歸調(diào)用自身 function foo() { foo(); } foo(); // 報(bào)錯(cuò):Uncaught RangeError: Maximum call stack size exceeded
Tips:
JS是“單線(xiàn)程”的,每次只執(zhí)行一段代碼
二、執(zhí)行棧
JS中的執(zhí)行棧,也就是在其它編程語(yǔ)言中所說(shuō)的“調(diào)用棧”,是一種擁有 LIFO(后進(jìn)先出)數(shù)據(jù)結(jié)構(gòu)的棧,被用來(lái)存儲(chǔ)代碼運(yùn)行時(shí)創(chuàng)建的所有執(zhí)行上下文。
當(dāng) JavaScript 引擎第一次遇到你的腳本時(shí),它會(huì)創(chuàng)建一個(gè)全局的執(zhí)行上下文并且壓入當(dāng)前執(zhí)行棧。每當(dāng)引擎遇到一個(gè)函數(shù)調(diào)用,它會(huì)為該函數(shù)創(chuàng)建一個(gè)新的執(zhí)行上下文并壓入棧的頂部。
引擎會(huì)執(zhí)行那些執(zhí)行上下文位于棧頂?shù)暮瘮?shù)。當(dāng)該函數(shù)執(zhí)行結(jié)束時(shí),執(zhí)行上下文從棧中彈出,控制流程到達(dá)當(dāng)前棧中的下一個(gè)上下文。
棧數(shù)據(jù)結(jié)構(gòu)
現(xiàn)在讓我們用一段代碼來(lái)理解執(zhí)行棧
let a = 'Hello World!'; function first() { console.log('Inside first function'); second(); console.log('Again inside first function'); } function second() { console.log('Inside second function'); } first(); console.log('Inside Global Execution Context');
下圖是上面代碼的執(zhí)行棧
當(dāng)上述代碼在瀏覽器加載時(shí),瀏覽器的JavaScript 引擎會(huì)創(chuàng)建一個(gè)全局執(zhí)行上下文并把它壓入當(dāng)前執(zhí)行棧。當(dāng)遇到函數(shù)調(diào)用時(shí),JavaScript 引擎為該函數(shù)創(chuàng)建一個(gè)新的執(zhí)行上下文并把它壓入當(dāng)前執(zhí)行棧的頂部。
當(dāng)從 first()
函數(shù)內(nèi)部調(diào)用 second()
函數(shù)時(shí),JavaScript 引擎為 second()
函數(shù)創(chuàng)建了一個(gè)新的執(zhí)行上下文并把它壓入當(dāng)前執(zhí)行棧的頂部。當(dāng) second()
函數(shù)執(zhí)行完畢,它的執(zhí)行上下文會(huì)從當(dāng)前棧彈出,并且控制流程到達(dá)下一個(gè)執(zhí)行上下文,即 first()
函數(shù)的執(zhí)行上下文。
當(dāng) first()
執(zhí)行完畢,它的執(zhí)行上下文從棧彈出,控制流程到達(dá)全局執(zhí)行上下文。一旦所有代碼執(zhí)行完畢,JavaScript 引擎從當(dāng)前棧中移除全局執(zhí)行上下文。
The Creation Phase
在 JavaScript 代碼執(zhí)行前,執(zhí)行上下文將經(jīng)歷創(chuàng)建階段。在創(chuàng)建階段會(huì)發(fā)生三件事:
- this 值的決定,即我們所熟知的 This 綁定。
- 創(chuàng)建詞法環(huán)境組件。
- 創(chuàng)建變量環(huán)境組件。
所以執(zhí)行上下文在概念上表示如下:
ExecutionContext = { ThisBinding = <this value>, LexicalEnvironment = { ... }, VariableEnvironment = { ... }, }
This 綁定:
在全局執(zhí)行上下文中,this
的值指向全局對(duì)象。(在瀏覽器中,this
引用 Window 對(duì)象)。
在函數(shù)執(zhí)行上下文中,this
的值取決于該函數(shù)是如何被調(diào)用的。如果它被一個(gè)引用對(duì)象調(diào)用,那么 this
會(huì)被設(shè)置成那個(gè)對(duì)象,否則 this
的值被設(shè)置為全局對(duì)象或者undefined
(在嚴(yán)格模式下)。例如:
let foo = { baz: function() { console.log(this); } } foo.baz(); // 'this' 引用 'foo', 因?yàn)?'baz' 被 // 對(duì)象 'foo' 調(diào)用 let bar = foo.baz; bar(); // 'this' 指向全局 window 對(duì)象,因?yàn)? // 沒(méi)有指定引用對(duì)象
詞法環(huán)境
官方的 ES6 文檔把詞法環(huán)境定義為
詞法環(huán)境是一種規(guī)范類(lèi)型,基于 ECMAScript 代碼的詞法嵌套結(jié)構(gòu)來(lái)定義標(biāo)識(shí)符和具體變量和函數(shù)的關(guān)聯(lián)。一個(gè)詞法環(huán)境由環(huán)境記錄器和一個(gè)可能的引用外部詞法環(huán)境的空值組成。
簡(jiǎn)單來(lái)說(shuō)詞法環(huán)境是一種持有標(biāo)識(shí)符—變量映射的結(jié)構(gòu)。(這里的標(biāo)識(shí)符指的是變量/函數(shù)的名字,而變量是對(duì)實(shí)際對(duì)象[包含函數(shù)類(lèi)型對(duì)象]或原始數(shù)據(jù)的引用)。
現(xiàn)在,在詞法環(huán)境的內(nèi)部有兩個(gè)組件:(1) 環(huán)境記錄器和 (2) 一個(gè)外部環(huán)境的引用。
- 環(huán)境記錄器是存儲(chǔ)變量和函數(shù)聲明的實(shí)際位置。
- 外部環(huán)境的引用意味著它可以訪(fǎng)問(wèn)其父級(jí)詞法環(huán)境(作用域)。
詞法環(huán)境有兩種類(lèi)型:
- 全局環(huán)境(在全局執(zhí)行上下文中)是沒(méi)有外部環(huán)境引用的詞法環(huán)境。全局環(huán)境的外部環(huán)境引用是 null。它擁有內(nèi)建的 Object/Array/等、在環(huán)境記錄器內(nèi)的原型函數(shù)(關(guān)聯(lián)全局對(duì)象,比如 window 對(duì)象)還有任何用戶(hù)定義的全局變量,并且
this
的值指向全局對(duì)象。 - 在函數(shù)環(huán)境中,函數(shù)內(nèi)部用戶(hù)定義的變量存儲(chǔ)在環(huán)境記錄器中。并且引用的外部環(huán)境可能是全局環(huán)境,或者任何包含此內(nèi)部函數(shù)的外部函數(shù)。
環(huán)境記錄器也有兩種類(lèi)型(如上?。?/p>
- 聲明式環(huán)境記錄器存儲(chǔ)變量、函數(shù)和參數(shù)。
- 對(duì)象環(huán)境記錄器用來(lái)定義出現(xiàn)在全局上下文中的變量和函數(shù)的關(guān)系。
簡(jiǎn)而言之
- 在全局環(huán)境中,環(huán)境記錄器是對(duì)象環(huán)境記錄器。
- 在函數(shù)環(huán)境中,環(huán)境記錄器是聲明式環(huán)境記錄器。
注意
對(duì)于函數(shù)環(huán)境,聲明式環(huán)境記錄器還包含了一個(gè)傳遞給函數(shù)的 arguments
對(duì)象(此對(duì)象存儲(chǔ)索引和參數(shù)的映射)和傳遞給函數(shù)的參數(shù)的 length。
抽象地講,詞法環(huán)境在偽代碼中看起來(lái)像這樣:
GlobalExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Object", // 在這里綁定標(biāo)識(shí)符 } outer: <null> } } FunctionExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // 在這里綁定標(biāo)識(shí)符 } outer: <Global or outer function environment reference> } }
變量環(huán)境:
它同樣是一個(gè)詞法環(huán)境,其環(huán)境記錄器持有變量聲明語(yǔ)句在執(zhí)行上下文中創(chuàng)建的綁定關(guān)系。
如上所述,變量環(huán)境也是一個(gè)詞法環(huán)境,所以它有著上面定義的詞法環(huán)境的所有屬性。
在 ES6 中,詞法環(huán)境組件和變量環(huán)境的一個(gè)不同就是前者被用來(lái)存儲(chǔ)函數(shù)聲明和變量(let
和 const
)綁定,而后者只用來(lái)存儲(chǔ) var
變量綁定。
我們看點(diǎn)樣例代碼來(lái)理解上面的概念:
let a = 20;const b = 30;var c; function multiply(e, f) { var g = 20; return e * f * g;} c = multiply(20, 30);
執(zhí)行上下文看起來(lái)像這樣:
GlobalExectionContext = { ThisBinding: <Global Object>, LexicalEnvironment: { EnvironmentRecord: { Type: "Object", // 在這里綁定標(biāo)識(shí)符 a: < uninitialized >, b: < uninitialized >, multiply: < func > } outer: <null> }, VariableEnvironment: { EnvironmentRecord: { Type: "Object", // 在這里綁定標(biāo)識(shí)符 c: undefined, } outer: <null> } } FunctionExectionContext = { ThisBinding: <Global Object>, LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // 在這里綁定標(biāo)識(shí)符 Arguments: {0: 20, 1: 30, length: 2}, }, outer: <GlobalLexicalEnvironment> }, VariableEnvironment: { EnvironmentRecord: { Type: "Declarative", // 在這里綁定標(biāo)識(shí)符 g: undefined }, outer: <GlobalLexicalEnvironment> } }
注意
只有遇到調(diào)用函數(shù) multiply
時(shí),函數(shù)執(zhí)行上下文才會(huì)被創(chuàng)建。
可能你已經(jīng)注意到 let
和 const
定義的變量并沒(méi)有關(guān)聯(lián)任何值,但 var
定義的變量被設(shè)成了 undefined
。
這是因?yàn)樵趧?chuàng)建階段時(shí),引擎檢查代碼找出變量和函數(shù)聲明,雖然函數(shù)聲明完全存儲(chǔ)在環(huán)境中,但是變量最初設(shè)置為 undefined
(var
情況下),或者未初始化(let
和const
情況下)。
這就是為什么你可以在聲明之前訪(fǎng)問(wèn) var
定義的變量(雖然是 undefined
),但是在聲明之前訪(fǎng)問(wèn) let
和 const
的變量會(huì)得到一個(gè)引用錯(cuò)誤。
這就是我們說(shuō)的變量聲明提升。
執(zhí)行階段
這是整篇文章中最簡(jiǎn)單的部分。在此階段,完成對(duì)所有這些變量的分配,最后執(zhí)行代碼。
注意
在執(zhí)行階段,如果 JavaScript 引擎不能在源碼中聲明的實(shí)際位置找到 let
變量的值,它會(huì)被賦值為 undefined
。
結(jié)論
我們已經(jīng)討論過(guò) JavaScript 程序內(nèi)部是如何執(zhí)行的。雖然要成為一名卓越的 JavaScript 開(kāi)發(fā)者并不需要學(xué)會(huì)全部這些概念,但是如果對(duì)上面概念能有不錯(cuò)的理解將有助于你更輕松,更深入地理解其他概念,如變量聲明提升,作用域和閉包。
參考文章:
https://juejin.cn/post/6844903682283143181
https://www.jianshu.com/p/6f8556b10379
https://juejin.cn/post/6844903704466833421
到此這篇關(guān)于JavaScript 中的執(zhí)行上下文和執(zhí)行棧實(shí)例講解的文章就介紹到這了,更多相關(guān)JavaScript 中的執(zhí)行上下文和執(zhí)行棧內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
深入理解JavaScript系列(50):Function模式(下篇)
這篇文章主要介紹了深入理解JavaScript系列(50):Function模式(下篇),本篇我們介紹的一些模式稱(chēng)為初始化模式和性能模式,主要是用在初始化以及提高性能方面,一些模式之前已經(jīng)提到過(guò),這里只是做一下總結(jié),需要的朋友可以參考下2015-03-03關(guān)于jQuery參考實(shí)例2.0 用jQuery選擇元素
本篇文章小編為大家介紹,關(guān)于jQuery參考實(shí)例2.0 用jQuery選擇元素,有需要的朋友可以參考一下。2013-04-04表單元素的submit()方法和onsubmit事件應(yīng)用概述
表單元素?fù)碛衧ubmit方法,同時(shí)也具有onsubmit事件句柄,用于監(jiān)聽(tīng)表單提交??梢允褂胑lemForm.submit();方法觸發(fā)表單提交,感興趣的朋友可以了解下,或許對(duì)你有所幫助2013-02-02JavaScript CSS修改學(xué)習(xí)第六章 拖拽
這是一個(gè)簡(jiǎn)單可用的拖拽代碼。用鼠標(biāo)和鍵盤(pán)都可以操作。2010-02-02JavaScript高級(jí)程序設(shè)計(jì)(第3版)學(xué)習(xí)筆記11 內(nèi)建js對(duì)象
內(nèi)建對(duì)象是指由ECMAScript實(shí)現(xiàn)提供的、不依賴(lài)于宿主環(huán)境的對(duì)象,這些對(duì)象在程序運(yùn)行之前就已經(jīng)存在了2012-10-10javascript學(xué)習(xí)筆記(一)基礎(chǔ)知識(shí)
本文是學(xué)習(xí)筆記系列的第一篇,跟以前一樣,介紹些基礎(chǔ)知識(shí),包括js基本概念、 JScript 的變量、js的數(shù)據(jù)類(lèi)型、3.JScript 的運(yùn)算符、js流程控制、js函數(shù)。有需要的朋友可以參考下2014-09-09