第 2 章 ECMAScript和TypeScript概述

第 2 章 ECMAScript和TypeScript概述

JavaScript语言每年都在进化。从2015年起,每年都有一个新版本发布,我们称其为ECMAScript。JavaScript是一门非常强大的语言,也用于企业级开发。在这类开发中(以及其他类型的应用中),类型变量是一个非常有用的功能。作为JavaScript的一个超集,TypeScript给我们提供了这样的功能。

本章,你将学习到自2015年起加入JavaScript的一些功能以及在项目中使用有类型版本的JavaScript的好处。本章内容涵盖如下几个方面:

  • 介绍ECMAScript
  • 浏览器与服务器中的JavaScript
  • 介绍TypeScript

2.1 ECMAScript还是JavaScript

当我们使用JavaScript时,常会在图书、博客和视频课程中看到ECMAScript这个术语。那么ECMAScript和JavaScript有什么关系,又有什么区别呢?

ECMA是一个将信息标准化的组织。长话短说:很久以前,JavaScript被提交到ECMA进行标准化,由此诞生了一个新的语言标准,也就是我们所知道的ECMAScript。JavaScript是该标准(最流行)的一个实现。

2.1.1 ES6、ES2015、ES7、ES2016、ES8、ES2017和ES.Next

我们知道,JavaScript是一种主要在浏览器中运行的语言(也可以运行于NodeJS服务端、桌面端和移动端设备中),每个浏览器都可以实现自己版本的JavaScript功能(稍后你将在本书中学习)。这个具体的实现是基于ECMAScript的,因此浏览器提供的功能大都相同(我们的JavaScript代码可以在所有浏览器中运行)。然而,不同的浏览器之间,每个功能的行为也会存在细微的差别。

目前为止,本章给出的所有代码都是基于2009年12月发布的ECMAScript 5(即ES5,其中的ES是ECMAScript的简称)。ECMAScript 2015(ES2015)在2015年6月标准化,距离它的上个版本过去了近6年。在ES2015发布前,ES6的名字已经变得流行了。

负责起草ECMAScript规范的委员会决定把定义新标准的模式改为每年更新一次,新的特性一旦通过就加入标准。因此,ECMAScript第六版更名为ECMAScript 2015(ES6)。

2016年6月,ECMAScript第七版被标准化,称为ECMAScript 2016ES2016ES7)。

2017年6月,ECMAScript第八版被标准化。我们称它为ECMAScript 2017ES2017ES8)。在写作本书时,这是最新的ES版本。

你可能在某些地方见过ES.Next。这种说法用来指代下一个版本的ECMAScript。

本节,我们会学习ES2015及之后版本中引入的一些新功能,它们对开发数据结构和算法都会有帮助。

兼容性列表

一定要明白,即便ES2015到ES2017已经发布,也不是所有的浏览器都支持新特性。为了获得更好的体验,最好使用你选择的浏览器的最新版本。

通过以下链接,你可以检查在各个浏览器中哪些特性可用。

  • ES2015(ES6):
  • ES2016+

在ES5之后,最大的ES发布版本是ES2015。根据上面链接中的兼容性表格来看,它的大部分功能在现代浏览器中都可以使用。即使有些ES2016+的特性尚未支持,我们也可以现在就开始用新语法和新功能。

对于开发团队交付的ES功能实现,Firefox默认开启支持。

在谷歌Chrome浏览器中,你可以访问chrome://flags/#enable-javascript-harmony,开启Experimental JavaScript标志,启用新功能,如下图所示。

在微软Edge浏览器中,你可以导航至about:flags页面并选择Enable experimental JavaScript features标志(和Chrome中的方法相似)。

 即使开启了Chrome或Edge浏览器的实验性JavaScript功能标志,ES2016+的部分特性也可能不受支持,Firefox同样如此。要了解各个浏览器所支持的特性,请查看兼容性列表。

2.1.2 使用Babel.js

Babel是一个JavaScript转译器,也称为源代码编译器。它将使用了ECMAScript语言特性的JavaScript代码转换成只使用广泛支持的ES5特性的等价代码。

使用Babel.js的方式多种多样。一种是根据设置文档()进行安装。另一种方式是直接在浏览器中试用(),如下图所示。

针对后续章节中出现的所有例子,我们都将提供一个在Babel中运行和测试的链接。

2.2 ECMAScript 2015+的功能

本节,我们将演示如何使用ES2015的一些新功能。这既对日常的JavaScript编码有用,也可以简化本书后面章节中的例子。

