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):kangax.github.io/compat
  • 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})。

你可以訪問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
};

你可以訪問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 + .,
這是新的一行
這也是);

我們可以用如下代碼改進上面這個console.log 輸出的語法。

console.log(`你正在閱讀${book.name}。
這是新的一行
這也是。`);

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

模板字面量也可以用於多行的字元串,再也不需要用
了。只要按下鍵盤上的Enter 就可以換一行,就像上面例子裏的這是新的一行。

這個功能對簡化我們例子的輸出非常有用!

你可以訪問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();

你可以訪問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 的參數默認值,代碼可以少寫好幾行。

你可以訪問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));

你可以訪問t.cn/EGbBP4e 執行展開運算符的例子,訪問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); // 輸出圖書標題

你可以訪問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 面向對象編程還是基於原型實現的。

你可以訪問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})。

你可以訪問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);

你可以訪問t.cn/EGbdT0r 執行上面的例子。

ES2015+還提供了一些其他功能,包括列表迭代器、類型數組、Set、Map、WeakSet、WeakMap、尾調用、for..of、Symbol、Array.prototype.includes、尾逗號、字元串補全、靜態對象方法,等等。

你可以在developer.mozilla.org/z 查閱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 模塊的信息,請查閱exploringjs.com/es6/ch_。你也可以下載本書的源代碼包來查看本示例的完整代碼。

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 安裝和使用細節可以在babeljs.io/docs/setupbabeljs.io/docs/usage/c查閱。

最好的方式是創建一個本地項目,並在其中進行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 導入功能的信息可以在github.com/nodejs/node- 查閱。

2. 在瀏覽器中運行ES2015 模塊

要在瀏覽器中運行ES2015 的代碼,有幾種不同的方式。第一種是生成傳統的代碼包(即轉譯成ES5 代碼的JavaScript文件)。我們可以使用流行的代碼打包工具,如Browserify 或Webpack。

通過這種方法,我們會創建可直接發布的文件(包),並且可以在HTML 文件中像引入其他JavaScript 代碼一樣引入它。

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

瀏覽器對ES2015 模塊的支持最終於2017 年初實現了。目前對該功能的支持情況(以及在實驗性模式下開啟它的方法)可以在caniuse.com/# 查閱,如下圖所示。

要在瀏覽器中使用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 模塊的信息,請閱讀medium.com/dev-channel/jakearchibald.com/2017/

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

這是個很有用的功能,可以確保我們在比較相同類型的對象。利用這個功能,我們還可以使用編輯器的代碼補全。

3.其他TypeScript 功能

以上是對TypeScript 的簡單介紹。TypeScript 文檔是學習所有其他功能以及瞭解本文話題相關細節的好地方,可以在typescriptlang.org/docs 找到。

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 章 演算法複雜度

購買:京東、噹噹


推薦閱讀:
相關文章