1. 程式人生 > >let關鍵字:加強版的var關鍵字

let關鍵字:加強版的var關鍵字

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