摘要:在執行上下文棧中,全局執行上下文處于棧底,頂部為當前的執行上下文。可以把所有的程序執行看作一個執行上下文棧,棧的頂部是正在激活的上下文。
前言
??本文內容主要涵蓋了執行上下文棧、執行上下文、變量對象、函數變量提升等內容。
??眾所周知,JavaScript是單線程編程語言,同一時間只能做一件事情,程序執行順序由上而下,程序的執行主要依托JavaScript引擎,JavaScript引擎也并非一行一行的分析執行代碼,而是一段一段的分析執行。
可執行代碼??JavaScript引擎執行的代碼當然是可執行代碼,在JavaScript中可執行代碼有三類:全局代碼、函數代碼以及Eval代碼。
javaScript運行原理??JavaScript程序的執行主要分語法檢查和運行兩個階段,語法檢查包括詞法分析和語法分析,目的是將JavaScript高級語言程序轉成抽象語法樹。
??語法檢查完成后,到了執行階段,執行階段包括預解析和執行,預解析首先會創建執行上下文(本文重點),將語法檢查正確后生成的抽象語法樹復制到當前執行上下文中,然后做屬性填充,對語法樹當中的變量名、函數聲明以及函數的形參進行屬性填充。最后就是執行。
??JavaScript運行原理會在后面的文章輸出,不是本文的重點,本文只需知道程序運行的大致是怎樣的過程,執行上下文何時創建。
執行上下文棧??何為執行上下文棧???
??在JavaScript解釋器運行階段(預解析)還維護了一個棧,用于管理執行上下文。在執行上下文棧中,全局執行上下文處于棧底,頂部為當前的執行上下文。當頂部的執行完成,就會彈出棧,類似于數據結構中的棧,每當有當前的執行上下文執行完就會從棧頂彈出,這種管理執行上下文的棧叫做執行上下文棧。
??一個執行上下文可以激活另一個執行上下文,類似于函數調用另一個函數,可以一層一層的調用下去。
??激活其它執行上下文的某執行上下文被稱為調用者(caller),被激活的執行上下文被稱為被調用者(callee)。一個執行上下文即可能是調用者也有可能是被調用者。
??當一個caller激活了一個callee時,caller會暫停自身的執行,將控制權交給callee,此時該callee被放進執行上下文棧,稱為進行中的上下文,當這個callee上下文結束后,把控制權交還給它的caller,caller會在剛才暫停的地方繼續執行。在這個caller結束后,會繼續觸發其他的上下文。
執行上下文棧在JavaScript中可以數組模擬:
ECStack = [];
??當瀏覽器首次載入腳本,會默認先進入到全局執行上下文,位于執行上下文棧的最底部,此時全局代碼會開始初始化,初始化生成相應的對象和函數,在全局上下文執行的過程中可能會激活一些其他的方法(如果有的話),然后進入它們的執行上下文,并將元素壓入執行上下文棧中。可以把所有的程序執行看作一個執行上下文棧,棧的頂部是正在激活的上下文。如下表所示:
EC stack | |
---|---|
Active EC | |
... | |
EC | |
Global EC |
??在程序結束之前,ECStack最底部永遠是globalContext:
ECStack = [ globalContext ];
?? 看看下面實例一,是一個怎么的過程:
// 實例一 function bar() { console.log("bar"); } function foo() { bar(); } foo();
??當執行一個函數時,會創建一個執行上下文并壓入執行上下文棧中,當函數執行完畢,就將該執行上下文彈出執行上下文棧。
// 創建執行上下文棧 ECStack = []; // foo() 創建該函數執行上下文并壓入棧中 ECStack.push(執行上下文(Execution Context)functionContext); // foo()中調用了bar(),創建bar()執行上下文并壓入棧中 ECStack.push( functionContext); // bar()執行完畢彈出 ECStack.pop(); // foo()執行完畢彈出 ECStack.pop();
??執行上下文在程序運行的預解析階段創建,預解析也就是代碼的真正的執行前,可以說是代碼執行前的準備工作,即創建執行上下文。
??執行上下文有何用,主要做了三件事:
this綁定;
創建變量對象;
創建作用域鏈。
??this、作用域和作用域鏈也是JavaScript中很重要的知識點,后面的文章會詳細的輸出。
??何為執行上下文?
??執行上下文理解為是執行環境的抽象概念,當JavaScript代碼執行一段可執行代碼時,都會創建對應的執行上下文,一個執行上下文可以抽象的理解為object,都包括三個重要屬性:
executionContext: { variable object:vars, functions, arguments scope chain: variable object + all parents scopes thisValue: context object }全局代碼
??全局代碼不包含函數內代碼,在初始化階段,執行上下文棧底部有一個全局執行上下文:
ECStack = [ globalContext ];函數代碼
??當進入函數代碼時,函數執行,創建該函數執行上下文并壓入棧中。需要注意的是函數代碼不包含內部函數代碼。
ECStack = [Eval代碼functionContext ... functionContext globalContext ];
??eval(...)有些陌生,平時也很少用到,eval(...)函數可以接受一個字符串為參數,并將其中的內容視為好像在書寫就存在于程序中這個位置的代碼。
??換句話說,可以在你寫的代碼中用程序生成代碼并運行,就好像是寫在那個位置的一樣。
eval("var x = 10"); (function foo() { eval("var y = 20"); })(); console.log(x); // 10 console.log(y); // "y is not defined"
?? 上面實例執行過程:
ECStack = [ globalContext ]; // eval("var x = 10")進棧 ECStack.push( evalContext, callingContext: globalContext ); // eval出棧 ECStack.pop(); // foo funciton 進棧 ECStack.push(變量對象functionContext); // eval("var y = 20") 進棧 ECStack.push( evalContext, callingContext: functionContext ); // eval出棧 ECStack.pop(); // foo 出棧 ECStack.pop();
??變量對象(variable object)是與執行上下文相關的數據作用域(scope of data),是與上下文相關的特殊對象,用與存儲被定義在上下文中的變量(variables)和函數聲明(function declarations)。變量對象是一個抽象的概念,不同的上下文,它表示使用不同的對象。
全局變量對象??全局變量對象是全局上下文的變量對象。全局變量對象就是全局對象,為啥這么說:
全局對象(Global object) 是在進入任何執行上下文之前就已經創建了的對象;這個對象只存在一份,它的屬性在程序中任何地方都可以訪問,全局對象的生命周期終止于程序退出那一刻。
全局對象初始創建階段將Math、String、Date、parseInt作為自身屬性,等屬性初始化,同樣也可以有額外創建的其它對象作為屬性(其可以指向到全局對象自身)
在DOM中,全局對象的window屬性就可以引用全局對象自身
可以通過全局上下文的this來訪問全局對象,同樣也可以遞歸引用自身
當訪問全局對象的屬性時通常會忽略掉前綴,全局對象是不能通過名稱直接訪問的
global = { Math: <...>, String: <...>, Date: <...>, ... window: global } console.log(Math.random()); //當訪問全局對象的屬性時通常會忽略掉前綴;初始創建階段將Math等作為自身屬性 console.log(this.Math.random()); // 通過this來訪問全局對象 console.log(this) // window 通過全局上下文的this來訪問全局對象 var a = 1; console.log(this.a); // 1 console.log((window.a); // 1 全局對象有 window 屬性指向自身 console.log(a); // 1 當訪問全局對象的屬性時通常會忽略掉前綴 this.window.b = 2; console.log(this.b); // 2
??上面的全局對象的定義和變量對像的定義對比,能知道全局變量對象就是全局對象,簡單的說,因為變量對象用于存儲被定義在上下文中的變量和函數聲明,全局對象在進入任何執行上下文前就已經創建了,同樣存儲著在全局范圍內定義的變量和函數聲明。
??需要注意的是全局上下文的變量對象允許通過VO屬性名稱來間接訪問,原因就是全局變量對象就是全局對象,在其他上下文中是不能直接VO對象。
??全局變量對象VO會有下列屬性:
函數聲明(FunctionDeclaration, FD)
所有的變量聲明(var, VariableDeclaration)
不存在所謂的函數形參
函數上下文變量對象(Variable object)??當進入執行上下文時,VO包含來下列屬性:
函數形參,屬性名就是參數名,其值是實參值,若沒有傳遞的參數,其值為undefined;
函數聲明(FunctionDeclaration, FD),由名稱和對應值組成一個變量對象的屬性被創建;如果變量對象已經存在相同屬性名稱,則完全替換這個屬性。
所有的變量聲明(var, VariableDeclaration),由名稱和對應值(undefined)組成一個變量對象的屬性被創建;如果變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會干擾已經存在的這類屬性。
function foo(a) { var b = 2; var c = function() {}; function bar() { console.log("bar"); } } foo(10);
??當進入帶有參數10的foo函數執行上下文時,VO:
VO = { a: 10, bar:, b: undefined, c: undefined }
??在函數聲明過程中,如果變量對象已經存在相同的屬性名稱,則完全替換這個屬性:
function foo(a) { console.log(a); function a() {} } foo(10) // function a(){}
??在變量聲明過程中,如果變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會干擾已經存在的這類屬性
// 與參數名同名 function foo(a) { console.log(a); var a = 20; } foo(10) // 10 // 與函數名同名 function bar(){ console.log(a) var a = 10 function a(){} } bar() // "function a(){}"
??VO創建過程中,函數形參的優先級是高于函數的聲明的,結果是函數體內部聲明的function a(){}覆蓋了函數形參a的聲明,因此最后輸出a是一個function。
??從上面的實例說明,函數聲明比變量聲明的優先級高,在定義的過程中不會被變量覆蓋,除非是賦值:
function foo(a){ var a = 10 function a(){} console.log(a) } foo(20) // 10 function bar(a){ var a function a(){} console.log(a) } bar(20) // "function a(){}"活動對象(Activation object)
??活動對象想必大家對這個概念都不陌生,但是容易和變量對象混淆。
??活動對象就是函數上下文中的變量對象,只是不同階段的不同叫法,在創建函數執行上下文階段,變量對象被創建,變量對象的屬性不能被訪問,此時的函數還沒有執行,當函數來到執行階段,變量對象被激活,變成了活動對象,并且里面的屬性都能訪問到,開始進行執行階段的操作。
// 執行階段 VO -> AO function foo(a) { var b = 2; var c = function() {}; function bar() { console.log("bar"); } } foo(10); VO = { arguments: { 0: 10, length: 1 }, a: 10, bar:, b: undefined, c: undefined } AO = { arguments: { 0: 10, length: 1 }, a: 10, bar: , b: 2, c: reference to FunctionExpression "c" }
??調用函數時,會為其創建一個Arguments對象,并自動初始化局部變量arguments,指代該Arguments對象。所有作為參數傳入的值都會成為Arguments對象的數組元素。
??簡潔的總結下上面的內容:
全局上下文的變量對象是全局對象;
函數上下文的變量對象初始化只包含Arguments對象;
在進入執行上下文時會給變量對象添加形參、函數聲明及變量聲明等屬性;
在代碼執行,可以通過賦值修改變量對象的屬性。
提升??提升一個很常見的話題,是面試中經常被問到的一部分,函數聲明優先級比變量聲明高,這句話應該是大部分同學都會回答,為啥,上面的內容已經很好的做出了解釋。看下面實例:
function test() { console.log(foo); // function foo(){} console.log(bar); // undefined var foo = "Hello"; console.log(foo); // Hello var bar = function () { return "world"; } function foo() { return "hello"; } } test();
// 創建階段 VO = { arguments: { length: 0 }, foo:, // 解釋了第一個輸出是foo引用,函數聲明優先變量被創建,同名屬性不會被干擾,在函數還沒有被調用前已經被創建了,即能輸出foo的引用 bar: undefined // 解釋了第二個輸出是undefined,函數表達式還是只是一個變量聲明,不是函數聲明,不會被提升 }
// 執行階段 VO -> OV OV = { arguments: { length: 0 }, foo: "Hello", // 這里解釋了為什么第三個輸出值為‘Hello’,做了賦值操作 bar: reference to FunctionExpression "bar" }
// 實例真實的執行順序 function test() { function foo() { return "hello"; } } var foo; var bar; console.log(foo); console.log(bar); foo = "Hello"; console.log(foo); bar = function () { return "world"; } }
??需要注意的是變量提升只存在使用var關鍵字聲明變量,如果是使用let聲明變量不存在變量提升。
??聲明變量的作用域限制在其聲明位置的上下文中,在上下文被創建的階段時創建了,如果沒有聲明的變量總是全局的,并且是在執行階段將賦值給未聲明的變量的值被隱式創建為全局變量,可以通過delete操作符刪除,聲明變量不可以。
function foo() { console.log(a); // Uncaught ReferenceError: a is not defined;a不存在VO中 a = 1; console.log(a); } foo(); function bar() { a = 1; console.log(a); // 1 可以在全局變量中找到a的值 } bar(); c = 10; console.log(delete c); // true var d = 10; console.log(delete d); // false
??如果清楚上下文相關的內容,提升的問題很好的能解答,在學習中我們還是需要了解一些底層的知識,這樣有助我們更好的進步。
結語??文章如有不正確的地方歡迎各位大佬指正,也希望有幸看到文章的同學也有收獲,一起成長!
——本文首發于個人公眾號———
最后,歡迎大家關注我的公眾號,一起學習交流。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/106685.html
摘要:也就是說,當代碼執行的時候,會進入不同的執行上下文,這些執行上下文就構成了一個執行上下文棧,。它是一個與上下文相關的特殊對象,其中存儲了在上下文中定義的變量和函數聲明。 明白的人,看標題這么寫,會發現是有問題的,對的,在JavaScript中執行上下文與執行環境是同一個東西,標題這么寫肯定是有問題的。但是有些人是搞不清執行上下文與執行環境的,所以我才這么寫,以便于他們好搜索到。下面我們...
摘要:執行上下文的執行階段,也有三個內容變量賦值函數引用執行其他代碼。的簡寫,叫做活動對象。先說一下變量對象,它的結構大致如此,在函數被調用的時候被創建變量對象包含函數的形參函數聲明變量聲明,三個內容。 關于javascript中的變量對象和活動對象 我GitHub上的菜鳥倉庫地址: 點擊跳轉查看其他相關文章 文章在我的博客上的地址: 點擊跳轉 ? ? ? ? 前面的文章說到, 執行上下...
摘要:關于提供了一種優雅的方式來隱式傳遞一個對象引用,因此可以將設計得更加簡潔并且易于復用。對于的誤解新手會誤認為指向函數本身。這時候,可以使用的方法強制使指向函數對象。的綁定和函數聲明的位置沒有任何關系,只取決于函數的調用方式。 關于this this 提供了一種優雅的方式來隱式傳遞一個對象引用,因此可以將API設計得更加簡潔并且易于復用。 /* *this 隱式傳遞...
摘要:深入系列第四篇,具體講解執行上下文中的變量對象與活動對象。下一篇文章深入之作用域鏈本文相關鏈接深入之執行上下文棧深入系列深入系列目錄地址。 JavaScript深入系列第四篇,具體講解執行上下文中的變量對象與活動對象。全局上下文下的變量對象是什么?函數上下文下的活動對象是如何分析和執行的?還有兩個思考題幫你加深印象,快來看看吧! 前言 在上篇《JavaScript深入之執行上下文棧》中...
摘要:一系列活動的執行上下文從邏輯上形成一個棧。棧底總是全局上下文,棧頂是當前活動的執行上下文。同樣的,當拋出未捕獲的異常時,也會退出一個或者多個執行上下文,也會做相應的退棧操作。 概要 本文將向大家介紹ECMAScript的執行上下文以及相關的可執行代碼類型。 定義 每當控制器到達ECMAScript可執行代碼的時候,控制器就進入了一個執行上下文。 執行上下文(簡稱:EC)是個抽象的...
閱讀 3392·2021-11-24 09:38
閱讀 1390·2021-11-22 15:08
閱讀 1463·2021-09-29 09:35
閱讀 483·2021-09-02 15:11
閱讀 1308·2019-08-30 12:55
閱讀 391·2019-08-29 17:16
閱讀 496·2019-08-29 11:30
閱讀 422·2019-08-26 13:23