不少編程語言在執行的時候都是自上而下執行,但實際上這種想法在JavaScript中並不徹底正確, 有一種特殊狀況會致使這個假設是錯誤的。來看看下面的代碼,編程
a = 2; var a; console.log( a );
console.log(a) 會輸出什麼呢?編程語言
有些人可能會認爲是 undefined,由於 var a 聲明在 a = 2 以後,他們天然而然地認爲變量被從新賦值了,所以會被賦予默認值 undefined。可是,真正的輸出結果是 2。 函數
先不急爲何,咱們再繼續看另一段代碼,spa
console.log( a ); var a = 2;
鑑於上一個代碼片斷所表現出來的某種非自上而下的行爲特色,你可能會認爲這個代碼片斷也會有一樣的行爲而輸出 2。還有人可能會認爲,因爲變量 a 在使用前沒有先進行聲明,所以會拋出 ReferenceError 異常。code
其實否則,兩種猜想都是不對的。輸出來的會是 undefined。
blog
引擎會在解釋 JavaScript 代碼以前首先對其進行編譯,簡單地說,任何 JavaScript 代碼片斷在執行前都要進行編譯(一般就在執行前,說一般是由於JavaScript 中存在兩個機制能夠「欺騙」 詞法做用域: eval(..) 和 with)。編譯階段中的一部分工做就是找到全部的聲明,並用合適的做用域將它們關聯起來,包括變量和函數在內的全部聲明都會在任何代碼被執行前首先被處理。這就是咱們一般說的「提高」。
注:只有聲明自己會被提高, 而賦值或其餘運行邏輯會留在原地。
ip
foo(); function foo() { console.log( a ); // undefined var a = 2; }
每一個做用域都會進行提高操做。因此 foo(..)函數自身也會在內部對 var a 進行提高(顯然並非提高到了整個程序的最上方)。在這裏,你或許會發現,爲何代碼裏面是先調用 foo() ,再聲明 foo() 這樣的順序,卻不會報錯。這是由於除了變量聲明會在其做用域內提高以外,函數聲明也具備類似的特效。所以這段代碼能夠暫時理解爲下面的形式:作用域
function foo() { var a; console.log( a ); // undefined a = 2; } foo();
能夠看到,函數聲明會被提高在做用域的頂部。可是有一點須要和變量聲明提高作區別的是:變量提高只是提高了變量的聲明,而變量賦值並無被提高。可是,函數的聲明有點不同,函數體也會一同被提高。開發
因此上面的一段暫時性的代碼實際上能夠這樣理解:it
var foo = { var a; console.log( a ); // undefined a = 2; } foo();
foo 函數的聲明(這個例子還包括實際函數的隱含值)被提高了,所以第一行中的調用能夠正常執行。
然而並非全部的函數都能提高!函數聲明會被提高,可是函數表達式卻不會被提高。
foo(); // 不是 ReferenceError, 而是 TypeError! var foo = function bar() { // ... };
上面這段程序中的變量標識符 foo() 被提高並分配給所在做用域,所以 foo() 不會致使 ReferenceError。可是 foo 此時並無賦值(若是它是一個函數聲明而不是函數表達式,那麼就會賦值)。foo() 因爲對 undefined 值進行函數調用而致使非法操做,所以拋出 TypeError 異常。
同時也要記住,即便是具名的函數表達式,名稱標識符在賦值以前也沒法在所在做用域中使用:
foo(); // TypeError bar(); // ReferenceError var foo = function bar() { // ... };
這個代碼片斷通過提高後,實際上會被理解爲如下形式:
var foo; foo(); // TypeError bar(); // ReferenceError foo = function() { var bar = ...self... // ... }
這裏咱們說到具名函數表達式,就順便插如一點具名函數表達式的知識點。咱們看看下面的例子:
function test() { var fn = function fn1() { log(fn === fn1); // true log(fn == fn1); // true } fn(); log(fn === fn1); // Uncaught ReferenceError: fn1 is not defined log(fn == fn1); // Uncaught ReferenceError: fn1 is not defined } test();
看上面這例子,是否是很疑惑?
具名函數表達式,是帶名字的函數賦值給一個變量,這個名字只在新定義的函數做用域內有效,由於規範規定了標示符不能在外圍的做用域內有效。也就是說,這個函數名只能在此函數內部使用,能夠理解爲這個函數名成了函數體內部的一個變量。
這裏還有一點須要注意的,函數定義了一個非標準的name屬性,經過這個屬性能夠訪問到給定函數指定的名字,這個屬性的值永遠等於跟在function關鍵字後面的標識符,匿名函數的name屬性爲空,而具名的函數表達式會修改到這個屬性。
var foo = function(){ //... }; console.log(foo.name); //foo var bar = function foobar(){ //... }; console.log(bar.name); //foobar name值被修改
函數聲明和變量聲明都會被提高。可是一個值得注意的細節(這個細節能夠出如今有多個「重複」 聲明的代碼中)是函數會首先被提高,而後纔是變量。
看一下下面的代碼:
foo(); // 1 var foo; function foo() { console.log( 1 ); } foo = function() { console.log( 2 ); };
會輸出 1 而不是 2 ! 這個代碼片斷會被引擎理解爲以下形式:
function foo() { console.log( 1 ); } foo(); // 1 foo = function() { console.log( 2 ); };
var foo 儘管出如今 function foo()... 的聲明以前,但它是重複的聲明(所以被忽略了),由於函數聲明會被提高到普通變量以前。儘管重複的 var 聲明會被忽略掉, 但出如今後面的函數聲明仍是能夠覆蓋前面的。
foo(); // 3 function foo() { console.log( 1 ); } var foo = function() { console.log( 2 ); }; function foo() { console.log( 3 ); }
咱們來看看下面這個,
function text1() { var a = 1; function b() { a = 10; return; function a() {} } b(); console.log(a); // ? } text1(); function text2() { var a = 1; function b() { a = 10; function a() {} } b(); console.log(a); // ? } text2();
想想,這兩段代碼輸出的結果會是什麼?
結果都是1!爲啥???
這裏須要注意的是,在 function b() 中,function a() 因爲存在函數提高,上述代碼實際上的運行代碼是這樣子的,
function text{ var a = 1; function b() { var a = function(){}; a = 10; //return; //這個return對這段代碼沒有任何影響 } b(); console.log(a); 1 }
是否是很神奇~~~~因此在寫代碼的時候,就要特別注意了,不要由於 JavaScript 的提高機制致使不少莫名其妙的bug出來。
最後還有一個要強調一下,因爲一個普通塊內部的函數聲明一般會被提高到所在做用域的頂部,這個過程不會像下面的代碼暗示的那樣能夠被條件判斷所控制:
foo(); // "b" var a = true; if (a) { function foo() { console.log("a"); } } else { function foo() { console.log("b"); } }
function hoistVariable() { if (!foo) { var foo = 5; } console.log(foo); // 5 } hoistVariable();
咱們習慣將 var a = 2; 看做一個聲明,而實際上 JavaScript 引擎並不這麼認爲。它將 var a和 a = 2 看成兩個單獨的聲明, 第一個是編譯階段的任務,而第二個則是執行階段的任務。這意味着不管做用域中的聲明出如今什麼地方,都將在代碼自己被執行前首先進行處理。能夠將這個過程形象地想象成全部的聲明(變量和函數)都會被「移動」到各自做用域的最頂端,這個過程被稱爲提高。
聲明自己會被提高,而包括函數表達式的賦值在內的賦值操做並不會提高。
要注意避免重複聲明,特別是當普通的 var 聲明和函數聲明混合在一塊兒的時候,不然會引發不少危險的問題!
理解變量提高和函數提高可使咱們更瞭解這門語言,更好地駕馭它,可是在開發中,咱們不該該使用這些技巧,而是要規範咱們的代碼,作到可讀性和可維護性。具體的作法是:不管變量仍是函數,都必須先聲明後使用。
若是對於新的項目,可使用let替換var,會變得更可靠,可維護性更高。值得一提的是,ES6中的class聲明也存在提高,不過它和let、const同樣,被約束和限制了,其規定,若是再聲明位置以前引用,則是不合法的,會拋出一個異常。
因此,不管是早期的代碼,仍是ES6中的代碼,咱們都須要遵循一點,先聲明,後使用。