深入理解JavaScript執(zhí)行上下文、函數(shù)堆棧、提升的概念
首先明確幾個概念:
EC :函數(shù)執(zhí)行環(huán)境(或執(zhí)行上下文),Execution Context
ECS :執(zhí)行環(huán)境棧,Execution Context Stack
VO :變量對象,Variable Object
AO :活動對象,Active Object
scope chain :作用域鏈
想當初自己看到這幾個概念的時候是一(m)臉(d)懵(z)逼(z),但是不得不說這幾個概念對以后深入學習JS有很大的幫助。來不及解釋了,趕緊上車~
EC(執(zhí)行上下文)每次當控制器轉到ECMAScript可執(zhí)行代碼的時候,就會進入到一個執(zhí)行上下文。
那什么是可執(zhí)行代碼呢?
可執(zhí)行代碼的類型
全局代碼( Global code )
這種類型的代碼是在'程序'級處理的:例如加載外部的js文件或者本地 <script></script> 標簽內的代碼。 全局代碼不包括任何function體內的代碼 。 這個是默認的代碼運行環(huán)境,一旦代碼被載入,引擎最先進入的就是這個環(huán)境。
函數(shù)代碼( Function code )
任何一個函數(shù)體內的代碼,但是需要注意的是, 具體的函數(shù)體內的代碼是不包括內部函數(shù)的代碼 。
Eval代碼( Eval code )
eval內部的代碼
這里僅僅引入EC這個概念,后面還有關于EC建立細節(jié)的介紹。
ECS(執(zhí)行環(huán)境棧)我們用MDN上的一個例子來引入函數(shù)執(zhí)行棧的概念
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
這里先不關心執(zhí)行結果。磨刀不誤砍柴功,先了解一下函數(shù)執(zhí)行上下文堆棧的概念。相信弄明白了下面的概念,一切也就水落石出了
我們都知道,瀏覽器中的JS解釋器被實現(xiàn)為單線程,這也就意味著同一時間只能發(fā)生一件事情,其他的行為或事件將會被放在叫做執(zhí)行棧里面排隊。下面的圖是單線程棧的抽象視圖:
當瀏覽器首次載入你的腳本,它將 默認進入全局執(zhí)行上下文 。如果,你在你的全局代碼中調用一個函數(shù),你程序的時序將進入被調用的函數(shù),并創(chuàng)建一個新的執(zhí)行上下文,并將新創(chuàng)建的上下文壓入執(zhí)行棧的頂部。
如果你調用當前函數(shù)內部的其他函數(shù),相同的事情會在此上演。 代碼的執(zhí)行流程進入內部函數(shù),創(chuàng)建一個新的執(zhí)行上下文并把它壓入執(zhí)行棧的頂部。瀏覽器總會執(zhí)行位于棧頂?shù)膱?zhí)行上下文,一旦當前上下文函數(shù)執(zhí)行結束,它將被從棧頂彈出,并將上下文控制權交給當前的棧 。這樣,堆棧中的上下文就會被依次執(zhí)行并且彈出堆棧,直到回到全局的上下文。
看到這里,想必大家都已經(jīng)深諳上述例子輸出結果的原因了,這里我大概繪了一個流程圖來幫助理解。
這里為什么要用一個 / 呢?按照字面理解,AO其實就是被激活的VO,兩個其實是一個東西。下面引用知乎上的一段話,幫助理解一下。 原文鏈接
變量對象 (Variable object) 是說JS的執(zhí)行上下文中都有個對象用來存放執(zhí)行上下文中可被訪問但是不能被 delete 的 函數(shù)標示符 、 形參 、 變量聲明 等。它們會被掛在這個對象上,對象的屬性對應它們的名字對象屬性的值對應它們的值但這個對象是規(guī)范上或者說是引擎實現(xiàn)上的不可在JS環(huán)境中訪問到活動對象
激活對象 (Activation object) 有了變量對象存每個上下文中的東西,但是它什么時候能被訪問到呢?就是每進入一個執(zhí)行上下文時,這個執(zhí)行上下文兒中的變量對象就被激活,也就是該上下文中的函數(shù)標示符、形參、變量聲明等就可以被訪問到了
EC 建立的細節(jié)
1、創(chuàng)建階段【當函數(shù)被調用,但未執(zhí)行任何其內部代碼之前】
創(chuàng)建作用域鏈(Scope Chain)
創(chuàng)建變量,函數(shù)和參數(shù)。
求”this“的值
2、執(zhí)行階段
初始化變量的值和函數(shù)的引用,解釋/執(zhí)行代碼。
我們可以將每個執(zhí)行上下文抽象為一個對象,這個對象具有三個屬性
ECObj: { scopeChain: { /* 變量對象(variableObject)+ 所有父級執(zhí)行上下文的變量對象*/ }, variableObject: { /*函數(shù) arguments/參數(shù),內部變量和函數(shù)聲明 */ }, this: {} }
解釋器執(zhí)行代碼的偽邏輯
1、查找調用函數(shù)的代碼。
2、執(zhí)行代碼之前,先進入創(chuàng)建上下文階段:
初始化作用域鏈
創(chuàng)建變量對象:
創(chuàng)建arguments對象,檢查上下文,初始化參數(shù)名稱和值并創(chuàng)建引用的復制。
掃描上下文的函數(shù)聲明(而非函數(shù)表達式):
為發(fā)現(xiàn)的每一個函數(shù),在變量對象上創(chuàng)建一個屬性——確切的說是函數(shù)的名字——其有一個指向函數(shù)在內存中的引用。
如果函數(shù)的名字已經(jīng)存在,引用指針將被重寫。
掃描上下文的變量聲明:
為發(fā)現(xiàn)的每個變量聲明,在變量對象上創(chuàng)建一個屬性——就是變量的名字,并且將變量的值初始化為undefined
如果變量的名字已經(jīng)在變量對象里存在,將不會進行任何操作并繼續(xù)掃描。
求出上下文內部“this”的值。
3、激活/代碼執(zhí)行階段:
在當前上下文上運行/解釋函數(shù)代碼,并隨著代碼一行行執(zhí)行指派變量的值。
VO --- 對應上述第二個階段
function foo(i){ var a = ’hello’ var b = function(){} function c(){}}foo(22)
當我們調用 foo(22) 時,整個創(chuàng)建階段是下面這樣的
ECObj = {scopChain: {...}, variableObject: { arguments: {0: 22,length: 1 }, i: 22, c: pointer to function c() a: undefined, b: undefined},this: { ... } }
正如我們看到的,在上下文創(chuàng)建階段,VO的初始化過程如下( 該過程是有先后順序的:函數(shù)的形參==>>函數(shù)聲明==>>變量聲明 ):
函數(shù)的形參(當進入函數(shù)執(zhí)行上下文時) —— 變量對象的一個屬性,其屬性名就是形參的名字,其值就是實參的值;對于沒有傳遞的參數(shù),其值為undefined
函數(shù)聲明(FunctionDeclaration, FD) —— 變量對象的一個屬性,其屬性名和值都是函數(shù)對象創(chuàng)建出來的; 如果變量對象已經(jīng)包含了相同名字的屬性,則替換它的值
變量聲明(var,VariableDeclaration) —— 變量對象的一個屬性,其屬性名即為變量名,其值為undefined; 如果變量名和已經(jīng)聲明的函數(shù)名或者函數(shù)的參數(shù)名相同,則不會影響已經(jīng)存在的屬性。
對于函數(shù)的形參沒有什么可說的,主要看一下函數(shù)的聲明以及變量的聲明兩個部分。
1、如何理解函數(shù)聲明過程中 如果變量對象已經(jīng)包含了相同名字的屬性,則替換它的值 這句話?
看如下這段代碼:
function foo1(a){ console.log(a) function a(){} }foo1(20)//’function a(){}’
根據(jù)上面的介紹,我們知道VO創(chuàng)建過程中,函數(shù)形參的優(yōu)先級是高于函數(shù)的聲明的,結果是函數(shù)體內部聲明的 function a(){} 覆蓋了函數(shù)形參 a 的聲明,因此最后輸出 a 是一個 function
2、如何理解變量聲明過程中 如果變量名和已經(jīng)聲明的函數(shù)名或者函數(shù)的參數(shù)名相同,則不會影響已經(jīng)存在的屬性 這句話?
//情景一:與參數(shù)名相同function foo2(a){ console.log(a) var a = 10}foo2(20) //’20’//情景二:與函數(shù)名相同function foo2(){ console.log(a) var a = 10 function a(){}}foo2() //’function a(){}’
下面是幾個比較有趣的例子,當做加餐小菜,大家細細品味。這里給出一句話當做參考:
函數(shù)的聲明比變量優(yōu)先級要高,并且定義過程不會被變量覆蓋,除非是賦值
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(){}’
AO --- 對應第三個階段
正如我們看到的,創(chuàng)建的過程僅負責處理定義屬性的名字,而并不為他們指派具體的值,當然還有對形參/實參的處理。一旦創(chuàng)建階段完成,執(zhí)行流進入函數(shù)并且激活/代碼執(zhí)行階段,看下函數(shù)執(zhí)行完成后的樣子:
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); // 函數(shù)指針 console.log(typeof bar); // undefined var foo = ’hello’,bar = function() { return ’world’;}; function foo() {return ’hello’; }}());
1、為什么我們能在foo聲明之前訪問它?
回想在 VO 的創(chuàng)建階段,我們知道函數(shù)在該階段就已經(jīng)被創(chuàng)建在變量對象中。所以在函數(shù)開始執(zhí)行之前,foo已經(jīng)被定義了。
2、Foo被聲明了兩次,為什么foo顯示為函數(shù)而不是undefined或字符串?
我們知道,在創(chuàng)建階段,函數(shù)聲明是優(yōu)先于變量被創(chuàng)建的。而且在變量的創(chuàng)建過程中,如果發(fā)現(xiàn) VO 中已經(jīng)存在相同名稱的屬性,則不會影響已經(jīng)存在的屬性。
因此,對 foo() 函數(shù)的引用首先被創(chuàng)建在活動對象里,并且當我們解釋到var foo時,我們看見 foo 屬性名已經(jīng)存在,所以代碼什么都不做并繼續(xù)執(zhí)行。
3、為什么bar的值是undefined?
bar 采用的是函數(shù)表達式的方式來定義的,所以 bar 實際上是一個變量,但變量的值是函數(shù),并且我們知道變量在創(chuàng)建階段被創(chuàng)建但他們被初始化為 undefined ,這也是為什么函數(shù)表達式不會被提升的原因。
總結:
1、 EC 分為兩個階段,創(chuàng)建執(zhí)行上下文和執(zhí)行代碼。
2、每個 EC 可以抽象為一個對象,這個對象具有三個屬性,分別為:作用域鏈 Scope , VO|AO ( AO , VO 只能有一個)以及 this 。
3、函數(shù) EC 中的 AO 在進入函數(shù) EC 時,確定了Arguments對象的屬性;在執(zhí)行函數(shù) EC 時,其它變量屬性具體化。
4、 EC 創(chuàng)建的過程是由先后順序的:參數(shù)聲明 > 函數(shù)聲明 > 變量聲明
參考
javascript 執(zhí)行環(huán)境,變量對象,作用域鏈
What is the Execution Context & Stack in JavaScript?
函數(shù)MDN
來自:https://segmentfault.com/a/1190000009041008
相關文章:
1. 解決Python 進程池Pool中一些坑2. Python如何讀寫CSV文件3. php網(wǎng)絡安全中命令執(zhí)行漏洞的產生及本質探究4. 三個不常見的 HTML5 實用新特性簡介5. 無線標記語言(WML)基礎之WMLScript 基礎第1/2頁6. ajax請求添加自定義header參數(shù)代碼7. php測試程序運行速度和頁面執(zhí)行速度的代碼8. Python獲取抖音關注列表封號賬號的實現(xiàn)代碼9. python利用os模塊編寫文件復制功能——copy()函數(shù)用法10. Python使用jupyter notebook查看ipynb文件過程解析
