在實際開發中,作用域對于任何語言而言都是一個不可忽視的極為重要的知識點。今天就來談談JavaScript的作用域和執行環境。
首先,看一下幾個重要概念。
- 執行環境:
執行環境(execution context,為簡單起見,有時也稱為“環境”)是JavaScript 中最為重要的一個概念。執行環境定義了變量或函數有權訪問的其他數據,決定了它們各自的行為。——《JavaScript高級程序設計(第3版)》
每個函數都有一個自己的執行環境。
某個執行環境中的所有代碼執行完畢后,該環境被銷毀,保存在其中的所有變量和函數定義也隨之銷毀(全局執行環境直到應用程序退出——例如關閉網頁或瀏覽器——時才會被銷毀)。——《J3》
- 變量對象:
每個執行環境都有一個與之關聯的變量對象(variable object),環境中定義的所有變量和函數都保存在這個對象中。雖然我們編寫的代碼無法訪問這個對象,但解析器在處理數據時會在后臺使用它。——《J3》
作用域鏈:
當代碼進入到某個執行環境,準備執行時,會為該執行環境對應的變量對象創建一個作用域鏈。作用域鏈其實就相當于一個變量對象的集合,其第一個元素是當前執行環境的變量對象,最后一個元素是全局執行環境的變量對象(在瀏覽器中即window
對象)。標識符解析:
標識符解析是沿著作用域鏈一級一級地搜索標識符的過程。——《J3》
標識符解析說白了就是查找變量(包括函數定義)。而且這個查找過程是按照作用域鏈的順序走的,也就是先搜索當前執行環境的變量對象,找到就終止,沒找到就繼續搜索上一層執行環境的變量對象,一直搜索到頂層的window
對象。
純文字的表述不容易理解,還是上代碼吧:
<script>
var common = 'Hello';
function sayHello() {
var say = 'Chuck';
say = common + say; //在sayHello()函數這個執行環境內調用common變量
alert(say);
}
sayHello(); //'HelloChuck'
</script>```
這段代碼之所以能在`sayHello()`這個函數中調用變量`common`成功,就是因為可以在函數`sayHello()`的執行環境的變量對象的作用域鏈上找到變量`common`。分析一下可以知道,這個作用域鏈包含2個變量對象,第一個就是`sayHello()`函數的變量對象,里邊只有一個局部變量`say`,這里找不到`common`,然后在第二個變量對象——`window`對象中找到了`common`。這個過程就是依靠于作用域鏈的標識符解析。
因此也可以理解,作用域鏈是一個線性的、單向的、層級的搜索機制。JS中所有變量或函數的訪問或調用,都遵循這一機制。
***
上面說的是JavaScript的執行環境和作用域鏈,下邊再來看一下JavaScript于其他語言的另一個不同點:塊級作用域。
直接上代碼:
```javascript
<script>
if (true) {
var color = "blue";
}
alert(color); //"blue"
</script>
這段代碼在js中沒有任何問題,但是對其他大部分語言而言,這段代碼并不能輸出 "blue"
而是會報錯,原因在于在其他語言中,由花括號 {}
封閉的 if
代碼塊是一個塊級作用域,在這個塊級作用域中聲明的局部變量會在此部分代碼執行完畢后立即銷毀,因而外部無法再獲取到這些局部變量。但是在 js 中,并沒有塊級作用域的概念(ES6中新增了塊級作用域的聲明及相關關鍵字,但在ES6得到規范性的支持和應用之前,我們這里都暫以ES5為準),其所有變量和函數的訪問和調用都是采用作用域鏈的機制執行搜索的。上面這段代碼屬于同一個執行環境,在 if
中聲明的變量會被添加到當前執行環境(在這里是全局環境)的變量對象中,直到當前執行環境所有的代碼都執行完畢后,當前執行環境才會被銷毀,其中的變量和函數也隨之銷毀(即執行環境的變量對象被銷毀)。因此,這段代碼在 js 中可正常輸出 "blue"
,因為 if 判斷執行完之后變量 color
并沒有被銷毀。
因此,也可以理解,js 中 for
循環的循環變量 i
在循環結束后仍然存在于循環外部的當前執行環境中,而不是像其他語言那樣, for
循環執行完畢后 i
立即被釋放而無法在該循環外部訪問。