[譯] ES6 核心特性

英文原文:http://exploringjs.com/es6/ch_core-features.html


本章講述 ES6 核心特性。這些核心特性很容易理解,庫作者會比較關心其它的沒講的內容。我會使用對應的 ES5 代碼來解釋講到的每一個特性。

1. 從 varlet/const

ES5 中申明使用 var,這些變量都是函數級作用域的,它們的作用域是包含它們的最內層的函數。var 的行爲偶爾會使人混淆,這裏有一個例子:

var x = 3;
function func(randomize) {
    if (randomize) {
        var x = Math.random(); // (A) 作用域: 整個函數
        return x;
    }
    return x; // 訪問的 x 是在 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 可以理解爲塊級作用域中的 varconstlet 類似,只是用 const 申明的變量其值是不可修改的。

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。在重構的時候必須非常小心。

我的建議是:

  • 首選 const。所有不會改變值的變量都可以使用它。

  • 其它的使用 let,用於值會被改變的變量。

  • 避免使用 var

更多信息: “變量和作用域”章節。

2. 從 IIFE 到塊

ES5 中如果你想限制變量 tmp 的作用範圍僅在某一塊代碼中有效,你不得不使用一個叫 IIFE(Immediately-Invoked Function Expression,立即執行函數表達式) 的模式:

(function () {  // IIFE 開始
    var tmp = ···;
    ···
}());  // IIFE 結束

console.log(tmp); // ReferenceError

ECMAScript 6 中可以簡單地使用塊和 let 申明(或 const 申明):

{  // 塊起始
    let tmp = ···;
    ···
}  // 塊結束

console.log(tmp); // ReferenceError

更多信息: “在 ES6 中避免使用 IIFE”.

3. 從字符串拼接,到模板字面量

ES6 中,JavaScript 終於有了字符串插值和多行文本。

3.1 String 插值

ES5 中你想把在字符串中引用一些值,你需要將那些值和一些零碎的字符串連接起來:

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

ES6 中你可以在模板字面量中使用字符串插值:

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

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. 從函數表達式到箭頭函數

當前 ES5 代碼中,在使用了函數表達式的時候,你必須小心處理 this。我會在下面的示例中創建一個 _this(A 行) 作爲輔助變量,這樣在 B 行纔可能訪問到指向 UiComponent 對象的 this

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 中你也可以使用 class 代替構建函數,這會在稍後詳述。)

對於一些簡短的只需要返回某個表達式值的簡短回調,用箭頭函數特別方便。

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 都可以。

更多信息:  “箭頭函數”.

5. 處理多個返回值

有一些函數或者方便會通過數組或對象返回多個值。在 ES5 中,你需要創建一個臨時變量來訪問那些值。但在 ES6 中你可以使用解構。

5.1 通過數組返回多個值

exec() 以僞數組對象的形式返回匹配到的各組。ES5 中需要一個臨時變量(下面示例中的matchOjb),即使你只關心配到的組:

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');

數組樣板最開始空了一個位置,這是用來跳過第 0 個數組元素的。

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 }

更多信息: “解構”.

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循環”.

7. 默認參數值

在 ES5 中指定參數的默認值需要 這樣:

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

ES6 有更漂亮的語法:

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

ES6 默認參數語法的好處在於,只有 undefined 會被替換成默認值,而在前面的 ES5 代碼中,所有判 false 的值都會被替換成默認值。

更多信息: “默認參數值”.

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 }) {
    ···
}

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 } = {}) {
    ···
}

更多信息: section “模擬命名參數”.

arguments 到剩餘參數

如果你想在 ES5 中讓函數(或方法)接受任意數量的參數,必須使用特殊變量 arguments

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

ES6 中則可以通過 ... 運算符定義一個剩餘參數(在下面示例中是args):

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

如果有一部分固定參數,剩餘參數就更適用了:

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

在 ES5 中處理同樣的事情有點麻煩:

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

剩餘參數使代碼變得簡單易讀:通過觀察函數的參數定義就能知道它有可變數量的參數。

更多信息: “剩餘參數”.

apply() 到擴展運算符 (...)

ES5 中可以用 apply() 把數組作爲參數使用。ES6 使用擴展運算符解決這個問題。

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

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']

更多信息: “擴展運算符 (...)”.

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' ]

更多信息: section “擴展運算符 (...)”.

12. 從對象字符量的函數表達式到方法定義

JavaScript 的方法是值爲函數的屬性。

ES5 對象字面量中,添加方法和添加其它屬性一樣,其屬性值是函數表達式。

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

ES6 引入了 方法定義,專門用於添加方法的語法:

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

更多信息: “方法定義”.

13. 從構造器到類

ES6 引入的類語法比原來的構建函數更爲方便。

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 關鍵字。也請注意類的各個部分之間沒有逗號。

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 + ')';
    }
}

更多信息: “類”.

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 {
}

更多信息: “從內置構建器繼承”.

15. 從對象到 Map

爲了處理字符串向其它類型值映射(一種數據結構),將 對象 當作映射表一直都是 JavaScript 中的臨時解決辦法。最安全的方法是創建一個原型是 null 的對象。然後你還得確保永遠不會有一個鍵是 '__proto__',因爲那個屬性名稱在很多 JavaScript 引擎中有着特殊的意義。

下面的 ES5 代碼含有函數 countWords,它把名爲 dict 的對象作爲映射表:

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,使它的時候不需要對鍵進行轉義。不過它有一個缺點是不太方便使用自增運算。

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

Map 帶來的另一個好處是你可以使用任意類型的值,而不一定是字符串值,來作爲鍵。

更多信息:

  • Section “字典模式:最好用沒有原型的對象來作爲映射表” in “Speaking JavaScript”

  • Chapter “映射表和集合”

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

更多信息: “新的字符串特性”

17. 新的數組方法

ES6 也爲數組提供了一些新的方法。

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

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']

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() 會把所有數組元素替換爲給定的值。

更多信息: Sect. “創建填充了值的數組”

18. 從 CommonJS 模塊到 ES6 模塊

在 ES5 中,基於 AMD 或者 CommonJS 語法的模塊系統已經取代了純手工解決方案,比如 提示模塊模式.

ES6 內置了對模塊的支持,可惜目前還沒有哪個 JavaScript 引擎原生支持這個特性。但像 browserify、webpack 和 jspm 這樣的工具可以讓你使用 ES6 語法來創建模塊,讓你的代碼提前用上新語法。

18.1 多項導出

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

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

18.2 單項導出

18.2.1 CommonJS 的單項導出

Node.js 擴展了 CommonJS 讓你可以通過 module.exports 導出單個值:

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

//------ main1.js ------
var myFunc = require('myFunc');
myFunc();

18.2.2 ES6 的單項導出

ES6 中使用 默認導出 來做同樣的事情(通過 export default 申明):

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

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

更多信息: “模塊”.

19. 接下來幹什麼

現在你已經瞭解了 ES6,接下來你可以瀏覽其它章節繼續探索:每個章節都涵蓋了某個特性,或者通過概述引開的一系列相關特性。最後一章 將所有這些概述的內容集中到了一起。

本文分享自微信公衆號 - 邊城客棧(fancyidea-full)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章