溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

JavaScript中如何執(zhí)行上下文和堆棧

發(fā)布時間:2021-07-24 10:43:34 來源:億速云 閱讀:169 作者:小新 欄目:web開發(fā)

小編給大家分享一下JavaScript中如何執(zhí)行上下文和堆棧,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!

什么是執(zhí)行上下文?

JavaScript的執(zhí)行環(huán)境非常重要,當(dāng)JavaScript代碼在行時,會被預(yù)處理為以下情況之一:

  • Global code - 首次執(zhí)行代碼的默認環(huán)境。

  • Function code - 每當(dāng)執(zhí)行流程進入函數(shù)體時。

  • Eval code - 要在eval函數(shù)內(nèi)執(zhí)行的文本。

你可以閱讀大量涉及作用域的在線資料,不過為了使事情更容易理解,讓我們將術(shù)語“執(zhí)行上下文”視為當(dāng)前代碼的運行環(huán)境或作用域。接下來讓我們看一個包含global和function / local上下文的代碼示例。

JavaScript中如何執(zhí)行上下文和堆棧

這里沒有什么特別之處,我們有一個由紫色邊框表示的全局上下文,和由綠色,藍色和橙色邊框表示的3個不同的函數(shù)上下文。 只能有1個全局上下文,可以從程序中的任何其他上下文訪問。

你可以擁有任意數(shù)量的函數(shù)上下文,并且每個函數(shù)調(diào)用都會創(chuàng)建一個新的上下文,從而創(chuàng)建一個私有作用域,其中無法從當(dāng)前函數(shù)作用域外直接訪問函數(shù)內(nèi)部聲明的任何內(nèi)容。 在上面的示例中,函數(shù)可以訪問在其當(dāng)前上下文之外聲明的變量,但外部上下文無法訪問在其中聲明的變量或函數(shù)。 為什么會這樣呢? 這段代碼究竟是如何處理的?

Execution Context Stack(執(zhí)行上下文堆棧)

瀏覽器中的JavaScript解釋器被實現(xiàn)為單個線程。 實際上這意味著在瀏覽器中一次只能做一件事,其他動作或事件在所謂的執(zhí)行堆棧中排隊。 下圖是單線程堆棧的抽象視圖:

JavaScript中如何執(zhí)行上下文和堆棧

我們已經(jīng)知道,當(dāng)瀏覽器首次加載腳本時,它默認進入全局上下文執(zhí)行。 如果在全局代碼中調(diào)用函數(shù),程序的順序流進入被調(diào)用的函數(shù),創(chuàng)建新的執(zhí)行上下文并將其推送到執(zhí)行堆棧的頂部。

如果在當(dāng)前函數(shù)中調(diào)用另一個函數(shù),則會發(fā)生同樣的事情。 代碼的執(zhí)行流程進入內(nèi)部函數(shù),該函數(shù)創(chuàng)建一個新的執(zhí)行上下文,該上下文被推送到現(xiàn)有堆棧的頂部。 瀏覽器將始終執(zhí)行位于堆棧頂部的當(dāng)前執(zhí)行上下文,并且一旦函數(shù)執(zhí)行完當(dāng)前執(zhí)行上下文后,它將從棧頂部彈出,把控制權(quán)返回到當(dāng)前棧中的下一個上下文。 下面的示例顯示了遞歸函數(shù)和程序的執(zhí)行堆棧:

(function foo(i) {
if (i === 3) {
return;
}
else {
foo(++i);
}
}(0));

JavaScript中如何執(zhí)行上下文和堆棧

代碼簡單地調(diào)用自身3次,并將i的值遞增1。每次調(diào)用函數(shù)foo時,都會創(chuàng)建一個新的執(zhí)行上下文。 一旦上下文完成執(zhí)行,它就會彈出堆棧并且講控制返回到它下面的上下文,直到再次達到全局上下文。
關(guān)于執(zhí)行堆棧execution stack有5個關(guān)鍵要點:

  • 單線程。

  • 同步執(zhí)行。

  • 一個全局上下文。

  • 任意多個函數(shù)上下文。

  • 每個函數(shù)調(diào)用都會創(chuàng)建一個新的執(zhí)行上下文execution context,甚至是對自身的調(diào)用。

執(zhí)行上下文的細節(jié)

所以我們現(xiàn)在知道每次調(diào)用一個函數(shù)時,都會創(chuàng)建一個新的執(zhí)行上下文。 但是,在JavaScript解釋器中,對執(zhí)行上下文的每次調(diào)用都有兩個階段:

1.創(chuàng)建階段 [調(diào)用函數(shù)時,但在執(zhí)行任何代碼之前]:

  • 創(chuàng)建作用域鏈。

  • 創(chuàng)建變量,函數(shù)和參數(shù)。

  • 確定“this”的值。

2.激活/代碼執(zhí)行階段:

  • 分配值,引用函數(shù)和解釋/執(zhí)行代碼。

