使用 let 與 const 宣告變數


在 ECMAScript 6 中,宣告變數的話建議使用 let,這可以讓變數在行為上看起來,比較像是其他程式語言中的變數該有的行為。例如,終於有區塊範圍了:

> if(true) {
...     let y = 10;
... }
undefined
> console.log(y);
ReferenceError: y is not defined
    at repl:1:13
    at ContextifyScript.Script.runInThisContext (vm.js:50:33)
    at REPLServer.defaultEval (repl.js:240:29)
    at bound (domain.js:301:14)
    at REPLServer.runBound [as eval] (domain.js:314:12)
    at REPLServer.onLine (repl.js:441:10)
    at emitOne (events.js:121:20)
    at REPLServer.emit (events.js:211:7)
    at REPLServer.Interface._onLine (readline.js:282:10)
    at REPLServer.Interface._line (readline.js:631:8)
>

在上面的例子中,由於 y 宣告在 if 的區塊中,範圍僅在區塊之內,在區塊外是看不到的,因而會發生 ReferenceError

另一方面,let 宣告的變數也不會有 Hoisting,例如:

console.log(x);  // ReferenceError
let x = 10;

在全域範圍使用 let 宣告變數,變數並不會成為全域物件的特性:

> let x = 10;
undefined
> this.x;
undefined
> 

(在支援模組的環境中,一個 .js 是一個模組,而在 .js 頂層使用 let 宣告的變數,會是以模組為範圍。)

你可以使用 {} 來定義區塊,而當中使用的 let 宣告的變數存在於該區塊範圍,例如:

let y = 10;
{
    let y = 20;
    console.log(y); // 20
}
console.log(y);     // 10

感覺不錯吧!不過,嗯?區塊內外都宣告了 y?在其他程式語言中,像是 Java 之類的並不能這麼做,編譯器對於區塊中的 y 會抱怨它已經宣告過了,然而,對於 ECMAScript 6,只要同名的變數不是宣告於同一個區塊範圍就可以了,同名變數包含了 var 宣告的變數,例如,以下都會發生 SyntaxError

