(function() {
console.log(typeof foo); // 這裏會打印出什麼?
console.log(typeof bar); // 這裏會打印出什麼?
var foo = 'hello',
bar = function() {
return 'vian';
};
function foo() {
return 'hello';
}
})();
複製代碼
咱們在接觸JavaScript這門語言時,會常常遇到這種問題,通過後續的學習,咱們可能知道了這種現象在JavaScript中叫聲明提高(hoisting),可是咱們可能只知道聲明提高的現象,卻不清楚形成這種現象的本質,而這個本質倒是JavaScript最爲重要的知識之一。解答上述代碼中的問題以前,咱們先看一下JavaScript中的執行環境。es6
執行環境(execution context)定義了變量或函數有權訪問的其餘數據,決定了它們各自的行爲。每一個執行環境都有一個與之關聯的變量對象(variable object),環境中定義的全部變量和函數都保存在這個對象中。雖然咱們編寫的代碼沒法訪問這個對象,但解析器在處理數據時會在後臺使用它。瀏覽器
在JavaScript中可執行代碼能夠分爲三類:bash
全局執行環境是最外圍的一個執行環境。在Web瀏覽器中,全局執行環境是window對象。當代碼載入瀏覽器時,全局執行環境被建立,直到網頁或瀏覽器被關閉,全局執行環境才被銷燬。 在一個Javascript程序中,必須且只能存在一個全局執行環境,和任意個的非全局執行環境,程序每調用一個函數,都會建立新的執行環境,以下圖所示。函數
該JavaScript程序中含有一個全局執行環境、三個函數執行環境。爲以正確的順序執行代碼,JavaScript中用堆棧的形式來處理執行環境——執行環境棧(Execution Context Stack)。瀏覽器中JavaScript是單線程的,這意味着運行在瀏覽器中的JavaScript代碼,同一時間只有一件事件、動做發生。除了當前正在運行的代碼,其餘代碼都在一個隊列中排隊,這個隊列就是執行環境棧。 爲了更好的說明執行環境棧,咱們結合一個例子來講明:學習
(function test(i) {
if (i === 1) {
return;
}
test(++i);
})(0);
複製代碼
下面咱們用圖來表示上述代碼執行時,執行環境棧中的變化過程: ui
當JavaScript代碼執行時,第一個建立的老是全局執行環境,所以全局執行環境也老是在執行環境棧的最底部。 this
代碼執行時,調用了函數test(0),此時建立新的執行環境test EC,並壓入執行棧。 spa
在執行第一次執行函數test(i)時(i=0),執行到函數體中的代碼test(++i),程序再一次調用函數test(i),建立新的執行環境test EC1,並壓入執行棧。 線程
函數test(i = 1)中的代碼執行完畢,該執行環境出棧銷燬,程序回到上一層執行環境中繼續執行。 code
5.函數test(i = 0)中的代碼執行完畢,該執行環境出棧銷燬,程序回到全局執行環境中繼續執行,全局環境的代碼即便執行完畢,也不會銷燬,直至網頁或瀏覽器被關閉了纔出棧銷燬。
上面就是程序執行過程當中,執行環境棧的變成過程。關於執行環境棧,有如下幾點要特別注意:
從上文咱們已經知道了,每當一個函數被調用時,一個新的執行環境就會被建立。實際上,執行環境能夠分爲兩個階段,建立階段和執行階段。JavaScript聲明提高的祕密也在其中,咱們繼續往下看。
咱們徹底能夠把執行環境看成一個含有三個屬性的對象,以下:
executionContextObj = {
'variableObject': {...}, //函數的arguments、參數、函數內的變量及函數聲明
'scopeChian': {...}, //本層變量對象及全部上層執行環境的變量對象
'this': {}
}
複製代碼
聲明提高的祕密就發生在變量對象VO中。
每一個執行環境都有一個與之關聯的變量對象,環境中定義的全部變量和函數都保存在這個對象中。
說到執行環境的建立過程就會涉及到變量對象和活動對象,不少人對這兩個概念會模糊不清。其實,變量對象VO和活動對象AO是同一個對象在不一樣階段的表現形式。當進入執行環境的創捷階段時,變量對象被建立,這時變量對象的屬性沒法被訪問。進入執行階段後,變量對象被激活變成活動對象,此時活動對象的屬性能夠被訪問。 下面來看一下,執行環境建立階段中變量對象建立中,JavaScript解析器作的事情:
JavaScript中聲明提高的背後緣由已經很清晰了,你發現了嗎?請先思考一下,咱們下文將結合例子進行講解。先讓咱們結合一段代碼,結合上文的知識,回顧一下代碼執行時,會發生什麼事情。
function greet(name) {
var say = 'hello';
function action() {
console.log(say + name);
}
action();
}
greet('vian');
複製代碼
executionContextObj = {
arguments: {0: 'vian', length: 1},
name: 'vian'
}
複製代碼
executionContextObj = {
arguments: {0: 'vian', length: 1},
name: 'vian',
action: <action>
}
複製代碼
executionContextObj = {
arguments: {0: 'vian', length: 1},
name: 'vian',
action: <action>,
say: undefined
}
複製代碼
經過對JavaScript中執行環境的瞭解,使人奇怪的聲明提高機制也變得清晰明瞭。回到本文的開頭,形成這種聲明提高現象的本質,到底是什麼呢?——執行環境的建立階段,變量對象建立的方式所形成。下面咱們來解釋一下本文開頭的代碼。
(function() {
console.log(typeof foo); // 這裏會打印出什麼?
console.log(typeof bar); // 這裏會打印出什麼?
var foo = 'hello',
bar = function() {
return 'vian';
};
function foo() {
return 'hello';
}
})();
複製代碼
根據上文中分析變量對象建立過程的方法:
executionContextObj = {}
1.初始化arguments對象,及形參
executionContextObj = {
arguments: {length: 0}
}
2.掃描函數聲明,並進行處理:
遇到函數聲明 function foo(){}
executionContextObj中沒有foo屬性,將foo設爲executionContextObj的屬性,函數引用做爲值。
executionContextObj = {
arguments: {length: 0},
foo: <function>
}
3.掃描變量聲明,並進行處理:
遇到變量聲明var foo,executionContextObj已存在foo屬性,跳過。
遇到變量聲明var bar,executionContextObj不存在bar屬性,將其設置爲變量對象屬性,值爲undefined。
executionContextObj = {
arguments: {length: 0},
foo: <function>,
bar: undefined
}
複製代碼
結論:
console.log(typeof foo); // 'function'
console.log(typeof bar); // 'undefined'
複製代碼
到這裏,咱們就不再怕聲明提高的坑和問題啦。須要注意的是,es6中新增的let和const變量聲明都不會進行變量提高,重複賦值及聲明前引用變量都會報錯(TDZ)。
var name = 'vian';
if (1) {
name = 'em'; //TDZ開始 ReferenceError: Cannot access 'name' before initialization
let name; //TDZ結束 name值不會被'em'覆蓋,由於塊級做用域中使用了let聲明name,此時name
//綁定在了塊級做用域中,且不受外部影響。
}
複製代碼
到了ES6,使用新增的let、const關鍵字聲明的變量,不會按照上文的規則進行變量提高。ES6明確規定,若是區塊中存在let和const關鍵字,區塊對這些這些聲明的變量,從一開始就造成一個封閉的做用域,這些變量再也不受外部的影響(可理解爲這些變量綁定在區塊上)。在聲明以前使用這些變量,就會報錯。
總之,在代碼塊內,使用let命令聲明變量以前,該變量都是不可用的。這在語法上,稱爲「暫時性死區」(temporal dead zone,簡稱 TDZ)
但願本文對你們有所幫助,互相學習,一塊兒提升。轉載請註明原帖。