摘要:原文鏈接變量對象是說的執行上下文中都有個對象用來存放執行上下文中可被訪問但是不能被的函數標示符形參變量聲明等。對于函數的形參沒有什么可說的,主要看一下函數的聲明以及變量的聲明兩個部分。
首先明確幾個概念:
EC:函數執行環境(或執行上下文),Execution Context
ECS:執行環境棧,Execution Context Stack
VO:變量對象,Variable Object
AO:活動對象,Active Object
scope chain:作用域鏈
想當初自己看到這幾個概念的時候是一(m)臉(d)懵(z)逼(z),但是不得不說這幾個概念對以后深入學習JS有很大的幫助。來不及解釋了,趕緊上車~
EC(執行上下文)每次當控制器轉到ECMAScript可執行代碼的時候,就會進入到一個執行上下文。
那什么是可執行代碼呢?
可執行代碼的類型這種類型的代碼是在"程序"級處理的:例如加載外部的js文件或者本地標簽內的代碼。全局代碼不包括任何function體內的代碼。 這個是默認的代碼運行環境,一旦代碼被載入,引擎最先進入的就是這個環境。
任何一個函數體內的代碼,但是需要注意的是,具體的函數體內的代碼是不包括內部函數的代碼。
eval內部的代碼
這里僅僅引入EC這個概念,后面還有關于EC建立細節的介紹。
ECS(執行環境棧)我們用MDN上的一個例子來引入函數執行棧的概念
function foo(i) { if (i < 0) return; console.log("begin:" + i); foo(i - 1); console.log("end:" + i); } foo(2); // 輸出: // begin:2 // begin:1 // begin:0 // end:0 // end:1 // end:2
這里先不關心執行結果。磨刀不誤砍柴功,先了解一下函數執行上下文堆棧的概念。相信弄明白了下面的概念,一切也就水落石出了
我們都知道,瀏覽器中的JS解釋器被實現為單線程,這也就意味著同一時間只能發生一件事情,其他的行為或事件將會被放在叫做執行棧里面排隊。下面的圖是單線程棧的抽象視圖:
當瀏覽器首次載入你的腳本,它將默認進入全局執行上下文。如果,你在你的全局代碼中調用一個函數,你程序的時序將進入被調用的函數,并創建一個新的執行上下文,并將新創建的上下文壓入執行棧的頂部。
如果你調用當前函數內部的其他函數,相同的事情會在此上演。代碼的執行流程進入內部函數,創建一個新的執行上下文并把它壓入執行棧的頂部。瀏覽器總會執行位于棧頂的執行上下文,一旦當前上下文函數執行結束,它將被從棧頂彈出,并將上下文控制權交給當前的棧。這樣,堆棧中的上下文就會被依次執行并且彈出堆棧,直到回到全局的上下文。
看到這里,想必大家都已經深諳上述例子輸出結果的原因了,這里我大概繪了一個流程圖來幫助理解。
VO(變量對象)/AO(活動對象)這里為什么要用一個/呢?按照字面理解,AO其實就是被激活的VO,兩個其實是一個東西。下面引用知乎上的一段話,幫助理解一下。原文鏈接
EC建立的細節變量對象(Variable object)是說JS的執行上下文中都有個對象用來存放執行上下文中可被訪問但是不能被delete的函數標示符、形參、變量聲明等。它們會被掛在這個對象上,對象的屬性對應它們的名字對象屬性的值對應它們的值但這個對象是規范上或者說是引擎實現上的不可在JS環境中訪問到活動對象
激活對象(Activation object)有了變量對象存每個上下文中的東西,但是它什么時候能被訪問到呢?就是每進入一個執行上下文時,這個執行上下文兒中的變量對象就被激活,也就是該上下文中的函數標示符、形參、變量聲明等就可以被訪問到了
創建作用域鏈(Scope Chain)
創建變量,函數和參數。
求”this“的值
初始化變量的值和函數的引用,解釋/執行代碼。
我們可以將每個執行上下文抽象為一個對象,這個對象具有三個屬性
ECObj: { scopeChain: { /* 變量對象(variableObject)+ 所有父級執行上下文的變量對象*/ }, variableObject: { /*函數 arguments/參數,內部變量和函數聲明 */ }, this: {} }解釋器執行代碼的偽邏輯
1、查找調用函數的代碼。
2、執行代碼之前,先進入創建上下文階段:
初始化作用域鏈
創建變量對象:
創建arguments對象,檢查上下文,初始化參數名稱和值并創建引用的復制。
掃描上下文的函數聲明(而非函數表達式):
為發現的每一個函數,在變量對象上創建一個屬性——確切的說是函數的名字——其有一個指向函數在內存中的引用。
如果函數的名字已經存在,引用指針將被重寫。
掃描上下文的變量聲明:
為發現的每個變量聲明,在變量對象上創建一個屬性——就是變量的名字,并且將變量的值初始化為undefined
如果變量的名字已經在變量對象里存在,將不會進行任何操作并繼續掃描。
求出上下文內部“this”的值。
3、激活/代碼執行階段:
在當前上下文上運行/解釋函數代碼,并隨著代碼一行行執行指派變量的值。
function foo(i){ var a = "hello" var b = function(){} function c(){} } foo(22)
當我們調用foo(22)時,整個創建階段是下面這樣的
ECObj = { scopChain: {...}, variableObject: { arguments: { 0: 22, length: 1 }, i: 22, c: pointer to function c() a: undefined, b: undefined }, this: { ... } }
正如我們看到的,在上下文創建階段,VO的初始化過程如下(該過程是有先后順序的:函數的形參==>>函數聲明==>>變量聲明):
函數的形參(當進入函數執行上下文時) —— 變量對象的一個屬性,其屬性名就是形參的名字,其值就是實參的值;對于沒有傳遞的參數,其值為undefined
函數聲明(FunctionDeclaration, FD) —— 變量對象的一個屬性,其屬性名和值都是函數對象創建出來的;如果變量對象已經包含了相同名字的屬性,則替換它的值
變量聲明(var,VariableDeclaration) —— 變量對象的一個屬性,其屬性名即為變量名,其值為undefined;如果變量名和已經聲明的函數名或者函數的參數名相同,則不會影響已經存在的屬性。
對于函數的形參沒有什么可說的,主要看一下函數的聲明以及變量的聲明兩個部分。
1、如何理解函數聲明過程中如果變量對象已經包含了相同名字的屬性,則替換它的值這句話?
看如下這段代碼:
function foo1(a){ console.log(a) function a(){} } foo1(20)//"function a(){}"
根據上面的介紹,我們知道VO創建過程中,函數形參的優先級是高于函數的聲明的,結果是函數體內部聲明的function a(){}覆蓋了函數形參a的聲明,因此最后輸出a是一個function
2、如何理解變量聲明過程中如果變量名和已經聲明的函數名或者函數的參數名相同,則不會影響已經存在的屬性這句話?
//情景一:與參數名相同 function foo2(a){ console.log(a) var a = 10 } foo2(20) //"20" //情景二:與函數名相同 function foo2(){ console.log(a) var a = 10 function a(){} } foo2() //"function a(){}"
下面是幾個比較有趣的例子,當做加餐小菜,大家細細品味。這里給出一句話當做參考:
函數的聲明比變量優先級要高,并且定義過程不會被變量覆蓋,除非是賦值
function foo3(a){ var a = 10 function a(){} console.log(a) } foo3(20) //"10" function foo3(a){ var a function a(){} console.log(a) } foo3(20) //"function a(){}"
正如我們看到的,創建的過程僅負責處理定義屬性的名字,而并不為他們指派具體的值,當然還有對形參/實參的處理。一旦創建階段完成,執行流進入函數并且激活/代碼執行階段,看下函數執行完成后的樣子:
ECObj = { scopeChain: { ... }, variableObject: { arguments: { 0: 22, length: 1 }, i: 22, c: pointer to function c() a: "hello", b: pointer to function privateB() }, this: { ... } }提升(Hoisting)
對于下面的代碼,相信很多人都能一眼看出輸出結果,但是卻很少有人能給出為什么會產生這種輸出結果的解釋。
(function() { console.log(typeof foo); // 函數指針 console.log(typeof bar); // undefined var foo = "hello", bar = function() { return "world"; }; function foo() { return "hello"; } }());
1、為什么我們能在foo聲明之前訪問它?
回想在VO的創建階段,我們知道函數在該階段就已經被創建在變量對象中。所以在函數開始執行之前,foo已經被定義了。
2、Foo被聲明了兩次,為什么foo顯示為函數而不是undefined或字符串?
我們知道,在創建階段,函數聲明是優先于變量被創建的。而且在變量的創建過程中,如果發現VO中已經存在相同名稱的屬性,則不會影響已經存在的屬性。
因此,對foo()函數的引用首先被創建在活動對象里,并且當我們解釋到var foo時,我們看見foo屬性名已經存在,所以代碼什么都不做并繼續執行。
3、為什么bar的值是undefined?
bar采用的是函數表達式的方式來定義的,所以bar實際上是一個變量,但變量的值是函數,并且我們知道變量在創建階段被創建但他們被初始化為undefined,這也是為什么函數表達式不會被提升的原因。
1、EC分為兩個階段,創建執行上下文和執行代碼。
2、每個EC可以抽象為一個對象,這個對象具有三個屬性,分別為:作用域鏈Scope,VO|AO(AO,VO只能有一個)以及this。
3、函數EC中的AO在進入函數EC時,確定了Arguments對象的屬性;在執行函數EC時,其它變量屬性具體化。
4、EC創建的過程是由先后順序的:參數聲明 > 函數聲明 > 變量聲明
javascript 執行環境,變量對象,作用域鏈
What is the Execution Context & Stack in JavaScript?
函數MDN
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/82450.html
摘要:首次運行代碼時,會創建一個全局執行上下文并到當前的執行棧中。執行上下文的創建執行上下文分兩個階段創建創建階段執行階段創建階段確定的值,也被稱為。 (關注福利,關注本公眾號回復[資料]領取優質前端視頻,包括Vue、React、Node源碼和實戰、面試指導) 本周正式開始前端進階的第一期,本周的主題是調用堆棧,,今天是第一天 本計劃一共28期,每期重點攻克一個面試重難點,如果你還不了解本進...
摘要:每次調用函數時,都會創建一個新的執行上下文。理解執行上下文和堆棧可以讓您了解代碼為什么要計算您最初沒有預料到的不同值的原因。 首發:https://www.love85g.com/?p=1723 在這篇文章中,我將深入研究JavaScript最基本的部分之一,即執行上下文。在這篇文章的最后,您應該更清楚地了解解釋器要做什么,為什么在聲明一些函數/變量之前可以使用它們,以及它們的值是如何...
摘要:為什么會這樣這段代碼究竟是如何運行的執行上下文堆棧瀏覽器中的解釋器單線程運行。瀏覽器始終執行位于堆棧頂部的,并且一旦函數完成執行當前操作,它將從堆棧頂部彈出,將控制權返回到當前堆棧中的下方上下文。確定在上下文中的值。 原文:What is the Execution Context & Stack in JavaScript? git地址:JavaScript中的執行上下文和隊列(...
摘要:理解執行上下文和執行堆棧對于理解的其它概念如提升,范圍和閉包至關重要。正確地理解執行上下文和執行堆棧將幫助你更好地使用開發應用。引擎執行位于執行堆棧頂部的方法。當調用時,為該函數創建一個新的執行上下文,并且把它推入到當前執行堆棧。 By Sukhjinder Arora | Aug 28, 2018 原文 如果你是或者你想要成為一名js開發者,那么你必須了解js程序內部的運作。理解執行...
閱讀 3637·2021-11-19 09:40
閱讀 3096·2019-08-30 15:54
閱讀 2313·2019-08-30 15:44
閱讀 3196·2019-08-29 15:35
閱讀 3331·2019-08-29 12:22
閱讀 2861·2019-08-28 18:01
閱讀 3141·2019-08-26 13:54
閱讀 902·2019-08-26 12:24