可以將每個執(zhí)行上下文在概念上表示為具有3個屬性的對象:

executionContextObj = {
'scopeChain': { /* variableObject + 所有父執(zhí)行上下文的variableObject */ },
'variableObject': { /* 函數(shù)實參/形參,內(nèi)部變量和函數(shù)聲明 */ },
'this': {}
}

激活對象/變量對象 [AO/VO]

在調(diào)用該函數(shù),并且在實際執(zhí)行函數(shù)之前,會創(chuàng)建這個executionContextObj。 這被稱為第1階段,即創(chuàng)造階段。 這時解釋器通過掃描函數(shù)傳遞的實參或形參、本地函數(shù)聲明和局部變量聲明來創(chuàng)建executionContextObj。 此掃描的結(jié)果將成為executionContextObj中的variableObject。

以下是解釋器如何預(yù)處理代碼的偽代碼概述:

1.找一些代碼來調(diào)用一個函數(shù)。

2.在執(zhí)行功能代碼之前,創(chuàng)建執(zhí)行上下文。

3.進入創(chuàng)建階段:

  • 初始化作用域鏈。

  • 創(chuàng)建variable object:

    • 對于找到的每個變量聲明,在variable object中創(chuàng)建一個屬性作為變量名稱,并將該值初始化為undefined。

    • 如果變量名稱已存在于variable object中,則不執(zhí)行任何操作并繼續(xù)掃描。

    • 對于找到的每個函數(shù),在variable object中創(chuàng)建一個屬性,該屬性是函數(shù)的確切名稱,該屬性存在指向內(nèi)存中函數(shù)的引用指針。

    • 如果函數(shù)名已存在,則將覆蓋引用指針值。

    • 創(chuàng)建arguments object,檢查參數(shù)的上下文,初始化名稱和值并創(chuàng)建引用副本。

    • 掃描上下文以獲取函數(shù)聲明:

    • 掃描上下文以獲取變量聲明:

  • 確定上下文中“this”的值。

4.激活/執(zhí)行階段:

  • 在上下文中運行/解釋函數(shù)代碼,并在代碼逐行執(zhí)行時分配變量值。

我們來看一個例子:

function foo(i) {
var a = 'hello';
var b = function privateB() {
};
function c() {
}
}
foo(22);

在調(diào)用foo(22)時,創(chuàng)建階段如下所示:

fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: undefined,
b: undefined
},
this: { ... }
}

如你所見,創(chuàng)建階段處理定義屬性的名稱,而不是為它們賦值,但正式的形參/實參除外。創(chuàng)建階段完成后,執(zhí)行流程進入函數(shù),激活/代碼執(zhí)行階段在函數(shù)執(zhí)行完畢后如下所示:

fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: 'hello',
b: pointer to function privateB()
},
this: { ... }
}

關(guān)于hoisting

你可以找到許多使用JavaScript定義術(shù)語hoisting的在線資源,解釋變量和函數(shù)聲明被hoisting到其函數(shù)范圍的頂部。 但是沒有人能夠詳細解釋為什么會發(fā)生這種情況,掌握了關(guān)于解釋器如何創(chuàng)建激活對象的新知識,很容易理解為什么。 請看下面的代碼示例:

(function() {
console.log(typeof foo); // function pointer
console.log(typeof bar); // undefined
var foo = 'hello',
bar = function() {
return 'world';
};
function foo() {
return 'hello';
}
}());

我們現(xiàn)在可以回答的問題是:

  • 為什么我們可以在聲明foo之前就能訪問?

    • 如果我們理解了創(chuàng)建階段,就知道在激活/代碼執(zhí)行階段之前已經(jīng)創(chuàng)建了變量。因此,當(dāng)函數(shù)流開始執(zhí)行時,已經(jīng)在激活對象中定義了foo。

  • Foo被聲明兩次,為什么foo顯示為function而不是undefined或string?

    • 即使foo被聲明兩次,我們通過創(chuàng)建階段知道函數(shù)在變量之前就被創(chuàng)建在激活對象上了,而且如果激活對象上已經(jīng)存在了屬性名稱,我們只是繞過了聲明這一步驟。

    • 因此,首先在激活對象上創(chuàng)建對函數(shù)foo()的引用,并且當(dāng)解釋器到達var foo時,我們已經(jīng)看到屬性名稱foo存在,因此代碼不執(zhí)行任何操作并繼續(xù)處理。

  • 為什么bar未定義?

    • bar實際上是一個具有函數(shù)賦值的變量,我們知道變量是在創(chuàng)建階段被創(chuàng)建的,但它們是使用undefined值初始化的。

以上是“JavaScript中如何執(zhí)行上下文和堆棧”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對大家有所幫助,如果還想學(xué)習(xí)更多知識,歡迎關(guān)注億速云行業(yè)資訊頻道!

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI