變量提高是 Javascript 中一個頗有趣,也讓不少人迷惑的特徵。那麼,Javascript 爲何要設計這個特徵呢?javascript
我來看 Javascript 創始人 Brendan Eich 的 twitter:html
A bit more history:var
hoisting was an implementation artifact.function
hoisting was better motivated: https://twitter.com/BrendanEi....
函數的提高使用明確的理由的。可是變量的提高,只是再實現的「順便」提高了。java
那麼函數爲何要提高呢?他也給出了理由:node
yes, function declaration hoisting is for mutual recursion & generally to avoid painful bottom-up ML-like order
函數提高是爲了能夠再函數定義以前調用函數。只有這樣纔可能支持兩個函數之間互相調用。同時,這樣能夠把程序的主要邏輯放在前部,而不是必須放在程序最後,是程序結果更加符合人的書寫與閱讀習慣。api
不少介紹變量提高的文章,提到變量提高能夠這樣理解:瀏覽器
/*... 一些代碼 ...*/ var x = 1;
等價於app
var x; /*... 一些代碼 ...*/ x = 1;
這很直觀。可是 Javascript 編譯、運行過程當中,顯然不會隨便修改用戶的代碼。那麼,變量提高在 Javascript 中具體是如何實現的呢?函數
爲了解決這個問題,咱們先看一下 Javascript 的整理運行邏輯。ui
在 ECMA 262 裏,對 ScriptEvaluationJob 如瑞啊的描述:lua
- Assert: sourceText is an ECMAScript source text (see clause 10).
- Let realm be the current Realm Record.
- Let s be ParseScript(sourceText, realm, hostDefined).
If s is a List of errors, then
- Perform HostReportErrors(s).
- Return NormalCompletion(undefined).
- Return ? ScriptEvaluation(s).
對 Module 來講,有 TopLevelModuleEvaluationJob:
- Assert: sourceText is an ECMAScript source text (see clause 10).
- Let realm be the current Realm Record.
- Let m be ParseModule(sourceText, realm, hostDefined).
If m is a List of errors, then
- Perform HostReportErrors(m).
- Return NormalCompletion(undefined).
- Perform ? m.Instantiate().
- Assert: All dependencies of m have been transitively resolved and m is ready for evaluation.
- Return ? m.Evaluate().
能夠看到,Javascirpt 雖然是解釋性執行的語言,可是它並非邊讀取邊解釋邊執行,而是必定要把整個腳本加載並解析完成(經過 ParseScript
或 ParseModule
)以後,纔開始執行。這樣,在腳本開始執行的時候,就能夠知道全部的變量與函數的聲明的信息,即便尚未執行到變量或函數聲明的地點。這就使得在 Javascript 裏引用「尚未聲明」的函數和變量成爲可能。
變量提高會在函數內,以及全局做用域發生。
在 ECMA-262 中,經過 VarScopedDeclarations 來收集 Script 中的 var 變量定義,以及頂級函數定義,並在執行腳本以前,經過 GlobalDeclarationInstantiation 註冊至全局環境。這些變量以及函數將被放在全局對象中。變量在此時將被初始化爲 undefined
,而函數則是直接被初始化爲函數自己(能夠直接調用)。
Script 全局的 VarScopedDeclarations 將收集:
var
聲明的變量。包括 for(var ...)
、for await (var ...)
中用 var 聲明的變量。同時,在各類控制結構內部,以及 Block 內部的都會被一塊兒收集。這一過程實在運行以前執行的,因此聲明是否會提高與代碼是否會被執行無關。如 if
等控制語句中,未被執行的代碼中定義的變量也會被提高。可是,函數定義內部的 var
定義不會被收集,也就是說函數內部的 var
定義不會被提高至函數外。說點細節的話,Script 的 VarScopedDeclarations 直接使用了其中的 StatementList 的 TopLevelVarScopedDeclarations 。StatementList 能夠包含 Statement 與 Declaration 。StatementList 的 TopLevelVarScopedDeclarations 收集了 Statement 的 VarScopedDeclarations 與 Declaration 中的函數聲明。
Statement 的 VarScopedDeclaration 與 Script 不一樣,直接遞歸收集了語句中(除函數定義體內部以外的)全部 var
聲明。
於是對於函數聲明,僅有頂級的會被提高。
Module 的作法略有不一樣。它經過 VarScopedDeclarations 來收集 var 變量定義,經過 LexicallyScopedDeclarations 來收集頂級函數定義,並在執行腳本以前,使用 InitializeEnvironment 註冊至全局環境。Module 沒有全局對象,全局變量是被放在全局的 Module 的 Lexical Scope 裏的。不一樣的 Module 之間,不會互相影響。變量一樣會被初始化爲 undefined
,而函數能夠直接使用。
函數中的情形與 Script 相似。只不過它不經過 Script ,而是使用 FunctionBody 的 VarScopedDeclarations 來收集須要提高的定義,並在 FunctionDeclarationInstantiation 裏註冊到運行環境中。
可是,函數中的 var 變量定義並不保存在對象裏。且 var
與 函數參數能夠認爲是出來同一個環境中的,因此,對於與函數參數同名的 var
變量,它的初始值將是函數的參數值,其它依然爲 undefined
。
上面說了,var
是能夠跨塊提高的,可是函數聲明不能夠。除了頂級(全局的最外層,或函數定義的最外層)的函數意外,其餘的函數聲明將經過 LexicallyScopedDeclarations ,並在進入塊的時候,經過 BlockDeclarationInstantiation 定義在一個新建的塊級做用域中。
注意,只有函數聲明纔會在相應的做用域引入一個函數對象。函數聲明語句是以 function
關鍵字開始,整條語句僅聲明一個函數。好比, var func = funciton() {}
格式的,不是函數聲明,按照 var
的規則處理。
在 ECMAScript 將塊級函數標準化以前,各家瀏覽器就已經各自實現了塊內定義的函數。這就致使你們的實現各不相同,而且持續至今。這也包括塊內聲明的函數是如何提高的。於是,能夠在瀏覽器裏觀察到與此處描述不一樣的塊級定義的函數的提高行爲。MDN 有不一樣瀏覽器塊內函數提高的在不一樣瀏覽器中的對比。
let
與 const
let
與 const
通常被認爲不提高。可是這也不太準確。他們與 var
有兩點不一樣。
let
與 const
不會跨塊。他們定義的變量僅在塊內(以及快內的嵌套塊、函數內)能夠訪問。在塊外不存在。(var
會提高到函數的頂層。)這與函數是一致的。而且與函數同樣,它的定義是經過 LexicallyScopedDeclarations 收集的。var
變量在建立時,會當即被初始化爲 undefined
。)在 Javascript ,使用未被初始化的變量會拋出異常。因此 var
提高以後,能夠在定義以前使用(由於被初始化了),可是 let
與 const
在做用域以內,定義以前使用就會拋出異常(由於他們進入做用域就已經存在了,可是未被初始化)。可是,若是在做用域外使用,在非嚴格模式下,會致使在全局對象中建立一個同名變量(由於他們不存在!),反而不會出錯。
這個爲啥要拿出來講呢,由於 node.js 的(除 ECMAScript module 外)代碼,都是會被放進一個函數運行的:
(function(exports, require, module, __filename, __dirname) { // Module code actually lives in here });
因此,在 node.js 裏,沒法在全局做用域寫代碼。於是 var
聲明的「全局」變量,也不會進入全局對象。
javascript 中全部變量、常量、函數聲明,都是在進入相應的做用域時生成的。
var
變量的做用域是其所在的函數(或全局做用域),let
、const
、函數的做用域是其所在的塊。
在變量生成的時候,var
變量會被初始化爲 undefined
;let
、const
不會被初始化;函數則直接被初始化爲實際的函數對象。
var
變量與函數參數重名,則會被初始化爲函數參數的值let
、const
變量的做用域中,定義語句以前使用他們會發生錯誤)var
變量僅執行 Initializer 的賦值(若是沒有 Initializer,則什麼也不作);let
、const
變量將初始化爲 Initializer 的值(如沒有 Initializer,初始化爲 undefined
);函數聲明處則什麼也不作