了解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件事:
· 聲明并初始化函數參數
· 聲明局部變量,包括将匿名函數賦給一個局部變量,但并不初始化它們
· 聲明并初始化函數
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiIXZ05WZD9CX5RXa2Fmcn9CXwczLcVmds92czlGZvwVP9EUTDZ0aRJkSwk0LcxGbpZ2LcBDM08CXlpXazRnbvZ2LcRlMMVDT2EWNvwFdu9mZvw1dFpWT4VEVPVTRUFGb41mYshmMjZXUYpVd1kmYr50MZV3YyI2cKJDT29GRjBjUIF2LcRHelR3LcJzLctmch1mclRXY39DN5YjMwQTNwIjNxMDM1EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
在第一輪執行的結果中,我們可以清晰的看見,局部變量并沒有被指派,因為它們最終的正确值可能需要在代碼執行後才能确定,而第一輪文法解釋階段是不會執行代碼的。但是函數的參數被指派了,因為在向函數傳遞參數之前,任何決定參數值的代碼都已經運作了。代碼示例:
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)
-----------------
作用域鍊的前端,始終都是目前執行的代碼所在的執行環境的執行環境對象。更詳細的内容可以參見維基百科。