我们将介绍以下功能。

  • 使用letconst声明变量
  • 模板字面量
  • 解构
  • 展开运算符
  • 箭头函数:=>
  • 模块

2.2.1 用let替代var声明变量

到ES5为止,我们可以在代码中任意位置声明变量,甚至重写已声明的变量,代码如下。

var framework = 'Angular';
var framework = 'React';
console.log(framework);

上面代码的输出是React,该值被赋给最后声明的framework变量。这段代码中有两个同名的变量,这是非常危险的,可能会导致错误的输出。

C、Java、C#等其他语言不允许这种行为。ES2015引入了一个let关键字,它是新的var,这意味着我们可以直接把var关键字都替换成let。以下代码就是一个例子。

let language = 'JavaScript!'; // {1}
let language = 'Ruby!'; // {2} - 抛出错误
console.log(language);

{2}会抛出错误,因为在同一作用域中已经声明过language变量(行{1})。后面会讨论let和变量作用域。

 你可以访问,测试和执行上面的代码。

ES2015还引入了const关键字。它的行为和let关键字一样,唯一的区别在于,用const定义的变量是只读的,也就是常量。

举例来说,考虑如下代码:

const PI = 3.141593;
PI = 3.0; // 抛出错误
console.log(PI);

当我们试图把一个新的值赋给PI,甚至只是用var PIlet PI重新声明时,代码就会抛出错误,告诉我们PI是只读的。

下面来看const的另一个例子。我们将使用const来声明一个对象。

constjsFramework = {
  name: 'Angular'
};

尝试改变jsFramework变量的name属性。

jsFramework.name = 'React';

如果试着执行这段代码,它会正常工作。但是const声明的变量是只读的!为什么这里可以执行上面的代码呢?对于非对象类型的变量,比如数、布尔值甚至字符串,我们不可以改变变量的值。当遇到对象时,只读的const允许我们修改或重新赋值对象的属性,但变量本身的引用(内存中的引用地址)不可以修改,也就是不能对这个变量重新赋值。

如果像下面这样尝试给jsFramework变量重新赋值,编译器会抛出异常("jsFramework" is read-only)。

// 错误,不能重新指定对象的引用
jsFramework = {
  name: 'Vue'
};

 你可以访问执行上面的例子。

letconst的变量作用域

我们通过下面这个例子()来理解letconst关键字声明的变量如何工作。

let movie = 'Lord of the Rings'; // {1}
//var movie = 'Batman v Superman'; // 抛出错误,movie变量已声明

function starWarsFan() {
  const movie = 'Star Wars'; // {2}
  return movie;
}

function marvelFan() {
  movie = 'The Avengers'; // {3}
  return movie;
}

function blizzardFan() {
  const isFan = true;
  let phrase = 'Warcraft'; // {4}
  console.log('Before if: ' + phrase);
  if (isFan) {
    let phrase = 'initial text'; // {5}
    phrase = 'For the Horde!'; // {6}
    console.log('Inside if: ' + phrase);
  }
  phrase = 'For the Alliance!'; // {7}
  console.log('After if: ' + phrase);
}

console.log(movie); // {8}
console.log(starWarsFan()); // {9}
console.log(marvelFan()); // {10}
console.log(movie); // {11}
blizzardFan(); // {12}

以上代码的输出如下。

Lord of the Rings
Star Wars
The Avengers
The Avengers
Before if: Warcraft
Inside if: For the Horde!
After if: For the Alliance!

现在,我们来讨论得到这些输出的原因。

  • 我们在行{1}声明了一个movie变量并赋值为Lord of the Rings,然后在行{8}输出它的值。你在本章已经学过,这个变量拥有全局作用域。
  • 我们在行{9}执行了starWarsFan函数。在这个函数里,我们也声明了一个movie变量(行{2})。这个函数的输出是Star Wars,因为行{2}的变量拥有局部作用域,也就是说它只在函数内部可见。
  • 我们在行{10}执行了marvelFan函数。在这个函数里,我们改变了movie变量的值(行{3})。这个变量是行{1}声明的全局变量。因此,行{11}的全局变量输出和行{10}的输出相同,都是The Avengers
  • 最后,我们在行{12}执行了blizzardFan函数。在这个函数里,我们声明了一个拥有函数内作用域的phrase变量(行{4})。然后,又声明了一个phrase变量(行{5}),但这个变量的作用域只在if语句内。
  • 我们在行{6}改变了phrase的值。由于还在if语句内,值发生改变的是在行{5}声明的变量。
  • 然后,我们在行{7}再次改变了phrase的值,但由于不是在if语句内,行{4}声明的变量的值改变了。

