在 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
變數。
沒錯,當 let
在 for
的括號中出現時,那它會建立一個隱含的區塊,這區塊包含住 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)
>