4. ES6 核心功能
目錄
請支持這本書:購買(PDF、EPUB、MOBI)捐款
(廣告,請不要封鎖)

4. ES6 核心功能

本章節說明 ES6 的核心功能。這些功能很容易採用;其餘的功能主要對函式庫作者有興趣。我透過對應的 ES5 程式碼來解釋每個功能。



4.1 varconst/let

在 ES5 中,您透過 var 來宣告變數。此類變數是函式作用域,其作用域是內層包覆的函式。var 的行為偶爾會令人困惑。以下是一個範例

var x = 3;
function func(randomize) {
    if (randomize) {
        var x = Math.random(); // (A) scope: whole function
        return x;
    }
    return x; // accesses the x from line A
}
func(false); // undefined

func() 會傳回 undefined,這可能會令人驚訝。如果您重新撰寫程式碼,使其更接近實際發生的情況,您就能看出原因

var x = 3;
function func(randomize) {
    var x;
    if (randomize) {
        x = Math.random();
        return x;
    }
    return x;
}
func(false); // undefined

在 ES6 中,您還可以透過 letconst 來宣告變數。此類變數是區塊作用域,其作用域是內層包覆的區塊。let 大致上是 var 的區塊作用域版本。const 的作用類似於 let,但建立的變數值無法變更。

letconst 的行為更嚴謹,而且會擲出更多例外狀況(例如,在宣告之前於其作用域內存取其變數時)。區塊作用域有助於讓程式碼片段的效果更為區域化(請參閱下一節的示範)。而且,它比函式作用域更主流,這讓在 JavaScript 和其他程式語言之間的轉換更為容易。

如果您在初始版本中將 var 替換為 let,您會得到不同的行為

let x = 3;
function func(randomize) {
    if (randomize) {
        let x = Math.random();
        return x;
    }
    return x;
}
func(false); // 3

這表示您無法在現有程式碼中盲目地將 var 替換為 letconst;您必須在重構時小心謹慎。

我的建議是

更多資訊:章節「變數和作用域」。

4.2 從 IIFE 到區塊

在 ES5 中,如果您想要將變數 tmp 的作用域限制在區塊中,您必須使用稱為 IIFE(立即呼叫函式運算式)的模式

(function () {  // open IIFE
    var tmp = ···;
    ···
}());  // close IIFE

console.log(tmp); // ReferenceError

在 ECMAScript 6 中,你可以簡單地使用一個區塊和一個 let 宣告(或是一個 const 宣告)

{  // open block
    let tmp = ···;
    ···
}  // close block

console.log(tmp); // ReferenceError

更多資訊:章節「避免在 ES6 中使用 IIFE」。

4.3 從字串串接轉為範本字串

有了 ES6,JavaScript 終於有了字串內插和多行字串的文字。

4.3.1 字串內插

在 ES5 中,你可以透過串接這些值和字串片段來將值放入字串中

function printCoord(x, y) {
    console.log('('+x+', '+y+')');
}

在 ES6 中,你可以透過範本字串使用字串內插

function printCoord(x, y) {
    console.log(`(${x}, ${y})`);
}

4.3.2 多行字串

範本字串也有助於表示多行字串。

例如,這是你在 ES5 中必須執行的動作來表示一個

var HTML5_SKELETON =
    '<!doctype html>\n' +
    '<html>\n' +
    '<head>\n' +
    '    <meta charset="UTF-8">\n' +
    '    <title></title>\n' +
    '</head>\n' +
    '<body>\n' +
    '</body>\n' +
    '</html>\n';

如果你透過反斜線跳脫換行符號,事情看起來會好一點(但你仍然必須明確加入換行符號)

var HTML5_SKELETON = '\
    <!doctype html>\n\
    <html>\n\
    <head>\n\
        <meta charset="UTF-8">\n\
        <title></title>\n\
    </head>\n\
    <body>\n\
    </body>\n\
    </html>';

ES6 範本字串可以跨越多行

const HTML5_SKELETON = `
    <!doctype html>
    <html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
    </body>
    </html>`;

(範例在包含多少空白方面有所不同,但在這種情況下這並不重要。)

更多資訊:章節「範本字串和標記範本」。

4.4 從函式運算式轉為箭頭函式

在目前的 ES5 程式碼中,每當你使用函式運算式時,都必須小心處理 this。在以下範例中,我在 A 行建立了輔助變數 _this,以便可以在 B 行存取 UiComponentthis

