1. 程式人生 > 實用技巧 >let、const命令——ES6學習筆記

let、const命令——ES6學習筆記

let

基本使用

ES6 新增了let命令,用來宣告變數。它的用法類似於var,但是所宣告的變數,只在let命令所在的程式碼塊內有效。

for迴圈的計數器,就很合適使用let命令。

var a = [];		// 輸出 10 ,如果是let宣告的,輸出 6 (這是因為 JavaScript 引擎內部會記住上一輪迴圈的值,初始化本輪的變數i時,就在上一輪迴圈的基礎上進行計算。)
for (var i = 0; i < 10; i++) { 
//for迴圈還有一個特別之處,就是設定迴圈變數的那部分是一個父作用域,而迴圈體內部是一個單獨的子作用域。
  a[i] = function () {
    console.log(i);
  };
}
a[6]();

不存在變數提升

var命令會發生“變數提升”現象,即變數可以在宣告之前使用,值為undefined。這種現象多多少少是有些奇怪的,按照一般的邏輯,變數應該在宣告語句之後才可以使用。

為了糾正這種現象,let命令改變了語法行為,它所宣告的變數一定要在聲明後使用,否則報錯。

暫時性死區

只要塊級作用域記憶體在let命令,它所宣告的變數就“繫結”(binding)這個區域,不再受外部的影響。

var tmp = 123;
if (true) {
  tmp = 'abc'; // ReferenceError  在let命令宣告變數tmp之前,都屬於變數tmp的“死區”。
  let tmp;
}

上面程式碼中,存在全域性變數tmp

,但是塊級作用域內let又聲明瞭一個區域性變數tmp,導致後者繫結這個塊級作用域,所以在let宣告變數前,對tmp賦值會報錯。ES6 明確規定,如果區塊中存在letconst命令,這個區塊對這些命令宣告的變數,從一開始就形成了封閉作用域。凡是在宣告之前就使用這些變數,就會報錯。總之,在程式碼塊內,使用let命令宣告變數之前,該變數都是不可用的。這在語法上,稱為“暫時性死區”(temporal dead zone,簡稱 TDZ)。

“暫時性死區”也意味著typeof不再是一個百分之百安全的操作。

typeof x; // ReferenceError 
//變數x使用let命令宣告,所以在宣告之前,都屬於x的“死區”,只要用到該變數就會報錯。因此,typeof執行時就會丟擲一個ReferenceError。
let x;
//作為比較,如果一個變數根本沒有被宣告,使用typeof反而不會報錯。
typeof undeclared_variable // "undefined"

// 不報錯
var x = x;
// 報錯
let x = x; // 在變數x的宣告語句還沒有執行完成前,就去取x的值,導致報錯”x 未定義“。
// ReferenceError: x is not defined

在沒有let之前,typeof運算子是百分之百安全的,永遠不會報錯。現在這一點不成立了。這樣的設計是為了讓大家養成良好的程式設計習慣,變數一定要在宣告之後使用,否則就報錯。

總之,暫時性死區的本質就是,只要一進入當前作用域,所要使用的變數就已經存在了,但是不可獲取,只有等到宣告變數的那一行程式碼出現,才可以獲取和使用該變數。

不允許重複宣告

let不允許在相同作用域內,重複宣告同一個變數。因此,不能在函式內部重新宣告引數。

function func(arg) {
  let arg;
}
func() // 報錯

function func(arg) {
  {
    let arg;
  }
}
func() // 不報錯

塊級作用域

為什麼需要?

ES5 只有全域性作用域和函式作用域,沒有塊級作用域,這帶來很多不合理的場景:

  • 第一種場景,內層變數可能會覆蓋外層變數。
  • 第二種場景,用來計數的迴圈變數洩露為全域性變數。
ES6的塊級作用域
// ES6 允許塊級作用域的任意巢狀。
{{{{
  {let insane = 'Hello World'}
  console.log(insane); // 報錯
}}}};
// 上面程式碼使用了一個五層的塊級作用域,每一層都是一個單獨的作用域。第四層作用域無法讀取第五層作用域的內部變數。

// 塊級作用域的出現,實際上使得獲得廣泛應用的匿名立即執行函式表示式(匿名 IIFE)不再必要了。
// IIFE 寫法
(function () {
  var tmp = ...;
  ...
}());

// 塊級作用域寫法
{
  let tmp = ...;
  ...
}
塊級作用域與函式宣告

ES6 引入了塊級作用域,明確允許在塊級作用域之中宣告函式。ES6 規定,塊級作用域之中,函式宣告語句的行為類似於let,在塊級作用域之外不可引用。

function f() { console.log('I am outside!'); }
(function () {
  if (false) {
    // 重複宣告一次函式f
    function f() { console.log('I am inside!'); }
  }
  f();
}());
// 上面程式碼在 ES5 中執行,會得到“I am inside!”,因為在if內宣告的函式f會被提升到函式頭部,實際執行的程式碼如下。
function f() { console.log('I am outside!'); }

(function () {
  function f() { console.log('I am inside!'); }
  if (false) {
  }
  f();
}());

