本文首發於個人網站:let關鍵字:加強版的var關鍵字
你好,今天大叔想和你嘮扯嘮扯 ES6 新增的關鍵字 —— let
。再說 let
的具體用法之前,大叔想先和你說說大叔自己對 let
的感受 —— let
其實就是加強版的 var
。為啥這么說呢?別急,且聽大叔慢慢道來。
首先,let
和 var
的作用是一樣一樣滴,都是用來聲明變量。看到這兒,你可能會有個問題啦,既然作用一樣,為啥還要再搞個什么新特性出來?
想要回答這個問題,就要說到 let
和 var
的不同之處了。比方說 var
聲明的全局變量會自動添加到頂級對象中作為屬性,而 let
就不會。再比方說 var
允許聲明提升或者重復聲明,而 let
就不允許這樣做。當然了,它們之間的不同可不止這些,大叔也只是舉個栗子而已。
如果你沒了解過 ES6 的內容,看到這兒可能有點懵。沒關系啊~ 別往心里去,因為接下來大叔就是要和你嘮扯嘮扯 let
的具體用法。
聲明的全局變量不是頂級對象的屬性
在整明白 let
和 var
第一點不同之前,大叔要先和你嘮扯嘮扯 var
這個關鍵字的一些用法。為啥?!var
你要是都整不明白的話,你還想整明白 let
,那就是一個美麗的扯!
首先,咱們都知道其實聲明一個全局變量,是既可以使用 var
進行聲明,也可以不使用 var
進行聲明的。比方說像下面這段代碼一樣:
var a = 'a'
console.log(a)
b = 'b'
console.log(b)
上面這段代碼不用大叔多扯,想必你也知道打印的結果是個啥 —— 打印 a 和 b 嘛。別急,這才是個開始,咱不點慢慢來不是~
接下來呢,大叔要用 delete
這個運算符來做個騷操作了 —— 先用 delete
刪除上面的兩個變量 a
和 b
,然后呢再分別打印這兩個變量的值。
你尋思一下這個時候應該打印的結果是啥呢?對啦!變量 a
的值會正常輸出 a,但變量 b
會報錯 b is not defined
。那為啥又是這樣一個結果吶?
大叔覺得你應該知道 delete
運算符的作用是用來刪除對象的屬性,但是 delete
是無法刪除變量的。對啦!你想的沒錯,這就說明上面聲明的 a
是變量但不是對象的屬性,而是 b
是對象的屬性但不是變量。
大叔這話說的有點繞,給你帶入一個場景吧。比如上面這段代碼是在一個 HTML 頁面中定義的 JavaScript 代碼,那 a
就是一個全局變量,b
就是向 window
對象添加了一個屬性。所以,delete
運算符可以刪除 b
,但不能刪除 a
的原因了。
那也就是說使用 var
關鍵字聲明的是變量,不使用 var
關鍵字聲明的是 window
對象的屬性唄。話嘮叨這兒,大叔還得來個騷操作。咱再看一段代碼:
var a = 'a'
console.log(window.a)
var b = 'b'
console.log(window.b)
這段代碼如果按照上面的結論,打印的結果就應該是 undefined 和 b。但是~ 你真實運行一下這段代碼,就應該知道實際上打印的結果是 a 和 b!
這咋和上面的結論不一樣呢?!是不是又有點懵?哈哈~ 別先急着懵逼,這個問題實際上是 JavaScript 的作者 Brendan Eich 當年在設計 JavaScript 這門語言時的一個小失誤:在全局作用域中聲明的變量同時會被作為屬性添加到頂級對象中。
可能嘮扯到這兒,你會滿屏的吐槽彈幕:這尼瑪誰不知道?!但大叔真正想和你嘮扯的就是這一點,這個小小的失誤,就導致了使用 var
關鍵字聲明的全局變量會污染全局對象的問題。
而 ES6 新增的 let
就很好滴彌補了這個問題!也就是說,使用 let
關鍵字聲明的全局變量不會污染全局對象。不信咱可以來試試嘛~ 還是剛才那個場景,在一個 HTML 頁面中定義 JavaScript 代碼,僅僅把 var
改成 let
:
let a = 'a'
console.log(a)
console.log(window.a)
這段代碼實際的運行結果就是 a 和 undefined。事實證明 let
有效滴解決了 var
的問題,所以你知道為啥 ES6 要新增一個關鍵字來完成和 var
一樣的事兒了吧?!
不允許重復聲明
但是,但可是,可但是~ let
就這么一點點和 var
的區別嗎?答案肯定不是滴。咱們還是先來嘮扯嘮扯 var
關鍵字,使用 var
聲明的變量是允許反復滴重復聲明的,就像下面這段代碼:
var a = 'a'
var a = 'aa'
console.log(a)
這段代碼最終打印的結果是 aa,原因就在於 var
聲明的變量是允許重復聲明的。可能這會兒你又會問了,這我也知道啊,有啥子問題嗎?
問題肯定是有滴,要是沒有大叔花這么多口舌和你在這兒叨逼叨干啥啊~ 大叔還是給你帶入一個場景,比方說你定義了一個 JS 文件是需要被其他小伙伴導入使用滴,那你在這個文件里面聲明的變量在人家那分分鍾被重新聲明了,你內心是個啥感受?
當然了,大叔就是舉個栗子,你也別太當真啦~ 總而言之,就是說咱們在真實開發時對變量的命名肯定是有規划的,不能隨意就被重新聲明使用,這樣會讓命名空間很亂很亂滴。
你可能有想問了,這個問題要怎么解決呢?答案其實很簡單,就是使用 ES6 新增的這個 let
關鍵字。因為 let
關鍵字聲明的變量是不允許被重復聲明,否則會報錯滴。不信你也可以看看嘛:
let a = 'a'
let a = 'aa'
console.log(a)
僅僅只是把 var
改成 let
,這個結果就是報錯了,報錯的內容是:SyntaxError: Identifier 'a' has already been declared
,大概的意思就是變量 a 已經被聲明過了。
所以,你看,let
可不是僅僅那么一點點的區別呢!
不允許聲明提前
這會兒你是不是又想問 let
和 var
之間還有沒有其他區別啊?大叔也不藏着掖着了,干脆一口氣都和你說了吧!你知道使用 var
關鍵字聲明的變量是允許聲明提前的嗎?啥?不知道!沒事兒,這個簡單,啥叫聲明提前,來看段代碼:
console.log(a)
var a = 'a'
你運行一下這段代碼,看看打印的結果是啥?沒錯~ 結果就是 undefined。為啥不是報錯呢?原因就是使用 var
關鍵字聲明的變量允許聲明提前。還是說人話吧,也就是說,上面這段代碼和下面這段代碼本質上是沒區別的:
var a
console.log(a)
a = 'a'
這樣嬸兒寫你可能就明白了為啥打印的結果是 undefined 而不是報錯了吧!但是,嘿嘿~ 咱們又得嘮扯嘮扯 let
了,因為 let
聲明的變量就不允許聲明提前。不信的話還是給你看段代碼先:
console.log(a)
let a = 'a'
這段代碼運行之后打印的結果就是報錯,報錯的內容是:ReferenceError: Cannot access 'c' before initialization
,大概的意思就是無法在聲明變量 c
之前訪問變量 c
。
暫時性死區(TDZ)
let
是不是挺屌的吧?!那你想不想知道 let
聲明的變量又為啥不允許聲明提前呢?嘿嘿~ 這是因為使用 let
聲明變量的過程中存在一個叫做暫時性死區(Temporal dead zone,簡稱 TDZ)的概念。
是不是覺得挺高深的?哈哈~ 其實沒啥高深的,大叔就給你嘮扯明白這個事兒。規矩不變,咱還是先看段代碼再說:
if (true) {
console.log(a)
let a;
console.log(a)
a = "a";
console.log(a)
}
大叔想先問問你這段代碼里面三處打印的結果分別是啥?你得認真的尋思尋思哈~ 這可都是大叔剛和你嘮過的內容。
- 第一處打印的結果是報錯,報錯內容就是
ReferenceError: Cannot access 'c' before initialization
- 第二處打印的結果是 undefined
- 第三處打印的結果是 b
對於這樣的結果,大叔估計你應該會明白,畢竟都是剛嘮過的內容。接下來,你得認真的看了,因為大叔要和你來嘮扯有關暫時性死區的概念了~
所謂的暫時性死區,就是說使用 let
關鍵字聲明的變量直到執行定義語句時才會被初始化。也就是說,從代碼從頂部開始執行直到變量的定義語句執行,這個過程中這個變量都是不能被訪問的,而這個過程就被叫做暫時性死區。
具體到上面這段代碼的話,實際上暫時性死區的開始和結束位置就像下面這段代碼標注的一樣嬸兒:
if (true) {
// 暫時性死區開始
console.log(a); // 報錯,ReferenceError: Cannot access 'a' before initialization
let a;
// 暫時性死區結束
console.log(a); // 輸出undefined
a = "a";
console.log(a); // 輸出a
}
撈到這會兒,大叔相信你應該可以明白啥子是暫時性死區了。其實啊,一些新的概念也沒啥難理解的,主要是你理解的角度和方式的問題。
typeof
運算符也不再安全
總體上來說,let
關鍵字要比 var
關鍵字嚴格了許多,導致我們開發時遇到的問題相應會減少許多。但 let
就沒有任何問題了嗎?答案顯然不是滴,大叔一直信奉一句話:任何技術都沒有最優,只有最適合。
ES6 新增的 let
關鍵字也是如此,就比方說剛才咱們撈的暫時性死區的內容,其實就有問題。啥問題呢?你還記得 JS 里面有個運算符叫做 typeof
吧,就是用來判斷原始數據類型的。這個運算符在 let
出現之前相對是比較安全的,說白了就是不容易報錯。但在 let
出現之后就不一定了,比方說如果你把它用在剛才說的暫時性死區里面,它就會報錯了:
if (true) {
console.log(typeof c)
let c;
}
這段代碼最終打印的結果同樣是報錯,報錯內容同樣是:ReferenceError: Cannot access 'c' before initialization
。
塊級作用域
關於 let
關鍵字咱們撈到這會兒,其實基本上已經嘮完了。但是,但可是,可但是~ 嘿嘿~ let
還有一個最重要的特性大叔還沒和你嘮呢,這重量級的都得最后出場不是?!
那這個最重要的特性就是啥呢?叫做塊級作用域。嘮到作用域想必你應該知道在 ES5 中存在兩個:全局作用域和函數作用域,但在 ES6 中又新增了一個塊級作用域。
為什么需要塊級作用域
想嘮明白什么是塊級作用域,咱就得從為啥需要塊級作用域嘮起啊~ 規矩不變,還是先看段代碼:
var a = "a"
function fn() {
console.log(a)
if (false) {
var a = "b"
}
}
fn()
你覺得這段代碼運行之后打印的結果應該是啥?是 a?是 b?還是... ...?其實結果是 undefined。當然了,這個結果不難得出,你運行一下就能看到。關鍵在於,為啥是這么個結果?!
因為就在於 ES5 只有全局作用域和函數作用域,而上面這段代碼的結果產生的原因就在於局部變量覆蓋了全局變量。當然了,還有比這更麻煩的問題呢,比方說咱們再看下面這段代碼:
for (var i = 0; i < 5; i++) {
console.log("循環內:" + i)
}
console.log("循環外:" + i)
是不是無比地熟悉吧?!不就是個 for
循環嘛!關鍵在哪?關鍵在於 for
循環結束之后,你會發現依舊能訪問到變量 i
。這說明啥?說明變量 i
現在是一個全局變量。當然了,你可能會說這沒啥問題,畢竟之前一直不都是這個樣子的嘛。
什么是塊級作用域
但是,大叔要和你說的是,現在不一樣了啊,現在有塊級作用域啦!啥是塊級作用域?還是看段代碼先:
if (true) {
let b = "b"
}
console.log(b)
這段代碼運行之后打印的結果是報錯,報錯的內容是:SyntaxError: Lexical declaration cannot appear in a single-statement context
。
這說明啥?這就說明現在你使用 let
聲明的變量在全局作用域中訪問不到了,原因就是因為使用 let
聲明的變量具有塊級作用域。
接下來你的問題可能就是這個塊級作用域在哪呢吧?其實這個塊級作用域就是在花括號({}
)里面。比方說,咱們現在把上面那個 for
循環的代碼用 let
改造一下再看看:
for (let i = 0; i < 5; i++) {
console.log("循環內:" + i)
}
console.log("循環外:" + i)
改造完的這段代碼運行之后的結果就是在循環結束后的打印結果是報錯,報錯內容大叔就不說了,因為都一個樣。
塊級作用域的注意事項
整明白了啥是塊級作用域,接下來大叔就得和你嘮叨嘮叨需要注意的事兒了。就是在使用 let
關鍵字聲明塊級作用域的變量時可必須在這對 {}
里面啊,不然同樣也會報錯滴。
比方說,咱們經常在使用 if
語句時愛把 {}
省略,但是如果 if
語句里面是使用 let
聲明變量的話就不行了。不信來看段代碼吧:
if (true) let c = 'c'
這段代碼的運行結果同樣是報錯,而且報錯內容都是一樣的。可是不能忘記哦~
塊級作用域的作用
好了,整明白啥是塊級作用域了,也嘮清楚需要注意的了,你是不是想問問這塊級作用域有啥子用處啊?大叔都想你心里面去了,嘿嘿~
你知道匿名自調函數吧?還記得怎么寫一個匿名自調函數嗎?是不是這樣嬸兒的:
(function(){
var msg = 'this is IIFE.'
console.log(msg)
})()
還記得匿名自調函數的作用不?是不是就是為了定義的變量和函數不污染全局命名空間?!有了 let
,有了塊級作用域,上面這段匿名自調函數就可以寫成這樣嬸兒的:
{
let msg = 'this is IIFE.'
console.log(msg)
}
簡化了不少吧?!
寫在最后的話
好了,整到這兒,ES6 新增的 let
關鍵字所有大叔想和你嘮扯的內容都嘮扯完了,也希望能對你有所幫助。最后再說一句:我是不想成熟的大叔,為前端學習不再枯燥、困難和迷茫而努力。你覺得這樣學習前端技術有趣嗎?有什么感受、想法,和好的建議可以在下面給大叔留言哦~