第二章 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聲明的變量:
- 隸屬於塊級作用域,塊級作用域外不可見
- 不存在“變量提升”
- 同一作用域內不得存在名稱相同的變量
- 當聲明爲全局變量時不會作爲全局對象的屬性