1. 程式人生 > 實用技巧 >let和const命令

let和const命令

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); /

上面程式碼中,變數ilet宣告的,當前的i只在本輪迴圈有效,所以每一次迴圈的i其實都是一個新的變數,所以最後輸出的是6。

一 不存在變數提升

let不像var那樣會發生“變數提升”現象。所以,變數一定要在聲明後使用,否則報錯

二 暫時性死區

ES6明確規定,如果區塊中存在letconst命令,這個區塊對這些命令宣告的變數,從一開始就形成了封閉作用域。凡是在宣告之前就使用這些變數,就會報錯。這在語法上,稱為“暫時性死區”(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;

有些“死區”比較隱蔽,不太容易發現。

function
bar(x = y, y = 2) { return [x, y]; } bar(); // 報錯

上面程式碼中,呼叫bar函式之所以報錯(某些實現可能不報錯),是因為引數x預設值等於另一個引數y,而此時y還沒有宣告,屬於”死區“。如果y的預設值是x,就不會報錯,因為此時x已經聲明瞭。

ES6規定暫時性死區和letconst語句不出現變數提升,主要是為了減少執行時錯誤,防止在變數宣告前就使用這個變數,從而導致意料之外的行為。這樣的錯誤在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

上面程式碼中,全域性變數avar命令宣告,所以它是全域性物件的屬性;全域性變數blet命令宣告,所以它不是全域性物件的屬性,返回undefined