function UiComponent() {
    var _this = this; // (A)
    var button = document.getElementById('myButton');
    button.addEventListener('click', function () {
        console.log('CLICK');
        _this.handleClick(); // (B)
    });
}
UiComponent.prototype.handleClick = function () {
    ···
};

在 ES6 中,你可以使用箭頭函式,它不會遮蔽 this(A 行)

function UiComponent() {
    var button = document.getElementById('myButton');
    button.addEventListener('click', () => {
        console.log('CLICK');
        this.handleClick(); // (A)
    });
}

(在 ES6 中,你也可以選擇使用類別,而不是建構函式。這會在稍後探討。)

箭頭函式特別適用於僅傳回運算式結果的簡短回呼。

在 ES5 中,此類回呼相對冗長

var arr = [1, 2, 3];
var squares = arr.map(function (x) { return x * x });

在 ES6 中,箭頭函式簡潔多了

const arr = [1, 2, 3];
const squares = arr.map(x => x * x);

在定義參數時,如果參數只是一個識別碼,你甚至可以省略括號。因此:(x) => x * xx => x * x 都是允許的。

更多資訊:章節「箭頭函式」。

4.5 處理多重回傳值

有些函式或方法會透過陣列或物件回傳多重值。在 ES5 中,如果你想要存取這些值,你總是需要建立中間變數。在 ES6 中,你可以透過解構來避免使用中間變數。

4.5.1 透過陣列的多重回傳值

exec() 會透過類陣列物件回傳擷取到的群組。在 ES5 中,你需要一個中間變數(在以下範例中為 matchObj),即使你只對群組有興趣。

var matchObj =
    /^(\d\d\d\d)-(\d\d)-(\d\d)$/
    .exec('2999-12-31');
var year = matchObj[1];
var month = matchObj[2];
var day = matchObj[3];

在 ES6 中,解構讓這段程式碼更為簡潔

const [, year, month, day] =
    /^(\d\d\d\d)-(\d\d)-(\d\d)$/
    .exec('2999-12-31');

陣列樣式的開頭處的空槽會跳過索引為零的陣列元素。

4.5.2 透過物件的多重回傳值

方法 Object.getOwnPropertyDescriptor() 會回傳一個屬性描述符,一個在屬性中儲存多重值的物件。

