前言 這幾天在看《javascript高級程式設計》,看到執行環境和作用域鏈的時候,就有些模糊了。書中還是講的不夠具體。通過上網查資料,特來總結,以備回顧和修正。 目錄: EC(執行環境或者執行上下文,Execution Context) ECS(執行環境棧Execution Context Sta ...
前言
這幾天在看《javascript高級程式設計》,看到執行環境和作用域鏈的時候,就有些模糊了。書中還是講的不夠具體。通過上網查資料,特來總結,以備回顧和修正。
目錄:
- EC(執行環境或者執行上下文,Execution Context)
- ECS(執行環境棧Execution Context Stack)
- VO(變數對象,Variable Object)|AO(活動對象,Active Object)
- Scope Chain(作用域鏈)和[[Scope]]屬性
EC——執行環境或執行上下文
每當控制器到達ECMAScript可執行代碼的時候,控制器就進入了一個執行上下文。
JavaScript中,EC分為三種:
- 全局級別的代碼——這個是預設的代碼運行環境,一旦代碼被載入,引擎最先進入的就是這個環境
- 函數級別的代碼——當執行一個函數式,運行函數體中的代碼
- Eval的代碼——在Eval函數內運行的代碼
EC建立分為倆個階段:
- 進入上下文階段:發生在函數調用時,但是在執行具體代碼之前(比如,對函數參數進行具體化之前)
- 執行代碼階段:變數賦值,函數引用,執行其它代碼
我們可以將EC看做是一個對象:
EC={ VO:{/* 函數中的arguments對象, 參數, 內部的變數以及函數聲明 */}, this:{}, Scope:{ /* VO以及所有父執行上下文中的VO */} }
ECS——執行環境棧
一系列活動的執行上文從邏輯上形成一個棧。棧底總是全局上下文,棧頂是當前(活動的)執行上下文。當在不同的執行上文間切換(退出的而進入新的執行上下文)的時候,棧會被修改(通過壓棧或退棧的形式)。
壓棧:全局EC → 局部EC1 → 局部EC2 → 當前EC
出棧:全局EC ←全局EC1 ←全局EC2 ←當前EC
我們可以用數組的形式來表示環境棧:
ECS=[局部EC,全局EC];
每次控制器進入一個函數(哪怕該函數被遞歸調用或者作為構造器),都會發生壓棧的操作。過程類似JavaScript數組的Push和Pop操作。
當JavaScript代碼文件被瀏覽器載入後,預設最先進入的是一個全局的執行上下文。當在全局上下文中調用執行一個函數時,程式流就進入該被調用函數內,此時引擎就會為該函數創建一個新的執行上下文,並且將其壓入到執行上下文堆棧的頂部。瀏覽器總是執行當前在堆棧頂部的上下文,一旦執行完畢,該上下文就會從堆棧頂部被彈出,然後,進入其下的上下文執行代碼。這樣,堆棧中的上下文就會被依次執行並且彈出堆棧,直到回到全局的上下文。
VO——變數對象|AO——活動對象
VO
每一個EC都對應一個變數對象VO,在該EC中定義的所有變數和函數都存在其對應的VO中。
VO分為全局上下文VO(全局對象,Global Object,我們通常說的Global對象)和函數上下文的AO
VO: { // 上下文中的數據 (變數聲明(var), 函數聲明(FD), 函數形參(function arguments)) }
- 進入執行上下文時,VO的初始化過程具體如下:
- 函數的形參(當進入函數執行上下文時)—— 變數對象的一個屬性,其屬性名就是形參的名字,其值就是實參的值;對於沒有傳遞的參數,其值為undefined
- 函數聲明(FunctionDeclaration, FD) —— 變數對象的一個屬性,其屬性名和值都是函數對象創建出來的;如果變數對象已經包含了相同名字的屬性,則替換它的值
- 變數聲明(var,VariableDeclaration) —— 變數對象的一個屬性,其屬性名即為變數名,其值為undefined;如果變數名和已經聲明的函數名或者函數的參數名相同,則不會影響已經存在的屬性。
註意:改過程是有先後順序的。
- 執行代碼階段時,VO中的一些屬性undefined值將會確定。
AO
在函數的執行上下文中,VO是不能直接訪問的。它主要扮演被稱作活躍對象(activation object)(簡稱:AO)的角色。
這句話怎麼理解呢,就是當EC環境為函數時,我們訪問的是AO,而不是VO。
VO(functionContext) === AO;
AO是在進入函數的執行上下文時創建的,併為該對象初始化一個arguments屬性,該屬性的值為Arguments對象。
AO = { arguments: { callee:, length:, properties-indexes: //函數傳參參數值 } };
FD的形式只能是如下這樣:
function f(){ }
示例
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 define
AO示例:
unction 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"> } };
可見,只有在這個階段,變數屬性才會被賦具體的值。
作用域鏈
在執行上下文的作用域中查找變數的過程被稱為標識符解析(indentifier resolution),這個過程的實現依賴於函數內部另一個同執行上下文相關聯的對象——作用域鏈。作用域鏈是一個有序鏈表,其包含著用以告訴JavaScript解析器一個標識符到底關聯著那一個變數的對象。而每一個執行上下文都有其自己的作用域鏈Scope。
一句話:作用域鏈Scope其實就是對執行上下文EC中的變數對象VO|AO有序訪問的鏈表。能按順序訪問到VO|AO,就能訪問到其中存放的變數和函數的定義。
Scope定義如下:
Scope = AO|VO + [[Scope]]
其中,AO始終在Scope的最前端,不然為啥叫活躍對象呢。即:
Scope = [AO].concat([[Scope]]);
這說明瞭,作用域鏈是在函數創建時就已經有了。
那麼[[Scope]]是什麼呢?
[[Scope]]是一個包含了所有上層變數對象的分層鏈,它屬於當前函數上下文,併在函數創建的時候,保存在函數中。
[[Scope]]是在函數創建的時候保存起來的——靜態的(不變的),只有一次並且一直都存在——直到函數銷毀。 比方說,哪怕函數永遠都不能被調用到,[[Scope]]屬性也已經保存在函數對象上了。
var x=10; function f1(){ var y=20; function f2(){ return x+y; } }
以上示例中,f2的[[scope]]屬性可以表示如下:
f2.[[scope]]=[
f2OuterContext.VO
]
而f2
的外部EC的所有上層變數對象包括了f1
的活躍對象f1Context.AO,再往外層的EC,就是global對象了。
所以,具體我們可以表示如下:
f2.[[scope]]=[
f1Context.AO,
globalContext.VO
]
對於EC執行環境是函數來說,那麼它的Scope表示為:
functionContext.Scope=functionContext.AO+function.[[scope]]
註意,以上代碼的表示,也體現了[[scope]]和Scope的差異,Scope是EC的屬性,而[[scope]]則是函數的靜態屬性。
(由於AO|VO在進入執行上下文和執行代碼階段不同,所以,這裡及以後Scope的表示,我們都預設為是執行代碼階段的Scope,而對於靜態屬性[[scope]]而言,則是在函數聲明時就創建了)
對於以上的代碼EC,我們可以給出其Scope的表示:
exampelEC={ Scope:[ f2Context.AO+f2.[[scope]], f1.context.AO+f1.[[scope]], globalContext.VO ] }
接下來,我們給出以上其它值的表示:
- globalContext.VO
globalContext.VO={ x:10, f1:<reference to FunctionDeclaration "f1"> }
- f2Context.AO
f2Context.AO={ f1Context.AO={ arguments:{ callee:f1, length:0 }, y:20, f2:<reference to FunctionDeclaration "f2"> } }
- f2.[[scope]]
f2Context.AO={ f1Context.AO:{ arguments:{ callee:f1, length:0 }, y:20, f2:<reference to FunctionDeclaration "f2"> }, globalContext.VO:{ x:10, f1:<reference to FunctionDeclaration "f1"> } }
- f1.[[scope]](f1的所有上層EC的VO)
f1.[[scope]]={
globalContext.VO:{
x:undefined,
f1:undefined
}
}
好,我們知道,作用域鏈Scope呢,是用來有序訪問VO|AO中的變數和函數,對於上面的示例,我們給出訪問的過程:
- x,f1
- "x" -- f2Context.AO // not found -- f1Context.AO // not found -- globalContext.VO // found - 10
f1的訪問過程類似。
- y
- "y" -- f2Context.AO // not found -- f1Context.AO // found -20
我們發現,在變數和函數的訪問過程,並沒有涉及到[[scope]],那麼[[scope]]存在的意義是什麼呢?
這個還是看下一篇文章吧。
總結
- EC分為倆個階段,進入執行上下文和執行代碼。
- ECStack管理EC的壓棧和出棧。
- 每個EC對應一個作用域鏈,VO|AO(AO,VO只能有一個),this。
- 函數EC中的Scope在進入函數EC是創建,用來有序方位該EC對象AO中的變數和函數。
- 函數EC中的AO在進入函數EC時,確定了Arguments對象的屬性;在執行函數EC時,其它變數屬性具體化。
- 函數的[[scope]]屬性在函數創建時就已經確定,並保持不變。
轉自:https://segmentfault.com/a/1190000000533094#articleHeader1