> {
...     let x = 1;
...     let x = 2;
    let x = 2;
        ^

SyntaxError: Identifier 'x' has already been declared

> {
...     var x = 1;
...     let x = 2;
... }
    var x = 1;
        ^

SyntaxError: Unexpected identifier

> {
...     let x = 1;
...     var x = 2;
... }
    let x = 1;
        ^

SyntaxError: Unexpected identifier

>

let 簡單來說,就是宣告區塊變數,然而這也就讓它還是有些地方與常見的程式語言變數不太一樣,例如,底下的程式會如何呢?

let y = 10;
{
    console.log(y); 
    let y = 20;
}

console.log(y) 那行不是顯示 10,而是會 ReferenceError,如果區塊內外有 let 宣告了同名變數,那麼區塊中在 let 宣告之前的地方,會是所謂的暫時死區(Temporal dead zone),在這個區域內不能存取同名變數。

如果你的區塊中程式碼很長,稍後又不小心又用 let 宣告了同名變數,前面的變數存取就突然 ReferenceError 了,這時可能會小小的驚喜一下吧!

如果可以使用 let 宣告變數,那麼〈名稱空間管理〉中的 IIFE 寫法就不需要了,可以直接這麼寫:

var openhome = openhome || {};
{
    let x = 10;
    function validate() { // validate 只在區塊中可見
        return x;
    }
    openhome.validate = validate;
    // 其他...
}

console.log(openhome.validate()); // 10

使用 var 宣告的變數,會存在整個範圍之內,因此像底下宣告在 for 之中的 i,在 for 之外可以取用:

for(var i = 0; i < 10; i++) {
    console.log(i);
}
console.log(i);  // 10

改成 let 的話就不會有這個問題:

for(let i = 0; i < 10; i++) {
    console.log(i);
}
console.log(i);  // ReferenceError

來看個大家在文件中,很愛舉的例子,用來說明 let 之後改善了什麼,是這樣的嗎?先來看這個使用 var 的例子:

var fs = [];
for(var i = 0; i < 10; i++) {
    fs[i] = function () {
        return i;
    };
}

console.log(fs[0]());  // 10
console.log(fs[1]());  // 10

var 的角度來看,它整個範圍中都見,因此全部收集到的函式都傳回 10 不意外,就算假設 var 有著範圍僅限於 for 區塊中的能力,從 Closure 的角度來看,它捕捉了變數而不是變數值,全部收集到的函式都會傳回值也是正常,這在支援 Closure 的語言中,也都是有如此的行為,這並不是 var 的問題。

反倒是改成了 let 之後,我覺得怪怪的:

var fs = [];
for(let i = 0; i < 10; i++) {
    fs[i] = function () {
        return i;
    };
}

console.log(fs[0]()); // 0
console.log(fs[1]()); // 1

有人說,這確確實實捕捉了 i 當次迭代的值(而這確實是有些人想要的目的),然而我想說的是,Closure 捕捉的應該是變數而不是值,這看似違反了 Closure 的行為,除非每次迭代時,let 都創造了一個新變數 i,而 Closure 捕捉了當次的 i 變數。

沒錯,當 letfor 的括號中出現時,那它會建立一個隱含的區塊,這區塊包含住 for{} 區塊,而每次的迭代都會在隱含的區塊中創造一個新變數,最後 i++,之後 i 的值被用來指定給下一次的新變數 i,就上例而言,每次的建立的函式捕捉住的,就是每次迭代時創造的新變數。

有人說,這樣的寫法改進了 var 的問題,不過我倒覺得,如果熟悉 Closure 的人,看到這個行為反而會覺得困擾呢!只能說,每個語言都有其特性了。

不過,整體來說,使用 let 還是比使用 var 來得好的,在可以的情況下,儘量使用 let 來宣告變數,並明確定義區塊範圍,在嚴格模式下,使用 let 宣告的變數,若嘗試使用 delete 刪除,也是發生 SyntaxError

至於 const,就是宣告的變數被指定值之後,就不可以再指定其他的值,其他的特性,const 基本上與 let 一樣,然而 JavaScript 中有意外也不意外,例如,喔!let 宣告一個變數,而不指定值的話,該變數會是 undefined

> let x;
undefined
> x
undefined
>

然而,const 宣告變數必須明確指定值:

> const y = 10;
undefined
> y = 20;
TypeError: Assignment to constant variable.
    at repl:1:3
    at ContextifyScript.Script.runInThisContext (vm.js:50:33)
    at REPLServer.defaultEval (repl.js:240:29)
    at bound (domain.js:301:14)
    at REPLServer.runBound [as eval] (domain.js:314:12)
    at REPLServer.onLine (repl.js:441:10)
    at emitOne (events.js:121:20)
    at REPLServer.emit (events.js:211:7)
    at REPLServer.Interface._onLine (readline.js:282:10)
    at REPLServer.Interface._line (readline.js:631:8)
> const z;
const z;
      ^

SyntaxError: Missing initializer in const declaration

>

真的不知道要指定啥值?宣告 const 變數時,可以直接指定它是 undefined,只是它就只能是 undefined 了(這種常數要幹嘛呢?):

> const z = undefined;
undefined
> z
undefined
> z = 'XD';
TypeError: Assignment to constant variable.
    at repl:1:3
    at ContextifyScript.Script.runInThisContext (vm.js:50:33)
    at REPLServer.defaultEval (repl.js:240:29)
    at bound (domain.js:301:14)
    at REPLServer.runBound [as eval] (domain.js:314:12)
    at REPLServer.onLine (repl.js:441:10)
    at emitOne (events.js:121:20)
    at REPLServer.emit (events.js:211:7)
    at REPLServer.Interface._onLine (readline.js:282:10)
    at REPLServer.Interface._line (readline.js:631:8)
>