[TOC]
## 執行環境與作用域
執行環境(execution context ,為簡單期間,有時被稱為“環境”)是JavaScript中最為重要的一個概念。執行環境定義了變量或函數有權訪問的其他數據,決定了它們各自的行為。每個執行環境都有一個與之關聯的變量對象(variable object),環境中定義的所有變量和函數都保存在這個對象中。雖然我們編寫的代碼無法訪問這個對象,但是解析器會在處理數據時訪問它。
全局執行環境是最外圍的一個執行環境。根據 ECMAScript 實現所在宿主環境不同,表示執行環境的對象也不一樣。在web游覽器中,全局執行環境默認為 `window` 對象,因此所有的全局變量和函數都作為 `window` 對象的屬性和方法創建。某個執行環境的所有代碼執行完畢后,該環境被銷毀,保存在其中的所有變量和函數定義也隨之銷毀(全局變量 可以認為一直存在,知道關閉網頁或者關閉游覽器時才會被銷毀)
- 每個函數都有自己的執行環境
當執行流進入一個函數時,函數的環境就會被推入一個環境執行棧中。而在函數執行之后,棧將其環境彈出,把控制權返回給之前的執行環境。ECMAScript程序中執行流正是由這個方便的機制控制著
當代碼在一個環境中執行時,會創建變量對象的一個作用域鏈(scope chain)。作用域鏈的用途,是保證對執行環境有權訪問的所有變量和函數逇有序訪問。作用域鏈的前端,始終都是執行當前執行的代碼所在環境的變量對象。如果這個環境是函數,則將其活動對象(activation object)作為變量對象。活動對象在最開始時只包含一個變量,即 `arguments` 對象(這個對象在全局環境中是不存在的)。作用域鏈中的下一個變量對象來自包含(外部)環境,而再下一個變量對象始終都是作用域中的最后一個對象。
標識符解析是沿著作用域一級一級地搜索標識符的過程。搜索過程始終從作用域鏈的前端開始,然后逐級地向后回溯,直至找到標識符為止(如果找不到標識符,通常會導致錯誤發生)。
```js
var color="blue";
function changeColor(){
if(color==="blue"){
color = "red";
}else{
color = "blue";
}
}
changeColor();
console.log(color); //red
```
這個簡單的例子中,函數 `changeColor()` 的作用域包含兩個對象:它自己的變量對象(其中定義著 `arguments` 對象)和全局環境的變量對象。可以在函數內部訪問變量 `color` ,就是因為可以在這個作用域鏈中找到它。
此外,在局部作用域中定義的變量可以在局部環境中與全局變量互換使用,如下面所示:
```js
var color="blue";
function changeColor(){
var anotherColor="red";
// 這里可以訪問全局變量color,以及自身定義的局部變量anotherColor
function swapColors(){
// 這里可以訪問全局變量color,以及changeColor的局部變量anotherColor,以及自身定義的局部變量tempColor
var tempColor=anotherColor;
anotherColor=color;
color=tempColor;
}
swapColors();
}
// 這里只能訪問全局變量color
changeColor();
```
以上代碼有3個執行環境:全局環境、`changeColor()` 的局部環境和 `swapColors()` 的局部環境。全局環境中有一個變量 `color` 和一個函數 `changeColor()` 兩個。 `changeColor()` 的局部環境中有一個名為 `anotherColor` 的變量和一個名為 `swapColor()` 的函數。`changeColor()` 是可以訪問 `color` 變量。`swapColor()` 函數中有一個 `tempColor` 變量,它可以訪問到全局變量 `color` 和 `changeColor()` 中的變量,但是 `changeColor()` 卻不可用訪問 `swapColor()` 中的變量。因為上面兩個是 `swapColor()` 的父執行環境,可以訪問,而子執行環境無法訪問。

