ES6 學習筆記

omnoob發表於2019-07-31

本文是基於 ECMAScript 6 入門 的學習筆記。
只是按照本人理解梳理內容,更加詳細的相關內容請移步 ECMAScript 6 入門


一. ES6 簡介

es6.ruanyifeng.com/#docs/intro

1. Bable 轉碼器

Babel 是一個廣泛使用的 ES6 轉碼器,可以將 ES6 程式碼轉為 ES5 程式碼,從而在現有環境執行。這意味著,你可以用 ES6 的方式編寫程式,又不用擔心現有環境是否支援。

1.1 安裝Babel

在專案目錄下,安裝Babel

$ npm install --save-dev @babel/core
複製程式碼

1.2 配置檔案.babelrc

Babel 的配置檔案是.babelrc,存放在專案的根目錄下。使用 Babel 的第一步,就是配置這個檔案。

(具體配置看原文)

注意,以下所有 Babel 工具和模組的使用,都必須先寫好.babelrc

1.3 命令列轉碼

Babel 提供命令列工具@babel/cli,用於命令列轉碼。 它的安裝命令如下:

$ npm install --save-dev @babel/cli
複製程式碼

基本用法如下:

# 轉碼結果輸出到標準輸出
$ npx babel example.js

# 轉碼結果寫入一個檔案
# --out-file 或 -o 引數指定輸出檔案
$ npx babel example.js --out-file compiled.js
# 或者
$ npx babel example.js -o compiled.js

# 整個目錄轉碼
# --out-dir 或 -d 引數指定輸出目錄
$ npx babel src --out-dir lib
# 或者
$ npx babel src -d lib

# -s 引數生成source map檔案
$ npx babel src -d lib -s
複製程式碼

babel-node

@babel/node模組的babel-node命令,提供一個支援 ES6 的 REPL 環境。它支援 Node 的 REPL 環境的所有功能,而且可以直接執行 ES6 程式碼。

@babel/register 模組

