javascript中的[[scope]],scope chain,execution context!

 

關於javascript的作用域的一些總結,主要參考以上文章,加上自己的整理的理解。

近日對javascript的作用域,以及它的運行過程,感覺不甚瞭解,尤其在使用閉包的時候,感覺都是模糊不清的。

與是在網上一探究竟,可是沒想,網上的大多文章都是含糊其次,且大多雷同,經多方查找,加上自己看一些書。

最終對javascript有一絲絲瞭解。記錄下來,以防忘記。

要徹底瞭解javascript的作用域就要瞭解一些概念。

1 運行期上下文

在javascript中,隻有函數能夠創建出獨立的作用域,需要註意的是for循環是不能創建的,否則你的代碼就可能得到意想不到的結果。

 

for (var k in {a: 1, b: 2}) {
  alert(k);
}
alert(k);

即使循環結束,一樣可以alert出k的值.

2變量對象以及活動對象(VO/AO)

變量對象(縮寫為VO)是一個與執行上下文相關的特殊對象,它存儲著在上下文中聲明的以下內容:

變量 (var,變量聲明);

函數聲明 (FunctionDeclaration,縮寫為FD);
函數的形參

隻有全局上下文的變量對象允許通過VO的屬性名稱來間接訪問(因為在全局上下文裡,全局對象自身就是變量對象),在其它上下文中是不能直接訪問VO對象的,因為它隻是內部機制的一個實現,通常使用AO(activation object來保存變量)。

VO保存變量對象示例:

 

var a = 10;
 functiontest(x) {
  var b = 20;
 };
 
test(30);

對應的變量對象是:

// 全局上下文的變量對象

 

VO(globalContext) = {
  a: 10,
  test:
};
// test函數上下文的變量對象
VO(test functionContext) = {
  x: 30,
  b: 20
};

2.1全局上下文中的變量對象(VO)

首先,我們要給全局對象一個明確的定義:

全局對象(Global object) 是在進入任何執行上下文之前就已經創建瞭的對象;
這個對象隻存在一份,它的屬性在程序中任何地方都可以訪問,全局對象的生命周期終止於程序退出那一刻。

全局對象初始創建階段將Math、String、Date、parseInt作為自身屬性,等屬性初始化,同樣也可以有額外創建的其它對象作為屬性(其可以指向到全局對象自身)。例如,在DOM中,全局對象的window屬性就可以引用全局對象自身(當然,並不是所有的具體實現都是這樣):

 

global = {
  Math: <...>,
  String:<...>
  ...
  ...
  window: global//引用自身
};

當訪問全局對象的屬性時通常會忽略掉前綴,這是因為全局對象是不能通過名稱直接訪問的。不過我們依然可以通過全局上下文的this來訪問全局對象,同樣也可以遞歸引用自身。例如,DOM中的window。綜上所述,代碼可以簡寫為:

 

String(10); // 就是global.String(10);
 
// 帶有前綴
window.a = 10; // === global.window.a = 10 ===global.a = 10;
 this.b = 20; //global.b = 20;
alert(this === window);
這裡可以看到,用winodow調用其實是用的VO中的一個屬性,而用this,則是用的全局變量。
因此,回到全局上下文中的變量對象——在這裡,變量對象就是全局對象自己:
VO(globalContext) === global;
非常有必要要理解上述結論,基於這個原理,在全局上下文中聲明的對應,我們才可以間接通過全局對象的屬性來訪問它(例如,事先不知道變量名稱)。
var a = new String('test');
alert(a); // 直接訪問,在VO(globalContext)裡找到:test
alert(window['a']); // 間接通過global訪問:global === VO(globalContext): test
alert(a === this.a); // true
var aKey = 'a';
alert(window[aKey]); // 間接通過動態屬性名稱訪問:test
 

2.2函數上下文中的變量對象(AO)

在函數執行上下文中,VO是不能直接訪問的,此時由活動對象(activation object,縮寫為AO)扮演VO的角色。