ES6 就完全不一樣了,理論上會得到“I am outside!”。因為塊級作用域內宣告的函式類似於let,對作用域之外沒有影響。但是,如果你真的在 ES6 瀏覽器中執行一下上面的程式碼,是會報錯的,這是為什麼呢?

考慮到環境導致的行為差異太大,應該避免在塊級作用域內宣告函式。如果確實需要,也應該寫成函式表示式,而不是函式宣告語句。

另外,還有一個需要注意的地方。ES6 的塊級作用域必須有大括號,如果沒有大括號,JavaScript 引擎就認為不存在塊級作用域。

// 第一種寫法,報錯
if (true) let x = 1;

// 第二種寫法,不報錯
if (true) {
  let x = 1;
}
// 第一種寫法沒有大括號,所以不存在塊級作用域,而let只能出現在當前作用域的頂層,所以報錯。第二種寫法有大括號,所以塊級作用域成立。
// 函式宣告也是如此,嚴格模式下,函式只能宣告在當前作用域的頂層。

const

基本使用
  • const宣告一個只讀的常量。一旦宣告,常量的值就不能改變。
  • const宣告的變數不得改變值,這意味著,const一旦宣告變數,就必須立即初始化,不能留到以後賦值。只宣告不賦值,就會報錯。
  • const的作用域與let命令相同:只在宣告所在的塊級作用域內有效。
  • const命令宣告的常量也是不提升,同樣存在暫時性死區,只能在宣告的位置後面使用。
  • const宣告的常量,也與let一樣不可重複宣告。
本質

const實際上保證的,並不是變數的值不得改動,而是變數指向的那個記憶體地址所儲存的資料不得改動。對於簡單型別的資料(數值、字串、布林值),值就儲存在變數指向的那個記憶體地址,因此等同於常量。但對於複合型別的資料(主要是物件和陣列),變數指向的記憶體地址,儲存的只是一個指向實際資料的指標,const只能保證這個指標是固定的(即總是指向另一個固定的地址),至於它指向的資料結構是不是可變的,就完全不能控制了。因此,將一個物件宣告為常量必須非常小心。

// 常量foo儲存的是一個地址,這個地址指向一個物件。不可變的只是這個地址,即不能把foo指向另一個地址,但物件本身是可變的,所以依然可以為其新增新屬性。
const foo = {};
foo.prop = 123; // 為 foo 新增一個屬性,可以成功
foo.prop // 123
// 將 foo 指向另一個物件,就會報錯
foo = {}; // TypeError: "foo" is read-only

// 常量a是一個數組,這個陣列本身是可寫的,但是如果將另一個數組賦值給a,就會報錯。
const a = [];
a.push('Hello'); // 可執行
a.length = 0;    // 可執行
a = ['Dave'];    // 報錯


// 如果真的想將物件凍結,應該使用Object.freeze方法。
const foo = Object.freeze({});
// 常規模式時,下面一行不起作用;嚴格模式時,該行會報錯
foo.prop = 123;

除了將物件本身凍結,物件的屬性也應該凍結。下面是一個將物件徹底凍結的函式。

var constantize = (obj) => { // 對每一項進行遍歷
  Object.freeze(obj);
  Object.keys(obj).forEach( (key, i) => {
    if ( typeof obj[key] === 'object' ) {
      constantize( obj[key] );
    }
  });
};

ES6宣告變數的6種方法

ES5 只有兩種宣告變數的方法:var命令和function命令。ES6 除了新增letconst命令,後面章節還會提到,另外兩種宣告變數的方法:import命令和class命令。所以,ES6 一共有 6 種宣告變數的方法。

var function let const import class

頂層物件的屬性

頂層物件,在瀏覽器環境指的是window物件,在 Node 指的是global物件。ES5 之中,頂層物件的屬性與全域性變數是等價的。

一方面規定,為了保持相容性,var命令和function命令宣告的全域性變數,依舊是頂層物件的屬性;另一方面規定,let命令、const命令、class命令宣告的全域性變數,不屬於頂層物件的屬性。也就是說,從 ES6 開始,全域性變數將逐步與頂層物件的屬性脫鉤。

globalThis物件

JavaScript 語言存在一個頂層物件,它提供全域性環境(即全域性作用域),所有程式碼都是在這個環境中執行。但是,頂層物件在各種實現裡面是不統一的。

很難找到一種方法,可以在所有情況下,都取到頂層物件。下面是兩種勉強可以使用的方法。

// 方法一
(typeof window !== 'undefined'
   ? window
   : (typeof process === 'object' &&
      typeof require === 'function' &&
      typeof global === 'object')
     ? global
     : this);

// 方法二
var getGlobal = function () {
  if (typeof self !== 'undefined') { return self; }
  if (typeof window !== 'undefined') { return window; }
  if (typeof global !== 'undefined') { return global; }
  throw new Error('unable to locate global object');
};

在語言標準的層面,引入globalThis作為頂層物件。也就是說,任何環境下,globalThis都是存在的,都可以從它拿到頂層物件,指向全域性環境下的this

參考教程:https://es6.ruanyifeng.com/#docs/let