簡單談談JavaScript變量提升
前言
在 ECMAScript6 中,新增了 let 和 const 關鍵字用來聲明變量。在前端面試中也常被問到 let、const和 var 的區(qū)別,這就涉及到了變量提升、暫時性死區(qū)等知識點。下面就來看看什么是變量提升和暫時性死區(qū)。
1. 什么變量提升?
先來看看MDN中對變量提升的描述:
變量提升(Hoisting)被認為是, Javascript中執(zhí)行上下文 (特別是創(chuàng)建和執(zhí)行階段)工作方式的一種認識。在 ECMAScript® 2015 Language Specification 之前的JavaScript文檔中找不到變量提升(Hoisting)這個詞。
從概念的字面意義上說,“變量提升”意味著變量和函數(shù)的聲明會在物理層面移動到代碼的最前面,但這么說并不準確。實際上變量和函數(shù)聲明在代碼里的位置是不會動的,而是在編譯階段被放入內(nèi)存中。
通俗來說,變量提升是指在 JavaScript 代碼執(zhí)行過程中,JavaScript 引擎把變量的聲明部分和函數(shù)的聲明部分提升到代碼開頭的行為。變量被提升后,會給變量設置默認值為 undefined。 正是由于 JavaScript 存在變量提升這種特性,導致了很多與直覺不太相符的代碼,這也是 JavaScript 的一個設計缺陷。雖然 ECMAScript6 已經(jīng)通過引入塊級作用域并配合使用 let、const 關鍵字,避開了這種設計缺陷,但是由于 JavaScript 需要向下兼容,所以變量提升在很長時間內(nèi)還會繼續(xù)存在。
在 ECMAScript6 之前,JS 引擎用 var 關鍵字聲明變量。在 var 時代,不管變量聲明是寫在哪里,最后都會被提到作用域的頂端。 下面在全局作用域中聲明一個num 變量,并在聲明之前打印它:
console.log(num) var num = 1
這里會輸出 undefined,因為變量的聲明被提升了,它等價于:
var num console.log(num) num = 1
可以看到,num 作為全局變量會被提升到全局作用域的頂端。
除此之外,在函數(shù)作用域中也存在變量提升:
function getNum() { console.log(num) var num = 1 } getNum()
這里也會輸出 undefined,因為函數(shù)內(nèi)部的變量聲明會被提升至函數(shù)作用域的頂端。它等價于:
function getNum() { var num console.log(num) num = 1 } getNum()
除了變量提升,函數(shù)實際上也是存在提升的。JavaScript中具名的函數(shù)的聲明形式有兩種:
//函數(shù)聲明式: function foo () {} //變量形式聲明: var fn = function () {}
當使用變量形式聲明函數(shù)時,和普通的變量一樣會存在提升的現(xiàn)象,而函數(shù)聲明式會提升到作用域最前邊,并且將聲明內(nèi)容一起提升到最上邊。如下:
fn() var fn = function () { console.log(1) } // 輸出結(jié)果:Uncaught TypeError: fn is not a function foo() function foo () { console.log(2) } // 輸出結(jié)果:2
可以看到,使用變量形式聲明fn并在其前面執(zhí)行時,會報錯fn不是一個函數(shù),因為此時fn只是一個變量,還沒有賦值為一個函數(shù),所以是不能執(zhí)行fn方法的。
2. 為什么會有變量提升?
變量提升和 JavaScript 的編譯過程密切相關:JavaScript 和其他語言一樣,都要經(jīng)歷編譯和執(zhí)行階段。在這個短暫的編譯階段,JS 引擎會搜集所有的變量聲明,并且提前讓聲明生效。而剩下的語句需要等到執(zhí)行階段、等到執(zhí)行到具體的某一句時才會生效。這就是變量提升背后的機制。
那為什么 JavaScript 中會存在變量提升這個特性呢?
首先要從作用域說起。作用域是指在程序中定義變量的區(qū)域,該位置決定了變量的生命周期。通俗理解,作用域就是變量與函數(shù)的可訪問范圍,即作用域控制著變量和函數(shù)的可見性和生命周期。
在 ES6 之前,作用域分為兩種:
- 全局作用域中的對象在代碼中的任何地方都可以訪問,其生命周期伴隨著頁面的生命周期。
- 函數(shù)作用域是在函數(shù)內(nèi)部定義的變量或者函數(shù),并且定義的變量或者函數(shù)只能在函數(shù)內(nèi)部被訪問。函數(shù)執(zhí)行結(jié)束之后,函數(shù)內(nèi)部定義的變量會被銷毀。
相較而言,其他語言則普遍支持塊級作用域。塊級作用域就是使用一對大括號包裹的一段代碼,比如函數(shù)、判斷語句、循環(huán)語句,甚至一個單獨的{}都可以被看作是一個塊級作用域(注意,對象聲明中的{}不是塊級作用域)。簡單來說,如果一種語言支持塊級作用域,那么其代碼塊內(nèi)部定義的變量在代碼塊外部是訪問不到的,并且等該代碼塊中的代碼執(zhí)行完成之后,代碼塊中定義的變量會被銷毀。
ES6 之前是不支持塊級作用域的,沒有塊級作用域,將作用域內(nèi)部的變量統(tǒng)一提升無疑是最快速、最簡單的設計,不過這也直接導致了函數(shù)中的變量無論是在哪里聲明的,在編譯階段都會被提取到執(zhí)行上下文的變量環(huán)境中,所以這些變量在整個函數(shù)體內(nèi)部的任何地方都是能被訪問的,這也就是 JavaScript 中的變量提升。
使用變量提升有如下兩個好處:
(1)提高性能
在JS代碼執(zhí)行之前,會進行語法檢查和預編譯,并且這一操作只進行一次。這么做就是為了提高性能,如果沒有這一步,那么每次執(zhí)行代碼前都必須重新解析一遍該變量(函數(shù)),這是沒有必要的,因為變量(函數(shù))的代碼并不會改變,解析一遍就夠了。
在解析的過程中,還會為函數(shù)生成預編譯代碼。在預編譯時,會統(tǒng)計聲明了哪些變量、創(chuàng)建了哪些函數(shù),并對函數(shù)的代碼進行壓縮,去除注釋、不必要的空白等。這樣做的好處就是每次執(zhí)行函數(shù)時都可以直接為該函數(shù)分配棧空間(不需要再解析一遍去獲取代碼中聲明了哪些變量,創(chuàng)建了哪些函數(shù)),并且因為代碼壓縮的原因,代碼執(zhí)行也更快了。
(2)容錯性更好
變量提升可以在一定程度上提高JS的容錯性,看下面的代碼:
a = 1; var a; console.log(a); // 1
如果沒有變量提升,這兩行代碼就會報錯,但是因為有了變量提升,這段代碼就可以正常執(zhí)行。
雖然在可以開發(fā)過程中,可以完全避免這樣寫,但是有時代碼很復雜,可能因為疏忽而先使用后定義了,而由于變量提升的存在,代碼會正常運行。當然,在開發(fā)過程中,還是盡量要避免變量先使用后聲明的寫法。
總結(jié):
- 解析和預編譯過程中的聲明提升可以提高性能,讓函數(shù)可以在執(zhí)行時預先為變量分配棧空間;
- 聲明提升還可以提高JS代碼的容錯性,使一些不規(guī)范的代碼也可以正常執(zhí)行。
3. 變量提升導致的問題
由于變量提升的存在,使用 JavaScript 來編寫和其他語言相同邏輯的代碼,都有可能會導致不一樣的執(zhí)行結(jié)果。主要有以下兩種情況。
(1)變量被覆蓋
來看下面的代碼:
var name = "JavaScript" function showName(){ console.log(name); if(0){ var name = "CSS" } } showName()
這里會輸出 undefined,而并沒有輸出“JavaScript”,為什么呢?
首先,當剛執(zhí)行 showName 函數(shù)調(diào)用時,會創(chuàng)建 showName 函數(shù)的執(zhí)行上下文。之后,JavaScript 引擎便開始執(zhí)行 showName 函數(shù)內(nèi)部的代碼。首先執(zhí)行的是:
console.log(name);
執(zhí)行這段代碼需要使用變量 name,代碼中有兩個 name 變量:一個在全局執(zhí)行上下文中,其值是JavaScript;另外一個在 showName 函數(shù)的執(zhí)行上下文中,由于if(0)永遠不成立,所以 name 值是 CSS。那該使用哪個呢?應該先使用函數(shù)執(zhí)行上下文中的變量。因為在函數(shù)執(zhí)行過程中,JavaScript 會優(yōu)先從當前的執(zhí)行上下文中查找變量,由于變量提升的存在,當前的執(zhí)行上下文中就包含了if(0)中的變量 name,其值是 undefined,所以獲取到的 name 的值就是 undefined。
這里輸出的結(jié)果和其他支持塊級作用域的語言不太一樣,比如 C 語言輸出的就是全局變量,所以這里會很容易造成誤解。
(2)變量沒有被銷毀
function foo(){ for (var i = 0; i < 5; i++) { } console.log(i); } foo()
使用其他的大部分語言實現(xiàn)類似代碼時,在 for 循環(huán)結(jié)束之后,i 就已經(jīng)被銷毀了,但是在 JavaScript 代碼中,i 的值并未被銷毀,所以最后打印出來的是 5。這也是由變量提升而導致的,在創(chuàng)建執(zhí)行上下文階段,變量 i 就已經(jīng)被提升了,所以當 for 循環(huán)結(jié)束之后,變量 i 并沒有被銷毀。
4. 禁用變量提升
為了解決上述問題,ES6 引入了 let 和 const 關鍵字,從而使 JavaScript 也能像其他語言一樣擁有塊級作用域。let 和 const 是不存在變量提升的。下面用 let 來聲明變量:
console.log(num) let num = 1 // 輸出結(jié)果:Uncaught ReferenceError: num is not defined
如果改成 const 聲明,也會是一樣的結(jié)果——用 let 和 const 聲明的變量,它們的聲明生效時機和具體代碼的執(zhí)行時機保持一致。
變量提升機制會導致很多誤操作:那些忘記被聲明的變量無法在開發(fā)階段被明顯地察覺出來,而是以 undefined 的形式藏在代碼中。為了減少運行時錯誤,防止 undefined 帶來不可預知的問題,ES6 特意將聲明前不可用做了強約束。不過,let 和 const 還是有區(qū)別的,使用 let 關鍵字聲明的變量是可以被改變的,而使用 const 聲明的變量其值是不可以被改變的。
下面來看看 ES6 是如何通過塊級作用域來解決上面的問題:
function fn() { var num = 1; if (true) { var num = 2; console.log(num); // 2 } console.log(num); // 2 } fn()
在這段代碼中,有兩個地方都定義了變量 num,函數(shù)塊的頂部和 if 的內(nèi)部,由于 var 的作用范圍是整個函數(shù),所以在編譯階段,會生成如下執(zhí)行上下文:
從執(zhí)行上下文的變量環(huán)境中可以看出,最終只生成了一個變量 num,函數(shù)體內(nèi)所有對 num 的賦值操作都會直接改變變量環(huán)境中的 num 的值。所以上述代碼最后輸出的是 2,而對于相同邏輯的代碼,其他語言最后一步輸出的值應該是 1,因為在 if 里面的聲明不應該影響到塊外面的變量。
下面來把 var 關鍵字替換為 let 關鍵字,看看效果:
function fn() { let num = 1; if (true) { let num = 2; console.log(num); // 2 } console.log(num); // 1 } fn()
執(zhí)行這段代碼,其輸出結(jié)果就和預期是一致的。這是因為 let 關鍵字是支持塊級作用域的,所以,在編譯階段 JavaScript 引擎并不會把 if 中通過 let 聲明的變量存放到變量環(huán)境中,這也就意味著在 if 中通過 let 聲明的關鍵字,并不會提升到全函數(shù)可見。所以在 if 之內(nèi)打印出來的值是 2,跳出語塊之后,打印出來的值就是 1 了。這就符合我們的習慣了 :作用塊內(nèi)聲明的變量不影響塊外面的變量。
5. JS如何支持塊級作用域
那么問題來了,ES6 是如何做到既要支持變量提升的特性,又要支持塊級作用域的呢?下面從執(zhí)行上下文的角度來看看原因。
JavaScript 引擎是通過變量環(huán)境實現(xiàn)函數(shù)級作用域的,那么 ES6 又是如何在函數(shù)級作用域的基礎之上,實現(xiàn)對塊級作用域的支持呢?先看下面這段代碼:
function fn(){ var a = 1 let b = 2 { let b = 3 var c = 4 let d = 5 console.log(a) console.log(b) console.log(d) } console.log(b) console.log(c) } fn()
當這段代碼執(zhí)行時,JavaScript 引擎會先對其進行編譯并創(chuàng)建執(zhí)行上下文,然后再按照順序執(zhí)行代碼。let 關鍵字會創(chuàng)建塊級作用域,那么 let 關鍵字是如何影響執(zhí)行上下文的呢?
(1)創(chuàng)建執(zhí)行上下文
創(chuàng)建的執(zhí)行上下文如圖所示:
通過上圖可知:
- 通過 var 聲明的變量,在編譯階段會被存放到變量環(huán)境中。
- 通過 let 聲明的變量,在編譯階段會被存放到詞法環(huán)境中。
- 在函數(shù)作用域內(nèi)部,通過 let 聲明的變量并沒有被存放到詞法環(huán)境中。
(2)執(zhí)行代碼
當執(zhí)行到代碼塊中時,變量環(huán)境中 a 的值已經(jīng)被設置成了 1,詞法環(huán)境中 b 的值已經(jīng)被設置成了 2,這時函數(shù)的執(zhí)行上下文如圖所示:
可以看到,當進入函數(shù)的作用域塊時,作用域塊中通過 let 聲明的變量,會被存放在詞法環(huán)境的一個單獨的區(qū)域中,這個區(qū)域中的變量并不影響作用域塊外面的變量,比如在作用域外面聲明了變量 b,在該作用域塊內(nèi)部也聲明了變量 b,當執(zhí)行到作用域內(nèi)部時,它們都是獨立的存在。
其實,在詞法環(huán)境內(nèi)部,維護了一個棧結(jié)構(gòu),棧底是函數(shù)最外層的變量,進入一個作用域塊后,就會把該作用域塊內(nèi)部的變量壓到棧頂;當作用域執(zhí)行完成之后,該作用域的信息就會從棧頂彈出,這就是詞法環(huán)境的結(jié)構(gòu)。這里的變量是指通過 let 或者 const 聲明的變量。
接下來,當執(zhí)行到作用域塊中的console.log(a)時,就需要在詞法環(huán)境和變量環(huán)境中查找變量 a 的值了,查找方式:沿著詞法環(huán)境的棧頂向下查詢,如果在詞法環(huán)境中的某個塊中查找到了,就直接返回給 JavaScript 引擎,如果沒有查找到,那么繼續(xù)在變量環(huán)境中查找。這樣變量查找就完成了:
當作用域塊執(zhí)行結(jié)束之后,其內(nèi)部定義的變量就會從詞法環(huán)境的棧頂彈出,最終執(zhí)行上下文如圖所示:
塊級作用域就是通過詞法環(huán)境的棧結(jié)構(gòu)來實現(xiàn)的,而變量提升是通過變量環(huán)境來實現(xiàn),通過這兩者的結(jié)合,JavaScript 引擎就同時支持了變量提升和塊級作用域。
6. 暫時性死區(qū)
最后再來看看暫時性死區(qū)的概念:
var name = 'JavaScript'; { name = 'CSS'; let name; } // 輸出結(jié)果:Uncaught ReferenceError: Cannot access 'name' before initialization
ES6 規(guī)定:如果區(qū)塊中存在 let 和 const,這個區(qū)塊對這兩個關鍵字聲明的變量,從一開始就形成了封閉作用域。假如嘗試在聲明前去使用這類變量,就會報錯。這一段會報錯的區(qū)域就是暫時性死區(qū)。上面代碼的第4行上方的區(qū)域就是暫時性死區(qū)。
如果想成功引用全局的 name 變量,需要把 let 聲明給去掉:
var name = 'JavaScript'; { name = 'CSS'; }
這時程序就能正常運行了。其實,這并不意味著引擎感知不到 name 變量的存在,恰恰相反,它感知到了,而且它清楚地知道 name 是用 let 聲明在當前塊里的。正因如此,它才會給這個變量加上暫時性死區(qū)的限制。一旦去掉 let 關鍵字,它也就不起作用了。
其實這也就是暫時性死區(qū)的本質(zhì):當程序的控制流程在新的作用域進行實例化時,在此作用域中用 let 或者 const 聲明的變量會先在作用域中被創(chuàng)建出來,但此時還未進行詞法綁定,所以是不能被訪問的,如果訪問就會拋出錯誤。因此,在這運行流程進入作用域創(chuàng)建變量,到變量可以被訪問之間的這段時間,就稱之為暫時死區(qū)。
在 let 和 const關鍵字出現(xiàn)之前,typeof運算符是百分之百安全的,現(xiàn)在也會引發(fā)暫時性死區(qū)的發(fā)生,像import關鍵字引入公共模塊、使用new class創(chuàng)建類的方式,也會引發(fā)暫時性死區(qū),究其原因還是變量的聲明先與使用。
typeof a // Uncaught ReferenceError: a is not defined let a = 1
可以看到,在a聲明之前使用typeof關鍵字報錯了,這就是暫時性死區(qū)導致的。
總結(jié)
到此這篇JavaScript變量提升的文章就介紹到這了,更多相關JavaScript變量提升內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
靜態(tài)頁面實現(xiàn) include 引入公用代碼的示例
下面小編就為大家?guī)硪黄o態(tài)頁面實現(xiàn) include 引入公用代碼的示例。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-09-09JavaScript 轉(zhuǎn)義字符JSON parse錯誤研究
這篇文章主要為大家介紹了JavaScript 轉(zhuǎn)義字符JSON parse錯誤研究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-10-10