VO(functionContext)=== AO;

活動對象是在進入函數上下文時刻被創建的,它通過函數的arguments屬性初始化。arguments屬性的值是Arguments對象:

 

AO = {
  arguments: 
};

Arguments對象是活動對象的一個屬性,它包括如下屬性:

callee — 指向當前函數的引用

length — 真正傳遞的參數個數

properties-indexes (字符串類型的整數) 屬性的值就是函數的參數值(按參數列表從左到右排列)。 properties-indexes內部元素的個數等於arguments.length. properties-indexes 的值和實際傳遞進來的參數之間是共享的。

例如:

 

function foo(x, y, z) {
  // 聲明的函數參數數量arguments (x, y, z)
  alert(foo.length); // 3
  // 真正傳進來的參數個數(only x, y)
 alert(arguments.length); // 2
  // 參數的callee是函數自身
  alert(arguments.callee=== foo); // true
  // 參數共享
  alert(x === arguments[0]); // true
  alert(x); //10
  arguments[0] =20;
  alert(x); //20
  x = 30;
 alert(arguments[0]); // 30
// 不過,沒有傳進來的參數z,和參數的第3個索引值是不共享的
  z = 40;
 alert(arguments[2]); // undefined
   arguments[2] = 50;
   alert(z); // 40
}
foo(10, 20);

這個例子的代碼,在當前版本的Google Chrome瀏覽器裡有一個bug — 即使沒有傳遞參數z,z和arguments[2]仍然是共享的。

3 作用域鏈

從上面可以知道,每個上下文擁有自己的變量對象:對於全局上下文,它是全局對象自身;對於函數,它是活動對象。

 

var x = 10;
function foo() {
var y = 20;
  function bar(){
alert(x +y);
}
return bar;
}
foo()(); // 30

作用域鏈正是內部上下文所有變量對象(包括父變量對象)的列表。此鏈用來變量查詢。即在上面的例子中,“bar”上下文的作用域鏈包括AO(bar)、AO(foo)和VO(global)。這裡的AO(foo)和VO(global)都是來自父變量對象.

 

作用域鏈與一個執行上下文相關,變量對象的鏈用於在標識符解析中變量查找。

 

函數上下文的作用域鏈在函數調用時創建的,包含活動對象和這個函數內部的[[scope]]屬性。

 

在上下文中示意如下:
activeExecutionContext= {
  VO: {...}, //or AO  上下文初始化的VO/AO
this: thisValue,
Scope: [ // Scope chain,作用域鏈
// 所有變量對象的列表
// for identifiers lookup
]
 };

其scope定義如下:

Scope= AO + [[Scope]]

這種聯合和標識符解析過程,我們將在下面討論,這與函數的生命周期相關。

對於Scope = AO + [[Scope]],在函數被調用的時候,上下文會創建一個Scope China,也就是Scope屬性,然後初始化為函數的內部屬性,也就是函數內部屬性[[Scope]],再將進入上下文時創建的VO/AO,壓入Scopechina的最前端。

3.1函數創建

函數的[[scope]]屬性是所有父變量對象的層級鏈,處於當前函數上下文之上,在函數創建時(函數生命周期分為函數創建和函數調用階段)存於其中。函數能訪問更高一層上下文的變量對象,這種機制是通過函數內部的[[scope]]屬性來實現的。

註意重要的一點--[[scope]]在函數創建時被存儲--靜態(不變的),永遠永遠,直至函數銷毀。即:函數可以永不調用,但[[scope]]屬性已經寫入,並存儲在函數對象中。由於是靜態存儲,再配合上內部函數的[[scope]]屬性是所有父變量的層級鏈,就導致瞭閉包的存在。如下:

 

var x = 10;
function foo() {
  alert(x);
}
 
(function () {
  var x = 20;
  foo(); // 10,but not 20,這裡會訪問foo中的[[scope]]的VO中的x
})();

