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