在這篇文章裡,我將深入研究JavaScript中最基本的部分——執行上下文(execution context)。讀完本文後,你應該清楚了解釋器做了什麼,為什麼函數和變量能在聲明前使用以及他們的值是如何決定的。
1、EC—執行環境或者執行上下文
每當控制器到達ECMAScript可執行代碼的時候,控制器就進入了一個執行上下文(好高大上的概念啊)。
javascript中,EC分為三種:
EC建立分為兩個階段:進入執行上下文(創建階段)和執行階段(激活/執行代碼)。
1)、進入上下文階段:發生在函數調用時,但是在執行具體代碼之前(比如,對函數參數進行具體化之前)
創建作用域鏈(Scope Chain)
創建變量,函數和參數。
求”this“的值。
2)、執行代碼階段:
變量賦值
函數引用
解釋/執行其他代碼。
我們可以將EC看做是一個對象。
EC={ VO:{/* 函數中的arguments對象, 參數, 內部的變量以及函數聲明 */}, this:{}, Scope:{ /* VO以及所有父執行上下文中的VO */} }
現在讓我們看一個包含全局和函數上下文的代碼例子:
很簡單的例子,我們有一個被紫色邊框圈起來的全局上下文和三個分別被綠色,藍色和橘色框起來的不同函數上下文。只有全局上下文(的變量)能被其他任何上下文訪問。
你可以有任意多個函數上下文,每次調用函數創建一個新的上下文,會創建一個私有作用域,函數內部聲明的任何變量都不能在當前函數作用域外部直接訪問。在上面的例子中,函數能訪問當前上下文外面的變量聲明,但在外部上下文不能訪問內部的變量/函數聲明。為什麼會發生這種情況?代碼到底是如何被解釋的?
2、ECS—執行上下文棧
一系列活動的執行上下文從邏輯上形成一個棧。棧底總是全局上下文,棧頂是當前(活動的)執行上下文。當在不同的執行上下文間切換(退出的而進入新的執行上下文)的時候,棧會被修改(通過壓棧或者退棧的形式)。
壓棧:全局EC—>局部EC1—>局部EC2—>當前EC
出棧:全局EC<—局部EC1<—局部EC2<—當前EC
我們可以用數組的形式來表示環境棧:
ECS=[局部EC,全局EC];
每次控制器進入一個函數(哪怕該函數被遞歸調用或者作為構造器),都會發生壓棧的操作。過程類似javascript數組的push和pop操作。
浏覽器裡的JavaScript解釋器被實現為單線程。這意味著同一時間只能發生一件事情,其他的行文或事件將會被放在叫做執行棧裡面排隊。下面的圖是單線程棧的抽象視圖:
我們已經知道,當浏覽器首次載入你的腳本,它將默認進入全局執行上下文。如果,你在你的全局代碼中調用一個函數,你程序的時序將進入被調用的函數,並穿件一個新的執行上下文,並將新創建的上下文壓入執行棧的頂部。
如果你調用當前函數內部的其他函數,相同的事情會在此上演。代碼的執行流程進入內部函數,創建一個新的執行上下文並把它壓入執行棧的頂部。浏覽器將總會執行棧頂的執行上下文,一旦當前上下文函數執行結束,它將被從棧頂彈出,並將上下文控制權交給當前的棧。下面的例子顯示遞歸函數的執行棧調用過程:
(function foo(i) { if (i === 3) { return; } else { foo(++i); } }(0));
這代碼調用自己三次,每次給i的值加一。每次foo函數被調用,將創建一個新的執行上下文。一旦上下文執行完畢,它將被從棧頂彈出,並將控制權返回給下面的上下文,直到只剩全局上下文能為止。
有5個需要記住的關鍵點,關於執行棧(調用棧):
3、VO—變量對象
每一個EC都對應一個變量對象VO,在該EC中定義的所有變量和函數都存放在其對應的VO中。
VO分為全局上下文VO(全局對象,Global object,我們通常說的global對象)和函數上下文的AO。
VO: { // 上下文中的數據 ( 函數形參(function arguments), 函數聲明(FD),變量聲明(var)) }
1)、進入執行上下文時,VO的初始化過程具體如下:
函數的形參(當進入函數執行上下文時)—— 變量對象的一個屬性,其屬性名就是形參的名字,其值就是實參的值;對於沒有傳遞的參數,其值為undefined;
函數聲明(FunctionDeclaration, FD) —— 變量對象的一個屬性,其屬性名和值都是函數對象創建出來的;如果變量對象已經包含了相同名字的屬性,則替換它的值;
變量聲明(var,VariableDeclaration) —— 變量對象的一個屬性,其屬性名即為變量名,其值為undefined;如果變量名和已經聲明的函數名或者函數的參數名相同,則不會影響已經存在的屬性。
注意:該過程是有先後順序的。
2)、 執行代碼階段時,VO中的一些屬性undefined值將會確定。
4、AO活動對象
在函數的執行上下文中,VO是不能直接訪問的。它主要扮演被稱作活躍對象(activation object)(簡稱:AO)的角色。
這句話怎麼理解呢,就是當EC環境為函數時,我們訪問的是AO,而不是VO。
VO(functionContext) === AO;
AO是在進入函數的執行上下文時創建的,並為該對象初始化一個arguments屬性,該屬性的值為Arguments對象。
AO = { arguments: { callee:, length:, properties-indexes: //函數傳參參數值 } };
FD的形式只能是如下這樣:
function f(){ }
當函數被調用是executionContextObj被創建,但在實際函數執行之前。這是我們上面提到的第一階段,創建階段。在此階段,解釋器掃描傳遞給函數的參數或arguments,本地函數聲明和本地變量聲明,並創建executionContextObj對象。掃描的結果將完成變量對象的創建。
內部的執行順序如下:
1、查找調用函數的代碼。
2、執行函數代碼之前,先創建執行上下文。
3、進入創建階段:
4、激活/代碼執行階段:
在當前上下文上運行/解釋函數代碼,並隨著代碼一行行執行指派變量的值。
示例
1、具體實例
function foo(i) { var a = ‘hello‘; var b = function privateB() { }; function c() { } } foo(22);
當調用foo(22)時,創建狀態像下面這樣:
fooExecutionContext = { scopeChain: { ... }, variableObject: { arguments: { 0: 22, length: 1 }, i: 22, c: pointer to function c() a: undefined, b: undefined }, this: { ... } }
真如你看到的,創建狀態負責處理定義屬性的名字,不為他們指派具體的值,以及形參/實參的處理。一旦創建階段完成,執行流進入函數並且激活/代碼執行階段,看下函數執行完成後的樣子:
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
進入執行上下文時,
ECObject={ VO:{ x:<reference to FunctionDeclaration "x"> } };
執行代碼時:
ECObject={ VO:{ x:20 //與函數x同名,替換掉,先是10,後變成20 } };
對於以上的過程,我們詳細解釋下。
在進入上下文的時候,VO會被填充函數聲明; 同一階段,還有變量聲明“x”,但是,正如此前提到的,變量聲明是在函數聲明和函數形參之後,並且,變量聲明不會對已經存在的同樣名字的函數聲明和函數形參發生沖突。因此,在進入上下文的階段,VO填充為如下形式:
VO = {}; VO['x'] = <引用了函數聲明'x'> // 發現var x = 10; // 如果函數“x”還未定義 // 則 "x" 為undefined, 但是,在我們的例子中 // 變量聲明並不會影響同名的函數值 VO['x'] = <值不受影響,仍是函數>
執行代碼階段,VO被修改如下:
VO['x'] = 10; VO['x'] = 20;
如下例子再次看到在進入上下文階段,變量存儲在VO中(因此,盡管else的代碼塊永遠都不會執行到,而“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
當進入test(10)的執行上下文時,它的AO為:
testEC={ AO:{ arguments:{ callee:test length:1, 0:10 }, a:10, c:undefined, d:<reference to FunctionDeclaration "d">, e:undefined } };
由此可見,在建立階段,VO除了arguments,函數的聲明,以及參數被賦予了具體的屬性值,其它的變量屬性默認的都是undefined。函數表達式不會對VO造成影響,因此,(function x() {})並不會存在於VO中。
當執行 test(10)時,它的AO為:
testEC={ AO:{ arguments:{ callee:test, length:1, 0:10 }, a:10, c:10, d:<reference to FunctionDeclaration "d">, e:<reference to FunctionDeclaration "e"> } };
可見,只有在這個階段,變量屬性才會被賦具體的值。
5、提升(Hoisting)解密
在之前的JavaScript Item中降到了變量和函數聲明被提升到函數作用域的頂部。然而,沒有人解釋為什麼會發生這種情況的細節,學習了上面關於解釋器如何創建active活動對象的新知識,很容易明白為什麼。看下面的例子:
(function() { console.log(typeof foo); // 函數指針 console.log(typeof bar); // undefined var foo = ‘hello‘, bar = function() { return ‘world‘; }; function foo() { return ‘hello‘; } }());
我們能回答下面的問題:
1、為什麼我們能在foo聲明之前訪問它?
如果我們跟隨創建階段,我們知道變量在激活/代碼執行階段已經被創建。所以在函數開始執行之前,foo已經在活動對象裡面被定義了。
2、foo被聲明了兩次,為什麼foo顯示為函數而不是undefined或字符串?
盡管foo被聲明了兩次,我們知道從創建階段函數已經在活動對象裡面被創建,這一過程發生在變量創建之前,並且如果屬性名已經在活動對象上存在,我們僅僅更新引用。
因此,對foo()函數的引用首先被創建在活動對象裡,並且當我們解釋到var foo時,我們看見foo屬性名已經存在,所以代碼什麼都不做並繼續執行。
3、為什麼bar的值是undefined?
bar實際上是一個變量,但變量的值是函數,並且我們知道變量在創建階段被創建但他們被初始化為undefined。
以上就是本文的全部內容,有詳細的問題解答,示例代碼,幫助大家更加了解javascript的執行上下文,希望大家喜歡這篇文章。