在 ES5 中,即使你只對物件的屬性感興趣,你仍然需要一個中間變數(在以下範例中為 propDesc

var obj = { foo: 123 };

var propDesc = Object.getOwnPropertyDescriptor(obj, 'foo');
var writable = propDesc.writable;
var configurable = propDesc.configurable;

console.log(writable, configurable); // true true

在 ES6 中,你可以使用解構

const obj = { foo: 123 };

const {writable, configurable} =
    Object.getOwnPropertyDescriptor(obj, 'foo');

console.log(writable, configurable); // true true

{writable, configurable}

{ writable: writable, configurable: configurable }

更多資訊:章節「解構」。

4.6 forforEach()for-of

在 ES5 之前,你會以如下方式迭代陣列

var arr = ['a', 'b', 'c'];
for (var i=0; i<arr.length; i++) {
    var elem = arr[i];
    console.log(elem);
}

在 ES5 中,你可以選擇使用陣列方法 forEach()

arr.forEach(function (elem) {
    console.log(elem);
});

for 迴圈的優點是你可以中斷它,forEach() 的優點是簡潔。

在 ES6 中,for-of 迴圈結合了兩者的優點

const arr = ['a', 'b', 'c'];
for (const elem of arr) {
    console.log(elem);
}

如果你想要每個陣列元素的索引和值,for-of 也能滿足你的需求,透過新的陣列方法 entries() 和解構

for (const [index, elem] of arr.entries()) {
    console.log(index+'. '+elem);
}

更多資訊:章節「for-of 迴圈」。

4.7 處理參數預設值

在 ES5 中,您可以這樣為參數指定預設值

function foo(x, y) {
    x = x || 0;
    y = y || 0;
    ···
}

ES6 有更棒的語法

function foo(x=0, y=0) {
    ···
}

ES6 的額外好處是,參數預設值僅會由 undefined 觸發,而它會由前一個 ES5 程式碼中的任何假值觸發。

更多資訊:區段「參數預設值」。

4.8 處理命名參數

在 JavaScript 中,命名參數的常見方式是透過物件文字(所謂的選項物件模式

selectEntries({ start: 0, end: -1 });

這種方法的兩個優點是:程式碼變得更能自我描述,而且更容易省略任意參數。

在 ES5 中,您可以如下實作 selectEntries()

function selectEntries(options) {
    var start = options.start || 0;
    var end = options.end || -1;
    var step = options.step || 1;
    ···
}

在 ES6 中,您可以在參數定義中使用解構,而且程式碼變得更簡單

function selectEntries({ start=0, end=-1, step=1 }) {
    ···
}

4.8.1 讓參數變成選配

若要在 ES5 中讓參數 options 變成選配,您會將 A 行新增至程式碼

function selectEntries(options) {
    options = options || {}; // (A)
    var start = options.start || 0;
    var end = options.end || -1;
    var step = options.step || 1;
    ···
}

在 ES6 中,您可以指定 {} 作為參數預設值

function selectEntries({ start=0, end=-1, step=1 } = {}) {
    ···
}

更多資訊:區段「模擬命名參數」。

4.9 arguments 到 rest 參數

在 ES5 中,如果您希望函式(或方法)接受任意數量的引數,您必須使用特殊變數 arguments

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

在 ES6 中,您可以透過 ... 營運子宣告 rest 參數(在下面的範例中為 args

function logAllArguments(...args) {
    for (const arg of args) {
        console.log(arg);
    }
}

如果您只對尾隨參數有興趣,rest 參數會更好用

function format(pattern, ...args) {
    ···
}

在 ES5 中處理這個案例很笨拙

function format(pattern) {
    var args = [].slice.call(arguments, 1);
    ···
}

Rest 參數讓程式碼更易於閱讀:您只要看其參數定義,就能知道函式有變數個參數。

更多資訊:區段「Rest 參數」。

4.10 apply() 到散佈運算子 (...)

在 ES5 中,您透過 apply() 將陣列轉換成參數。ES6 有散佈運算子可供此目的使用。

4.10.1 Math.max()

Math.max() 傳回其引數中數值最大的值。它適用於任意數量的引數,但不適用於陣列。

ES5 – apply()

> Math.max.apply(Math, [-1, 5, 11, 3])
11

ES6 – 展開運算子

> Math.max(...[-1, 5, 11, 3])
11

4.10.2 Array.prototype.push()

Array.prototype.push() 會將其所有參數作為元素附加到其接收者。沒有方法可以破壞性地將一個陣列附加到另一個陣列。

ES5 – apply()

var arr1 = ['a', 'b'];
var arr2 = ['c', 'd'];

arr1.push.apply(arr1, arr2);
    // arr1 is now ['a', 'b', 'c', 'd']

ES6 – 展開運算子

const arr1 = ['a', 'b'];
const arr2 = ['c', 'd'];

arr1.push(...arr2);
    // arr1 is now ['a', 'b', 'c', 'd']

更多資訊:章節「展開運算子 (...)」。

4.11 concat() 到展開運算子 (...)

展開運算子也可以(非破壞性地)將其運算元的內容轉換為陣列元素。這表示它成為陣列方法 concat() 的替代方案。

ES5 – concat()

var arr1 = ['a', 'b'];
var arr2 = ['c'];
var arr3 = ['d', 'e'];

console.log(arr1.concat(arr2, arr3));
    // [ 'a', 'b', 'c', 'd', 'e' ]

ES6 – 展開運算子

const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];

console.log([...arr1, ...arr2, ...arr3]);
    // [ 'a', 'b', 'c', 'd', 'e' ]

更多資訊:章節「展開運算子 (...)」。

4.12 從物件文字中的函式表達式到方法定義

在 JavaScript 中,方法是其值為函式的屬性。

在 ES5 物件文字中,方法的建立方式與其他屬性相同。屬性值透過函式表達式提供。

var obj = {
    foo: function () {
        ···
    },
    bar: function () {
        this.foo();
    }, // trailing comma is legal in ES5
}

ES6 有方法定義,這是建立方法的特殊語法

const obj = {
    foo() {
        ···
    },
    bar() {
        this.foo();
    },
}

更多資訊:章節「方法定義」。

4.13 從建構函式到類別

ES6 類別大多只是建構函式的更方便語法。

4.13.1 基底類別

在 ES5 中,您直接實作建構函式

function Person(name) {
    this.name = name;
}
Person.prototype.describe = function () {
    return 'Person called '+this.name;
};

在 ES6 中,類別提供建構函式稍更方便的語法

class Person {
    constructor(name) {
        this.name = name;
    }
    describe() {
        return 'Person called '+this.name;
    }
}

請注意方法定義的簡潔語法 – 不需要關鍵字 function。另請注意,類別各部分之間沒有逗號。

4.13.2 衍生類別

在 ES5 中,子類別化很複雜,特別是參考超建構函式和超屬性。這是建立 Person 的子建構函式 Employee 的標準方式

function Employee(name, title) {
    Person.call(this, name); // super(name)
    this.title = title;
}
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
Employee.prototype.describe = function () {
    return Person.prototype.describe.call(this) // super.describe()
           + ' (' + this.title + ')';
};

ES6 透過 extends 子句內建對子類別化的支援

class Employee extends Person {
    constructor(name, title) {
        super(name);
        this.title = title;
    }
    describe() {
        return super.describe() + ' (' + this.title + ')';
    }
}

更多資訊:章節「類別」。

4.14 從自訂錯誤建構函式到 Error 的子類別

在 ES5 中,無法對內建的例外建構函式 Error 進行子類別化。以下程式碼顯示了一個解決方法,它為建構函式 MyError 提供了堆疊追蹤等重要功能

function MyError() {
    // Use Error as a function
    var superInstance = Error.apply(null, arguments);
    copyOwnPropertiesFrom(this, superInstance);
}
MyError.prototype = Object.create(Error.prototype);
MyError.prototype.constructor = MyError;

function copyOwnPropertiesFrom(target, source) {
    Object.getOwnPropertyNames(source)
    .forEach(function(propKey) {
        var desc = Object.getOwnPropertyDescriptor(source, propKey);
        Object.defineProperty(target, propKey, desc);
    });
    return target;
};

在 ES6 中,所有內建建構函式都可以進行子類別化,這就是以下程式碼可以達成 ES5 程式碼只能模擬的效果的原因

class MyError extends Error {
}

更多資訊:章節「內建建構函式的子類別化」。

4.15 從物件到 Map

在 JavaScript 中,一直以來使用語言建構 物件作為從字串到任意值的對應(資料結構)都是一種權宜之計。最安全的方法是建立原型為 null 的物件。然後你仍然必須確保沒有任何鍵是字串 '__proto__',因為此屬性鍵會在許多 JavaScript 引擎中觸發特殊功能。

以下 ES5 程式碼包含使用物件 dict 作為對應的函式 countWords

var dict = Object.create(null);
function countWords(word) {
    var escapedWord = escapeKey(word);
    if (escapedWord in dict) {
        dict[escapedWord]++;
    } else {
        dict[escapedWord] = 1;
    }
}
function escapeKey(key) {
    if (key.indexOf('__proto__') === 0) {
        return key+'%';
    } else {
        return key;
    }
}

在 ES6 中,你可以使用內建資料結構 Map,而且不必跳脫鍵。缺點是,在 Map 中遞增值較不方便。

const map = new Map();
function countWords(word) {
    const count = map.get(word) || 0;
    map.set(word, count + 1);
}

Map 的另一個好處是你可以使用任意值作為鍵,而不仅仅是字串。

更多資訊

4.16 新的字串方法

ECMAScript 6 標準程式庫提供多種新的字串方法。

indexOfstartsWith

if (str.indexOf('x') === 0) {} // ES5
if (str.startsWith('x')) {} // ES6

indexOfendsWith

function endsWith(str, suffix) { // ES5
  var index = str.indexOf(suffix);
  return index >= 0
    && index === str.length-suffix.length;
}
str.endsWith(suffix); // ES6

indexOfincludes

if (str.indexOf('x') >= 0) {} // ES5
if (str.includes('x')) {} // ES6

joinrepeat(ES5 重複字串的方式比較像是權宜之計)

new Array(3+1).join('#') // ES5
'#'.repeat(3) // ES6

更多資訊:章節「新的字串功能

4.17 新的陣列方法

ES6 中也有多種新的陣列方法。

4.17.1 Array.prototype.indexOfArray.prototype.findIndex

後者可用於尋找 NaN,而前者無法偵測

const arr = ['a', NaN];

arr.indexOf(NaN); // -1
arr.findIndex(x => Number.isNaN(x)); // 1

順帶一提,新的 Number.isNaN() 提供一種安全的偵測 NaN 的方法(因為它不會將非數字強制轉換為數字)

> isNaN('abc')
true
> Number.isNaN('abc')
false

4.17.2 Array.prototype.slice()Array.from() 或展開運算子

在 ES5 中,Array.prototype.slice() 用於將類陣列物件轉換為陣列。在 ES6 中,您有 Array.from()

var arr1 = Array.prototype.slice.call(arguments); // ES5
const arr2 = Array.from(arguments); // ES6

如果一個值是可迭代的(現在所有類陣列 DOM 資料結構都是),您也可以使用展開運算子(...)將其轉換為陣列

const arr1 = [...'abc'];
    // ['a', 'b', 'c']
const arr2 = [...new Set().add('a').add('b')];
    // ['a', 'b']

4.17.3 apply()Array.prototype.fill()

在 ES5 中,您可以使用 apply() 作為一種技巧,建立一個任意長度的陣列,並用 undefined 填滿

// Same as Array(undefined, undefined)
var arr1 = Array.apply(null, new Array(2));
    // [undefined, undefined]

在 ES6 中,fill() 是更簡單的替代方案

const arr2 = new Array(2).fill(undefined);
    // [undefined, undefined]

如果您想建立一個填滿任意值的陣列,fill() 甚至更方便

// ES5
var arr3 = Array.apply(null, new Array(2))
    .map(function (x) { return 'x' });
    // ['x', 'x']

// ES6
const arr4 = new Array(2).fill('x');
    // ['x', 'x']

fill() 會用給定的值取代所有陣列元素。孔洞會被視為元素。

更多資訊:章節「建立填滿值的陣列

4.18 從 CommonJS 模組到 ES6 模組

即使在 ES5 中,基於 AMD 語法或 CommonJS 語法的模組系統大多已取代手寫的解決方案,例如 揭露模組模式

ES6 已內建支援模組。但遺憾的是,目前還沒有 JavaScript 引擎原生支援它們。但諸如 browserify、webpack 或 jspm 等工具讓您可以使用 ES6 語法建立模組,讓您編寫的程式碼能因應未來。

4.18.1 多重匯出

4.18.1.1 CommonJS 中的多重匯出

在 CommonJS 中,您可以如下匯出多個實體

//------ lib.js ------
var sqrt = Math.sqrt;
function square(x) {
    return x * x;
}
function diag(x, y) {
    return sqrt(square(x) + square(y));
}
module.exports = {
    sqrt: sqrt,
    square: square,
    diag: diag,
};

//------ main1.js ------
var square = require('lib').square;
var diag = require('lib').diag;

console.log(square(11)); // 121
console.log(diag(4, 3)); // 5

或者,您可以將整個模組匯入為一個物件,並透過它存取 squarediag

//------ main2.js ------
var lib = require('lib');
console.log(lib.square(11)); // 121
console.log(lib.diag(4, 3)); // 5
4.18.1.2 ES6 中的多重匯出

在 ES6 中,多重匯出稱為命名匯出,並像這樣處理

//------ lib.js ------
export const sqrt = Math.sqrt;
export function square(x) {
    return x * x;
}
export function diag(x, y) {
    return sqrt(square(x) + square(y));
}

//------ main1.js ------
import { square, diag } from 'lib';
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5

將模組匯入為物件的語法如下所示(第 A 行)

//------ main2.js ------
import * as lib from 'lib'; // (A)
console.log(lib.square(11)); // 121
console.log(lib.diag(4, 3)); // 5

4.18.2 單一匯出

4.18.2.1 CommonJS 中的單一匯出

Node.js 延伸 CommonJS,並讓您可以透過 module.exports 從模組中匯出單一值

//------ myFunc.js ------
module.exports = function () { ··· };

//------ main1.js ------
var myFunc = require('myFunc');
myFunc();
4.18.2.2 ES6 中的單一匯出

在 ES6 中,相同的事情是透過所謂的預設匯出(透過 export default 宣告)來完成

//------ myFunc.js ------
export default function () { ··· } // no semicolon!

//------ main1.js ------
import myFunc from 'myFunc';
myFunc();

更多資訊:章節「模組」。

4.19 下一步

現在您已經初步體驗 ES6,您可以瀏覽各章節繼續探索:每個章節涵蓋一個功能或一組相關功能,並從概觀開始。 最後一章在單一位置收集所有這些概觀區段。

下一步:II 資料