這個例子也清晰的表明,一個函數(這個例子中為從函數“foo”返回的匿名函數)的[[scope]]持續存在,即使是在函數創建的作用域已經完成之後。

另外一個需要考慮的是--與作用域鏈對比,[[scope]]是函數的一個屬性而不是上下文。考慮到上面的例子,函數“foo”的[[scope]]如下:


foo.[[Scope]] = [ globalContext.VO // === Global ];

舉例來說,我們用通常的ECMAScript 數組展現作用域和[[scope]]。

3.2函數激活

正如在定義中說到的,進入上下文創建AO/VO之後,上下文的Scope屬性(變量查找的一個作用域鏈)作如下定義:

Scope= AO|VO + [[Scope]]

上面代碼的意思是:活動對象是作用域數組的第一個對象,即添加到作用域的前端。

Scope= [AO].concat([[Scope]]);

這個特點對於標示符解析的處理來說很重要。

標示符解析是一個處理過程,用來確定一個變量(或函數聲明)屬於哪個變量對象。

這個算法的返回值中,我們總有一個引用類型,它的base組件是相應的變量對象(或若未找到則為null),屬性名組件是向上查找的標示符的名稱。

標識符解析過程包含與變量名對應屬性的查找,即作用域中變量對象的連續查找,從最深的上下文開始,繞過作用域鏈直到最上層。

這樣一來,在向上查找中,一個上下文中的局部變量較之於父作用域的變量擁有較高的優先級。萬一兩個變量有相同的名稱但來自不同的作用域,那麼第一個被發現的是在最深作用域中。

我們用一個稍微復雜的例子描述上面講到的這些。

 

var x = 10;
functionfoo() {
var y = 20;
function bar() {
var z = 30;
alert(x +  y +z);
  }
  bar();
}
foo(); //60

 

對此,我們有如下的變量/活動對象,函數的的[[scope]]屬性以及上下文的作用域鏈:

全局上下文的變量對象是:

 

globalContext.VO=== Global = {
  x: 10
  foo: 
};

 

在“foo”創建時,“foo”的[[scope]]屬性是:

foo.[[Scope]]= [
  globalContext.VO
];

在“foo”激活時(進入上下文),“foo”上下文的活動對象是:

 

fooContext.AO= {
  y: 20,
  bar: 
};

“foo”上下文的作用域鏈為:

 

fooContext.Scope= fooContext.AO + foo.[[Scope]] // i.e.:
 
fooContext.Scope= [
  fooContext.AO,
  globalContext.VO
];

內部函數“bar”創建時,其[[scope]]為:

 

bar.[[Scope]]= [
  fooContext.AO,
  globalContext.VO
];

 

在“bar”激活時,“bar”上下文的活動對象為:

 

barContext.AO= {
  z: 30
};

“bar”上下文的作用域鏈為:

 

barContext.Scope= barContext.AO + bar.[[Scope]] // i.e.:
 
barContext.Scope= [
  barContext.AO,
  fooContext.AO,
  globalContext.VO
];

對“x”、“y”、“z”的標識符解析如下:

 

-x
--barContext.AO // not found
--fooContext.AO // not found
--globalContext.VO // found - 10
 
-y
--barContext.AO // not found
--fooContext.AO // found - 20
 
-z
--barContext.AO // found – 30

4 閉包

 

創建閉包的常見方式是在一個函數內部創建另一個函數,以create ()函數為例:

 

function create(){
var x = 0
return function(){
       alert(++x);
};
}
var c = create();
c();     //alert 1not 0
c();     //alert 2not 0
c();     //alert 3not 0
 

上面代碼中的內部函數(一個匿名函數),訪問瞭外部函數中的變量x。即使這個內部函數被返回瞭,且在其他地方被調用瞭,但它仍可訪問變量x.之所以還能夠訪問這個變量,是因為內部函數的作用域鏈中包含create ()的作用域。

當某個函數第一次被調用時,會創建一個執行環境及相應的作用域鏈,並把一個特殊的內部屬性(即[[Scope]] ) 賦值給作用域鏈。然後使用this、arguments和其他命名參數的值來初始化函數的活動對象。然後把該活動對象壓入作用域鏈的最前端。所以在作用域鏈中,父級活動對象始終處於第二位,直到作用域鏈終點的全局執行環境。

在函數執行過程中,為讀取和寫入變量的值,需要在作用域中查找變量:

 

 function compare(value1,value2){
     if(value2value2){
          return1;
    }
     else {
        return0;
    }
   }
   var result =compare(5,10);

上面的代碼先定義瞭compare()函數,又在全局作用域調用瞭它。第一次調用compare()時,會創建一個包含this、arguments、value1、value2的活動對象。全局執行環境的變量對象(包含this,result,compare)在compare()執行環境的作用域中處於第二位,如圖:

後臺的每個執行環境都有一個表示變量的對象--變量對象。全局環境的變量對象始終存在,而像compare()函數這樣的局部環境的變量對象則隻在函數執行過程中存在。在創建compare()函數(註意,這裡是創建)時,會創建一個預先包含全局變量對象的作用域鏈,這個作用域鏈被保存在內部的[[Scope]]屬性中。當調用compare函數時,會為函數創建一個執行環境,然後通過復制函數的[[Scope]]屬性中的對象構建起執行環境的作用域鏈。

此後又有一個活動對象(在此作為變量對象使用)被創建並被推入執行環境作用域鏈的前端。對於這個例子中compare()函數的執行環境而言,其作用域鏈中包含兩個變量對象:本地活動對象和全局變量對象。

作用域鏈本質上是一個指向變量對象的指針列表,它隻引用但不實際包含變量對象

一般來講在當函數執行完畢後,局部活動對象就會被銷毀,內在中僅保存全局作用域,但閉包情況有所不同。另一個函數內部定義的函數會將包含函數的活動對象添加到它的作用域鏈中。因此,createComparisonFunction()函數內定義的匿名函數的作用域鏈中,實際上將會包含外部函數createComparisonFunction()的活動對象,

 

function createComparisonFunction(propertyName){
   return function(object1,object2){
        varvalue1 = object1[propertyName];
        varvalue1 = object2[propertyName];
        if(value1 < value2){return -1;}
        elseif (value1>value2){return 1;}
        else{return 0;}
   };
}

 

var compare =createComparisonFunction(name);
var result =compare({name:'Nicholas},{name:Greg});

在匿名函數從createComparisonFunction()中被返回後,它的作用域鏈被初始化為包含createComparisonFunction()函數的活動對象和全局變量對象。這樣createComparisonFunction()函數執行完畢後其活動對象也不會被銷毀,因為匿名函數的作用域鏈仍然在引用這個活動對象。換句話說,createComparisonFunction()函數返回後,其執行環境的作用域鏈會被銷毀,但它的活動對象仍會留在內存中;直到匿名函數被銷毀後,createComparisonFunction()的活動對象才會被銷毀。

5閉包和變量

看如下代碼:

 

function fn(){
    var result =new Array();
    for (vari=0;i<10;i++){
      result[i]= function(){return i;}
   }
    returnresult;
}
var funcs = fn();
for (var i=0;i);
}

這段代碼代碼可能說是堪稱經典。你也許知道結果會輸出10個10。

這裡的原因還是因為作用域的問題。

在fn 被調用結束後,那麼在fn的作用鏈的AO中保存i的值為10。

主要的問題就是result[i]的方法的作用鏈中保存瞭父級活動對象。那麼當激活result方法時,會先去自已的活動對象中搜索,當沒有發現表示符時,則會去父級活動對象中搜索,當搜索到i的時候,這個i已經是10瞭。這就是為什麼結果都為10。當然需要如果需要得到你想要的結果,那麼可以采用如下的方式:

 

 

result[i] = function(num){
     returnfunction(){
        returnnum;
    }
}(i);

這裡為什麼能得出正確的結果呢?其實這裡是將修改瞭result的活動對象。

第一個result的函數的活動對象中多瞭一個 num屬性,而這個屬性是用一個自啟動函數傳入的。每次激活函數的時候直接在自已的活動對象中去搜索到num就返回瞭,不再去父級變量中搜索瞭。這樣就能結果問題瞭。

6 javascript 代碼執行過程

javascript中的function的一些東西這裡就不說瞭。

關於function對象的[[scope]]是一個內部屬性。

Functiion對象在創建的時候會自動創建一個[[scope]]內部屬性,且隻有js引擎才能夠去訪問。同時也會創建一個scope chian的作用域鏈。且[[scope]]鏈接到scope china中。

 

 

function add(){
  Var name = “hello”;
}

那麼如上:

Function add 在創建的時候就會創建一個[[scope]]的屬性,且指向scope chian.由於是在全局的環境中,那麼scope chian剛指向window action object.(這裡不是很清楚).

這麼以上的情況都是要定義的時候完成的,也可以說是在js解釋的時候完成的。

 

執行此函數時會創建一個稱為“運行期上下文(execution context)”的內部對象,運行期上下文定義瞭函數執行時的環境。每個運行期上下文都有自己的作用域鏈,用於標識符解析,當運行期上下文被創建時,而它的作用域鏈初始化為當前運行函數的[[Scope]]所包含的對象。

 

運行期上下文的代碼被分成兩個基本的階段來處理:

進入執行上下文

執行代碼

變量對象的修改變化與這兩個階段緊密相關。

註:這2個階段的處理是一般行為,和上下文的類型無關(也就是說,在全局上下文和函數上下文中的表現是一樣的)。

6.1 進入執行上下文

當進入執行上下文(代碼執行之前)時,VO/AO裡已經包含瞭下列屬性(前面已經說瞭):

函數的所有形參(如果我們是在函數執行上下文中)

— 由名稱和對應值組成的一個變量對象的屬性被創建;沒有傳遞對應參數的話,那麼由名稱和undefined值組成的一種變量對象的屬性也將被創建。

所有函數聲明(FunctionDeclaration,FD)

—由名稱和對應值(函數對象(function-object))組成一個變量對象的屬性被創建;如果變量對象已經存在相同名稱的屬性,則完全替換這個屬性。

所有變量聲明(var,VariableDeclaration)

— 由名稱和對應值(undefined)組成一個變量對象的屬性被創建;如果變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會幹擾已經存在的這類屬性。

讓我們看一個例子:

 

function test(a, b) {
  var c = 10;
  function d(){}
  var e =function _e() {};
  (function x(){});
}
test(10); // call

 

當進入帶有參數10的test函數上下文時,AO表現為如下:

 

AO(test) = {
  a: 10,
  b: undefined,
  c: undefined,
  d:
  e: undefined
};

註意,AO裡並不包含函數“x”。這是因為“x”是一個函數表達式(FunctionExpression, 縮寫為 FE) 而不是函數聲明,函數表達式不會影響VO。 不管怎樣,函數“_e” 同樣也是函數表達式,但是就像我們下面將看到的那樣,因為它分配給瞭變量“e”,所以它可以通過名稱“e”來訪問。

這之後,將進入處理上下文代碼的第二個階段— 執行代碼。

 

6.2 代碼執行

這個周期內,AO/VO已經擁有瞭屬性(不過,並不是所有的屬性都有值,大部分屬性的值還是系統默認的初始值undefined )。

還是前面那個例子, AO/VO在代碼解釋期間被修改如下:

 

AO['c'] = 10;
AO['e'] = ;

 

再次註意,因為FunctionExpression“_e”保存到瞭已聲明的變量“e”上,所以它仍然存在於內存中。而FunctionExpression “x”卻不存在於AO/VO中,也就是說如果我們想嘗試調用“x”函數,不管在函數定義之前還是之後,都會出現一個錯誤“x is not defined”,未保存的函數表達式隻有在它自己的定義或遞歸中才能被調用。

 

另一個經典例子:

 

alert(x); // function
var x = 10;
alert(x); // 10
x = 20;
function x() {};
alert(x); // 20

為什麼第一個alert “x” 的返回值是function,而且它還是在“x” 聲明之前訪問的“x” 的?為什麼不是10或20呢?因為,根據規范函數聲明是在當進入上下文時填入的; 同意周期,在進入上下文的時候還有一個變量聲明“x”,那麼正如我們在上一個階段所說,變量聲明在順序上跟在函數聲明和形式參數聲明之後,而且在這個進入上下文階段,變量聲明不會幹擾VO中已經存在的同名函數聲明或形式參數聲明,因此,在進入上下文時,VO的結構如下:

 

VO = {};
VO['x'] = 
// 找到var x = 10;
// 如果functionx沒有已經聲明的話
// 這時候x的值應該是undefined
// 但是這個case裡變量聲明沒有影響同名的function的值
VO['x'] = 
緊接著,在執行代碼階段,VO做如下修改:
VO['x'] = 10;
 VO['x'] = 20;

我們可以在第二、三個alert看到這個效果。

在下面的例子裡我們可以再次看到,變量是在進入上下文階段放入VO中的。(因為,雖然else部分代碼永遠不會執行,但是不管怎樣,變量“b”仍然存在於VO中。)

 

if (true) {
var a = 1;
 } else {
var b = 2;
 }
alert(a); //
alert(b); // undefined,不是b沒有聲明,而是b的值是undefined

 

7 關於變量

通常,各類文章和JavaScript相關的書籍都聲稱:“不管是使用var關鍵字(在全局上下文)還是不使用var關鍵字(在任何地方),都可以聲明一個變量”。請記住,這是錯誤的概念:

任何時候,變量隻能通過使用var關鍵字才能聲明。

上面的賦值語句:

a= 10;

這僅僅是給全局對象創建瞭一個新屬性(但它不是變量)。“不是變量”並不是說它不能被改變,而是指它不符合ECMAScript規范中的變量概念,所以它“不是變量”(它之所以能成為全局對象的屬性,完全是因為VO(globalContext) === global,大傢還記得這個吧?)。

讓我們通過下面的實例看看具體的區別吧:

 

alert(a); // undefined
alert(b); // b 沒有聲明
b = 10;
var a = 20;

 

所有根源仍然是VO和進入上下文階段和代碼執行階段:

進入上下文階段:

 

VO = {
a: undefined
};

 

我們可以看到,因為“b”不是一個變量,所以在這個階段根本就沒有“b”,“b”將隻在代碼執行階段才會出現(但是在我們這個例子裡,還沒有到那就已經出錯瞭)。

讓我們改變一下例子代碼:

 

alert(a); // undefined, 這個大傢都知道,
b = 10;
alert(b); // 10, 代碼執行階段創建
var a = 20;

 

關於變量,還有一個重要的知識點。變量相對於簡單屬性來說,變量有一個特性(attribute):{DontDelete},這個特性的含義就是不能用delete操作符直接刪除變量屬性。

 

a = 10;
alert(window.a); // 10
alert(delete a); // true
alert(window.a); // undefined
var b = 20;
 alert(window.b); // 20
alert(delete b); // false
alert(window.b); // still 20
但是這個規則在有個上下文裡不起走樣,那就是eval上下文,變量沒有{DontDelete}特性。
eval('var a = 10;');
alert(window.a); // 10
alert(delete a); // true
alert(window.a); // undefined

 

使用一些調試工具(例如:Firebug)的控制臺測試該實例時,請註意,Firebug同樣是使用eval來執行控制臺裡你的代碼。因此,變量屬性同樣沒有{DontDelete}特性,可以被刪除。

 

如果能認真看完博客,相信你一定對javascript很感興趣。

文檔如果對你有一絲絲的幫助,那麼恭喜。

發佈留言