var a = 1; function foo() { if (!a) { var a = 2; } alert(a); }; foo();
上面這段代碼在運行時會產生什麼結果?javascript
儘管對於有經驗的程序員來講這只是小菜一碟,不過我仍是順着初學者常見的思路作一番描述:html
建立了全局變量 a
,定義其值爲 1
java
建立了函數 foo
程序員
在 foo
的函數體內,if
語句將不會執行,由於 !a
會將變量 a
轉變成布爾的假值,也就是 false
閉包
跳過條件分支,alert
變量 a
,最終的結果應該是輸出 1
函數
嗯,看起來無懈可擊的推理啊,但讓人驚訝的是:答案居然是 2
!爲何?this
彆着急,我會解釋給你聽。首先我要告訴你這不是什麼錯誤,而是 JavaScript 語言解釋器的一個(非官方的)特性,某人(Ben Cherry)把這個特性叫作:Hoisting(目前還沒有有標準的翻譯,比較常見的是提高)。翻譯
爲了理解 Hoisting,咱們先來看一個簡單的狀況:code
var a = 1;
你是否想過,上面這句代碼在運行的時候到底發生了什麼?
你是否知道,就這句代碼而言,「聲明變量 a
」 和 「定義變量 a
」這兩個說法哪個纔是正確的?htm
下例叫作 「聲明變量」:
var a;
下例叫作 「定義變量」:
var a = 1;
聲明:是指你聲稱某樣東西的存在,好比一個變量或一個函數;但你沒有說明這樣東西究竟是什麼,僅僅是告訴解釋器這樣東西存在而已;
定義:是指你指明瞭某樣東西的具體實現,好比一個變量的值是多少,一個函數的函數體是什麼,確切的表達了這樣東西的意義。
總結一下:
var a; // 這是聲明 a = 1; // 這是定義(賦值) var a = 1; // 合二爲一:聲明變量的存在並賦值給它
重點來了:當你覺得你只作了一件事情的時候(
var a = 1
),實際上解釋器把這件事情分解成了兩個步驟,一個是聲明(var a
),另外一個是定義(a = 1
)。
回到最開始的那個使人困惑的例子,我告訴你解釋器是如何分析你的代碼的:
var a; a = 1; function foo() { var a; // 關鍵在這裏 if (!a) { a = 2; } alert(a); // 此時的 a 並不是函數體外的那個全局變量 }
如代碼所示,在進入函數體後解釋器聲明瞭新的變量 a
,當時其值爲 undefined
,因而 if
語句條件判斷結果爲真,接着爲新的變量 a
賦值爲 2
。你若不相信能夠在函數體外面 alert(a)
,而後再執行 foo()
對比一下結果就知道了。
有人可能會問了:「爲何不是在 if
語句內聲明變量 a
?」
由於 JavaScript 沒有塊級做用域(Block Scoping),只有函數做用域(Function Scoping),因此說不是看見一對花括號 {}
就表明產生了新的做用域,和 C 不同!
當解析器讀到 if
語句的時候,它發現此處有一個變量聲明和賦值,因而解析器會將其聲明提高至當前做用域的頂部(這是默認行爲,而且沒法更改),這個行爲就叫作 Hoisting。
OK,你們都懂了,你懂了嗎……
懂了不表明就會用了,就拿最開始的例子來講,若是我就是想要 alert(a)
出那個 1
可咋整呢?
alert(a)
在執行的時候,會去尋找變量 a
的位置,它從當前做用域開始向上(或者說向外)一直查找到頂層做用域爲止,如果找不到就報 undefined
。
由於在 alert(a)
的同級做用域裏,咱們再次聲明瞭本地變量 a
,因此它報 2
;因此咱們能夠把本地變量 a
的聲明向下(或者說向內)移動,這樣 alert(a)
就找不到它了。
記住:JavaScript 只有函數做用域!
var a = 1; function foo() { if (!a) { (function() { // 這是上一篇說到過的 IIFE,它會建立一個新的函數做用域 var a = 2; // 而且該做用域在 foo() 的內部,因此 alert 訪問不到 }()); // 不過這個做用域能夠訪問上層做用域哦,這就叫:「閉包」 }; alert(a); }; foo();
你或許在無數的 JavaScript 書籍和文章裏讀到過:「請始終保持做用域內全部變量的聲明放置在做用域的頂部」,如今你應該明白爲何有此一說了吧?由於這樣能夠避免 Hoisting 特性給你帶來的困擾(我不是很情願這麼說,由於 Hoisting 自己並無什麼錯),也能夠很明確的告訴全部閱讀代碼的人(包括你本身)在當前做用域內有哪些變量能夠訪問。可是,變量聲明的提高並不是 Hoisting 的所有。在 JavaScript 中,有四種方式可讓命名進入到做用域中(按優先級):
語言定義的命名:好比 this
或者 arguments
,它們在全部做用域內都有效且優先級最高,因此在任何地方你都不能把變量命名爲 this
之類的,這樣是沒有意義的
形式參數:函數定義時聲明的形式參數會做爲變量被 hoisting 至該函數的做用域內。因此形式參數是本地的,不是外部的或者全局的。固然你能夠在執行函數的時候把外部變量傳進來,可是傳進來以後就是本地的了
函數聲明:函數體內部還能夠聲明函數,不過它們也都是本地的了
變量聲明:這個優先級其實仍是最低的,不過它們也都是最經常使用的
另外,還記得以前咱們討論過 聲明 和 定義 的區別吧?當時我並無說爲何要理解這個區別,不過如今是時候了,記住:
Hosting 只提高了命名,沒有提高定義
這一點和咱們接下來要講到的東西息息相關,請看:
先看兩個例子:
function test() { foo(); function foo() { alert("我是會出現的啦……"); } } test();
function test() { foo(); var foo = function() { alert("我不會出現的哦……"); } } test();
同窗,在瞭解了 Scoping & Hoisting 以後,你知道怎麼解釋這一切了吧?
在第一個例子裏,函數 foo
是一個聲明,既然是聲明就會被提高(我特地包裹了一個外層做用域,由於全局做用域須要你的想象,不是那麼直觀,可是道理是同樣的),因此在執行 foo()
以前,做用域就知道函數 foo
的存在了。這叫作函數聲明(Function Declaration),函數聲明會連通命名和函數體一塊兒被提高至做用域頂部。
然而在第二個例子裏,被提高的僅僅是變量名 foo
,至於它的定義依然停留在原處。所以在執行 foo()
以前,做用域只知道 foo
的命名,不知道它究竟是什麼,因此執行會報錯(一般會是:undefined is not a function
)。這叫作函數表達式(Function Expression),函數表達式只有命名會被提高,定義的函數體則不會。
尾記:Ben Cherry 的原文解釋的更加詳細,只不過是英文而已。我這篇是借花獻佛,主要是更淺顯的解釋給初學者聽,若要看更多的示例,請移步原做,謝謝。