每日一記 3分鐘從編譯後的程式碼裡學 let 和 const 命令

羅小黑寫寫文字發表於2019-01-20

每日一記 3分鐘從編譯後的程式碼裡學 let 和 const 命令

新系列導讀

學習程式語言是一件持之以恆的事情,從學會簡單的語法就能寫出程式,到理解型別和設計模式,再到考慮程式碼的組織架構。誰不是從這樣一點點深入和積累的呢?入門總是輕鬆又令人愉悅的,但隨著知識點越來越多學習的曲線卻驟然陡峭。但隨著對語言的深入理解,再回頭來重新審閱基本的知識,又會有柳暗花明又一村的豁然感,「啊,原來是這樣的」那種感覺。

這個 「3分鐘系列」 將利用 babel 編譯工具,來學習分析 es6+ 的部分特性。通過編譯後的 es5 程式碼,我們可以從中瞭解到 es6+ 特性的實現細節,更好的掌握新特性的適用性。

本文大量使用了阮一峰「 ECMAScript 6 入門」和「你不知道的 JavaScript」書中的程式碼。

cutting line

環境和配置

// @babel/core: 7.2.2
// @babel/preset-env: 7.2.3

// .babelrc
{
  "presets": [
    ["@babel/preset-env", {
      "ignoreBrowserslistConfig": true
    }]
  ]
}
複製程式碼

cutting line

塊級作用域

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

  • 內層變數可能會覆蓋外層變數。
  • for 迴圈中的計數變數會洩露為全域性變數。

在 ES5 中為了建立一個塊級作用域,除了普通的函式宣告外,就是立即執行函式表示式了(IIFE)。

// es5
var a = 2;
(function IIFE () {
	var a = 3;
	console.log(a); // 3
})();

console.log(a); // 2
複製程式碼

cutting line

ES6 中引入了塊級作用域,當在花括號中存在 let 或者 const 時,花括號內為塊級作用域:

  • 外層作用域無法讀取內層 letconst 所宣告的變數。
  • 內層 letconst 所宣告的變數名可以和外層相同。
  • 立即執行函式表示式不再必要了。
// ------ 原始碼區 ------
var x = 1;
let y = 1;
if (true) {
  var x = 2;
  let y = 2;
}
console.log(x); // 2
console.log(y); // 1


// ------ 編譯區 ------
"use strict";
var x = 1;
var y = 1;

if (true) {
  var x = 2;
  var _y = 2;
}
console.log(x); // 2
console.log(y); // 1
複製程式碼

特性:由於 let 使花括號提升為塊級作用域,使得即使宣告瞭相同的變數名 y 也互不干擾。

Babel:為了實現此效果,Babel 重新命名了塊級作用域內 let 宣告的變數名。

cutting line

for 迴圈

// ------ 原始碼區 ------
let a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6]();


// ------ 編譯區 ------
"use strict";
var a = [];

var _loop = function _loop(i) {
  a[i] = function () {
    console.log(i);
  };
};

for (var i = 0; i < 10; i++) {
  _loop(i);
}
a[6]();
複製程式碼

特性:當用 var 宣告變數 i 時,a[6]() 輸出的是 10,因為迴圈體沒有快取變數 i

Babel:當用 let 宣告時,Babel 建立了一個閉包 _loop 來快取變數。

cutting line

cutting line

// ------ 原始碼區 ------
let i = 1;

for (let i = 0; i < 3; i++) {
  let i = 'abc';
  console.log(i);
}


// ------ 編譯區 ------
"use strict";
var i = 1;
for (var _i = 0; _i < 3; _i++) {
  var _i2 = 'abc';
  console.log(_i2);
}
複製程式碼

特性for 迴圈中,初始化變數的部分和迴圈體內分別是兩個作用域。

Babel:Babel 重新命名了迴圈體內的變數 i

cutting line

let

ES6 新增了 let 命令,用來宣告變數。

let 擁有如下特性:

  • 僅在其作用域內有效。
  • 不存在變數提升。
  • 產生暫時性死區。
  • 不允許重複宣告。

僅在其作用域內有效

// ------ 原始碼區 ------
{
  let a = 10;
  var b = 1;
}
a // ReferenceError: a is not defined.
b // 1


// ------ 編譯區 ------
"use strict";
{
  var _a = 10;
  var b = 1;
}
a; // ReferenceError: a is not defined.
b; // 1
複製程式碼

特性let 所宣告的變數只會在其作用域內有效,作用域外呼叫該變數會報錯。

Babel:為了用 ES5 實現相同的特性,Babel 重新命名了 let 宣告的變數名,使得作用域內外的變數名不同。

cutting line

不存在變數提升

// ------ 原始碼區 ------
// var 的情況
console.log(foo); // 輸出undefined
var foo = 2;

