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 大致相当于块级作用域的 varconst 的工作方式类似于 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。在以下示例中,我创建了辅助变量 _this(A 行),以便可以在 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 到剩余参数

在 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);
    ···
}

剩余参数使代码更易于阅读:您只需查看函数的参数定义即可判断该函数是否具有可变数量的参数。

更多信息:剩余参数”一节。

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 从对象到映射

在 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);
}

映射的另一个好处是您可以使用任意值作为键,而不仅仅是字符串。

更多信息

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 数据