@babel/register模組改寫require命令,為它加上一個鉤子。此後,每當使用require載入.js.jsx.es和`.es6字尾名的檔案,就會先用 Babel 進行轉碼。

使用時,必須首先載入@babel/register

// index.js
require('@babel/register');
require('./es6.js');
複製程式碼

需要注意的是,@babel/register只會對require命令載入的檔案轉碼,而不會對當前檔案轉碼。另外,由於它是實時轉碼,所以只適合在開發環境使用。

1.4 babel API

如果某些程式碼需要呼叫 Babel 的 API 進行轉碼,就要使用@babel/core模組。

1.5 @babel/polyfill

Babel 預設只轉換新的 JavaScript 句法(syntax),而不轉換新的 API,比如IteratorGeneratorSetMapProxyReflectSymbolPromise等全域性物件,以及一些定義在全域性物件上的方法(比如Object.assign)都不會轉碼。

舉例來說,ES6 在Array物件上新增了Array.from方法。Babel 就不會轉碼這個方法。如果想讓這個方法執行,必須使用babel-polyfill,為當前環境提供一個墊片。

Babel 預設不轉碼的 API 非常多,詳細清單可以檢視babel-plugin-transform-runtime模組的definitions.js檔案。

瀏覽器環境

Babel 也可以用於瀏覽器環境,使用@babel/standalone模組提供的瀏覽器版本,將其插入網頁。

<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
// Your ES6 code
</script>
複製程式碼

注意,網頁實時將 ES6 程式碼轉為 ES5,對效能會有影響。生產環境需要載入已經轉碼完成的指令碼。

線上轉換

Babel 提供一個REPL 線上編譯器,可以線上將 ES6 程式碼轉為 ES5 程式碼。轉換後的程式碼,可以直接作為 ES5 程式碼插入網頁執行。

2. Traceur 轉碼器

Google 公司的Traceur轉碼器,也可以將 ES6 程式碼轉為 ES5 程式碼。

2.1 直接插入網頁

Traceur 允許將 ES6 程式碼直接插入網頁。首先,必須在網頁頭部載入 Traceur 庫檔案。

// 第一個是載入 Traceur 的庫檔案
<script src="https://google.github.io/traceur-compiler/bin/traceur.js"></script>
// 第二個和第三個是將這個庫檔案用於瀏覽器環境
<script src="https://google.github.io/traceur-compiler/bin/BrowserSystem.js"></script>
<script src="https://google.github.io/traceur-compiler/src/bootstrap.js"></script>
// 第四個則是載入使用者指令碼,這個指令碼里面可以使用 ES6 程式碼。
<script type="module">
  import './Greeter.js';
</script>
複製程式碼

注意,第四個script標籤的type屬性的值是module,而不是text/javascript。這是 Traceur 編譯器識別 ES6 程式碼的標誌,編譯器會自動將所有type=module的程式碼編譯為 ES5,然後再交給瀏覽器執行.

除了引用外部 ES6 指令碼,也可以直接在網頁中放置 ES6 程式碼。 如果想對 Traceur 的行為有精確控制,可以採用下面引數配置的寫法。(看原文)

2.2 線上轉換

Traceur 也提供一個線上編譯器,可以線上將 ES6 程式碼轉為 ES5 程式碼。轉換後的程式碼,可以直接作為 ES5 程式碼插入網頁執行。

2.3 命令列轉換

作為命令列工具使用時,Traceur 是一個 Node 的模組,首先需要用 npm 安裝。

$ npm install -g traceur
複製程式碼

Traceur 直接執行 ES6 指令碼檔案,會在標準輸出顯示執行結果。以下面的calc.js為例。

<script type="module">
  class Calc {
    constructor() {
      console.log('Calc constructor');
    }
    add(a, b) {
      return a + b;
    }
  }

  var c = new Calc();
  console.log(c.add(4,5));
</script>
複製程式碼
$ traceur calc.js
複製程式碼

如果要將 ES6 指令碼轉為 ES5 儲存,要採用下面的寫法。

$ traceur --script calc.es6.js --out calc.es5.js --experimental
複製程式碼

--script選項表示指定輸入檔案,--out選項表示指定輸出檔案。 為了防止有些特性編譯不成功,最好加上--experimental選項。

2.4 Node 環境的用法


二. let 和 const 命令

es6.ruanyifeng.com/#docs/let

1. let 命令

相關解釋看原文

基本用法:

  • let命令宣告的變數,只在let命令所在的程式碼塊內有效。
{
  let a = 10;
  var b = 1;
}

a // ReferenceError: a is not defined.
b // 1
複製程式碼
{
    console.log(i)
    let i=1
}
Uncaught ReferenceError: i is not defined
//在一個塊作用域裡,只要使用let/const命令宣告i,在let/const命令前使用i都會報錯,
//這就是暫時性死區。
複製程式碼

不存在變數提升

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

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

// var 的情況
console.log(foo); // 輸出undefined
var foo = 2;

以上程式碼實際為:
var foo
console.log(foo); // 所以此時foo為undefined
foo = 2;

// let 的情況
// 因為 let 沒有變數提升,所以 console 語句時 bar 是不存在的
console.log(bar); // 報錯ReferenceError:bar is not defined
let bar = 2;
複製程式碼

暫時性死區

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

var tmp = 123;

if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}
複製程式碼

上面程式碼中,存在全域性變數tmp,但是塊級作用域內let又宣告瞭一個區域性變數tmp,導致後者繫結這個塊級作用域,所以在let宣告變數前,對tmp賦值會報錯。

ES6 明確規定,如果區塊中存在letconst命令,這個區塊對這些命令宣告的變數,從一開始就形成了封閉作用域。凡是在宣告之前就使用這些變數,就會報錯。

總之,在程式碼塊內,使用let命令宣告變數之前,該變數都是不可用的。這在語法上,稱為“暫時性死區”(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不再是一個百分之百安全的操作。

// 1. 直接對一個沒有宣告的變數使用typeof 得到的是 undefined
typeof undeclared_variable // "undefined"
// 2. 但是如果這個變數是let宣告的,就會報錯。
typeof x; // ReferenceError
let x;
複製程式碼

這樣的設計是為了讓大家養成良好的程式設計習慣,變數一定要在宣告之後使用,否則就報錯。

有些死區比較隱蔽:

// x的預設值是y,但是此時y未宣告,x=y 就是死區,所以就報錯了。
function bar(x = y, y = 2) {
  return [x, y];
}

bar(); // 報錯
複製程式碼

使用let宣告變數時,只要變數在還沒有宣告完成前使用,就會報錯:

// 不報錯
var x = x;

// 報錯
let x = x;
// ReferenceError: x is not defined
複製程式碼

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

不允許重複宣告

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

// 報錯
function func() {
  let a = 10;
  var a = 1;
}

// 報錯
function func() {
  let a = 10;
  let a = 1;
}
複製程式碼

因此,不能在函式內部重新宣告引數。

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

function func(arg) {
  {
    let arg;
  }
}
func() // 不報錯
複製程式碼

2. 塊級作用域

  • 為什麼需要塊級作用域?
    在 ES5 只有全域性作用域和函式作用域,這會導致很多不合理的場景。
    第一種場景,內層變數可能會覆蓋外層變數。
var tmp = new Date();

function f() {
  console.log(tmp);
  if (false) {
    var tmp = 'hello world';
  }
}

f(); // undefined

此處 console 語句是想外部使用外部的 tmp ,if內部使用內部的 tmp 。
但是 if 內部的 tmp 洩露到 if 外 ,導致了我們預期外的結果。
複製程式碼

第二種場景,用來計數的迴圈變數洩露為全域性變數。

var s = 'hello';

for (var i = 0; i < s.length; i++) {
  console.log(s[i]);
}

console.log(i); // 5

此處 for 結束後,i就應該消失,但是卻還能列印出i,
這會干擾的別的也使用 i 作為全域性變數的地方。 
複製程式碼
  • let實際上為 JavaScript 新增了塊級作用域。
function f1() {
  let n = 5;
  if (true) {
    let n = 10;
  }
  console.log(n); // 5
}

此處 iflet宣告的n 就不會干擾到 if 外的 n 。
如果使用的都是var 則最後列印出的是 10 。
複製程式碼
  • ES6 允許塊級作用域的任意巢狀。
  • 內層作用域可以定義外層作用域的同名變數。
  • 塊級作用域的出現,實際上使得獲得廣泛應用的匿名立即執行函式表示式(匿名 IIFE)不再必要了。
// IIFE 寫法
(function () {
  var tmp = ...;
  ...
}());

// 塊級作用域寫法
{
  let tmp = ...;
  ...
}
複製程式碼

塊級作用域與函式宣告

  • ES5 規定,函式只能在頂層作用域和函式作用域之中宣告,不能在塊級作用域宣告。(實際上瀏覽器為了相容舊程式碼,並未遵守此規定)
  • ES6 引入了塊級作用域,明確允許在塊級作用域之中宣告函式。ES6 規定,塊級作用域之中,函式宣告語句的行為類似於let,在塊級作用域之外不可引用。
  • 但是!為了減少因第二條規定造成的不相容問題,ES6 規定瀏覽器的實現可以有自己的行為方式:
- 允許在塊級作用域內宣告函式。
- 函式宣告類似於var,即會提升到全域性作用域或函式作用域的頭部。
- 同時,函式宣告還會提升到所在的塊級作用域的頭部。

上面三條規則只對 ES6 的瀏覽器實現有效,其他環境的實現不用遵守,
還是將塊級作用域的函式宣告當作let處理。
複製程式碼

綜上,應該避免在塊級作用域內宣告函式。如果確實需要,也應該寫成函式表示式,而不是函式宣告語句。

// 塊級作用域內部,優先使用函式表示式
{
  let a = 'secret';
  let f = function () {
    return a;
  };
}
複製程式碼

ES6 的塊級作用域必須有大括號,如果沒有大括號,JavaScript 引擎就認為不存在塊級作用域。

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

// 第二種寫法,不報錯
if (true) {
  let x = 1;
}
複製程式碼

函式宣告也是如此,嚴格模式下,函式只能宣告在當前作用域的頂層。

3. const命令

基本用法

const宣告一個只讀的常量。一旦宣告,常量的值就不能改變。

const PI = 3.1415;
PI // 3.1415

PI = 3;
// TypeError: Assignment to constant variable.
複製程式碼

const宣告的變數不得改變值,這意味著,const一旦宣告變數,就必須立即初始化,不能留到以後賦值。

const foo;
// SyntaxError: Missing initializer in const declaration
複製程式碼

constlet 一樣, 有塊級作用域,暫時性死區,宣告的常量不提升,也不能重複宣告。

ES6 宣告變數的六種方法

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

4. 頂層物件的屬性

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

頂層物件的屬性與全域性變數掛鉤,被認為是 JavaScript 語言最大的設計敗筆之一。這樣的設計帶來了幾個很大的問題,

  • 首先是沒法在編譯時就報出變數未宣告的錯誤,只有執行時才能知道(因為全域性變數可能是頂層物件的屬性創造的,而屬性的創造是動態的);
  • 其次,程式設計師很容易不知不覺地就建立了全域性變數(比如打字出錯);
  • 最後,頂層物件的屬性是到處可以讀寫的,這非常不利於模組化程式設計。
  • 另一方面,window物件有實體含義,指的是瀏覽器的視窗物件,頂層物件是一個有實體含義的物件,也是不合適的。

ES6 為了改變這一點,且保證相容性,規定 varfunction 宣告的全域性變數依舊是頂層物件的屬性;而letconstclass 宣告的全域性變數不屬於頂層物件的屬性。

var a = 1;
// 如果在 Node 的 REPL 環境,可以寫成 global.a
// 或者採用通用方法,寫成 this.a
window.a // 1

let b = 1;
window.b // undefined
複製程式碼

5. globalThis 物件

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

  • 瀏覽器裡面,頂層物件是window,但 Node 和 Web Worker 沒有window。
  • 瀏覽器和 Web Worker 裡面,self也指向頂層物件,但是 Node 沒有self。
  • Node 裡面,頂層物件是global,但其他環境都不支援。

同一段程式碼為了能夠在各種環境,都能取到頂層物件,現在一般是使用this變數,但是有侷限性。

  • 全域性環境中,this會返回頂層物件。但是,Node 模組和 ES6 模組中,this返回的是當前模組。
  • 函式裡面的this,如果函式不是作為物件的方法執行,而是單純作為函式執行,this會指向頂層物件。但是,嚴格模式下,這時this會返回undefined
  • 不管是嚴格模式,還是普通模式,new Function('return this')(),總是會返回全域性物件。但是,如果瀏覽器用了 CSP(Content Security Policy,內容安全策略),那麼evalnew Function這些方法都可能無法使用。

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

墊片庫global-this模擬了這個提案,可以在所有環境拿到globalThis

相關文章