// let 的情況
console.log(bar); // 報錯ReferenceError
let bar = 2;


// ------ 編譯區 ------
"use strict";
// var 的情況
console.log(foo); // 輸出undefined
var foo = 2; // let 的情況

console.log(bar); // 報錯ReferenceError
var bar = 2;
複製程式碼

特性let 必須先宣告再使用,這種語法行為糾正了 var 變數提升的現象。

Babel:Babel 在此處並沒有做特殊的處理。

重要提示: let 在編譯後沒有新增異常提示,Babel 在變數提升細節上處理不佳,你的程式碼執行結果可能會和你預想中的有差異。

養成良好的程式碼習慣,有助於避免此坑。

變數提升 | MDN

cutting line

暫時性死區

縮寫為「TDZ」(temporal dead zone)。

當區塊中存在 letconst 命令,這個區塊對這些命令宣告的變數就形成了封閉區域,凡是在宣告前就使用這些變數,就會報錯。

// ------ 原始碼區 ------
var tmp = 123;

{
  tmp = 'abc'; // ReferenceError
  let tmp;
}


// ------ 編譯區 ------
"use strict";
var tmp = 123;
{
  _tmp = 'abc'; // ReferenceError
  var _tmp;
}
複製程式碼

特性:上面的原始碼區中,期望給外部的 tmp 賦值 abc 。但由於在區塊中宣告瞭同名變數,所以此時 tmp 變數被內部佔用。

Babel:Babel 很好的處理了這個特性,將區塊內的 tmp 變數更名為 _tmp 以區分。但是,仍然會存在變數提升的問題。

cutting line

不允許重複宣告

// ------ 原始碼區 ------
function foo() {
  let a = 10;
  let a = 1;
}

// ------ 編譯區 ------
// 編譯報錯 Duplicate declaration "a"
複製程式碼

特性let不允許在相同作用域內,重複宣告同一個變數。

Babel:當重複宣告同一個變數時,編譯無法通過。

cutting line

const

const 宣告一個只讀的常量。

const 有如下特性:

  • 變數一旦宣告,其值(記憶體地址)就不可改變。
  • 宣告時必須賦值。
// ------ 原始碼區 ------
const a = 1;


// ------ 編譯區 ------
var a = 1;
複製程式碼

特性:最普通的使用方式。

Babel:如果上下文沒有違背規範,則會直接用 var 來宣告。

cutting line

變數一旦宣告,其值(記憶體地址)就不可改變。

// ------ 原始碼區 ------
const a = 0;
a = 1;


// ------ 編譯區 ------
"use strict";
function _readOnlyError(name) { throw new Error("\"" + name + "\" is read-only"); }

var a = 0;
a = (_readOnlyError("a"), 1);
複製程式碼

特性:一旦 const 宣告瞭一個變數後嘗試再次賦值,會報異常。

Babel:Babel 檢測到變數被在此賦值,主動插入了一個報錯,並終止程式執行。

cutting line

宣告時必須賦值

// ------ 原始碼區 ------
const a;


// ------ 編譯區 ------
// 編譯報錯 Unexpected token
複製程式碼

cutting line

總結

Babel 在處理 letconst 的大部分特性時都不錯,但是在 變數先宣告後使用 的細節上處理不佳。需要我們保持良好的變數宣告習慣。

cutting line

相容性表

每日一記 3分鐘從編譯後的程式碼裡學 let 和 const 命令

目前 86% 左右的瀏覽器都原生支援 letconst

cutting line

後續

當我文章寫到此處,我仍然疑惑為什麼 Babel 為什麼會沒有正確編譯暫時性死區的特性,留下這樣的問題。

直到我找到了 @babel/plugin-transform-block-scoping 外掛。

原來想要 Babel 編譯時正確實現該特性,需要引入這個外掛並開啟配置。

// .babelrc
{
  "presets": [
    ["@babel/preset-env", {
      "ignoreBrowserslistConfig": true
    }]
  ],
  "plugins": [
    ["@babel/plugin-transform-block-scoping", {
      "tdz": true
    }]
  ]
}
複製程式碼

cutting line

此時 Babel 會為程式碼插入一個異常。

// ------ 原始碼區 ------
i;
let i = 1;


// ------ 編譯區 ------
"use strict";
(function () {
  throw new ReferenceError("i is not defined - temporal dead zone");
})();

var i = 1;
複製程式碼

但是文件中也說了,這個外掛沒有覆蓋所有邊界情況,也應該小心使用。

Temporal Dead Zone · Issue #826 · babel/website · GitHub

@babel/plugin-transform-block-scoping · Babel

cutting line

相關閱讀

2019 年的 JavaScript 新特性學習指南

相關文章