深入ES6 (二)let和const
第二章 let和const
ES6提供了let和const兩種新的變數宣告方式,使得在JS中變數的宣告更像java那樣。這章主要包括了一下內容:
- ES6的塊級作用域
- let宣告變數與var的區別
- 死區
- const與物件凍結
2.1 let與var
塊級作用域
在ES5中存在一個 很經典的迴圈事件繫結的問題,我們可以使用陣列模擬dom集合來還原這個問題:
var arr = []; for(var i = 0; i <6; i++){ arr.push(function () { console.log(i); }); } arr[0](); arr[1](); arr[2]();
不難理解,arr[X]()
輸出的都會是5,因為在ES5中不存在塊級作用域的概念,在for迴圈的括號中宣告的變數就像在外面宣告的變數那樣,每執行一次迴圈,新的i值就會覆蓋舊的i值,導致最後輸出的是以後一輪迴圈的i值。為了更好的讓你理解,請看下面的情況:
let arr = []; for(var i = 0; i <6; i++){ arr.push(function () { console.log(i); i++; // 注意這裡 }); } arr[0](); // =>6 arr[1](); // =>7 arr[2](); // =>8
可見,之所以上一組程式碼全部輸出6是因為arr中的所有函式共享一個i。我們可以配合閉包來解決這個問題:
for(var i = 0; i <6; i++){
(function(i){
arr.push(function () {
console.log(i);
});
}(i));
}
而在ES6中,我們可以使用let宣告變數來處理這個問題。let的用法與var類似,但是其宣告的變數只在let命令所在的程式碼塊中有效。
let arr = []; for(let i = 0; i <6; i++){ arr.push(function () { console.log(i); i++; }); } arr[0](); arr[0](); // 注意這裡輸出1 arr[1](); arr[2]();
上面的程式碼中,i只在本輪迴圈中有效,每一次迴圈的i其實都是一個新的變數,於是最後輸出0,1,1,2。可見,不同的變數i通過閉包儲存到了各個函式中。
塊級作用域的出現也使得廣泛使用的匿名立即執行函式不再必要了。
(function(){
var a = 10;
... ...
}())
// 等價於
{
let a = 10;
}
阮一峰大神在ES6入門經典中舉了這樣一個例子
function foo () {
console.log('I am the outside one')
};
(function(){
if(false){
function foo() {
console.log('I am the inside one')
}
}
foo();
}());
ES6中,函式本身的作用域在其所在塊級作用域之內,所以立即執行函式裡的function雖然存在向上整體提升效果,但只能上浮到if語句塊,所以最後執行結果輸出inside。但在ES5中,很最後會輸出outside,因為不存在if塊級作用域的限制。
但這個特性很容易引起衝突,因為我們很難判斷我們程式碼的執行環境究竟在哪裡,是遵循ES5的法則還是遵循ES6的法則(即使使用babel轉碼,babel也很難判斷按照哪個法則來)。所以當這段程式碼執行在nodejs環境中的時候,編譯器會選擇直接報錯,而並不像理論上分析得到的結果那樣。
我們應該儘量規避上面那種情況,使用嚴格模式。在嚴格模式下,函式必須定義在頂級作用域,定義在if,for語句中會直接報錯。
不存在變數提升與死區
使用let宣告的變數不會出現像var那樣存在“變數提升”現象。但本質上,二者是相同的,它們都會在初始化時先初始化屬性,再初始化邏輯,然而二者的區別在於使用let宣告的變數雖然一開始就存在,但是不能使用,而使用var宣告的變數則可以。一定要在聲明後使用,否則將會報錯。JS不像java那樣對不同作用域的同名變數有嚴格的控制。比如下面的程式碼在java裡無法執行,因為存在兩個名字叫做foo的區域性變數:
String foo = 'foo'
if(true){
String foo = 'foo bar';
System.out.print(foo);
}
然而在JS裡上面的寫法卻是允許的,實際上,if語句裡面的foo變數不受花括號的限制,它頂替了外部的foo:
var foo = 'foo';
if(true){
console.log(foo); // foo
var foo = 'foo bar'
console.log(foo); // foo bar
}
但當我們使用let宣告變數時,陷阱來了,請看下面的程式碼:
let foo = 'foo';
if(true){
console.log(foo);
let foo = 'foo bar';
console.log(foo);
}
下面的程式碼在第一次輸出foo的時候會報錯,提示foo沒有定義,這就是死區效應。
只要塊級作用域記憶體在let命令,它所宣告的變數就繫結在這個區域,不再受外部影響。ES6明確規定,只要塊級作用域中存在let命令,則這個塊區對這些命令宣告的變數從一開始就形成封閉的作用域。只要宣告之前,使用這些變數,就會報錯。這樣說有些抽象,你只需要記住:在塊級作用域內如果使用let聲明瞭某個變數,那麼這個變數名必須在宣告它的語句後使用,即使塊外部的變數有與之重名的也不行。從塊開頭到宣告變數的語句中間的部分,稱為這個變數的“暫時性死區”。
這樣也意味著我們不再能使用typeof關鍵字檢測某個變數是否被聲明瞭:
typeof x; // 返回'undefined',即使x沒有宣告
typeof x // 與let x =10。一起使用則報錯。
let x = 10;
ES6之所以如此設計,是為了減少執行時錯誤,防止變數在宣告前使用。
為了避免死區,我提供兩種方法:一是像java那樣在編寫程式碼時裡層和外層儘量不重名。二是像編寫傳統的js程式碼那樣,把變數在塊級作用域頂層進行宣告,雖然let的產生實現了java中宣告變數的效果,很多人推薦使用就近原則。
不允許重複宣告
let不允許在相同作用域內重複宣告同一個變數,即同一個作用域內不允許出現名稱相同的變數。比如下面幾種形式,只能出現其中一個:
let a = 10;
let a = 5;
var a = 15;
function a {... ...}
const a = 25;
class a {... ...}
在處理函式形參時容易掉進陷阱:
function foo(a, b){
{
let b = 10; // okay,因為是子作用域
}
let a = a+1; // 報錯
}
形參a作為foo作用域內的區域性變數不能重複宣告。
全域性物件的屬性
在ES5中,全域性物件的屬性和全域性變數是等價的。ES6規定,使用var, function宣告的全域性變數依舊作為全域性變數的屬性存在,而使用let,const,class宣告的全域性變數則不屬於全域性變數的屬性。
var foo = 'foo';
let bar = 'bar';
foo === window.foo; // =>true
bar === window.bar; // => false
2.2 const命令
const與let的特性基本相同,但顧名思義,const用於宣告常量,一旦宣告,必須立即賦值,且以後不可更改。
注意,使用const宣告物件的時候,只能保證物件的引用地址不被更改,並非此物件不被修改。
const foo = {nickname:'John Doe'}
foo.nickname = 'Jane'; //okay
foo.age = 25; // okay
foo = {nickname:'Kyle Hu'} // 報錯,因為改變了引用關係
如果你真的想保證你的物件絕對安全,可以使用Object.freeze方法:
let foo = {nickname:'John Doe'};
Object.freeze(foo);
foo.nickname = 'Jane'; //no change
foo.age = 25; // no change
但即使這樣做,當物件中的某個屬性是引用資料型別的時候也必須要小心,因為它們仍然可以被改變:
let foo = {nickname:'John Doe', bar:{gender:'boy'}};
Object.freeze(foo);
foo.nickname = 'Jane'; //no change
foo.bar = {type:'animal'}; // no change
foo.bar.gender = 'girl'; // changed
所以,物件的屬性也應該被凍結。通過深度變數物件,可以實現對整個物件的凍結:
let foo = {nickname: 'John Doe', bar: {gender: 'boy'}};
let constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach((key) => {
if(obj[key]&&typeof obj[key] === 'object'){
constantize(obj[key])
}
});
};
constantize(foo);
foo.nickname = 'Jane'; //no change
foo.bar = {type:'animal'}; // no change
foo.bar.gender = 'girl'; // no changed
console.log(foo);
此外,Object還提供了其它兩個用來凍結物件的方法,它們的威力依次增強:Object.preventExtensions()使得物件不能增加新屬性;Object.seal()使得物件既不能增加新屬性也不能刪除屬性。當然Object.freeze()威力最強大,使得物件的屬性既不能增加,也不能修改,更不能刪除。
注意與小結
ES5只有兩種宣告變數的方法,即var和function。在ES6中,又新增加了4種,分別是:let,const,class和import。
let可以完全取代var,因為二者作用幾乎相同,且let沒有任何副作用。在let和const之間,優先使用const,尤其是隻應該設定常量的全域性環境。大部分的函式一旦定義就不會改變(除了使用初始化分支的方式覆寫函式的時候),所以,我們一般推薦使用const來宣告一個函式。最後,V8只在嚴格模式下支援let和const的宣告方式。
最後再對let/const和var的區別進行一下彙總,使用let宣告的變數:
- 隸屬於塊級作用域,塊級作用域外不可見
- 不存在“變數提升”
- 同一作用域內不得存在名稱相同的變數
- 當宣告為全域性變數時不會作為全域性物件的屬性