每日一記 3分鐘從編譯後的代碼裏學 let 和 const 命令

新系列導讀

學習編程語言是一件鍥而不捨的事情,從學會簡單的語法就能寫出程序,到理解類型和設計模式,再到考慮代碼的組織架構。誰不是從這樣一點點深刻和積累的呢?入門老是輕鬆又使人愉悅的,但隨着知識點愈來愈多學習的曲線卻驟然陡峭。但隨着對語言的深刻理解,再回頭來從新審閱基本的知識,又會有柳暗花明又一村的豁然感,「啊,原來是這樣的」那種感受。javascript

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

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

cutting linees6

環境和配置

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

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

cutting linegithub

塊級做用域

ES5 只有全局做用域和函數做用域,沒有塊級做用域,帶來了不少不合理的場景:web

  • 內層變量可能會覆蓋外層變量。
  • for 循環中的計數變量會泄露爲全局變量。

在 ES5 中爲了建立一個塊級做用域,除了普通的函數聲明外,就是當即執行函數表達式了(IIFE)。編程

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

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

cutting linejson

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

兼容性表

目前 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 新特性學習指南

相關文章
相關標籤/搜索