【轉】javascript 執行環境,變量對象,做用域鏈

這篇文章比較清晰的解釋了一些做用域鏈相關的概念,忍不住收藏了javascript

原文地址:http://segmentfault.com/a/1190000000533094前端

前言

這幾天在看《javascript高級程序設計》,看到執行環境和做用域鏈的時候,就有些模糊了。書中仍是講的不夠具體。
經過上網查資料,特來總結,以備回顧和修正。java

要講的依次爲:segmentfault

  • EC(執行環境或者執行上下文,Execution Context)
  • ECS(執行環境棧Execution Context Stack)
  • VO(變量對象,Variable Object)|AO(活動對象,Active Object)
  • scope chain(做用域鏈)和[[scope]]屬性

EC

每當控制器到達ECMAScript可執行代碼的時候,控制器就進入了一個執行上下文(好高大上的概念啊)。數組

javascript中,EC分爲三種:瀏覽器

  • 全局級別的代碼 – 這個是默認的代碼運行環境,一旦代碼被載入,引擎最早進入的就是這個環境。
  • 函數級別的代碼 – 當執行一個函數時,運行函數體中的代碼。
  • Eval的代碼 – 在Eval函數內運行的代碼。

EC創建分爲兩個階段:進入執行上下文和執行階段。函數

1.進入上下文階段:發生在函數調用時,可是在執行具體代碼以前(好比,對函數參數進行具體化以前)
2.執行代碼階段:變量賦值,函數引用,執行其餘代碼。ui

咱們能夠將EC看作是一個對象。this

EC={
    VO:{/* 函數中的arguments對象, 參數, 內部的變量以及函數聲明 */}, this:{}, Scope:{ /* VO以及全部父執行上下文中的VO */} } 

ECS

一系列活動的執行上下文從邏輯上造成一個棧。棧底老是全局上下文,棧頂是當前(活動的)執行上下文。當在不一樣的執行上下文間切換(退出的而進入新的執行上下文)的時候,棧會被修改(經過壓棧或者退棧的形式)。spa

壓棧:全局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)) } 

1.進入執行上下文時,VO的初始化過程具體以下:

  • 函數的形參(當進入函數執行上下文時)
    —— 變量對象的一個屬性,其屬性名就是形參的名字,其值就是實參的值;對於沒有傳遞的參數,其值爲undefined

  • 函數聲明(FunctionDeclaration, FD) —— 變量對象的一個屬性,其屬性名和值都是函數對象建立出來的;若是變量對象已經包含了相同名字的屬性,則替換它的值

  • 變量聲明(var,VariableDeclaration) —— 變量對象的一個屬性,其屬性名即爲變量名,其值爲undefined;若是變量名和已經聲明的函數名或者函數的參數名相同,則不會影響已經存在的屬性。

注意:該過程是有前後順序的。

2.執行代碼階段時,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 defined" 

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"> } }; 

可見,只有在這個階段,變量屬性纔會被賦具體的值。

做用域鏈

在執行上下文的做用域中查找變量的過程被稱爲標識符解析(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"> } } 
  • f1Context.AO
f1Context.AO={
  arguments:{ callee:f1, length:0 }, y:20, f2:<reference to FunctionDeclaration "f2"> } 
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]]存在的意義是什麼呢?

這個仍是看下一篇文章吧。

總結

  1. EC分爲兩個階段,進入執行上下文和執行代碼。
  2. ECStack管理EC的壓棧和出棧。
  3. 每一個EC對應一個做用域鏈Scope,VO|AO(AO,VO只能有一個),this。
  4. 函數EC中的Scope在進入函數EC時建立,用來有序訪問該EC對象AO中的變量和函數。
  5. 函數EC中的AO在進入函數EC時,肯定了Arguments對象的屬性;在執行函數EC時,其它變量屬性具體化。
  6. 函數的[[scope]]屬性在函數建立時就已經肯定,並保持不變。
相關文章
相關標籤/搜索