最近在研究Js,發現自己對作用域,作用域鏈,活動對象這幾個概念,理解得不是很清楚,所以拜讀了@田小計划大神的博客與其他文章,受益匪淺,寫這篇隨筆算是自己的讀書筆記吧~。
作用域
首先明確一個概念,js只有函數作用域(function-based),沒有塊級作用域,也就是只有函數會有自己的作用域,其他都沒有。
接着,作用域分為全局作用域與局部作用域。
全局作用域中的對象可以在代碼的任何地方訪問,一般來說,下面情況的對象會在全局作用域中:
- 最外層函數和在最外層函數外面定義的變量
- 沒有通過關鍵字"var"聲明的變量
- 瀏覽器中,window對象的屬性
局部作用域又被稱為函數作用域(Function scope),所有的變量和函數只能在作用域內部使用。
var foo = 1;
window.bar = 2;
function baz(){
a = 3;
var b = 4;
}
// Global scope: foo, bar, baz, a
// Local scope: b
在創建這個函數的時候,這個函數的作用域與作用域鏈(函數的作用域鏈將會在運行時用到)就已經決定了,而是不是在調用的時候,這句話至管重要。
作用域鏈
每一個Javascript函數都被表示為對象,它是一個函數實例。它包含我們編程定義的可訪問屬性,和一系列不能被程序訪問,僅供Javascript引擎使用的內部屬性,其中一個內部屬性是[[Scope]],由ECMA-262標准第三版定義。
內部[[Scope]]屬性包含一個函數被創建的作用域中對象的集合。此集合被稱為函數的作用域鏈,它決定哪些數據可以由函數訪問。此函數中作用域鏈中每個對象被稱為一個可變對象,以“鍵值對”表示。當一個函數創建以后,它的作用域鏈被填充以這些對象,它們代表創建此函數的環境中可訪問的數據:
1 function add(num1, num2){
2 var sum = num1 + num2;
3 return sum;
4 }
當add()函數創建以后,它的作用域鏈中填入了一個單獨可變對象,此全局對象代表了所有全局范圍定義的變量。此全局對象包含諸如窗口、瀏覽器和文檔之類的訪問接口。如下圖所示:(add()函數的作用域鏈,注意這里只畫出全局變量中很少的一部分)

add函數的作用域鏈將會在運行時用到,假設運行了如下代碼:
1 var total = add(5,10);
運行此add函數時會建立一個內部對象,稱作“運行期上下文”(execution context),一個運行期上下文定義了一個函數運行時的環境。且對於單獨的每次運行而言,每個運行期上下文都是獨立的,多次調用就會產生多此創建。而當函數執行完畢,運行期上下文被銷毀。
一個運行期上下文有自己的作用域鏈,用於解析標識符。當運行期上下文被創建的時,它的作用域被初始化,連同運行函數的作用域鏈[[Scope]]屬性所包含的對象。這些值按照它們出現在函數中的順序,被復制到運行期上下文的作用域鏈中。這項工作一旦執行完畢,一個被稱作“激活對象”的新對象就創建好了。此激活對象作為函數執行期一個可變對象,包含了訪問所有局部變量,命名參數,參數集合和this的接口。然后,此對象被推入到作用域鏈的最前端。當作用域鏈被銷毀時,激活對象也一同被銷毀。如下所示:(運行add()時的作用域鏈)

在函數運行的過程中,每遇到一個變量,就要進行標識符識別。標識符識別這個過程要決定從哪里獲得數據或者存取數據。此過程搜索運行期上下文的作用域鏈,查找同名的標識符。搜索工作從運行函數的激活目標的作用域前端開始。如果找到了,就使用這個具有指定標識符的變量;如果沒找到,搜索工作將進入作用域鏈的下一個對象,此過程持續運行,直到標識符被找到或者沒有更多可用對象可用於搜索,這種情況視為標識符未定義。正是這種搜索過程影響了性能。如果想了解如何編寫高性能的js,建議看看這篇blog,這個小結也是摘自於它——http://www.cnblogs.com/coco1s/p/4017544.html
執行上下文
在JavaScript中有三種代碼運行環境:
-
Global Code
- JavaScript代碼開始運行的默認環境
-
Function Code
- 代碼進入一個JavaScript函數
-
Eval Code
- 使用eval()執行代碼
為了表示不同的運行環境,JavaScript中有一個執行上下文(Execution context,EC)的概念。也就是說,當JavaScript代碼執行的時候,會進入不同的執行上下文,這些執行上下文就構成了一個執行上下文棧(Execution context stack,ECS)。
執行上下文包含三個重要的概念,彼此聯系且不好理解,導致了新手很難做到一次理解清楚,如下圖所示:

每個執行上下文都有三個重要的屬性,變量對象(Variable object,VO),作用域鏈(Scope chain)和this,當然還有一些附加的屬性。

當一段JavaScript代碼執行的時候,JavaScript解釋器會創建Execution Context,其實這里會有兩個階段:
-
創建階段(當函數被調用,但是開始執行函數內部代碼之前)
- 創建Scope chain
- 創建VO/AO(variables, functions and arguments)
- 設置this的值
-
激活/代碼執行階段
- 設置變量的值、函數的引用,然后解釋/執行代碼
這里想要詳細介紹一下"創建VO/AO"中的一些細節,因為這些內容將直接影響代碼運行的行為。
對於"創建VO/AO"這一步,JavaScript解釋器主要做了下面的事情:
- 根據函數的參數,創建並初始化arguments object
-
掃描函數內部代碼,查找函數聲明(Function declaration)
- 對於所有找到的函數聲明,將函數名和函數引用存入VO/AO中
- 如果VO/AO中已經有同名的函數,那么就進行覆蓋
-
掃描函數內部代碼,查找變量聲明(Variable declaration)
- 對於所有找到的變量聲明,將變量名存入VO/AO中,並初始化為"undefined"
- 如果變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會干擾已經存在的這類屬性
看下面的例子:
function foo(i) {
var a = 'hello';
var b = function privateB() {
};
function c() {
}
}
foo(22);
對於上面的代碼,在"創建階段",可以得到下面的Execution Context object:
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: undefined,
b: undefined
},
this: { ... }
}
在"激活/代碼執行階段",Execution Context object就被更新為:
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: 'hello',
b: pointer to function privateB()
},
this: { ... }
}
總結
函數在定義時就會確定他的作用域與作用域鏈(靜態),只有調用的時候才會創建一個執行上下文,其中包含了調用時的形參,其中的函數聲明與變量(VO), 同時創建活動對象(AO),並將AO壓入執行上下文的作用域鏈的最前端並且包含了this的屬性,執行上下文的作用域鏈是通過正在被調用函數的作用域鏈得到的(動態)。
以上的理解如有錯誤,敬請指正,敬禮~
參考
http://www.cnblogs.com/coco1s/p/4017544.html
http://www.cnblogs.com/wilber2013/p/4909459.html
http://www.cnblogs.com/wilber2013/p/4909430.html#_nav_3

