let 與 const,為年輕時犯下的“錯誤”買單!
在講解es6之前,我們必須要提一下es5中的var,也就是曾經的那個錯誤。
if (condition) {
var value = 1;
}
console.log(value);
複製程式碼
很簡單的分析一下,初學者可能會認為,只有在condition為true的時候,value才會被賦值,如果condition為false的時候,程式碼應該會報錯才對!但是,事實並不是這樣的,瀏覽器在執行這段程式碼的時候,並不是這麼解析的!
var value;
if (condition) {
value = 1;
}
console.log(value);
複製程式碼
這樣,很容易我們就能判斷出,如果condition的值為false的時候,console出來的值為undefined。
原因就是我們常說的,變數提升。
為了加強對變數生命週期的控制,ECMAScript 6 引入了塊級作用域。
塊級作用域存在於:
-
函式內部
-
塊中(字元 { 和 } 之間的區域)
引出我們今天的主角—let和const
let和const的特點:
1.不會被提升(真的是這樣的嗎?)
if (condition) {
let value = 1;
}
console.log(value); // Uncaught ReferenceError: value is not defined
複製程式碼
在程式碼塊外面訪問,直接判定,未定義。
2.重複宣告報錯
var value = 1;
let value = 2; // Uncaught SyntaxError: Identifier 'value' has already been declared
複製程式碼
這在以前是完全可以的,後定義的回覆蓋以前的。
3.不繫結全域性作用域
let value = 1 ;
console.log(window.value);
複製程式碼
const也是相同的,都訪問不到。
const和let的區別:
const 用於宣告常量,其值一旦被設定不能再被修改,否則會報錯。
值得一提的是:const 宣告不允許修改繫結,但允許修改值。這意味著當用 const 宣告物件時:
const data = {
value: 1
}
// 沒有問題
data.value = 2;
data.num = 3;
// 報錯
data = {}; // Uncaught TypeError: Assignment to constant variable.
複製程式碼
臨時死區
臨時死區(Temporal Dead Zone),簡寫為 TDZ。
let 和 const 宣告的變數不會被提升到作用域頂部,如果在宣告之前訪問這些變數,會導致報錯。
console.log(typeof value); // Uncaught ReferenceError: value is not defined
let value = 1;
複製程式碼
來個例子~
var value = "global";
// 例子1
(function() {
console.log(value);
let value = 'local';
}());
// 例子2
{
console.log(value);
const value = 'local';
};
複製程式碼
結果是:都報錯了~!
這是因為 JavaScript 引擎在掃描程式碼發現變數宣告時,要麼將它們提升到作用域頂部(遇到 var 宣告),要麼將宣告放在 TDZ 中(遇到 let 和 const 宣告)。訪問 TDZ 中的變數會觸發執行時錯誤。只有執行過變數宣告語句後,變數才會從 TDZ 中移出,然後方可訪問。 迴圈中的塊級作用域:
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs[i] = function () {
console.log(i);
};
}
funcs[0](); // 3
複製程式碼
如何改變現狀呢?我要的是funcs0 == 0
在沒有es6 之前,這個事兒麻煩了,還得使用閉包的方式!
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs[i] = (function(i){
return function() {
console.log(i);
}
}(i))
}
funcs[0](); // 0
複製程式碼
ES6 的 let 為這個問題提供了新的解決方法:
var funcs = [];
for (let i = 0; i < 3; i++) {
funcs[i] = function () {
console.log(i);
};
}
funcs[0](); // 0
複製程式碼
問題在於,上面講了 let 不提升,不能重複宣告,不能繫結全域性作用域等等特性,可是為什麼在這裡就能正確列印出 i 值呢? 如果是不重複宣告,在迴圈第二次的時候,又用 let宣告瞭i,應該報錯呀,就算因為某種原因,重複宣告不報錯,一遍一遍迭代,i 的值最終還是應該是 3 呀,還有人說 for 迴圈的 設定迴圈變數的那部分是一個單獨的作用域,就比如:
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
複製程式碼
這個例子是對的,如果我們把 let 改成 var 呢?
for (var i = 0; i < 3; i++) {
var i = 'abc';
console.log(i);
}
// abc
複製程式碼
經查, for 迴圈中使用 let 和 var,底層會使用不同的處理方式。 簡單的來說,就是在 for (let i = 0; i < 3; i++) 中,即圓括號之內建立一個隱藏的作用域,這就可以解釋為什麼:
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
複製程式碼
然後每次迭代迴圈時都建立一個新變數,並以之前迭代中同名變數的值將其初始化。
var funcs = [];
for (let i = 0; i < 3; i++) {
funcs[i] = function () {
console.log(i);
};
}
funcs[0](); // 0
複製程式碼
相當於:
// 虛擬碼
(let i = 0) {
funcs[0] = function() {
console.log(i)
};
}
(let i = 1) {
funcs[1] = function() {
console.log(i)
};
}
(let i = 2) {
funcs[2] = function() {
console.log(i)
};
};
複製程式碼
到此,我們就講完了嗎?沒有,並沒有,上面還有個提升的問題麼!
首先明確一點:提升不是一個技術名詞。
要搞清楚提升的本質,需要理解 JS 變數的 這就解釋了為什麼在 let x 之前使用 x 會報錯:
假設有如下程式碼:
function fn(){
var x = 1
var y = 2
}
fn()
複製程式碼
在執行 fn 時,會有以下過程(不完全):
-
進入 fn,為 fn 建立一個環境。
-
找到 fn 中所有用 var 宣告的變數,在這個環境中「建立」這些變數(即 x 和 y)。
-
將這些變數「初始化」為 undefined。
-
開始執行程式碼
-
x = 1 將 x 變數「賦值」為 1
-
y = 2 將 y 變數「賦值」為 2
也就是說 var 宣告會在程式碼執行之前就將「建立變數,並將其初始化為 undefined」。
這就解釋了為什麼在 var x = 1 之前 console.log(x) 會得到 undefined。
接下來來看 function 宣告的「建立、初始化和賦值」過程
假設程式碼如下:
fn2()
function fn2(){
console.log(2)
}
複製程式碼
JS 引擎會有一下過程:
-
找到所有用 function 宣告的變數,在環境中「建立」這些變數。
-
將這些變數「初始化」並「賦值」為 function(){ console.log(2) }。
-
開始執行程式碼 fn2()
也就是說 function 宣告會在程式碼執行之前就「建立、初始化並賦值」。
接下來看 let 宣告的「建立、初始化和賦值」過程
{
let x = 1
x = 2
}
複製程式碼
我們只看 {} 裡面的過程:
-
找到所有用 let 宣告的變數,在環境中「建立」這些變數
-
開始執行程式碼(注意現在還沒有初始化)
-
執行 x = 1,將 x 「初始化」為 1(這並不是一次賦值,如果程式碼是 let x,就將 x 初始化為 undefined)
-
執行 x = 2,對 x 進行「賦值」
let x = 'global'
{
console.log(x) // Uncaught ReferenceError: x is not defined
let x = 1
}
複製程式碼
這就解釋了為什麼在 let x 之前使用 x 會報錯: 原因有兩個:
-
console.log(x) 中的 x 指的是下面的 x,而不是全域性的 x
-
執行 log 時 x 還沒「初始化」,所以不能使用(也就是所謂的暫時死區)
看到這裡,你應該明白了 let 到底有沒有提升:
-
let 的「建立」過程被提升了,但是初始化沒有提升。
-
var 的「建立」和「初始化」都被提升了。
-
function 的「建立」「初始化」和「賦值」都被提升了。
最後看 const,其實 const 和 let 只有一個區別,那就是 const 只有「建立」和「初始化」,沒有「賦值」過程!!!
這四種宣告,用下圖就可以快速理解:
所謂暫時死區,就是不能在初始化之前,使用變數。