本文翻譯自 https://blog.bitsrc.io/hoisting-in-modern-javascript-let-const-and-var-b290405adfda,做者 Sukhjinder Arora,內容有部分刪改,標題有改動。
許多 JavaScript 程序員將提高解釋爲 JavaScript 將聲明(變量和函數)移至其當前做用域(函數或全局)頂部的行爲。好像它們實際上已經移到了代碼的頂部,事實並不是如此。例如:javascript
console.log(a); var a = 'Hello World!';
他們會說,上面的代碼將在提高後轉換爲如下代碼:前端
var a; console.log(a); a = 'Hello World!';
儘管看起來是這樣,由於代碼也工做正常了,可是 JavaScript 引擎事實上並非這麼作的,你的代碼仍是在這裏。java
在編譯階段,即在代碼執行前的幾微秒內,將對其進行掃描以查找函數和變量聲明。全部這些函數和變量聲明都添加到內存中稱爲詞法環境的 JavaScript 數據結構內部。這樣,即便在源代碼中實際聲明它們以前也可使用它們。程序員
詞法環境是用來保存標識符和變量映射關係的地方。標識符是變量或者函數的名字,變量是對實際對象(包括函數對象和數組對象)或者原始值的引用。es6
簡而言之,詞法環境是存儲變量和對象引用的地方。面試
詞法環境的結構以下:數組
LexicalEnvironment = { Identifier: <value>, Identifier: <function object> }
若是想要了解更多詞法環境相關的內容,能夠查看我翻譯的這篇文章 面試必備 | 一文讀懂 JavaScript 中的執行上下文。瀏覽器
如今咱們知道了提高的內部原理是什麼,讓咱們看看函數和變量(let
、const
、var
)聲明的提高是如何發生的。數據結構
helloWorld(); // prints 'Hello World!' to the console function helloWorld(){ console.log('Hello World!'); }
咱們已經知道函數聲明是在編譯階段添加到內存的,所以咱們能夠在實際函數聲明以前在代碼中對其進行訪問。閉包
所以,以上代碼的詞法環境以下所示:
lexicalEnvironment = { helloWorld: <func> }
所以,當 JavaScript 引擎遇到 helloWorld()
時,它將查看詞法環境,找到該函數並可以執行它。
JavaScript 引擎只會提高函數聲明,並不會提高函數表達式。看下面的例子:
helloWorld(); // TypeError: helloWorld is not a function var helloWorld = function(){ console.log('Hello World!'); }
因爲 JavaScript 僅提高聲明,而不賦值,helloWorld
會被視爲變量而不是函數。由於 helloWorld
是一個 var
聲明的變量,因此在提高階段引擎將會給它賦值 undefined
,因此上述代碼會報錯。
下面的代碼是正常的:
var helloWorld = function(){ console.log('Hello World!'); prints 'Hello World!' } helloWorld();
讓咱們看一些示例,以瞭解 var
變量的提高。
console.log(a); // outputs 'undefined' var a = 3;
咱們指望獲得 3
,可是獲得了 undefined
。爲何?
請記住,JavaScript 僅是提高聲明,並不會提高賦值操做。也就是說,在編譯期間,JavaScript 僅將函數和變量聲明存儲在內存中,並沒把賦值操做也一塊兒提高,而 function
聲明的函數會被總體提高。
但爲何是 undefined
呢?
當 JavaScript 引擎在編譯階段找到一個 var
變量聲明時,它會把該變量添加到詞法環境中,並給它賦值 undefined
做爲初始值,而後當代碼執行到賦值語句時,會把實際的值賦到詞法環境中對應的變量。
所以,以上代碼的初始詞法環境以下所示:
LexicalEnvironment = { a: undefined }
這就是咱們獲得 undefined
而不是 3
的緣由。在執行階段,當代碼執行到實際賦值的那一行的時候,會把值賦給詞法環境中對應的變量。因此賦值以後的詞法環境將會是下面這樣:
LexicalEnvironment = { a: 3 }
先看下面的例子:
console.log(a); let a = 3;
執行代碼,將會報錯 Uncaught ReferenceError: Cannot access 'a' before initialization
。
那麼,報錯是由於 let
和 const
聲明的變量沒有提高嗎。
答案要複雜得多。 全部聲明(function
,var
,let
,const
和 class
)都會提高,而 var
聲明會被初始化爲 undefined
,可是 let
和 const
聲明保持未初始化 uninitialized
。
只有當 JavaScript 事實上執行過了聲明語句以後,它們纔會被初始化,JS 引擎作了限制,你不能在初始化它們以前就使用它們。這也就是咱們說的暫時性死區。
若是 JavaScript 引擎在聲明它們的行上仍找不到 let
或 const
的值,它將爲它們分配 undefined
或返回錯誤(若是爲 const
)。
看下面的例子,因爲 const
聲明的變量是不可改變的,因此聲明的時候沒有賦值將會直接報錯。
const ast // VM275:1 Uncaught SyntaxError: Missing initializer in const declaration
再看下面的例子:
let a; console.log(a); // outputs undefined a = 5;
在編譯階段,JavaScript 引擎遇到該變量 a
並將其存儲在詞法環境中,可是因爲它是 let
變量,所以引擎不會使用任何值對其進行初始化。所以,在編譯階段,詞法環境將以下所示:
lexicalEnvironment = { a: <uninitialized> }
如今,若是咱們嘗試在聲明變量以前訪問變量,則 JavaScript 引擎將嘗試從詞法環境中獲取變量的值,由於該變量未初始化,所以將引起引用錯誤。
在執行期間,當引擎到達聲明該變量的行時,它將嘗試給該變量賦值,由於該變量沒有與之關聯的值,所以將爲其分配 undefined
。
所以,執行第二行後,詞法環境將以下所示:
lexicalEnvironment = { a: undefined }
因此 undefined
將會打印到控制檯,而後詞法環境中的 a
將會更新,a
將會被賦值爲 5
。
只要在變量聲明以前不執行該代碼,咱們甚至能夠在聲明它們以前在代碼(例如,函數主體)中引用 let
和 const
變量。
例如,此代碼是徹底有效的。
function foo () { console.log(a); } let a = 20; foo(); // This is perfectly valid
可是下面的代碼將會報錯。
function foo() { console.log(a); // ReferenceError: a is not defined } foo(); // This is not valid let a = 20;
緣由是函數後執行時,在做用域鏈中找到的 a
的值是已經被賦予了 20
的,若是函數先執行而後再賦值,訪問到的 a
是未被初始化的。
class
是 ES6 中出現的一個關鍵字,它也會提高,方式和 let
const
一致,也會產生暫時性死區,它在初始狀況下也是未初始化的,直到執行賦值。
// Uncaught ReferenceError: Cannot access 'Person' before initialization let peter = new Person('Peter', 25); console.log(peter); class Person { constructor(name, age) { this.name = name; this.age = age; } }
所以,要訪問 class
,你必須先聲明它們。例如:
class Person { constructor(name, age) { this.name = name; this.age = age; } } let peter = new Person('Peter', 25); console.log(peter); // Person { name: 'Peter', age: 25 }
讓咱們從詞法環境的角度分析下。在編譯階段,上面代碼的詞法環境以下:
lexicalEnvironment = { Person: <uninitialized> }
當執行到 class 聲明的那段代碼,Person
會被初始化爲對應的值。
lexicalEnvironment = { Person: <Person object> }
就像函數表達式同樣,類表達式也不會提高。例如,此代碼將不起做用。
// VM266:1 Uncaught ReferenceError: Cannot access 'Person' before initialization let peter = new Person('Peter', 25); console.log(peter); let Person = class { constructor(name, age) { this.name = name; this.age = age; } }
正確的方法是這樣的:
let Person = class { constructor(name, age) { this.name = name; this.age = age; } } let peter = new Person('Peter', 25); console.log(peter); // Person { name: 'Peter', age: 25 }
所以,如今咱們知道在提高過程當中,JavaScript 引擎實際不會移動代碼。正確理解提高機制將有助於您避免未來因爲提高而引發的任何錯誤和混亂。爲避免未定義的變量或引用錯誤(ReferenceError
)等提高的反作用,請始終嘗試在變量的各自做用域頂部聲明變量,並始終在聲明變量時嘗試初始化變量。
上面章節都是翻譯的,接下來要講解一個漏掉的知識點。咱們知道在 ES6 中提出了塊級做用域的概念,塊級做用域中聲明的變量也會存在變量提高,可是部分提高的方式和其餘做用域稍微不一樣。
看下面的例子,是否是和咱們日常碰到的狀況不太同樣:
// undefined console.log('a1', a) { // function a console.log('a2', a) a = 100 // 100 console.log('a3', a) function a() {} // 100 console.log('a4', a) } // 100 console.log('a5', a)
如下內容來自 阮一峯-ES6 入門
http://es6.ruanyifeng.com/#docs/let#%E5%9D%97%E7%BA%A7%E4%BD%9C%E7%94%A8%E5%9F%9F:容許在塊級做用域內聲明函數。
函數聲明相似於 var
,即會提高到全局做用域或函數做用域的頭部。同時,函數聲明還會提高到所在的塊級做用域的頭部。
由以上咱們知道,塊級做用域內聲明的函數會有兩個操做:1. 提高到全局做用域;2. 提高到所在塊級做用域內部。
這兩個過程以及提高的時機用下面的代碼來描述(來自 https://stackoverflow.com/questions/31419897/what-are-the-precise-semantics-of-block-level-functions-in-es6):
// 在函數內的塊級做用域內聲明瞭一個函數 `compat` function enclosing(…) { … { … function compat(…) { … } … } … }
提高的過程表示以下:
function enclosing(…) { var compat₀ = undefined; // function-scoped … { let compat₁ = function compat(…) { … }; // block-scoped … compat₀ = compat₁; … } … }
提高的過程存在三個步驟:
var
聲明的變量,並賦值爲 undefined
,相似於塊級做用域內部 var
聲明的變量;let
聲明一個同名變量,並賦值爲這個函數。注意內層不只提高了並且賦值了。let
聲明的變量的值賦值給塊級做用域外層用 var
聲明的同名變量。外層的變量就是在這個時候被賦值的。從第1步能夠知道 a1
爲 undefined
,從第2步能夠知道 a2
爲 function a
,從第3步能夠知道 a5
爲 100
。
往期精彩:
關注公衆號能夠看更多哦。
感謝閱讀,歡迎關注個人公衆號 雲影 sky,帶你解讀前端技術,掌握最本質的技能。關注公衆號能夠拉你進討論羣,有任何問題都會回覆。