上圖實際的展示了執行環境的作用域鏈,每個執行環境可以調用父級執行環境,但是無法調用子執行環境。也就是說內部環境可以通過作用域鏈來訪問所有的外部環境,但外部環境不能訪問內部環境中的任何變量和函數。
### 延長作用域鏈
雖然上面我們看到執行環境只有兩種——全局和局部(在es6中才會有塊級作用域,在這里我們只討論es5),但是天無絕人之路,我們可以使用其他辦法來進行延長作用域鏈。
這么說是因為有些語句可以在作用域鏈的前端臨時增加一個變量對象,該變量對象會在代碼執行后被移除。在兩種情況下會發生這種現象。具體來說,就是當執行流進入下列任何一個語句時,作用域鏈就會得到加長:
* `try-catch` 語句的 `catch` 模塊
* `with` 語句
這連個語句都會在作用域鏈的前端添加一個變量對象。對 `with` 語句來說,會將制定的對象添加到作用域鏈中。對 `catch` 語句來說,會創建一個新的變量對象,其中包含的是被拋出的錯誤對象的聲明。
```js
function buildUrl(){
var qs="?debug=true";
with(location){
var url=href+qs;
}
return url;
}
```
```js
//with的簡單常規用法,用于理解
var obj=new Object();
obj.name="SpiritLing";
obj.age=22;
with(obj){
console.log(name); //SpiritLing
console.log(age); //22
}
```
這兩個語句接受的是 `location` 對象,因此其變量對象就包含了 `location` 對象的所有屬性和方法,而這個變量對象被添加到作用域鏈的前端。 `buildUrl()` 函數中定義了一個變量 `qs` 。當在 `with` 語句中引用變量 `href` 時(實際是引用 `location.href`),可以在當前執行環境的變量對象中找到。當引用變量 `qs` 時,引用的則是在 `buildUrl()` 中定義的那個變量,而該變量位于函數環境的變量對象中。至于 `with` 語句內部,則定義了一個名為 `url` 的變量,因而 `url` 就成了函數執行環境的一部分,所以可以作為函數的值被返回。
### 沒有塊級作用域(ES6中已出現)
在ES6之前的JavaScript中是沒有塊級作用域的,這會在理解上出現困惑。在其他語言中,由花括號封閉的代碼塊都有自己的作用域,因而支持根據條件來的定義變量。
```js
if(true){
var color="red";
}
console.log(color); //red
```
這是一個在 `if` 語句中定義變量 `color` 。如果是在其他語言中,`color` 會隨著 `if` 語句執行完畢后被銷毀。但在JavaScript中,`if` 語句中的變量聲明會將變量添加到當前的執行環境(在這里就是全局變量)中。
- 所以我們在使用 `if-else` 、`switch-case`、`for` 等等時,需要注意這些其中使用的變量都會是它們當前所屬的執行環境
```js
for(var i=0;i<10;i++){
doSomething(i);
}
console.log(i); //10
```
對于有塊級作用域的語言來說。`for` 語句初始化變量的表達式所定義的變量,只會存在于循環語句 `for` 的執行環境中,而不會在父執行環境出現。但是通過上面的代碼塊,我們發現在 `for ` 循環外面,我們輸出了 `i` 。這說明,我們在循環語句的外圍訪問到了這個變量,意味著,這個變量是屬于當前循環塊所處的執行環境中,而不是循環塊的執行環境。如何這個時候我們對變量不使用 `var` 時,則自動添加到全局變量中去,所以,我們在JavaScript中使用 `for`,`if` 等的時候,都需要注意這點。
#### 1. 聲明變量
使用 `var` 聲明的變量會自動被添加到最接近的環境中。在函數內部,最接近的環境就是函數的局部環境;在 `with` 語句中,最接近的環境是函數環境。如果初始化變量時沒有使用 `var` 聲明,該變量會自動被添加到全局環境中。
```js
function add(num1,num2){
var sum=num1+num2;
return sum;
}
var result=add(10,20); //30
console.log(sum); //sum is not defined
```
以上代碼中的函數 `add()` 定義了一個名為 `sum` 的局部變量,該變量存放加法操作的結果。雖然結果值從函數中返回了,但變量 `sum` 在函數外部無法訪問到。如果省略 `var` 的聲明,那么 `sum` 就變為全局環境了,就可以訪問到。
```js
function add(num1,num2){
sum = num1+num2;
return sum;
}
var result = add(10,20); //30
console.log(sum); //30
```
上面的例子中說明了,沒有使用 `var` 聲明的變量,會自動添加到全局變量中去,所以,我們可以在函數外部訪問到。
#### 2. 查詢標識符
當在某個環境中為了讀取或寫入而引入一個標識符時,必須通過搜索確定標識符實際代表什么。搜索過程從作用域的前端開始,向上逐級查詢與給定名字匹配的標識符。如果在局部環境中找到該標識符,則停止搜索,變量就緒。如果在局部環境中沒有找到,則繼續沿作用域鏈向上搜索。搜索過程直到全局環境的變量對象。如果在這里還是沒有找到,則意味著該變量尚未聲明。
- 例子1:
```js
var color="red"
function getColor(){
return color;
}
console.log(getColor()); //"red";
```
- 例子2:
```js
var color="red"
function getColor(){
var color="blue";
return color;
}
console.log(getColor()); //"blue";
```
下面這張圖可以說明情況:

> 當然查詢變量也是需要耗費時間的,所以在使用中,盡量多使用局部變量,全局變量盡量少用,否則會污染全局環境,導致命名沖突。
### 變量提升
> 在說變量提升之前,我們需要先看會前面的執行環境和作用域。
接下來我們看下面這個例子:
```js
var color="blue";
function getColor(){
console.log(color);
}
getColor(); //blue
```
上面的例子,想必所有人都可以不假思索的說出輸出是 blue ,那么下面這個例子呢?
```js
var color="blue";
function getColor(){
console.log(color);
var color="red";
}
getColor(); //???
```
1. "blue"???
2. "red"???
3. "undefined"???
估計有上面三種的結果,都可以在編輯器或者F12打開控制臺測試下,返現輸出是 `undefined` ,當然也許有些游覽器輸出有點不同,那就是游覽器的解析差異了,但是標準是輸出 `undefined` ,這是為什么呢?
因為js中的變量是先聲明在賦值的,所以變量聲明在前,賦值在后,于是我們改寫下上面的例子:
```js
var color;
color="blue";
function getColor(){
var color;
console.log(color); //undefined
color="red";
}
getColor(); //undefined
```
最后正真的就是上面的這個例子,在 `getColor()` 函數中,當進行 `var` 定義時,變量定義會找到當前執行環境,進行聲明,然后輸出 `color` ,這時的 `color` 沒有賦值,所以輸出為 `undefined` ,在下面才進行了變量賦值。這就是變量提升的問題。
### 函數作用域提升
如果是函數和變量類型同時申明定義了,會發生什么事情呢?看下面的代碼
```js
console.log(foo);
function foo(){};
var foo="FOO";
```
上面結果輸出是 `? foo(){}` ,也就是函數內容。這時為什么呢??
**上面聲明函數的方式叫做函數申明,因為函數申明的形式,在提升的時候,會被整個提升上去,包括函數定義的部分!**
如果是下面結果就不同了
```js
console.log(foo);
var foo=function(){};
var foo="FOO";
```
上面例子是使用變量形式進行聲明函數,叫做函數表達式,和變量的聲明一致,先聲明在賦值,所以顯示為 `undefined`,上面兩個例子就是函數作用域提升。