深入ES6 (二)let和const

第二章 let和const

ES6提供了let和const兩種新的變量聲明方式,使得在JS中變量的聲明更像java那樣。這章主要包括了一下內容:

  1. ES6的塊級作用域
  2. let聲明變量與var的區別
  3. 死區
  4. 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聲明的變量:

  1. 隸屬於塊級作用域,塊級作用域外不可見
  2. 不存在“變量提升”
  3. 同一作用域內不得存在名稱相同的變量
  4. 當聲明爲全局變量時不會作爲全局對象的屬性
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章