1. 程式人生 > >深入理解JavaScript中變數作用域

深入理解JavaScript中變數作用域

理解JavaScript變數作用域:

------------------

變數作用域又叫做變數的可見性。在JavaScript中,變數的作用域是由函式限定的,它們要麼是全域性的,要麼是區域性的。·顧名思義,全域性變數處處可以訪問,區域性變數只有在聲明瞭它的地方才能訪問。(PS:在JavaScript 1.7+ 版本中引入了一種全新的塊級作用域構造器,使用 **let** 語句可以實現類似於C、C++、Java等語言中的塊級作用域,不過並不是所有瀏覽器都實現了這一語法特性。使用示例:`let(v = 'block scope'){console.log(v)}; console.log(v);`)

變數作用域與其生命週期:

------------

變數的生命週期是指一個變數何時被建立和釋放。變數作用域注重於“在形式上能操作一個變數的範圍有多大”,變數的生命週期則注重於“在實現中一個變數建立和銷燬的時機”。

在JavaScript中,一個變數的建立是在以下情況下發生的:

· 引擎做語法分析,發現顯示生命時

· 引擎做程式碼執行,發現試圖**寫**(例如賦值操作)一個未被建立的變數時(**讀取**一個未被建立的變數將出錯!)

一個變數的釋放則是發生在:

· 引擎執行到函式結束/退出操作時,將清除函式內未被引用的變數

· 引擎執行到全域性的程式碼塊終結或引擎解除安裝和過載入時,將清除全域性的變數和資料的引用

所以,在JavaScript中的變數的生命週期只有兩個:函式內的區域性執行期間和函式外引擎的全域性執行期間。這些是由JavaScript引擎的實現所選擇的技術手段決定的,而不是語法和語義上的約定。

變數提升:

----

在JavaScript中,當變數被宣告時,宣告會被提升到它所在的函式的頂部,並賦予undefined值(這一點我們可以通過瀏覽器自帶的除錯工具進行檢視哦!)。這就使得在函式的任意位置宣告的變數的作用域擴大到了整個函式中,儘管在賦值之前,它的值一直為 undefined 。

function hoisted(){ 	console.log(v);	var v = 123;}

上面的程式碼片段等價於:

function hoisted(){	var v;	console.log(v);	v = 123;}

記住:**JavaScript的變數宣告會被提升到它們所在的函式頂部,而初始化的地方仍舊在原來的地方。JavaScript引擎並沒有重寫程式碼,每次呼叫函式時,宣告都會重新提升。**

function demo(){	console.log(v); // undefined	var v = 123;	console.log(v); // 123}
因為變數宣告總是被提升到函式作用域的頂部,所以在函式的頂部使用單一 var 宣告變數總是最好的做法(這裡與《程式碼大全》中推薦的“變數在哪裡使用就在最近的地方進行變數宣告”有一點點的“小衝突”。不過這是長久以來的編碼習慣啦!)。

執行環境、執行環境物件和高階變數提升

------------------

**提升**

在JavaScript中,語法解釋與程式碼執行是分兩個階段完成的(或者說:“JavaScript引擎在進入作用域時,會對程式碼分兩輪處理。第一輪時初始化變數;第二輪則是執行程式碼;”),變數的宣告是在第一輪也就是語法解釋階段進行處理的。 在第一輪,JavaScript引擎分析程式碼,並做出以下3件事:

· 宣告並初始化函式引數

· 宣告區域性變數,包括將匿名函式賦給一個區域性變數,但並不初始化它們

· 宣告並初始化函式

在第一輪執行的結果中,我們可以清晰的看見,區域性變數並沒有被賦值,因為它們最終的正確值可能需要在程式碼執行後才能確定,而第一輪語法解釋階段是不會執行程式碼的。但是函式的引數被賦值了,因為在向函式傳遞引數之前,任何決定引數值的程式碼都已經運行了。程式碼示例:

var v = 123; // 宣告一個全域性變數 function demo(){	console.log(v);  // 輸出undefined,區域性變數宣告被提升,覆蓋了全域性變數。這裡和變數作用域鏈有關,之後討論。	var v = 456;}demo(); // 呼叫函式// 說明引數在第一輪就被賦值了 var v= 123;function demo(v){	console.log(v); // 輸出789	var v = 456;}demo(789) // 呼叫函式,並將引數傳入其中

**執行環境(execution context)**:

在JavaScript中,JavaScript引擎把變數作為屬性儲存在一個物件上,這個物件被稱為“**執行環境物件**”。 每當函式被呼叫時就會產生一個新的執行環境。**執行環境是一種概念,是執行中的函式的意思,它不是物件**。執行環境定義了變數或函式有權訪問的其它資料,決定了它們各自的行為。每個執行環境都有一個與之關聯的**變數物件**(variable object),環境中定義的所有變數和函式都儲存在這個物件中,這個物件我們編碼人員是不能直接訪問到的。(執行環境也依賴於JavaScript引擎的具體實現)。

所有在函式中定義的變數和函式都是執行環境的一部分,它們都被儲存在執行環境物件中。如果變數在當前的執行環境中可以訪問到,那麼變數就在作用域內,換一種說法則是:“如果在函式執行時變數可以被訪問到,那麼該變數在作用域內”;

如上圖所示:

(1)在<script>標籤內的所有東西都在全域性執行環境中

(2)呼叫first_function時,會在全域性執行環境中建立一個新的執行環境。在first_function執行時,它有許可權訪問巢狀建立它的執行環境裡面的變數。比如圖中所示:first_function有許可權訪問在全域性執行環境中定義的變數以first_function中定義的變數,我們稱這些變數在作用域中。但是first_function沒有許可權訪問巢狀建立在其執行環境中的second_function的執行環境中定義的變數。

(3)呼叫second_function時,會在first_function的執行環境中巢狀建立一個新的執行環境,這裡的second_function有許可權訪問first_function的執行環境中的變數。

(4)再次呼叫second_function時,這次是在全域性環境中呼叫的。這裡的second_function沒有許可權訪問在first_function的執行環境中定義的變數,因為second_function不是在first_function的執行環境中被呼叫的。也就是說second_function的執行環境不是在first_function中巢狀建立的。這裡的second_function也沒有許可權訪問先前呼叫的second_function中的變數,因為它們發生在不同的執行環境中。

JavaScript引擎在執行環境物件中訪問作用域內的變數,查詢的順序叫做作用域鏈(scope chain),它和原型鏈一個,描述了JavaScript訪問變數和屬性的順序。(PS:JavaScript中的函式的作用域是通過詞法來劃分的,也就是說:在定義函式的時候作用域鏈就固定了)。

作用域鏈(scope chain)

-----------------

作用域鏈的前端,始終都是當前執行的程式碼所在的執行環境的執行環境物件。更詳細的內容可以參見維基百科。