-->

ECMAScript 和TypeScript概述

2019-06-21 06:35发布

JavaScript 语言每年都在进化。从2015 年起,每年都有一个新版本发布,我们称其为ECMAScript。

JavaScript 是一门非常强大的语言,也用于企业级开发。在这类开发中(以及其他类型的应用中),类型变量是一个非常有用的功能。作为JavaScript 的一个超集,TypeScript 给我们提供了这样的功能。

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

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

ECMAScript 还是JavaScript

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

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

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

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

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

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

2016 年6 月,ECMAScript 第七版被标准化,称为ECMAScript 2016 或ES2016(ES7)。

2017 年6 月,ECMAScript 第八版被标准化。我们称它为ECMAScript 2017 或ES2017(ES8)。

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

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

兼容性列表

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

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

  • ES2015(ES6):http://kangax.github.io/compat-table/es6/
  • ES2016+:ECMAScript 2016+ compatibility table

在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. 使用Babel.js

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

使用Babel.js 的方式多种多样。一种是根据设置文档(https://babeljs.io/docs/setup/)进行安装。

另一种方式是直接在浏览器中试用(https://babeljs.io/repl/),如下图所示。

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

ECMAScript 2015+的功能

本节,我们将演示如何使用ES2015 的一些新功能。这对日常的JavaScript 编码很有用。

我们将介绍以下功能。

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

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})。

你可以访问http://t.cn/EGbEFux,测试和执行上面的代码。

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

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

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

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

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

constjsFramework = {
name: 'Angular'
};

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

jsFramework.name = 'React';

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

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

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

你可以访问http://t.cn/EGbnYXG 执行上面的例子。

let 和const 的变量作用域

我们通过下面这个例子(http://sina.lt/fQNW)来理解let 或const 关键字声明的变量如何工作。

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 的。

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

2.模板字面量

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

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

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

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

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

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

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

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

你可以访问http://t.cn/EGb17Xt 执行上面的例子。

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

你可以访问http://t.cn/EGb1fte 执行上面的例子。

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 的参数默认值,代码可以少写好几行。

你可以访问http://t.cn/EGb1QHS 执行上面的例子。

5.声明展开和剩余参数

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

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

你可以访问http://t.cn/EGbBP4e 执行展开运算符的例子,访问http://t.cn/EGbBqXf执行剩余参数的例子。

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 执行上面三个例子。

数组解构:Babel · The compiler for next generation JavaScript

变量互换:Babel · The compiler for next generation JavaScript

属性简写:Babel · The compiler for next generation JavaScript

7.使用类进行面向对象编程

ES2015 还引入了一种更简洁的声明类的方式。你已经在前面学习了像下面这样声明一个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); // 输出图书标题

你可以访问http://t.cn/EGbroRC 执行上面的例子。

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 面向对象编程还是基于原型实现的。

你可以访问http://sina.lt/fQPa 执行上面的例子。

2. 使用属性存取器

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

下面的例子是一个声明了get 和set 函数的类。

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

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

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

_name 并非真正的私有属性,我们仍然可以引用它(行{6})。

你可以访问http://t.cn/EGbd6GL 执行上面的例子。

8.乘方运算符

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

const area = 3.14 * r * r;

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

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

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

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

你可以访问http://t.cn/EGbdT0r 执行上面的例子。

ES2015+还提供了一些其他功能,包括列表迭代器、类型数组、Set、Map、WeakSet、WeakMap、尾调用、for..of、Symbol、Array.prototype.includes、尾逗号、字符串补全、静态对象方法,等等。

你可以在https://developer.mozilla.org/zh-CN/docs/Web/JavaScript 查阅JavaScript和ECMAScript 的完整功能列表。

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 模块的信息,请查阅http://exploringjs.com/es6/ch_modules.html。你也可以下载本书的源代码包来查看本示例的完整代码。

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 安装和使用细节可以在https://babeljs.io/docs/setup 和https://babeljs.io/docs/usage/cli/查阅。

最好的方式是创建一个本地项目,并在其中进行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

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

更多有关Node.js 支持原生ES2015 导入功能的信息可以在https://github.com/nodejs/node-eps/blob/master/002-es-modules.md 查阅。

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

要在浏览器中运行ES2015 的代码,有几种不同的方式。第一种是生成传统的代码包(即转译成ES5 代码的JavaScript文件)。我们可以使用流行的代码打包工具,如Browserify 或Webpack。

通过这种方法,我们会创建可直接发布的文件(包),并且可以在HTML 文件中像引入其他JavaScript 代码一样引入它。


浏览器对ES2015 模块的支持最终于2017 年初实现了。目前对该功能的支持情况(以及在实验性模式下开启它的方法)可以在http://caniuse.com/#feat=es6-module 查阅,如下图所示。

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

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

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


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

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


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

要了解更多有关在浏览器中运行ES2015 模块的信息,请阅读https://medium.com/dev-channel/es6-modules-in-chrome-canary-m60-ba588dfb8ab7 和https://jakearchibald.com/2017/es-modules-in-browsers/。

3. ES2015+的向后兼容性

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

介绍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 代码。

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.接口

在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 有一个name和age。变量mary 除了name 和age 之外,还有一个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 {
compareTo(b: T): number;
}

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

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

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

3.其他TypeScript 功能

以上是对TypeScript 的简单介绍。TypeScript 文档是学习所有其他功能以及了解本文话题相关细节的好地方,可以在https://www.typescriptlang.org/docs/home.html 找到。

TypeScript 也有一个在线体验功能(和Babel 类似),可以在里面运行一些代码示例,地址是https://www.typescriptlang.org/play/index.html。

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

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

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

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

——本文选自《学习JavaScript数据结构与算法(第3版)》

用JavaScript学习常用的数据结构和算法,高效解决编程常见问题

数据结构是计算机为了高效地利用资源而组织数据的一种方式。数据结构与算法是解决一切编程问题的基础。本书用JavaScript语言介绍了各种数据结构与算法,通俗易懂、循序渐进,有助于计算机科学专业的学生和刚刚开启职业生涯的技术人员探索JavaScript。

本书首先介绍了JavaScript语言的基础知识(包括ECMAScript和TypeScript),其次讨论了数组、栈、队列、双端队列和链表等重要的数据结构,随后分析了集合、字典和散列表的工作原理,接下来阐述了递归的原理、什么是树以及二叉堆和堆排序,然后介绍了图、DFS和BFS算法、各种排序(冒泡排序、选择排序、插入排序、归并排序、快速排序、计数排序、桶排序和基数排序)和搜索(顺序搜索、二分搜索和内插搜索)算法以及随机算法,接着介绍了分而治之、动态规划、贪心算法和回溯算法等高级算法以及函数式编程,最后还介绍了如何计算算法的复杂度。

目录

第 1 章 JavaScript简介

第 2 章 ECMAScript和TypeScript概述

第 3 章 数组

第 4 章 栈

第 5 章 队列和双端队列

第 6 章 链表

第 7 章 集合

第 8 章 字典和散列表

第 9 章 递归

第 10 章 树

第 11 章 二叉堆和堆排序

第 12 章 图

第 13 章 排序和搜索算法

第 14 章 算法设计与技巧

第 15 章 算法复杂度

文章来源: https://www.toutiao.com/group/6698480302068597262/