1. 程式人生 > >深入ES6 (二)let和const

深入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. 當宣告為全域性變數時不會作為全域性物件的屬性