作用域的行为与在Java或C等其他编程语言中一样。然而,这是ES2015(ES6)才引入到JavaScript的。

 注意,在本节展示的代码中,我们混用了letconst。应该使用哪一个呢?有些开发者(和一些检查工具)倾向于在变量的引用不会改变时使用const。但是,这是个人喜好问题,没有哪个是错的!

2.2.2 模板字面量

模板字面量真的很棒,因为我们创建字符串的时候不必再拼接值。

举例来说,考虑如下ES5代码。

const book = {
  name: '学习JavaScript数据结构与算法'
};
console.log('你正在阅读' + book.name + '.,\n这是新的一行\n 这也是');

我们可以用如下代码改进上面这个console.log输出的语法。

console.log(`你正在阅读${book.name}。
  这是新的一行
  这也是。`);

模板字面量用一对`包裹。要插入变量的值,只要把变量放在${}里就可以了,就像例子中的book.name

模板字面量也可以用于多行的字符串,再也不需要用\n了。只要按下键盘上的Enter就可以换一行,就像上面例子里的这是新的一行

这个功能对简化我们例子的输出非常有用!

 你可以访问执行上面的例子。

2.2.3 箭头函数

ES2015的箭头函数极大地简化了函数的语法。考虑如下例子。

var circleAreaES5 = function circleArea(r) {
  var PI = 3.14;
  var area = PI * r * r;
  return area;
};
console.log(circleAreaES5(2));

上面这段代码的语法可以简化为如下代码。

const circleArea = r => { // {1}
  const PI = 3.14;
  const area = PI * r * r;
  return area;
};
console.log(circleArea(2));

这个例子最大的区别在于行{1},我们可以省去function关键字,只用=>

如果函数只有一条语句,还可以变得更简单,连return关键字都可以省去。看看下面的代码。

const circleArea2 = r => 3.14 * r * r;
console.log(circleArea2(2));

如果函数不接收任何参数,我们就使用一对空的圆括号,这在ES5中经常出现。

const hello = () => console.log('hello!');
hello();

 你可以访问执行上面的例子。

2.2.4 函数的参数默认值

在ES2015里,函数的参数还可以定义默认值。下面是一个例子。

function sum(x = 1, y = 2, z = 3) {
  return x + y + z;
}
console.log(sum(4, 2)); // 输出9

由于我们没有传入参数z,它的值默认为3。因此,4 + 2 + 3 == 9

在ES2015之前,上面的函数只能写成下面这样。

function sum(x, y, z) {
  if (x === undefined) x = 1;
  if (y === undefined) y = 2;
  if (z === undefined) z = 3;
  return x + y + z;
}

也可以写成下面这样。

function sum() {
  var x = arguments.length > 0 && arguments[0] !== undefined ? arguments[0]
: 1;
  var y = arguments.length > 1 && arguments[1] !== undefined ? arguments[1]
: 2;
  var z = arguments.length > 2 && arguments[2] !== undefined ? arguments[2]
: 3;
  return x + y + z;
}

 JavaScript函数中有一个内置的对象,叫作arguments对象。它是一个数组,包含函数被调用时传入的参数。即使不知道参数的名称,我们也可以动态获取并使用这些参数。

有了ES2015的参数默认值,代码可以少写好几行。

 你可以访问执行上面的例子。

2.2.5 声明展开和剩余参数

在ES5中,我们可以用apply()函数把数组转化为参数。为此,ES2015有了展开运算符(...)。举例来说,考虑我们上一节声明的sum函数。可以执行如下代码来传入参数xyz

let params = [3, 4, 5];
console.log(sum(...params));

以上代码和下面的ES5代码的效果是相同的。

console.log(sum.apply(undefined, params));

在函数中,展开运算符(...)也可以代替arguments,当作剩余参数使用。考虑如下这个例子。

function restParamaterFunction (x, y, ...a) {
  return (x + y) * a.length;
}
console.log(restParamaterFunction(1, 2, "hello", true, 7));

以上代码和下面代码的效果是相同的(同样输出9)。

function restParamaterFunction (x, y) {
  var a = Array.prototype.slice.call(arguments, 2);
  return (x + y) * a.length;
}
console.log(restParamaterFunction(1, 2, 'hello', true, 7));

 你可以访问执行展开运算符的例子,访问执行剩余参数的例子。

2.2.6 增强的对象属性

ES2015引入了数组解构的概念,可以用来一次初始化多个变量。考虑如下例子。

let [x, y] = ['a', 'b'];

以上代码和下面代码的效果是相同的。

let x = 'a';
let y = 'b';

数组解构也可以用来进行值的互换,而不需要创建临时变量,如下所示。

[x, y] = [y, x];

以上代码和下面代码的效果是相同的。

var temp = x;
x = y;
y = temp;

这对你学习排序算法会很有用,因为互换值的情况很常见。

还有一个称为属性简写的功能,它是对象解构的另一种方式。考虑如下例子。

let [x, y] = ['a', 'b'];
let obj = { x, y };
console.log(obj); // { x: "a", y: "b" }

以上代码和下面代码的效果是相同的。

var x = 'a';
var y = 'b';
var obj2 = { x: x, y: y };
console.log(obj2); // { x: "a", y: "b" }

本节要讨论的最后一个功能是简写方法名(shorthand method name)。这使得开发者可以在对象中像属性一样声明函数。下面是一个例子。

const hello = {
  name: 'abcdef',
  printHello() {
    console.log('Hello');
  }
};
console.log(hello.printHello());

以上代码也可以写成下面这样。

var hello = {
  name: 'abcdef',
  printHello: function printHello() {
    console.log('Hello');
  }
};
console.log(hello.printHello());

 你可以访问以下URL执行上面三个例子。

  • 数组解构
  • 变量互换
  • 属性简写

2.2.7 使用类进行面向对象编程

ES2015还引入了一种更简洁的声明类的方式。你已经在1.6节学习了像下面这样声明一个Book类的方式。

function Book(title, pages, isbn) { // {1}
  this.title = title;
  this.pages = pages;
  this.isbn = isbn;
}
Book.prototype.printTitle = function() {
  console.log(this.title);
};

我们可以用ES2015把语法简化,如下所示。

class Book { // {2}
  constructor(title, pages, isbn) {
    this.title = title;
    this.pages = pages;
    this.isbn = isbn;
  }
  printIsbn() {
    console.log(this.isbn);
  }
}

只需要使用class关键字,声明一个有constructor函数和诸如printIsbn等其他函数的类。ES2015的类是基于原型语法的语法糖。行{1}声明Book类的代码与行{2}声明的代码具有相同的效果和输出。

let book = new Book('title', 'pag', 'isbn');
console.log(book.title); // 输出图书标题
book.title = 'new title'; // 更新图书标题
console.log(book.title); // 输出图书标题

 你可以访问执行上面的例子。

  1. 继承

    ES2015中,类的继承也有简化的语法。我们看一个例子。

    class ITBook extends Book { // {1}
      constructor (title, pages, isbn, technology) {
        super(title, pages, isbn); // {2}
        this.technology = technology;
      }
    
      printTechnology() {
        console.log(this.technology);
      }
    }
    let jsBook = new ITBook('学习JS算法', '200', '1234567890', 'JavaScript');
    console.log(jsBook.title);
    console.log(jsBook.printTechnology());

    我们可以用extends关键字扩展一个类并继承它的行为(行{1})。在构造函数中,我们也可以通过super关键字引用父类的构造函数(行{2})。

    尽管在JavaScript中声明类的新方式所用的语法与Java、C、C++等其他编程语言很类似,但JavaScript面向对象编程还是基于原型实现的。

     你可以访问执行上面的例子。

  2. 使用属性存取器

    ES2015也可以为类属性创建存取器函数。虽然不像其他面向对象语言(封装概念),类的属性不是私有的,但最好还是遵循一种命名模式。

    下面的例子是一个声明了getset函数的类。

    class Person {
      constructor (name) {
        this._name = name; // {1}
      }
      get name() { // {2}
        return this._name;
      }
      set name(value) { // {3}
        this._name = value;
      }
    }
    
    let lotrChar = new Person('Frodo');
    console.log(lotrChar.name); // {4}
    lotrChar.name = 'Gandalf'; // {5}
    console.log(lotrChar.name);
    lotrChar._name = 'Sam'; // {6}
    console.log(lotrChar.name);

    要声明getset函数,只需要在我们要暴露和使用的函数名前面加上getset关键字(行{2}和行{3})。我们可以用相同的名字声明类属性,或者在属性名前面加下划线(行{1}),让这个属性看起来像是私有的。

    然后,只要像普通的属性一样,引用它们的名字(行{4}和行{5}),就可以执行getset函数了。

    _name并非真正的私有属性,我们仍然可以引用它(行{6})。本书后面的章节还会谈到这一点。

     你可以访问执行上面的例子。

2.2.8 乘方运算符

乘方运算符在进行数学计算时非常有用。作为示例,我们使用公式计算一个圆的面积。

const area = 3.14 * r * r;

也可以使用Math.pow函数来写出具有相同功能的代码。

const area = 3.14 * Math.pow(r, 2);

ES2016中引入了**运算符,用来进行指数运算。我们可以像下面这样使用指数运算符计算一个圆的面积。

const area = 3.14 * (r ** 2);

 你可以访问执行上面的例子。

ES2015+还提供了一些其他功能,包括列表迭代器、类型数组、SetMapWeakSetWeakMap、尾调用、for..ofSymbolArray.prototype.includes、尾逗号、字符串补全、静态对象方法,等等。我们在后续章节会学习到其中的一些功能。

 你可以在查阅JavaScript和ECMAScript的完整功能列表。

2.2.9 模块

Node.js开发者已经很熟悉用require语句(CommonJS模块)进行模块化开发了。同样,还有一个流行的JavaScript模块化标准,叫作异步模块定义(AMD)。RequireJS是AMD最流行的实现。ES2015在JavaScript标准中引入了一种官方的模块功能。让我们来创建并使用模块吧。

要创建的第一个模块包含两个用来计算几何图形面积的函数。在一个文件(17-CalcArea.js)中添加如下代码。

const circleArea = r => 3.14 * (r ** 2);

const squareArea = s => s * s;

export { circleArea, squareArea }; // {1}

这表示我们暴露出了这两个函数,以便其他文件使用(行{1})。只有被导出的成员才对其他模块或文件可见。

在本示例的主文件(17-ES2015-ES6-Modules.js)中,我们会用到在17-CalcArea.js文件中声明的函数。下面的代码片段展示了如何使用这两个函数。

import { circleArea, squareArea } from './17-CalcArea'; // {2}

console.log(circleArea(2));
console.log(squareArea(2));

首先,需要在文件中导入要使用的函数(行{2}),之后就可以调用它们了。

如果需要使用circleArea函数,也可以只导入这个函数。

import { circleArea } from './17-CalcArea';

基本上,模块就是在单个文件中声明的JavaScript代码。我们可以用JavaScript代码直接从其他文件中导入函数、变量和类(不需要像几年前JavsScript还不够流行的时候那样,事先在HTML中按顺序引入若干文件)。模块功能让我们在创建代码库或开发大型项目时能够更好地组织代码。

我们可以像下面这样,在导入成员后对其重命名。

import { circleArea as circle } from './17-CalcArea';

也可以在导出函数时就对其重命名。

export { circleArea as circle, squareArea as square };

这种情况下,在导入被导出的成员时,需要使用导出时重新命名的名字,而不是原来内部使用的名字。

import { circle, square } from './17-CalcArea';

同样,我们也可以使用其他方式在另一个模块中导入函数。

import * as area from './17-CalcArea';

console.log(area.circle(2));
console.log(area.square(2));

这种情况下,可以把整个模块当作一个变量来导入,然后像使用类的属性和方法那样调用被导出的成员。

还可以在需要被导出的函数或变量前添加export关键字。这样就不需要在文件末尾写导出声明了。

export const circleArea = r => 3.14 * (r ** 2);
export const squareArea = s => s * s;

假设模块中只有一个成员,而且需要将其导出。可以像下面这样使用export default关键字。

export default class Book {
  constructor(title) {
    this.title = title;
  }
  printTitle() {
    console.log(this.title);
  }
}

可以使用如下代码在另一个模块中导入上面的类。

import Book from './17-Book';

const myBook = new Book('some title');
myBook.printTitle();

注意,在这种情况下,我们不需要将类名包含在花括号({})中。只在模块有多个成员被导出时使用花括号。

在后面的章节中,我们需要使用模块来创建数据结构和算法库。

 要了解更多有关ES2015模块的信息,请查阅。你也可以下载本书的源代码包来查看本示例的完整代码。

  1. 在浏览器中使用Node.js运行ES2015模块

    我们尝试像下面这样直接执行node指令来运行17-ES2015-ES6-Modules.js文件。

    cd path-source-bundle/examples/chapter01
    node 17-ES2015-ES6-Modules

    我们会得到错误信息SyntaxError: Unexpected token import。这是因为在写作本书的时候,Node.js还不支持原生的ES2015模块。Node.js使用的是CommonJS模块的require语法。这表示我们需要转译ES2015代码,使得Node可以理解。有不同的工具可以完成这项任务。简单起见,我们将使用Babel命令行工具。

     完整的Babel安装和使用细节可以在和查阅。

    最好的方式是创建一个本地项目,并在其中进行Babel的配置。遗憾的是,这些细节不在本书的讨论范围之内(这应该是Babel相关图书的主题)。为了使本例保持简单,我们将用npm安装在全局使用的Babel命令行工具。

    npm install -g babel-cli

    如果你使用的是Linux或Mac OS,可能需要在命令前加上sudo指令来获取管理员权限(sudo npm install -g babel-cli)。

    在chapter01目录中,我们需要用Babel将之前创建的3个JavaScript模块文件转译成CommonJS代码,使得Node.js可以执行它们。我们会用以下命令将转译后的代码放在chapter01/lib目录中。

    babel 17-CalcArea.js --out-dir lib
    babel 17-Book.js --out-dir lib
    babel 17-ES2015-ES6-Modules.js --out-dir lib

    接下来,创建一个叫作17-ES2015-ES6-Modules-node.js的JavaScript文件,这样就可以在其中使用area函数和Book类了。

    const area = require('./lib/17-CalcArea');
    const Book = require('./lib/17-Book');
    
    console.log(area.circle(2));
    console.log(area.square(2));
    
    const myBook = new Book('some title');
    myBook.printTitle();

    代码基本是一样的,区别在于Node.js(目前)不支持import语法,需要使用require关键字。

    可以使用下面的命令来执行代码。

    node 17-ES2015-ES6-Modules-node

    在下图中能看到使用的命令和输出结果,这样就可以确认代码能够用Node.js运行。

    • 在Node.js中使用原生的ES2015导入功能

      如果能在Node.js中使用原生的ES2015导入功能,而不用转译的话就更好了。从Node 8.5版本开始,我们可以将ES2015导入作为实验功能来开启。

      要演示这个示例,我们将在chapter01中创建一个新的目录,叫作17-ES2015-Modules-node。将17-CalcArea.js、17-Book.js和17-ES2015- ES6-Modules.js文件复制到此目录中,然后将文件的扩展名由js修改为mjs(.mjs是本例成功运行的必要条件)。在17-ES2015-ES6-Modules.mjs文件中更新导入语句,像下面这样添加.mjs扩展名。

      import * as area from './17-CalcArea.mjs';
      import Book from './17-Book.mjs';

      我们将在node命令后添加--experimental-modules来执行代码,如下所示。

      cd 17-ES2015-Modules-node
      node --experimental-modules 17-ES2015-ES6-Modules.mjs

      在下图中,我们可以看到命令和输入结果。

      在写作本书的时候,可支持ES2015导入功能的Node.js版本是Node 10 LTS。

       更多有关Node.js支持原生ES2015导入功能的信息可以在查阅。

  2. 在浏览器中运行ES2015模块

    要在浏览器中运行ES2015的代码,有几种不同的方式。第一种是生成传统的代码包(即转译成ES5代码的JavaScript文件)。我们可以使用流行的代码打包工具,如BrowserifyWebpack。通过这种方法,我们会创建可直接发布的文件(包),并且可以在HTML文件中像引入其他JavaScript代码一样引入它。

    <script src="./lib/17-ES2015-ES6-Modules-bundle.js"></script>

    浏览器对ES2015模块的支持最终于2017年初实现了。在写作本书的时候,它还是实验性的功能,并没有得到所有现代浏览器的支持。目前对该功能的支持情况(以及在实验性模式下开启它的方法)可以在查阅,如下图所示。

    要在浏览器中使用import关键字,首先需要在代码的import语句后加上.js文件扩展名,如下所示。

    import * as area from './17-CalcArea.js';
    import Book from './17-Book.js';

    其次,只需要在script标签中增加type="module"就可以导入我们创建的模块了。

    <script type="module" src="17-ES2015-ES6-Modules.js"></script>

    如果执行代码并打开Developer Tools | Network标签页,就会看到我们创建的所有文件都被加载了。

    如果要保证不支持该功能的浏览器向后兼容,可以使用nomodule

    <script nomodule src="./lib/17-ES2015-ES6-Modules-bundle.js"></script>

    在大多数现代浏览器都支持该功能之前,我们仍然需要使用打包工具将代码转译至ES2015+。

     要了解更多有关在浏览器中运行ES2015模块的信息,请阅读和。

  3. ES2015+的向后兼容性

    需要把现有的JavaScript代码更新到ES2015吗?答案是:只要你愿意就行!ES2015+是JavaScript语言的超集,所有符合ES5规范的特性都可以继续使用。不过,你可以开始使用ES2015+的新语法,让代码变得更加简单易读。

    在本书接下来的章节中,我们会尽可能地使用ES2015+。假设我们想根据本书内容创建一个数据结构和算法库。这通常需要支持想在浏览器(ES5)和Node.js环境下使用该代码库的开发者。目前可以采取的方法是,将我们的代码转译成通用模块定义(UMD)。要了解更多有关UMD的信息,请访问。我们会在第4章学习使用Babel将ES2015代码转译成UMD的更多方法。

    对于所有使用模块的示例,源代码包除了ES2015+语法之外还提供了转译后的版本,因此你可以在任意浏览器中运行源代码。

2.3 介绍TypeScript

TypeScript是一个开源的、渐进式包含类型的JavaScript超集,由微软创建并维护。创建它的目的是让开发者增强JavaScript的能力并使应用的规模扩展变得更容易。它的主要功能之一是为JavaScript变量提供类型支持。在JavaScript中提供类型支持可以实现静态检查,从而更容易地重构代码和寻找bug。最后,TypeScript会被编译为简单的JavaScript代码。

考虑到本书的范围,有了TypeScript,就可以使用一些JavaScript中没有提供的面向对象的概念了,例如接口和私有属性(这在开发数据结构和排序算法时非常有用)。当然,我们也可以利用在一些数据结构中非常重要的类型功能。

所有这些功能在编译时都是可用的。只要我们在写代码,就将其编译成普通的JavaScript代码(ES5、ES2015+和CommonJS等)。

要开始使用TypeScript,我们需要用npm来安装它。

npm install -g typescript

接下来,需要创建一个以.ts为扩展名的文件,比如hello-world.ts。

let myName = 'Packt';
myName = 10;

以上是简单的ES2015代码。现在,我们用tsc命令来编译它。

tsc hello-world

在终端输出中,我们会看到下面的警告。

hello-world.ts(2,1): error TS2322: Type '10' is not assignable to type
'string'.

这表示类型10不可赋值给字符串类型。但是如果检查创建文件的目录,我们会发现一个包含如下内容的hello-world.js文件。

var myName = 'Packt';
myName = 10;

上面生成的是ES5代码。即使在终端输出了错误信息(实际上是警告,而不是错误),TypeScript编译器还是会生成ES5代码。这表明尽管TypeScript在编译时进行了类型和错误检测,但并不会阻止编译器生成JavaScript代码。这意味着开发者在写代码时可以利用这些验证结果写出具有较少错误和bug的JavaScript代码。

2.3.1 类型推断

在使用TypeScript的时候,我们会经常看到下面这样的代码。

let age: number = 20;
let existsFlag: boolean = true;
let language: string = 'JavaScript';

TypeScript允许我们给变量设置一个类型,不过上面的写法太啰唆了。TypeScript有一个类型推断机制,也就是说TypeScript会根据为变量赋的值自动给该变量设置一个类型。我们用更简洁的语法改写上面的代码。

let age = 20; // 数
let existsFlag = true; // 布尔值
let language = 'JavaScript'; // 字符串

在上面的代码中,TypeScript仍然知道age是一个数、existsFlag是一个布尔值,以及language是一个字符串。因此不需要显式地给这些变量设置类型。

那么,什么时候需要给变量设置类型呢?如果声明了一个变量但没有设置其初始值,推荐为其设置一个类型,如下所示。

let favoriteLanguage: string;
let langs = ['JavaScript', 'Ruby', 'Python'];
favoriteLanguage = langs[0];

如果没有为变量设置类型,它的类型会被自动设置为any,意思是可以接收任何值,就像在普通JavaScript中一样。

2.3.2 接口

在TypeScript中,有两种接口的概念。第一种就像给变量设置一个类型,如下所示。

interface Person {
  name: string;
  age: number;
}

function printName(person: Person) {
  console.log(person.name);
}

第一种TypeScript接口的概念是把接口看作一个实际的东西。它是对一个对象必须包含的属性和方法的描述。

这使得VSCode这样的编辑器能通过IntelliSense实现自动补全,如下图所示。

现在,试着使用printName函数。

const john = { name: 'John', age: 21 };
const mary = { name: 'Mary', age: 21, phone: '123-45678' };
printName(john);
printName(mary);

上面的代码没有任何编译错误。像printName函数希望的那样,变量john有一个nameage。变量mary除了nameage之外,还有一个phone的信息。

为什么这样的代码可以工作呢?TypeScript有一个名为鸭子类型的概念:如果它看起来像鸭子,像鸭子一样游泳,像鸭子一样叫,那么它一定是一只鸭子!在本例中,变量mary的行为和Person接口定义的一样,那么它就是一个Person。这是TypeScript的一个强大功能。

再次运行tsc命令之后,我们会在hello-world.js文件中得到下面的结果。

function printName(person) {
    console.log(person.name);
}
var john = { name: 'John', age: 21 };
var mary = { name: 'Mary', age: 21, phone: '123-45678' };

上面的代码只是普通的JavaScript。代码补全以及类型和错误检查只在编译时是可用的。

第二种TypeScript接口的概念和面向对象编程相关,与其他面向对象语言(如Java、C#和Ruby等)中的概念是一样的。接口就是一份合约。在这份合约里,我们可以定义实现这份合约的类或接口的行为。试想ECMAScript标准,ECMAScript就是JavaScript语言的一个接口。它告诉JavaScript语言需要有怎样的功能,但不同的浏览器可以有不同的实现方式。

考虑下面的代码:

interface Comparable {
  compareTo(b): number;
}

class MyObject implements Comparable {
  age: number;
  compareTo(b): number {
    if (this.age === b.age) {
      return 0;
    }
    return this.age > b.age ? 1 : -1;
  }
}

Comparable接口告诉MyObject类,它需要实现一个叫作compareTo的方法,并且该方法接收一个参数。在该方法内部,我们可以实现需要的逻辑。在本例中,我们比较了两个数,但也可以用不同的逻辑来比较两个字符串,甚至是包含不同属性的更复杂的对象。该接口的行为在JavaScript中并不存在,但它在进行一些工作(如开发排序算法)时非常有用。

泛型

另一个对数据结构和算法有用的强大TypeScript特性是泛型这一概念。我们修改一下Comparable接口,以便定义compareTo方法作为参数接收的对象是什么类型。

interface Comparable<T> {
  compareTo(b: T): number;
}

用尖括号向Comparable接口动态地传入T类型,可以指定compareTo函数的参数类型。

class MyObject implements Comparable<MyObject> {
  age: number;

  compareTo(b: MyObject): number {
    if (this.age === b.age) {
      return 0;
    }
    return this.age > b.age ? 1 : -1;
  }
}

这是个很有用的功能,可以确保我们在比较相同类型的对象。利用这个功能,我们还可以使用编辑器的代码补全。

2.3.3 其他TypeScript功能

以上是对TypeScript的简单介绍。TypeScript文档是学习所有其他功能以及了解本章话题相关细节的好地方,可以在找到。

TypeScript也有一个在线体验功能(和Babel类似),可以在里面运行一些代码示例,地址是。

 本书的源代码包中有一个额外的资源,那就是我们会在本书中开发完成的JavaScript数据结构和算法库的TypeScript版本!

2.3.4 TypeScript中对JavaScript文件的编译时检查

一些开发者还是更习惯使用普通的JavaScript语言,而不是TypeScript来进行开发。但是在JavaScript中使用一些类型和错误检测功能也是很不错的!

好消息是TypeScript提供了一个特殊的功能,允许我们在编译时对代码进行错误检测和类型检测!要使用它的话,需要在计算机上全局安装TypeScript。使用时,只需要在JavaScript文件的第一行添加一句// @ts-check,如下图所示。

向代码中添加JSDoc(JavaScript文档)之后,类型检测将被启用。如果试着向circle(或circleArea)方法中传入一个字符串,会得到一个编译错误。

2.4 小结

本章,我们学习了ECMAScript 2015+的一些新功能,会让后续例子的语法变得更加简练。本章还介绍了TypeScript以帮助我们利用静态类型和错误检测。

下一章,我们要学习第一种数据结构:数组。许多语言都对数组有原生的支持,包括JavaScript。

目录