let和const命令
var a = []; for (let i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); /
上面程式碼中,變數i
是let
宣告的,當前的i
只在本輪迴圈有效,所以每一次迴圈的i
其實都是一個新的變數,所以最後輸出的是6。
一 不存在變數提升
let
不像var
那樣會發生“變數提升”現象。所以,變數一定要在聲明後使用,否則報錯
二 暫時性死區
ES6明確規定,如果區塊中存在let
和const
命令,這個區塊對這些命令宣告的變數,從一開始就形成了封閉作用域。凡是在宣告之前就使用這些變數,就會報錯。這在語法上,稱為“暫時性死區”(temporal dead zone,簡稱TDZ)。
if (true) { // TDZ開始 tmp = 'abc'; // ReferenceError console.log(tmp); // ReferenceError let tmp; // TDZ結束 console.log(tmp); // undefined tmp = 123; console.log(tmp); // 123 }
“暫時性死區”也意味著typeof
不再是一個百分之百安全的操作。
typeof x; // ReferenceError let x;
有些“死區”比較隱蔽,不太容易發現。
functionbar(x = y, y = 2) { return [x, y]; } bar(); // 報錯
上面程式碼中,呼叫bar
函式之所以報錯(某些實現可能不報錯),是因為引數x
預設值等於另一個引數y
,而此時y
還沒有宣告,屬於”死區“。如果y
的預設值是x
,就不會報錯,因為此時x
已經聲明瞭。
ES6規定暫時性死區和let
、const
語句不出現變數提升,主要是為了減少執行時錯誤,防止在變數宣告前就使用這個變數,從而導致意料之外的行為。這樣的錯誤在ES5是很常見的,現在有了這種規定,避免此類錯誤就很容易了。
總之,暫時性死區的本質就是,只要一進入當前作用域,所要使用的變數就已經存在了,但是不可獲取,只有等到宣告變數的那一行程式碼出現,才可以獲取和使用該變數。
三 不允許重複宣告
// 報錯 function () { let a = 10; var a = 1; } // 報錯 function () { let a = 10; let a = 1; } function func(arg) { let arg; // 報錯 } function func(arg) { { let arg; // 不報錯 } }
四 塊級作用域
ES5只有全域性作用域和函式作用域,沒有塊級作用域,這帶來很多不合理的場景。
第一種場景,內層變數可能會覆蓋外層變數。
var tmp = new Date(); function f() { console.log(tmp); if (false) { var tmp = "hello world"; } } f(); // undefined
上面程式碼中,函式f執行後,輸出結果為undefined
,原因在於變數提升,導致內層的tmp變數覆蓋了外層的tmp變數。
第二種場景,用來計數的迴圈變數洩露為全域性變數。
var s = 'hello'; for (var i = 0; i < s.length; i++) { console.log(s[i]); } console.log(i); // 5
上面程式碼中,變數i只用來控制迴圈,但是迴圈結束後,它並沒有消失,洩露成了全域性變數。
塊級作用域的出現,實際上使得獲得廣泛應用的立即執行匿名函式(IIFE)不再必要了。
// IIFE寫法 (function () { var tmp = ...; ... }()); // 塊級作用域寫法 { let tmp = ...; ... }
const命令
const
宣告一個只讀的常量。一旦宣告,常量的值就不能改變。
const
宣告的變數不得改變值,這意味著,const一旦宣告變數,就必須立即初始化,不能留到以後賦值。
const foo; // SyntaxError: Missing initializer in const declaration
const
的作用域與let
命令相同:只在宣告所在的塊級作用域內有效。
const
命令宣告的常量也是不提升,同樣存在暫時性死區,只能在宣告的位置後面使用。
const
宣告的常量,也與let
一樣不可重複宣告。
const a = []; a.push('Hello'); // 可執行 a.length = 0; // 可執行 a = ['Dave']; // 報錯
上面程式碼中,常量a
是一個數組,這個陣列本身是可寫的,但是如果將另一個數組賦值給a
,就會報錯。
如果真的想將物件凍結,應該使用Object.freeze
方法。
const foo = Object.freeze({}); // 常規模式時,下面一行不起作用; // 嚴格模式時,該行會報錯 foo.prop = 123;
上面程式碼中,常量foo
指向一個凍結的物件,所以新增新屬性不起作用,嚴格模式時還會報錯。
除了將物件本身凍結,物件的屬性也應該凍結。下面是一個將物件徹底凍結的函式。
var constantize = (obj) => { Object.freeze(obj); Object.keys(obj).forEach( (key, value) => { if ( typeof obj[key] === 'object' ) { constantize( obj[key] ); } }); };
全域性物件的屬性
未宣告的全域性變數,自動成為全域性物件window
的屬性,這被認為是JavaScript語言最大的設計敗筆之一。這樣的設計帶來了兩個很大的問題,首先是沒法在編譯時就報出變數未宣告的錯誤,只有執行時才能知道,其次程式設計師很容易不知不覺地就建立了全域性變數(比如打字出錯)。另一方面,從語義上講,語言的頂層物件是一個有實體含義的物件,也是不合適的。
ES6為了改變這一點,一方面規定,為了保持相容性,var
命令和function
命令宣告的全域性變數,依舊是全域性物件的屬性;另一方面規定,let
命令、const
命令、class
命令宣告的全域性變數,不屬於全域性物件的屬性。也就是說,從ES6開始,全域性變數將逐步與全域性物件的屬性脫鉤。
var a = 1; // 如果在Node的REPL環境,可以寫成global.a // 或者採用通用方法,寫成this.a window.a // 1 let b = 1; window.b // undefined
上面程式碼中,全域性變數a
由var
命令宣告,所以它是全域性物件的屬性;全域性變數b
由let
命令宣告,所以它不是全域性物件的屬性,返回undefined
。