跟我學(xué)習(xí)javascript的執(zhí)行上下文
在這篇文章里,我將深入研究JavaScript中最基本的部分——執(zhí)行上下文(execution context)。讀完本文后,你應(yīng)該清楚了解釋器做了什么,為什么函數(shù)和變量能在聲明前使用以及他們的值是如何決定的。
1、EC—執(zhí)行環(huán)境或者執(zhí)行上下文
每當(dāng)控制器到達(dá)ECMAScript可執(zhí)行代碼的時(shí)候,控制器就進(jìn)入了一個(gè)執(zhí)行上下文(好高大上的概念?。?。
javascript中,EC分為三種:
- 全局級(jí)別的代碼 –– 這個(gè)是默認(rèn)的代碼運(yùn)行環(huán)境,一旦代碼被載入,引擎最先進(jìn)入的就是這個(gè)環(huán)境。
- 函數(shù)級(jí)別的代碼 ––當(dāng)執(zhí)行一個(gè)函數(shù)時(shí),運(yùn)行函數(shù)體中的代碼。
- Eval的代碼 –– 在Eval函數(shù)內(nèi)運(yùn)行的代碼。
EC建立分為兩個(gè)階段:進(jìn)入執(zhí)行上下文(創(chuàng)建階段)和執(zhí)行階段(激活/執(zhí)行代碼)。
1)、進(jìn)入上下文階段:發(fā)生在函數(shù)調(diào)用時(shí),但是在執(zhí)行具體代碼之前(比如,對(duì)函數(shù)參數(shù)進(jìn)行具體化之前)
創(chuàng)建作用域鏈(Scope Chain)
創(chuàng)建變量,函數(shù)和參數(shù)。
求”this“的值。
2)、執(zhí)行代碼階段:
變量賦值
函數(shù)引用
解釋/執(zhí)行其他代碼。
我們可以將EC看做是一個(gè)對(duì)象。
EC={ VO:{/* 函數(shù)中的arguments對(duì)象, 參數(shù), 內(nèi)部的變量以及函數(shù)聲明 */}, this:{}, Scope:{ /* VO以及所有父執(zhí)行上下文中的VO */} }
現(xiàn)在讓我們看一個(gè)包含全局和函數(shù)上下文的代碼例子:
很簡(jiǎn)單的例子,我們有一個(gè)被紫色邊框圈起來(lái)的全局上下文和三個(gè)分別被綠色,藍(lán)色和橘色框起來(lái)的不同函數(shù)上下文。只有全局上下文(的變量)能被其他任何上下文訪問(wèn)。
你可以有任意多個(gè)函數(shù)上下文,每次調(diào)用函數(shù)創(chuàng)建一個(gè)新的上下文,會(huì)創(chuàng)建一個(gè)私有作用域,函數(shù)內(nèi)部聲明的任何變量都不能在當(dāng)前函數(shù)作用域外部直接訪問(wèn)。在上面的例子中,函數(shù)能訪問(wèn)當(dāng)前上下文外面的變量聲明,但在外部上下文不能訪問(wèn)內(nèi)部的變量/函數(shù)聲明。為什么會(huì)發(fā)生這種情況?代碼到底是如何被解釋的?
2、ECS—執(zhí)行上下文棧
一系列活動(dòng)的執(zhí)行上下文從邏輯上形成一個(gè)棧。棧底總是全局上下文,棧頂是當(dāng)前(活動(dòng)的)執(zhí)行上下文。當(dāng)在不同的執(zhí)行上下文間切換(退出的而進(jìn)入新的執(zhí)行上下文)的時(shí)候,棧會(huì)被修改(通過(guò)壓?;蛘咄藯5男问剑?。
壓棧:全局EC—>局部EC1—>局部EC2—>當(dāng)前EC
出棧:全局EC<—局部EC1<—局部EC2<—當(dāng)前EC
我們可以用數(shù)組的形式來(lái)表示環(huán)境棧:
ECS=[局部EC,全局EC];
每次控制器進(jìn)入一個(gè)函數(shù)(哪怕該函數(shù)被遞歸調(diào)用或者作為構(gòu)造器),都會(huì)發(fā)生壓棧的操作。過(guò)程類似javascript數(shù)組的push和pop操作。
瀏覽器里的JavaScript解釋器被實(shí)現(xiàn)為單線程。這意味著同一時(shí)間只能發(fā)生一件事情,其他的行文或事件將會(huì)被放在叫做執(zhí)行棧里面排隊(duì)。下面的圖是單線程棧的抽象視圖:
我們已經(jīng)知道,當(dāng)瀏覽器首次載入你的腳本,它將默認(rèn)進(jìn)入全局執(zhí)行上下文。如果,你在你的全局代碼中調(diào)用一個(gè)函數(shù),你程序的時(shí)序?qū)⑦M(jìn)入被調(diào)用的函數(shù),并穿件一個(gè)新的執(zhí)行上下文,并將新創(chuàng)建的上下文壓入執(zhí)行棧的頂部。
如果你調(diào)用當(dāng)前函數(shù)內(nèi)部的其他函數(shù),相同的事情會(huì)在此上演。代碼的執(zhí)行流程進(jìn)入內(nèi)部函數(shù),創(chuàng)建一個(gè)新的執(zhí)行上下文并把它壓入執(zhí)行棧的頂部。瀏覽器將總會(huì)執(zhí)行棧頂?shù)膱?zhí)行上下文,一旦當(dāng)前上下文函數(shù)執(zhí)行結(jié)束,它將被從棧頂彈出,并將上下文控制權(quán)交給當(dāng)前的棧。下面的例子顯示遞歸函數(shù)的執(zhí)行棧調(diào)用過(guò)程:
(function foo(i) { if (i === 3) { return; } else { foo(++i); } }(0));
這代碼調(diào)用自己三次,每次給i的值加一。每次foo函數(shù)被調(diào)用,將創(chuàng)建一個(gè)新的執(zhí)行上下文。一旦上下文執(zhí)行完畢,它將被從棧頂彈出,并將控制權(quán)返回給下面的上下文,直到只剩全局上下文能為止。
有5個(gè)需要記住的關(guān)鍵點(diǎn),關(guān)于執(zhí)行棧(調(diào)用棧):
- 單線程。
- 同步執(zhí)行。
- 一個(gè)全局上下文。
- 無(wú)限制函數(shù)上下文。
- 每次函數(shù)被調(diào)用創(chuàng)建新的執(zhí)行上下文,包括調(diào)用自己。
3、VO—變量對(duì)象
每一個(gè)EC都對(duì)應(yīng)一個(gè)變量對(duì)象VO,在該EC中定義的所有變量和函數(shù)都存放在其對(duì)應(yīng)的VO中。
VO分為全局上下文VO(全局對(duì)象,Global object,我們通常說(shuō)的global對(duì)象)和函數(shù)上下文的AO。
VO: { // 上下文中的數(shù)據(jù) ( 函數(shù)形參(function arguments), 函數(shù)聲明(FD),變量聲明(var)) }
1)、進(jìn)入執(zhí)行上下文時(shí),VO的初始化過(guò)程具體如下:
函數(shù)的形參(當(dāng)進(jìn)入函數(shù)執(zhí)行上下文時(shí))—— 變量對(duì)象的一個(gè)屬性,其屬性名就是形參的名字,其值就是實(shí)參的值;對(duì)于沒(méi)有傳遞的參數(shù),其值為undefined;
函數(shù)聲明(FunctionDeclaration, FD) —— 變量對(duì)象的一個(gè)屬性,其屬性名和值都是函數(shù)對(duì)象創(chuàng)建出來(lái)的;如果變量對(duì)象已經(jīng)包含了相同名字的屬性,則替換它的值;
變量聲明(var,VariableDeclaration) —— 變量對(duì)象的一個(gè)屬性,其屬性名即為變量名,其值為undefined;如果變量名和已經(jīng)聲明的函數(shù)名或者函數(shù)的參數(shù)名相同,則不會(huì)影響已經(jīng)存在的屬性。
注意:該過(guò)程是有先后順序的。
2)、 執(zhí)行代碼階段時(shí),VO中的一些屬性u(píng)ndefined值將會(huì)確定。
4、AO活動(dòng)對(duì)象
在函數(shù)的執(zhí)行上下文中,VO是不能直接訪問(wèn)的。它主要扮演被稱作活躍對(duì)象(activation object)(簡(jiǎn)稱:AO)的角色。
這句話怎么理解呢,就是當(dāng)EC環(huán)境為函數(shù)時(shí),我們?cè)L問(wèn)的是AO,而不是VO。
VO(functionContext) === AO;
AO是在進(jìn)入函數(shù)的執(zhí)行上下文時(shí)創(chuàng)建的,并為該對(duì)象初始化一個(gè)arguments屬性,該屬性的值為Arguments對(duì)象。
AO = { arguments: { callee:, length:, properties-indexes: //函數(shù)傳參參數(shù)值 } };
FD的形式只能是如下這樣:
function f(){ }
當(dāng)函數(shù)被調(diào)用是executionContextObj被創(chuàng)建,但在實(shí)際函數(shù)執(zhí)行之前。這是我們上面提到的第一階段,創(chuàng)建階段。在此階段,解釋器掃描傳遞給函數(shù)的參數(shù)或arguments,本地函數(shù)聲明和本地變量聲明,并創(chuàng)建executionContextObj對(duì)象。掃描的結(jié)果將完成變量對(duì)象的創(chuàng)建。
內(nèi)部的執(zhí)行順序如下:
1、查找調(diào)用函數(shù)的代碼。
2、執(zhí)行函數(shù)代碼之前,先創(chuàng)建執(zhí)行上下文。
3、進(jìn)入創(chuàng)建階段:
- 初始化作用域鏈:
- 創(chuàng)建變量對(duì)象:
- 創(chuàng)建arguments對(duì)象,檢查上下文,初始化參數(shù)名稱和值并創(chuàng)建引用的復(fù)制。
- 掃描上下文的函數(shù)聲明:為發(fā)現(xiàn)的每一個(gè)函數(shù),在變量對(duì)象上創(chuàng)建一個(gè)屬性(確切的說(shuō)是函數(shù)的名字),其有一個(gè)指向函數(shù)在內(nèi)存中的引用。如果函數(shù)的名字已經(jīng)存在,引用指針將被重寫。
- 掃面上下文的變量聲明:為發(fā)現(xiàn)的每個(gè)變量聲明,在變量對(duì)象上創(chuàng)建一個(gè)屬性——就是變量的名字,并且將變量的值初始化為undefined,如果變量的名字已經(jīng)在變量對(duì)象里存在,將不會(huì)進(jìn)行任何操作并繼續(xù)掃描。
- 求出上下文內(nèi)部“this”的值。
4、激活/代碼執(zhí)行階段:
在當(dāng)前上下文上運(yùn)行/解釋函數(shù)代碼,并隨著代碼一行行執(zhí)行指派變量的值。
示例
1、具體實(shí)例
function foo(i) { var a = ‘hello‘; var b = function privateB() { }; function c() { } } foo(22);
當(dāng)調(diào)用foo(22)時(shí),創(chuàng)建狀態(tài)像下面這樣:
fooExecutionContext = { scopeChain: { ... }, variableObject: { arguments: { 0: 22, length: 1 }, i: 22, c: pointer to function c() a: undefined, b: undefined }, this: { ... } }
真如你看到的,創(chuàng)建狀態(tài)負(fù)責(zé)處理定義屬性的名字,不為他們指派具體的值,以及形參/實(shí)參的處理。一旦創(chuàng)建階段完成,執(zhí)行流進(jìn)入函數(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: { ... } }
2、VO示例:
alert(x); // function var x = 10; alert(x); // 10 x = 20; function x() {}; alert(x); // 20
進(jìn)入執(zhí)行上下文時(shí),
ECObject={ VO:{ x:<reference to FunctionDeclaration "x"> } };
執(zhí)行代碼時(shí):
ECObject={ VO:{ x:20 //與函數(shù)x同名,替換掉,先是10,后變成20 } };
對(duì)于以上的過(guò)程,我們?cè)敿?xì)解釋下。
在進(jìn)入上下文的時(shí)候,VO會(huì)被填充函數(shù)聲明; 同一階段,還有變量聲明“x”,但是,正如此前提到的,變量聲明是在函數(shù)聲明和函數(shù)形參之后,并且,變量聲明不會(huì)對(duì)已經(jīng)存在的同樣名字的函數(shù)聲明和函數(shù)形參發(fā)生沖突。因此,在進(jìn)入上下文的階段,VO填充為如下形式:
VO = {}; VO['x'] = <引用了函數(shù)聲明'x'> // 發(fā)現(xiàn)var x = 10; // 如果函數(shù)“x”還未定義 // 則 "x" 為undefined, 但是,在我們的例子中 // 變量聲明并不會(huì)影響同名的函數(shù)值 VO['x'] = <值不受影響,仍是函數(shù)>
執(zhí)行代碼階段,VO被修改如下:
VO['x'] = 10; VO['x'] = 20;
如下例子再次看到在進(jìn)入上下文階段,變量存儲(chǔ)在VO中(因此,盡管else的代碼塊永遠(yuǎn)都不會(huì)執(zhí)行到,而“b”卻仍然在VO中)
if (true) { var a = 1; } else { var b = 2; } alert(a); // 1 alert(b); // undefined, but not "b is not defined"
3、AO示例:
function test(a, b) { var c = 10; function d() {} var e = function _e() {}; (function x() {}); } test(10); // call
當(dāng)進(jìn)入test(10)的執(zhí)行上下文時(shí),它的AO為:
testEC={ AO:{ arguments:{ callee:test length:1, 0:10 }, a:10, c:undefined, d:<reference to FunctionDeclaration "d">, e:undefined } };
由此可見,在建立階段,VO除了arguments,函數(shù)的聲明,以及參數(shù)被賦予了具體的屬性值,其它的變量屬性默認(rèn)的都是undefined。函數(shù)表達(dá)式不會(huì)對(duì)VO造成影響,因此,(function x() {})并不會(huì)存在于VO中。
當(dāng)執(zhí)行 test(10)時(shí),它的AO為:
testEC={ AO:{ arguments:{ callee:test, length:1, 0:10 }, a:10, c:10, d:<reference to FunctionDeclaration "d">, e:<reference to FunctionDeclaration "e"> } };
可見,只有在這個(gè)階段,變量屬性才會(huì)被賦具體的值。
5、提升(Hoisting)解密
在之前的JavaScript Item中降到了變量和函數(shù)聲明被提升到函數(shù)作用域的頂部。然而,沒(méi)有人解釋為什么會(huì)發(fā)生這種情況的細(xì)節(jié),學(xué)習(xí)了上面關(guān)于解釋器如何創(chuàng)建active活動(dòng)對(duì)象的新知識(shí),很容易明白為什么。看下面的例子:
(function() { console.log(typeof foo); // 函數(shù)指針 console.log(typeof bar); // undefined var foo = ‘hello‘, bar = function() { return ‘world‘; }; function foo() { return ‘hello‘; } }());
我們能回答下面的問(wèn)題:
1、為什么我們能在foo聲明之前訪問(wèn)它?
如果我們跟隨創(chuàng)建階段,我們知道變量在激活/代碼執(zhí)行階段已經(jīng)被創(chuàng)建。所以在函數(shù)開始執(zhí)行之前,foo已經(jīng)在活動(dòng)對(duì)象里面被定義了。
2、foo被聲明了兩次,為什么foo顯示為函數(shù)而不是undefined或字符串?
盡管foo被聲明了兩次,我們知道從創(chuàng)建階段函數(shù)已經(jīng)在活動(dòng)對(duì)象里面被創(chuàng)建,這一過(guò)程發(fā)生在變量創(chuàng)建之前,并且如果屬性名已經(jīng)在活動(dòng)對(duì)象上存在,我們僅僅更新引用。
因此,對(duì)foo()函數(shù)的引用首先被創(chuàng)建在活動(dòng)對(duì)象里,并且當(dāng)我們解釋到var foo時(shí),我們看見foo屬性名已經(jīng)存在,所以代碼什么都不做并繼續(xù)執(zhí)行。
3、為什么bar的值是undefined?
bar實(shí)際上是一個(gè)變量,但變量的值是函數(shù),并且我們知道變量在創(chuàng)建階段被創(chuàng)建但他們被初始化為undefined。
以上就是本文的全部?jī)?nèi)容,有詳細(xì)的問(wèn)題解答,示例代碼,幫助大家更加了解javascript的執(zhí)行上下文,希望大家喜歡這篇文章。
- 淺析JavaScript作用域鏈、執(zhí)行上下文與閉包
- 深入理解JavaScript 中的執(zhí)行上下文和執(zhí)行棧
- 深入理解JavaScript系列(11) 執(zhí)行上下文(Execution Contexts)
- 一篇文章弄懂javascript中的執(zhí)行棧與執(zhí)行上下文
- 對(duì)于Javascript 執(zhí)行上下文的全面了解
- 通過(guò)實(shí)例了解JS執(zhí)行上下文運(yùn)行原理
- 深入探討JavaScript的最基本部分之執(zhí)行上下文
- 簡(jiǎn)單了解JavaScript中的執(zhí)行上下文和堆棧
- Javascript執(zhí)行上下文順序的深入講解
相關(guān)文章
Javascript oop設(shè)計(jì)模式 面向?qū)ο缶幊毯?jiǎn)單實(shí)例介紹
這篇文章主要介紹了Javascript oop設(shè)計(jì)模式 面向?qū)ο缶幊毯?jiǎn)單實(shí)例介紹的相關(guān)資料,這里附有實(shí)例代碼幫助大家學(xué)習(xí)理解,需要的朋友可以參考下2016-12-12JS/jquery實(shí)現(xiàn)一個(gè)網(wǎng)頁(yè)內(nèi)同時(shí)調(diào)用多個(gè)倒計(jì)時(shí)的方法
這篇文章主要介紹了JS/jquery實(shí)現(xiàn)一個(gè)網(wǎng)頁(yè)內(nèi)同時(shí)調(diào)用多個(gè)倒計(jì)時(shí)的方法,涉及js與jQuery基于定時(shí)器的時(shí)間相關(guān)操作技巧,需要的朋友可以參考下2017-04-04js使用oclif開發(fā)命令行工具實(shí)現(xiàn)批量修改文件名
前端開發(fā)工作中常用的很多CLI命令相信大家已經(jīng)很熟悉了,很方便很實(shí)用,能夠快速幫助你創(chuàng)建項(xiàng)目,快速執(zhí)行某些重復(fù)性操作,下面我們就來(lái)學(xué)習(xí)一下如何使用CLI命令批量修改文件名吧2023-12-12js實(shí)現(xiàn)獲取div坐標(biāo)的方法
這篇文章主要介紹了js實(shí)現(xiàn)獲取div坐標(biāo)的方法,通過(guò)調(diào)用jQuery插件實(shí)現(xiàn)獲取div元素坐標(biāo)的功能,非常簡(jiǎn)單實(shí)用,需要的朋友可以參考下2015-11-11js實(shí)現(xiàn)字符串的16進(jìn)制編碼不加密
關(guān)于十六進(jìn)制編碼的問(wèn)題在很多情況下都會(huì)用到,所以使用JS寫了個(gè)小程序轉(zhuǎn)換一下2014-04-04高效的獲取當(dāng)前元素是父元素的第幾個(gè)子元素
例如處理事件的時(shí)候,有時(shí)候需要知道當(dāng)前點(diǎn)擊的是第幾個(gè)子節(jié)點(diǎn),而HTML DOM本身并沒(méi)有直接提供相應(yīng)的屬性,需要自己來(lái)計(jì)算。感興趣的朋友可以了解下本文2013-10-10瀏覽器調(diào)試動(dòng)態(tài)js腳本的方法(圖解)
這篇文章主要介紹了瀏覽器調(diào)試動(dòng)態(tài)js腳本的方法,文中給大家?guī)?lái)兩種調(diào)試方法,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2018-01-01canvas實(shí)現(xiàn)弧形可拖動(dòng)進(jìn)度條效果
本篇文章主要介紹了canvas實(shí)現(xiàn)弧形可拖動(dòng)進(jìn)度條的實(shí)例方法,具有很好的參考價(jià)值。下面跟著小編一起來(lái)看下吧2017-05-05javascript另類方法實(shí)現(xiàn)htmlencode()與htmldecode()函數(shù)實(shí)例分析
這篇文章主要介紹了javascript另類方法實(shí)現(xiàn)htmlencode()與htmldecode()函數(shù),結(jié)合實(shí)例形式分析了javascript字符編碼與解碼操作的相關(guān)技巧,需要的朋友可以參考下2016-11-11