ES5 to ESNext —  自 2015 以來 JavaScript 新增的所有新特性

icepy發表於2019-03-19


ES5 to ESNext —  自 2015 以來 JavaScript 新增的所有新特性

img

這篇文章的出發點是為了幫助前端開發者串聯 ES6前後的 JavaScript 知識,並且可以快速瞭解 JavaScript 語言的最新進展。

JavaScript 在當下處於特權地位,因為它是唯一可以在瀏覽器中執行的語言,並且是被高度整合和優化過的。

JavaScript 在未來有著極好的發展空間,跟上它的變化不會比現在更加的困難。我的目標是讓你能夠快速且全面的瞭解這門語言可以使用的新內容。

點選這裡獲取 PDF/ePub/Mobi 版本

目錄

ECMAScript 簡介

ES2015

ES2016

ES2017

ES2018

ESNext

ECMAScript 簡介

每當閱讀 JavaScript 相關的文章時,我都會經常遇到如下術語: ES3, ES5, ES6, ES7, ES8, ES2015, ES2016, ES2017, ECMAScript 2017, ECMAScript 2016, ECMAScript 2015 等等,那麼它們是指代的是什麼?

它們都是指代一個名為 ECMAScript 的標準。

JavaScript 就是基於這個標準實現的,ECMAScript 經常縮寫為 ES。

除了 JavaScript 以外,其它基於 ECMAScript 實現語言包括:

  • ActionScript ( Flash 指令碼語言),由於 Adobe 將於 2020 年末停止對 Flash 的支援而逐漸失去熱度。
  • JScript (微軟開發的指令碼語言),在第一次瀏覽器大戰最激烈的時期,JavaScript 只被Netscape所支援,微軟必須為 Internet Explorer 構建自己的指令碼語言。

但是現在流傳最廣、影響最大的基於 ES 標準的語言實現無疑就是 JavaScript了

為啥要用這個奇怪的名字呢?Ecma International 是瑞士標準協會,負責制定國際標準。

JavaScript 被建立以後,經由 Netscape 和 Sun Microsystems 公司提交給歐洲計算機制造商協會進行標準化,被採納的 ECMA-262 別名叫 ECMAScript

This press release by Netscape and Sun Microsystems (the maker of Java) might help figure out the name choice, which might include legal and branding issues by Microsoft which was in the committee, according to Wikipedia.

IE9 之後微軟的瀏覽器中就看不到對 JScript 這個命名的引用了,取而代之都統稱為 JavaScript。

因此,截至201x,JavaScript 成為最流行的基於 ECMAScript 規範實現的語言。

ECMAScript 當前的版本。

目前的最新的 ECMAScript 版本是 ES2018

於 2018 年 6 月釋出。

TC39 是什麼?

TC39(Technical Committee 39)是一個推動 JavaScript 發展的委員會。

TC39的成員包括各個主流瀏覽器廠商以及業務與瀏覽器緊密相連的公司,其中包括 Mozilla,Google ,Facebook,Apple,Microsoft,Intel,PayPal,SalesForce等。

每個標準版本提案都必須經過四個不同的階段,這裡有詳細的解釋

ES Versions

令我費解的是 ES 版本的命名依據有時根據迭代的版本號,有時卻根據年份來進行命名。而這個命名的不確定性又使得人們更加容易混淆 JS/ES 這個兩個概念?。

在 ES2015 之前,ECMAScript 各個版本的命名規範通常與跟著標準的版本更新保持一致。因此,2009年 ECMAScript 規範更新以後的的正式版本是 ES5。

Why does this happen? During the process that led to ES2015, the name was changed from ES6 to ES2015, but since this was done late, people still referenced it as ES6, and the community has not left the edition naming behind — the world is still calling ES releases by edition number. 為什麼會發生這一切?在ES2015誕生的過程中,名稱由ES6更改為ES2015,但由於最終完成太晚,人們仍然稱其為ES6,社群也沒有將版本號完全拋之於後 — 世界仍然使用 ES 來定義版本號。

下圖比較清晰的展示了版本號與年份的關聯:

img

接下來,我們來深入瞭解 JavaScript 自 ES5 以來增加的特性。

let和const

ES2015 之前, var 是唯一可以用來宣告變數的語句。

var a = 0
複製程式碼

上面語句如果你遺漏了 var,那麼你會把這個值(0)賦給一個未宣告的變數,其中宣告和未宣告變數之間存在一些差異。

在現代瀏覽器開啟嚴格模式時,給未宣告的變數賦值會丟擲 ReferenceError 異常,在較老的瀏覽器(或者禁用嚴格模式)的情況下,未宣告的變數在執行賦值操作時會隱式的變為全域性物件的屬性。

當你宣告一個變數卻沒有進行初始化,那麼它的值直到你對它進行賦值操作之前都是 undefined

var a //typeof a === 'undefined'
複製程式碼

你可以對一個變數進行多次重新宣告,並覆蓋它:

var a = 1
var a = 2
複製程式碼

你也可以在一條宣告語句中一次宣告多個變數:

var a = 1, b = 2
複製程式碼

作用域是變數可訪問的程式碼部分。

在函式之外用 var 宣告的會分配給全域性物件,這種變數可以在全域性作用域中被訪問到。而在函式內部宣告的變數只能在函式區域性作用域被訪問到,這類似於函式引數。

在函式中定義的區域性變數名如何跟全域性變數重名,那麼區域性變數的優先順序更高,在函式內無法訪問到同名的全域性變數。

需要注意的是,var 是沒有塊級作用域(識別符號是一對花括號)的,但是 var 是有函式作用域的,所以在新建立的塊級作用域或者是函式作用域裡面宣告變數會覆蓋全域性同名變數,因為 var 在這兩種情況下沒有建立新的作用域。

在函式內部,其中定義的任何變數在所有函式程式碼中都是可見的,因為JavaScript在執行程式碼之前實際上將所有變數都移到了頂層(被稱為懸掛的東西)。 在函式的內部定義的變數在整個函式作用域中都是可見(可訪問),即使變數是在函式體末尾被宣告,但是仍然可以再函式體開頭部分被引用,因為 JavaScript存在變數提升機制。為避免混淆,請在函式開頭宣告變數,養成良好的編碼規範。

Using let

let 是ES2015中引入的新功能,它本質上是具有塊級作用域的 var 。它可以被當前作用域(函式以及塊級作用域)以及子級作用域訪問到。

現代 JavaScript 開發者在 letvar 的選擇中可能會更傾向於前者。

如果 let 看起來是一個很抽象的術語,當你閱讀到 let color = 'red' 這一段,因為使用 let 定義了color 為紅色,那麼這一切就變的有意義了。

在任何函式之外用 let 宣告變數,和 var相反的是 它並不會建立全域性變數。

Using const

使用變數 varlet 宣告的變數可以被重新賦值。 使用 const 宣告的變數一經初始化,它的值就永遠不能再改變,即不可重新被賦值。

const a = 'test'
複製程式碼

我們不能再為 a 進行賦值操作。然而,a 如果它是一個具有屬性或者方法的物件,那麼我們可以改變它的屬性或者方法。

const 並不意味著具有不可變性,只是保證用 const 宣告的變數的引用地址不被變更。

類似於 letconst 也具有塊級作用域。

現代 JavaScript 開發者在遇到不會進行二次賦值的變數宣告時,應該儘量使用 const

箭頭函式

箭頭函式的引入極大的改變了程式碼的書寫風格和一些工作機制。

在我看來,箭頭函式很受開發者歡迎,現在很少在比較新的程式碼庫中看到 function 關鍵字了,雖然它並未被廢棄。

箭頭函式看起來會更加的簡潔,因為它允許你使用更短的語法來書寫函式:

const myFunction = function() {
  //...
}
複製程式碼

const myFunction = () => {
  //...
}
複製程式碼

如果函式體中只包含一條語句,你甚至可以省略大括號並直接書寫這條語句:

const myFunction = () => doSomething()
複製程式碼

引數在括號中傳遞:

const myFunction = (param1, param2) => doSomething(param1, param2)
複製程式碼

如果該函式只有一個引數,那麼可以省略掉括號:

const myFunction = param => doSomething(param)
複製程式碼

由於這種簡短的語法,使得我們可以更便捷的使用比較簡短的函式

隱式返回

箭頭函式支援隱式返回:可以正常的 return 一個返回值但是可以不使用 return 關鍵字。

隱式返回只在函式體內只包含一條語句的情況下生效:

const myFunction = () => 'test'
myFunction() //'test'
複製程式碼

需要注意的一種情況,當返回一個物件時,記得將大括號括在括號中以避免產生歧義,誤將其(大括號)解析為函式體的大括號。

const myFunction = () => ({ value: 'test' })
myFunction() //{value: 'test'}
複製程式碼

箭頭函式中的 this

this 可能是一個很難掌握的概念,因為它會根據上下文而進行變化,並且會在不同的 JavaScript的模式(是否為嚴格模式)下表現出差異。

理解 this 這個概念對於箭頭函式的使用很重要,因為與常規函式相比,箭頭函式的表現非常不同。

物件的方法為常規函式時,方法中的this指向這個物件,因此可以這樣做:

const car = {
  model: 'Fiesta',
  manufacturer: 'Ford',
  fullName: function() {
    return `${this.manufacturer} ${this.model}`
  }
}
複製程式碼

執行 car.fullName() 會返回 "Ford Fiesta"

如果上述方法使用是是箭頭函式,由於箭頭中的 this 的作用域繼承自執行上下文,箭頭函式自身不繫結 this,因此 this 的值將在呼叫堆疊中查詢,因此在此程式碼 car.fullName() 中不會返回常規函式那樣的結果,實際會返回字串 "undefined undefined":

const car = {
  model: 'Fiesta',
  manufacturer: 'Ford',
  fullName: () => {
    return `${this.manufacturer} ${this.model}`
  }
}
複製程式碼

因此,箭頭函式不適合作為物件方法。

同樣,箭頭函式也不適合使用在作為建立建構函式,因為在例項化物件時會丟擲 TypeError

所以在不需要動態上下文時請使用常規函式。

當然,在事件監聽器上使用箭頭函式也會存在問題。因為 DOM 事件偵聽器會自動將 this 與目標元素繫結,如果該事件處理程式的邏輯依賴 this,那麼需要常規函式:

const link = document.querySelector('#link')
link.addEventListener('click', () => {
  // this === window
})
const link = document.querySelector('#link')
link.addEventListener('click', function() {
  // this === link
})
複製程式碼

Classes類

JavaScript 實現繼承的方式比較罕見:原型繼承。原型繼承雖然在我看來很棒,但與其它大多數流行的程式語言的繼承實現機制不同,後者是基於類的。

因此 Java、Python 或其它語言的開發者很難理解原型繼承的方式,因此 ECMAScript 委員會決定在原型繼承之上實現 class 的語法糖,這樣便於讓其它基於類實現繼承的語言的開發者更好的理解 JavaScript 程式碼。

注意:class 並沒有對 JavaScript 底層做修改,你仍然可以直接訪問物件原型。

class 定義

如下是一個 class 的例子:

class Person {
  constructor(name) {
    this.name = name
  }
  hello() {
    return 'Hello, I am ' + this.name + '.'
  }
}
複製程式碼

class 具有一個識別符號,我們可以使用 new ClassIdentifier() 來建立一個物件例項。

初始化物件時,呼叫 constructor方法,並將引數傳遞給此方法。

類宣告語句中也可以增加類需要的一些原型方法。在這種情況下 helloPerson 類的一個原型方法,可以在這個類的物件例項上呼叫:

const flavio = new Person('Flavio')
flavio.hello()
複製程式碼

Class 繼承

一個子類可以 extend 另一個類,通過子類例項化出來的物件可以繼承這兩個類的所有方法。

如果子類中的方法與父類中的方法名重複,那麼子類中的同名方法優先順序更高:

class Programmer extends Person {
  hello() {
    return super.hello() + ' I am a programmer.'
  }
}
const flavio = new Programmer('Flavio')
flavio.hello()
複製程式碼

(上述程式碼會列印出:“Hello, I am Flavio. I am a programmer.”)

類沒有顯示的類變數宣告,但你必須在初始化建構函式 constructor 中去初始化類成員變數。

在子類中,你可以通過呼叫super()引用父類。

靜態方法

在類中,通常會把方法直接掛載到例項物件上,直接在例項物件上呼叫。

而靜態方法則是直接使用類名來呼叫,而不是通過物件例項呼叫:

class Person {
  static genericHello() {
    return 'Hello'
  }
}
Person.genericHello() //Hello
複製程式碼

私有方法

JavaScript 沒有內建真正意義上的受保護的私有方法。

社群有解決方法,但我不會在這裡做講解。

Getters 和 setters

你可以通過增加方法 字首 get 或者 set 建立一個 getter 和 setter,getter 和 setter會在你去獲取特定值或者修改特定值的時候執行 get 或者 set內的相關方法。

class Person {
  constructor(name) {
    this._name = name
  }
  set name(value) {
    this._name = value
  }
  get name() {
    return this._name
  }
}
複製程式碼

如果你只有 getter,該屬性無法被設定,並且設定此屬性的操作都會被忽略:

class Person {
  constructor(name) {
    this._name = name
  }
  get name() {
    return this._name
  }
}
複製程式碼

如果你只有一個 setter,則可以更改該值,但不能從外部訪問它:

class Person {
  constructor(name) {
    this._name = name
  }
  set name(value) {
    this._name = value
  }
}
複製程式碼

預設引數

函式 doSomething 接收一個 param1 引數。

const doSomething = (param1) => {
}
複製程式碼

我們可以給 param1 設定預設值,如果在呼叫函式時未傳入引數,那麼該引數自動設定未預設值。

const doSomething = (param1 = 'test') => {
}
複製程式碼

當然,這種機制同樣適用於多個引數:

const doSomething = (param1 = 'test', param2 = 'test2') => {
}
複製程式碼

假如你的函式是一個具有特定屬性的物件該怎麼處理?

曾幾何時,如果我們必須要取一個物件的特定屬性值,為了做相容處理(物件格式不正確),你必須在函式中新增一些程式碼:

const colorize = (options) => {
  if (!options) {
    options = {}
  }
  const color = ('color' in options) ? options.color : 'yellow'
  ...
}
複製程式碼

通過解構,你可以給特定屬性提供預設值,如此可以大大簡化程式碼:

const colorize = ({ color = 'yellow' }) => {
  ...
}
複製程式碼

如果在呼叫 colorize 函式時沒有傳遞任何物件,我們同樣可以得到一個預設物件作為引數以供使用:

const spin = ({ color = 'yellow' } = {}) => {
  ...
}
複製程式碼

模板字串

模板字串不同於 ES5 以前的版本,你可以用新穎的方式使用字串。

這個語法看起來非常簡便,只需要使用一個反引號替換掉單引號或雙引號:

const a_string = `something`
複製程式碼

這個用法是獨一無二的,因為它提供了許多普通字串所沒有的功能,如下:

  • 它為定義多行字串提供了一個很好的語法
  • 它提供了一種在字串中插入變數和表示式的簡單方法
  • 它允許您建立帶有模板標籤的DSL (DSL意味著領域特定語言,例如:就如同在 React 中使用 styled-components 定義你元件的 CSS 一樣)

下面讓我們深入每個功能的細節。

多行字串

在 ES6 標準之前,建立跨越兩行的字串只能在一行的結尾使用 '' 字元:

const string =
  'first part \
second part'
複製程式碼

這樣使得你建立的字串雖然跨越了兩漢,但是渲染時仍然表現成一行:

first part second part
複製程式碼

需要渲染為多行的話,需要在一行結尾新增 '\n',比如這樣:

const string =
  'first line\n \
second line'
複製程式碼

或者

const string = 'first line\n' + 'second line'
複製程式碼

模板字串使得定義多行字串變得更加簡便。

一個模板字串由一個反引號開始,你只需要按下Enter鍵來建立新的一行,不需要插入特殊符號,最終的渲染效果如下所示:

const string = `Hey
this
string
is awesome!`
複製程式碼

需要特別留意空格在這裡是有特殊意義的,如果這樣做的話:

const string = `First
                Second`
複製程式碼

那麼它會建立出像下面的字串:

First
                Second
複製程式碼

有一個簡單的方法可以修復這個問題,只需要將第一行置為空,然後新增了右邊的翻譯好後呼叫一個 trim() 方法,就可以消除第一個字元前的所有空格:

const string = `
First
Second`.trim()
複製程式碼

插值

模板字串提供了插入變數和表示式的便捷方法

你只需要使用 ${...} 語法

const var = 'test'
const string = `something ${var}` //something test
複製程式碼

在 ${} 裡面你可以加入任何東西,甚至是表示式:

const string = `something ${1 + 2 + 3}`
const string2 = `something ${foo() ? 'x' : 'y'}`
複製程式碼

Template tags

標記模板可能是一個聽起來不太有用的功能,但它實際上被許多流行的庫使用,如 Styled Components 、Apollo 、GraphQL客戶端/伺服器庫,因此瞭解它的工作原理至關重要。

在 Styled Components 模板標籤中用於定義CSS字串

const Button = styled.button`
  font-size: 1.5em;
  background-color: black;
  color: white;
`
複製程式碼

在 Apollo 中,模板標籤用於定義 GraphQL 查詢模式:

const query = gql`
  query {
    ...
  }
`
複製程式碼

上面兩個例子中的styled.buttongql模板標籤其實都是函式:

function gql(literals, ...expressions) {}
複製程式碼

這個函式返回一個字串,可以是任意型別的計算結果。

字面量(literals)是一個包含了表示式插值的模板字面量的序列。 表示式(expressions)包含了所有的插值。

舉個例子:

const string = `something ${1 + 2 + 3}`
複製程式碼

這個例子裡面的字面量是由2個部分組成的序列。第1部分就是something,也就是第一個插值位置(${})之前的字串,第2部分就是一個空字串,從第1個插值結束的位置直到字串的結束。

這個例子裡面的表示式就是隻包含1個部分的序列,也就是6

舉一個更復雜的例子:

const string = `something
another ${'x'}
new line ${1 + 2 + 3}
test`
複製程式碼

這個例子裡面的字面量的序列裡面,第1個部分是:

;`something
another `
複製程式碼

第2部分是:

;`
new line `
複製程式碼

第3部分是:

;`
test`
複製程式碼

這個例子裡面的表示式包含了2個部分:x6

拿到了這些值的函式就可以對其做任意處理,這就是這個特性的威力所在。

比如最簡單的處理就是字串插值,把字面量表示式拼接起來:

const interpolated = interpolate`I paid ${10}€`
複製程式碼

插值的過程就是:

function interpolate(literals, ...expressions) {
  let string = ``
  for (const [i, val] of expressions) {
    string += literals[i] + val
  }
  string += literals[literals.length - 1]
  return string
}
複製程式碼

解構賦值

給定一個object,你可以抽取其中的一些值並且賦值給命名的變數:

const person = {
  firstName: 'Tom',
  lastName: 'Cruise',
  actor: true,
  age: 54, //made up
}
const {firstName: name, age} = person
複製程式碼

nameage就包含了對應的值。

這個語法同樣可以用到陣列當中:

const a = [1,2,3,4,5]
const [first, second] = a
複製程式碼

下面這個語句建立了3個新的變數,分別取的是陣列a的第0、1、4下標對應的值:

const [first, second, , , fifth] = a
複製程式碼

更強大的物件字面量

ES2015賦予了物件字面量更大的威力。

簡化了包含變數的語法

原來的寫法:

const something = 'y'
const x = {
  something: something
}
複製程式碼

新的寫法:

const something = 'y'
const x = {
  something
}
複製程式碼

原型

原型可以這樣指定:

const anObject = { y: 'y' }
const x = {
  __proto__: anObject
}
複製程式碼

super()

const anObject = { y: 'y', test: () => 'zoo' }
const x = {
  __proto__: anObject,
  test() {
    return super.test() + 'x'
  }
}
x.test() //zoox
複製程式碼

動態屬性

const x = {
  ['a' + '_' + 'b']: 'z'
}
x.a_b //z
複製程式碼

For-of迴圈

2009年的ES5引入了forEach()迴圈,雖然很好用,但是它跟for迴圈不一樣,沒法break。

ES2015引入了**for-of** 迴圈,就是在forEach的基礎上加上了break的功能:

//iterate over the value
for (const v of ['a', 'b', 'c']) {
  console.log(v);
}
//get the index as well, using `entries()`
for (const [i, v] of ['a', 'b', 'c'].entries()) {
  console.log(index) //index
  console.log(value) //value
}
複製程式碼

留意一下const的使用。這個迴圈在每次迭代中都會建立一個新的作用域,所以我們可以使用const來代替let

它跟for...in的區別在於:

  • for...of 遍歷屬性值
  • for...in 遍歷屬性名

Promises

promise的一般定義: 它是一個代理,通過它可以最終得到一個值.

Promise是處理非同步程式碼的一種方式,可以少寫很多回撥。

非同步函式是建立在promise API上面的,所以理解Promise是一個基本的要求。

promise的原理簡述

一個promise被呼叫的時候,首先它是處於pending狀態。在promise處理的過程中,呼叫的函式(caller)可以繼續執行,直到promise給出反饋。

此時,呼叫的函式等待的promise結果要麼是resolved狀態,要麼是rejected狀態。但是由於JavaScript是非同步的,所以promise處理的過程中,函式會繼續執行

為什麼JS API使用promises?

除了你的程式碼和第三方庫的程式碼之外,promise在用在現代的Web API中,比如:

在現代的JavaScript中,不使用promise是不太可能的,所以我們來深入研究下promise吧。

建立一個promise

Promise API暴露了一個Promise建構函式,可以通過new Promise()來初始化:

let done = true
const isItDoneYet = new Promise((resolve, reject) => {
  if (done) {
    const workDone = 'Here is the thing I built'
    resolve(workDone)
  } else {
    const why = 'Still working on something else'
    reject(why)
  }
})
複製程式碼

promise會檢查done這個全域性變數,如果為true,就返回一個resolved promise,否則就返回一個rejected promise。

通過resolvereject,我們可以得到一個返回值,返回值可以是字串也可以是物件。

使用一個promise

上面講了怎麼建立一個promise,下面就講怎麼使用(consume)這個promise。

const isItDoneYet = new Promise()
//...
const checkIfItsDone = () => {
  isItDoneYet
    .then(ok => {
      console.log(ok)
    })
    .catch(err => {
      console.error(err)
    })
}
複製程式碼

執行checkIfItsDone()方法時,會執行isItDoneYet()這個promise,並且等待它resolve的時候使用then回撥,如果有錯誤,就用catch回撥來處理。

鏈式promise

一個promise可以返回另一個promise,從而建立promise鏈條(chain)。

一個很好的例子就是Fetch API,它是基於XMLHttpRequest API的一個上層API,我們可以用它來獲取資源,並且在獲取到資源的時候鏈式執行一系列promise。

Fetch API是一個基於promise的機制,呼叫fetch()相當於使用new Promise()來宣告我們自己的promise。

鏈式promise的例子

const status = response => {
  if (response.status >= 200 && response.status < 300) {
    return Promise.resolve(response)
  }
  return Promise.reject(new Error(response.statusText))
}
const json = response => response.json()
fetch('/todos.json')
  .then(status)
  .then(json)
  .then(data => {
    console.log('Request succeeded with JSON response', data)
  })
  .catch(error => {
    console.log('Request failed', error)
  })
複製程式碼

在這個例子當中,我們呼叫fetch(),從根目錄的todos.json檔案中獲取一系列的TODO專案,並且建立一個鏈式promise。

執行fetch()方法會返回一個response,它包含很多屬性,我們從中引用如下屬性:

  • status, 一個數值,表示HTTP狀態碼
  • statusText, 一個狀態訊息,當請求成功的時候返回OK

response還有一個json()方法,它返回一個promise,返回內容轉換成JSON後的結果。

所以這些promise的呼叫過程就是:第一個promise執行一個我們定義的status()方法,檢查response status,判斷是否一個成功的響應(status在200和299之間),如果不是成功的響應,就reject這個promise。

這個reject操作會導致整個鏈式promise跳過後面的所有promise直接到catch()語句,列印Request failed和錯誤訊息。

如果這個promise成功了,它會呼叫我們定義的json()函式。因為前面的promise成功之後返回的response物件,我們可以拿到並作為第2個promise的引數傳入。

在這個例子裡面,我們返回了JSON序列化的資料,所以第3個promise直接接收這個JSON:

.then((data) => {
  console.log('Request succeeded with JSON response', data)
})
複製程式碼

然後我們把它列印到console。

處理錯誤

在上一節的的例子裡面,我們有一個catch接在鏈式promise後面。

當promise鏈中的任意一個出錯或者reject的時候,就會直接跳到promise鏈後面最近的catch()語句。

new Promise((resolve, reject) => {
  throw new Error('Error')
}).catch(err => {
  console.error(err)
})
// or
new Promise((resolve, reject) => {
  reject('Error')
}).catch(err => {
  console.error(err)
})
複製程式碼

級聯錯誤

如果在catch()裡面丟擲一個錯誤,你可以在後面接上第二個catch()來處理這個錯誤,以此類推。

new Promise((resolve, reject) => {
  throw new Error('Error')
})
  .catch(err => {
    throw new Error('Error')
  })
  .catch(err => {
    console.error(err)
  })
複製程式碼

組織多個promise

Promise.all()

如果你要同時完成不同的promise,可以用Promise.all()來宣告一系列的promise,然後當它們全部resolve的時候再執行一些操作。

例子:

const f1 = fetch('/something.json')
const f2 = fetch('/something2.json')
Promise.all([f1, f2])
  .then(res => {
    console.log('Array of results', res)
  })
  .catch(err => {
    console.error(err)
  })
複製程式碼

結合ES2015的解構賦值語法,你可以這樣寫:

Promise.all([f1, f2]).then(([res1, res2]) => {
  console.log('Results', res1, res2)
})
複製程式碼

當然這不限於使用fetch這適用於任何promise.

Promise.race()

Promise.race()執行所有傳遞進去的promise,但是隻要有其中一個resolve了,就會執行回撥函式,並且只執行一次回撥,回撥的引數就是第一個resolve的promise返回的結果。

例子:

const promiseOne = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, 'one')
})
const promiseTwo = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'two')
})
Promise.race([promiseOne, promiseTwo]).then(result => {
  console.log(result) // 'two'
})
複製程式碼

模組

ES Module是用於處理模組的ECMAScript標準。

雖然 Node.js 多年來一直使用 CommonJS標準,但瀏覽器卻從未有過模組系統,因為模組系統的決策首先需要 ECMAScript 標準化後才由瀏覽器廠商去實施實現。

這個標準化已經完成在 ES2015中,瀏覽器也開始實施實現這個標準,大家試圖保持一致,以相同的方式工作。現在 ES Module 可以在 Chrome Safari Edge 和 Firefox(從60版本開始) 中使用。

模組非常酷,他們可以讓你封裝各種各樣的功能,同時將這些功能作為庫暴露給其它 JavaScript 檔案使用。

ES 模組語法

引入模組的語法:

import package from 'module-name'
複製程式碼

CommonJS 則是這樣使用:

const package = require('module-name')
複製程式碼

一個模組是一個 JavaScript 檔案,這個檔案使用 export 關鍵字 匯出 一個或多個值(物件、函式或者變數)。例如,下面這個模組提供了一個將字串變成大寫形式的函式:

uppercase.js

export default str => str.toUpperCase()
複製程式碼

在這個例子中,這個模組定義了唯一一個 default export,因此可以是一個匿名函式。否則,需要一個名稱來和其它 匯出 做區分。

現在,任何其它的 JavaScript 模組 可以通過 import 匯入 uppercase.js 的這個功能。

一個 HTML 頁面可以通過使用了特殊的 type=module 屬性的 <script> 標籤新增一個模組。

<script type="module" src="index.js"></script>
複製程式碼

注意: 這個模組匯入的行為就像 *defer* 指令碼載入一樣。具體可以看 efficiently load JavaScript with defer and async

需要特別注意的是,任何通過 type="module" 載入的指令碼會使用 嚴格模式 載入。

在這個例子中,uppercase.js 模組定義了一個 default export,因此當我們在匯入它的時候,我們可以給他起一個任何我們喜歡的名字:

import toUpperCase from './uppercase.js'
複製程式碼

同時我們可以這樣使用它:

toUpperCase('test') //'TEST'
複製程式碼

你也可以通過一個絕對路徑來匯入模組,下面是一個引用來自其它域底下定義的模組的例子:

import toUpperCase from 'https://flavio-es-modules-example.glitch.me/uppercase.js'
複製程式碼

下面同樣是一些合法的 import語法:

import { toUpperCase } from '/uppercase.js'
import { toUpperCase } from '../uppercase.js'
複製程式碼

下面是錯誤的使用:

import { toUpperCase } from 'uppercase.js'
import { toUpperCase } from 'utils/uppercase.js'
複製程式碼

因為這裡既不是使用絕對地址,也不是使用的相對地址。

其它的 import/export 語法

我們瞭解了上面的例子:

export default str => str.toUpperCase()
複製程式碼

這裡生成了一個 default export。然而,你可以通過下面的語法在一個檔案裡面 匯出 多個功能:

const a = 1
const b = 2
const c = 3
export { a, b, c }
複製程式碼

另外一個模組可以使用下面的方式 import 匯入所有:

import * from 'module'
複製程式碼

你也可以通過解構賦值的方式僅僅 import 匯出一部分:

import { a } from 'module'
import { a, b } from 'module'
複製程式碼

為了方便,你還可以使用 as 重新命名任何 import 的東西:

import { a, b as two } from 'module'
複製程式碼

你可以匯入模組中的預設出口以及通過名稱匯入任何非預設的出口:

import React, { Component } from 'react'
複製程式碼

這是一篇關於 ES 模組的文章,可以看一下: glitch.com/edit/#!/fla…

CORS(跨域資源共享)

進行遠端獲取模組的時候是遵循 CORS 機制的。這意味著當你引用遠端模組的時候,必須使用合法的 CORS 請求頭來允許跨域訪問(例如:Access-Control-Allow-Origin: *)。

對於不支援模組的瀏覽器應該怎麼做?

結合 type="module"nomodule 一起使用:

<script type="module" src="module.js"></script>
<script nomodule src="fallback.js"></script>
複製程式碼

包裝模組

ES 模組是現代瀏覽器中的一大特性。這些特性是 ES6 規範中的一部分,要在瀏覽器中全部實現這些特性的路還很漫長。

我們現在就能使用它們!但是我們同樣需要知道,有一些模組會對我們的頁面效能產生效能影響。因為瀏覽器必須要在執行時執行它們。

Webpack 可能仍然會被大量使用,即使 ES 模組可以在瀏覽器中執行。但是語言內建這個特性對於客戶端和 nodejs 在使用模組的時候是一種巨大的統一。

新的字串方法

任何字串有了一些例項方法:

  • repeat()
  • codePointAt()

repeat()

根據指定的次數重複字串:

'Ho'.repeat(3) //'HoHoHo'
複製程式碼

沒有提供引數以及使用 0 作為引數的時候返回空字串。如果給一個負數引數則會得到一個 RangeError 的錯誤。

codePointAt()

這個方法能用在處理那些需要 2 個 UTF-16 單元表示的字元上。

使用 charCodeAt 的話,你需要先分別得到兩個 UTF-16 的編碼然後結合它們。但是使用 codePointAt() 你可以直接得到整個字元。

下面是一個例子,中文的 “?” 是由兩個 UTF-16 編碼組合而成的:

"?".charCodeAt(0).toString(16) //d842
"?".charCodeAt(1).toString(16) //dfb7
複製程式碼

如果你將兩個 unicode 字元組合起來:

"\ud842\udfb7" //"?"
複製程式碼

你也可以用 codePointAt() 得到同樣的結果:

"?".codePointAt(0) //20bb7
複製程式碼

如果你將得到的 unicode 編碼組合起來:

"\u{20bb7}" //"?"
複製程式碼

更多關於 Unicode 的使用方法,參考我的Unicode guide

新的物件方法

ES2015 在 Object 類下引入了一些靜態方法:

  • Object.is() 確定兩個值是不是同一個
  • Object.assign() 用來淺拷貝一個物件
  • Object.setPrototypeOf 設定一個物件的原型

Object.is()

這個方法用來幫助比較物件的值:

使用方式:

Object.is(a, b)
複製程式碼

返回值在下列情況之外一直是 false

  • ab 是同一個物件
  • ab 是相等的字串(用同樣的字元組合在一起的字串是相等的)
  • ab 是相等的數字
  • ab 都是 undefined, null, NaN, true 或者都是 false

0-0 在 JavaScript 裡面是不同的值, 所以對這種情況要多加小心(例如在比較之前,使用 + 一元操作符將所有值轉換成 +0)。

Object.assign()

ES2015 版本中引入,這個方法拷貝所有給出的物件中的可列舉的自身屬性到另一個物件中。

這個 API 的基本用法是建立一個物件的淺拷貝。

const copied = Object.assign({}, original)
複製程式碼

作為淺拷貝,值會被複制,物件則是拷貝其引用(不是物件本身),因此當你修改了源物件的一個屬性值,這個修改也會在拷貝出的物件中生效,因為內部引用的物件是相同的。:

const original = {
  name: 'Fiesta',
  car: {
    color: 'blue'
  }
}
const copied = Object.assign({}, original)
original.name = 'Focus'
original.car.color = 'yellow'
copied.name //Fiesta
copied.car.color //yellow
複製程式碼

我之前提到過,源物件可以是一個或者多個:

const wisePerson = {
  isWise: true
}
const foolishPerson = {
  isFoolish: true
}
const wiseAndFoolishPerson = Object.assign({}, wisePerson, foolishPerson)
console.log(wiseAndFoolishPerson) //{ isWise: true, isFoolish: true }
複製程式碼

Object.setPrototypeOf()

設定一個物件的原型。可以接受兩個引數:物件以及原型。

使用方法:

Object.setPrototypeOf(object, prototype)
複製程式碼

例子:

const animal = {
  isAnimal: true
}
const mammal = {
  isMammal: true
}
mammal.__proto__ = animal
mammal.isAnimal //true
const dog = Object.create(animal)
dog.isAnimal  //true
console.log(dog.isMammal)  //undefined
Object.setPrototypeOf(dog, mammal)
dog.isAnimal //true
dog.isMammal //true
複製程式碼

展開操作符

你可以展開一個陣列、一個物件甚至是一個字串,通過使用展開操作符 ...

讓我們以陣列來舉例,給出:

const a = [1, 2, 3]
複製程式碼

你可以使用下面的方式建立出一個新的陣列:

const b = [...a, 4, 5, 6]
複製程式碼

你也可以像下面這樣建立一個陣列的拷貝:

const c = [...a]
複製程式碼

這中方式對於物件仍然有效。使用下面的方式克隆一個物件:

const newObj = { ...oldObj }
複製程式碼

用在字串上的時候,展開操作符會以字串中的每一個字元建立一個陣列:

const hey = 'hey'
const arrayized = [...hey] // ['h', 'e', 'y']
複製程式碼

這個操作符有一些非常有用的應用。其中最重要的一點就是以一種非常簡單的方式使用陣列作為函式引數的能力:

const f = (foo, bar) => {}
const a = [1, 2]
f(...a)
複製程式碼

(在之前的語法規範中,你只能通過 f.apply(null, a) 的方式來實現,但是這種方式不是很友好和易讀。)

剩餘引數(rest element)在和陣列解構(array destructuring)搭配使用的時候非常有用。

const numbers = [1, 2, 3, 4, 5]
[first, second, ...others] = numbers
複製程式碼

下面是展開元素 (spread elements):

const numbers = [1, 2, 3, 4, 5]
const sum = (a, b, c, d, e) => a + b + c + d + e
const sum = sum(...numbers)
複製程式碼

ES2018 引入了 剩餘屬性 ,同樣的操作符但是隻能用在物件上。

剩餘屬性(Rest properties):

const { first, second, ...others } = {
  first: 1,
  second: 2,
  third: 3,
  fourth: 4,
  fifth: 5
}
first // 1
second // 2
others // { third: 3, fourth: 4, fifth: 5 }
複製程式碼

屬性展開(Spread properties)允許我們結合跟在 ... 操作符之後物件的屬性:

const items = { first, second, ...others }
items //{ first: 1, second: 2, third: 3, fourth: 4, fifth: 5 }
複製程式碼

Set

一個 Set 資料結構允許我們在一個容器裡面增加資料。

一個 Set 是一個物件或者基礎資料型別(strings、numbers或者booleans)的集合,你可以將它看作是一個 Map,其中值作為對映鍵,map 值始終為 true。

初始化一個 Set

Set 可以通過下面的方式初始化:

const s = new Set()
複製程式碼

向 Set 中新增一項

你可以使用 add 方法向 Set 中新增項:

s.add('one')
s.add('two')
複製程式碼

Set 僅會存貯唯一的元素,因此多次呼叫 s.add('one') 不會重複新增新的元素。

你不可以同時向 set 中加入多個元素。你需要多次呼叫 add() 方法。

檢查元素是否在 set 中

我們可以通過下面的方式檢查元素是否在 set 中:

s.has('one') //true
s.has('three') //false
複製程式碼

從 set 中刪除一個元素:

使用 delete() 方法:

s.delete('one')
複製程式碼

確定 set 中元素的數量

使用 size 屬性:

s.size
複製程式碼

刪除 set 中的全部元素

使用 clear() 方法:

s.clear()
複製程式碼

對 set 進行迭代

使用 keys() 或者 values() 方法 - 它們等價於下面的程式碼:

for (const k of s.keys()) {
  console.log(k)
}
for (const k of s.values()) {
  console.log(k)
}
複製程式碼

entries() 方法返回一個迭代器,你可以這樣使用它:

const i = s.entries()
console.log(i.next())
複製程式碼

呼叫 i.next() 將會以 { value, done = false } 物件的形式返回每一個元素,直到迭代結束,這時 donetrue

你也可以呼叫 set 的 forEach() 方法:

s.forEach(v => console.log(v))
複製程式碼

或者你就直接使用 for..of 迴圈吧:

for (const k of s) {
  console.log(k)
}
複製程式碼

使用一些初始值初始化一個 set

你可以使用一些值初始化一個 set:

const s = new Set([1, 2, 3, 4])
複製程式碼

將 set 轉換為一個陣列

const a = [...s.keys()]
// or
const a = [...s.values()]
複製程式碼

WeakSet

一個 WeakSet 是一個特殊的 Set.

在 set 中,元素不會被 gc(垃圾回收)。一個 weakSet 讓它的所有元素都是可以被 gc 的。weakSet 中的每個鍵都是一個物件。當這個物件的引用消失的時候,對應的值就可以被 gc 了。

下面是主要的不同點:

  1. WeakSet 不可迭代
  2. 你不能清空 weakSet 中的所有元素
  3. 不能夠得到 weakSet 的大小

一個 weakSet 通常是在框架級別的程式碼中使用,僅僅暴露了下面的方法:

  • add()
  • has()
  • delete()

Map

一份map結構的資料允許我們建立資料和key的關係

在ES6之前

在引入Map之前,開發者通常把物件(Object)當Map使用,把某個object或value值與指定的key進行關聯:

const car = {}
car['color'] = 'red'
car.owner = 'Flavio'
console.log(car['color']) //red
console.log(car.color) //red
console.log(car.owner) //Flavio
console.log(car['owner']) //Flavio
複製程式碼

引入Map之後

ES6引入了Map資料結構,它為我們處理這種資料結構提供了一種合適的工具

Map的初始化:

const m = new Map()
複製程式碼

新增條目到Map中

你可以通過set()方法把條目設定到map中:

m.set('color', 'red')
m.set('age', 2)
複製程式碼

通過key值從map中獲取條目

你可以通過get()方法從map中取出條目:

const color = m.get('color')
const age = m.get('age')
複製程式碼

通過key值從map中刪除條目

使用delete()方法:

m.delete('color')
複製程式碼

從map中刪除所有條目

使用clear()方法:

m.clear()
複製程式碼

通過key值檢查map中是否含有某個條目

使用has()方法

const hasColor = m.has('color')
複製程式碼

獲取map中的條目數量

使用 size 屬性:

const size = m.size
複製程式碼

用value值初始化一個map

你可以用一組value來初始化一個map:

const m = new Map([['color', 'red'], ['owner', 'Flavio'], ['age', 2]])
複製程式碼

Map 的key值

任何值(物件,陣列,字串,數字)都可以作為一個map的value值(使用key-value鍵值的形式),任何值也可以用作key,即使是object物件。

如果你想通過get()方法從map中獲取不存在的key,它將會返回undefined

在真實世界中你幾乎不可能找到的詭異情況

const m = new Map()
m.set(NaN, 'test')
m.get(NaN) //test
const m = new Map()
m.set(+0, 'test')
m.get(-0) //test
複製程式碼

使用Iterate迭代器獲取map的keys值

Map提供了keys()方法,通過該方法我們可以迭代出所有的key值:

for (const k of m.keys()) {
  console.log(k)
}
複製程式碼

使用Iterate迭代器獲取map的values值

Map提供了values()方法,通過該方法我們可以迭代出所有的value值:

for (const v of m.values()) {
  console.log(v)
}
複製程式碼

使用Iterate迭代器獲取key-value組成的鍵值對

Map提供了entries()方法,通過該方法我們可以迭代出所有的鍵值對:

for (const [k, v] of m.entries()) {
  console.log(k, v)
}
複製程式碼

使用方法還可以簡化為:

for (const [k, v] of m) {
  console.log(k, v)
}
複製程式碼

將map的keys值轉換為陣列

const a = [...m.keys()]
複製程式碼

將map的values值轉換為陣列

const a = [...m.values()]
複製程式碼

WeakMap

WeakMap是一種特殊的Map

在一個map物件中,定義在其上資料永遠不會被垃圾回收,WeakMap替而代之的是它允許在它上面定義的資料可以自由的被垃圾回收走,WeakMap的每一個key都是一個物件,當指向該物件的指標丟失,與之對應的value就會被垃圾回收走。

這是WeakMap的主要不同處:

  1. 你不可以在WeakMap上迭代keys值和values值(或者key-value鍵值對)
  2. 你不可以從WeakMap上清除所有條目
  3. 你不可以獲取WeakMap的大小

WeakMap提供瞭如下幾種方法,這些方法的使用和在Map中一樣:

  • get(k)
  • set(k, v)
  • has(k)
  • delete(k)

關於WeakMap的用例不如Map的用例那麼明顯,你可能永遠也不會在哪裡會用到它,但從實際出發,WeakMap可以構建不會干擾到垃圾回收機制的記憶體敏感性快取,還可以滿足封裝的嚴謹性及資訊的隱藏性需求。

Generators生成器

Generators是一種特殊的函式,它能夠暫停自身的執行並在一段時間後再繼續執行,從而允許其它的程式碼在此期間執行(有關該主題的詳細說明,請參閱完整的“javascript生成器指南”)。

Generators的程式碼決定它必須等待,因此它允許佇列中的其它程式碼執行,並保留“當它等待的事情”完成時恢復其操作的權力。

所有這一切都是通過一個簡單的關鍵字“yield`”完成的。當生成器包含該關鍵字時,將停止執行。

generator生成器可以包含許多yield關鍵字,從而使自己能多次停止執行,它是由*function關鍵字標識(不要將其與C、C++或Go等低階語言中使用的取消指標引用操作符混淆)。

Generators支援JavaScript中全新的程式設計正規化,包括:

  • 在generator執行時支援雙向通訊
  • 不會“凍結”長期執行在程式中的while迴圈

這裡有一個解釋generator如何工作的例子:

function *calculator(input) {
  var doubleThat = 2 * (yield (input / 2))
  var another = yield (doubleThat)
  return (input * doubleThat * another)
}
複製程式碼

我們先初始化它:

const calc = calculator(10)
複製程式碼

然後我們在generator中開始進行iterator迭代:

calc.next()
複製程式碼

第一個迭代器開始了迭代,程式碼返回如下object物件:

{
  done: false
  value: 5
}
複製程式碼

具體過程如下:程式碼執行了函式,並把input=10傳入到生成器建構函式中,該函式一直執行直到抵達yield,並返回yield輸出的內容: input / 2 = 5,因此,我們得到的值為5,並告知迭代器還沒有done(函式只是暫停了)。

在第二個迭代處,我們輸入7:

calc.next(7)
複製程式碼

然後我們得到了結果:

{
  done: false
  value: 14
}
複製程式碼

7被作為doubleThat的值,注意:你可能會把input/2作為輸入引數,但這只是第一次迭代的返回值。現在我們忽略它,使用新的輸入值7,並將其乘以2.

然後,我們得到第二個yield的值,它返回doubleThat,因此返回值為14

在下一個,也是最後一個迭代器,我們輸入100

calc.next(100)
複製程式碼

這樣我們得到:

{
  done: true
  value: 14000
}
複製程式碼

當迭代器完成時(沒有更多的yield關鍵字),我們返回input * doubleThat * another,這相當於10 * 14 * 100


這些都是在2015年的ES2015引入的特性,現在我們深入瞭解下ES2016,它的作用域範圍更小。


Array.prototype.includes()

該特性引入了一種更簡潔的語法,同來檢查陣列中是否包含指定元素。

對於ES6及更低版本,想要檢查陣列中是否包含指定元素,你不得不使用indexOf方法,它檢查陣列中的索引,如果元素不存在,它返回-1,由於-1被計算為true,你需對其進行取反操作,例子如下:

if (![1,2].indexOf(3)) {
  console.log('Not found')
}
複製程式碼

通過ES7引入的新特性,我們可以如此做:

if (![1,2].includes(3)) {
  console.log('Not found')
}
複製程式碼

求冪運算子

求冪運算子**相當於Math.pow()方法,但是它不是一個函式庫,而是一種語言機制:

Math.pow(4, 2) == 4 ** 2
複製程式碼

對於需要進行密集數學運算的程式來說,這個特性是個很好的增強,在很多語言中,**運算子都是標準(包括Python、Ruby、MATLAB、Perl等其它多種語言)。

img


這些都是2016年引入的特性,現在讓我們進入2017年。


字串填充

字串填充的目的是給字串新增字元,以使其達到指定長度

ES2017引入了兩個String方法:padStart()padEnd()

padStart(targetLength [, padString])
padEnd(targetLength [, padString])
複製程式碼

使用例子:

img

Object.values()

該方法返回一個陣列,陣列包含了物件自己的所有屬性,使用如下:

const person = { name: 'Fred', age: 87 }
Object.values(person) // ['Fred', 87]
複製程式碼

Object.values()也可以作用於陣列:

const people = ['Fred', 'Tony']
Object.values(people) // ['Fred', 'Tony']
複製程式碼

Object.entries()

該方法返回一個陣列,陣列包含了物件自己的所有屬性鍵值對,是一個[key, value]形式的陣列,使用如下:

const person = { name: 'Fred', age: 87 }
Object.entries(person) // [['name', 'Fred'], ['age', 87]]
複製程式碼

Object.entries()也可以作用於陣列:

const people = ['Fred', 'Tony']
Object.entries(people) // [['0', 'Fred'], ['1', 'Tony']]
複製程式碼

Object.getOwnPropertyDescriptors()

該方法返回自己(非繼承)的所有屬性描述符,JavaScript中的任何物件都有一組屬性,每個屬性都有一個描述符,描述符是屬性的一組屬性(attributes),由以下部分組成:

  • value: 熟悉的value值
  • writable: 屬性是否可以被更改
  • get: 屬性的getter函式, 當屬性讀取時被呼叫
  • set: 屬性的setter函式, 當屬性設定值時被呼叫
  • configurable: 如果為false, 不能刪除該屬性,除了它的value值以為,也不能更改任何屬性。
  • enumerable: 該屬性是否能列舉

Object.getOwnPropertyDescriptors(obj)接受一個物件,並返回一個帶有描述符集合的物件。

In what way is this useful?

ES6給我們提供了Object.assign()方法,它從一個一個或多個物件中複製所有可列舉的屬性值,並返回一個新物件。

但是,這也存在著一個問題,因為它不能正確的複製一個具有非預設屬性值的屬性。

如果物件只有一個setter,那麼它就不會正確的複製到一個新物件上,使用Object.assign()進行如下操作:

const person1 = {
    set name(newName) {
        console.log(newName)
    }
}
複製程式碼

這將不會起作用:

const person2 = {}
Object.assign(person2, person1)
複製程式碼

但這將會起作用:

const person3 = {}
Object.defineProperties(person3,
  Object.getOwnPropertyDescriptors(person1))
複製程式碼

通過一個簡單的console控制檯,你可以檢視以下程式碼:

person1.name = 'x'
"x"
person2.name = 'x'
person3.name = 'x'
"x"
複製程式碼

person2沒有setter,它沒能複製進去,物件的淺複製限定也出現在**Object.create()**方法中。

尾逗號

該特性允許在函式定義時有尾逗號,在函式使用時可以有尾逗號:

const doSomething = (var1, var2,) => {
  //...
}
doSomething('test2', 'test2',)
複製程式碼

該改變將鼓勵開發者停止“在一行開始時寫逗號”的醜陋習慣

非同步函式

JavaScript在很短的時間內從回撥函式進化到Promise函式(ES2015),並自從ES2017以來,非同步JavaScript的async/wait語法變得更加簡單。 非同步函式是Promise和generator的結合,基本上,它是比Promise更高階的抽象,我再重複一般:async/await是基於Promise建立的

為什麼要引入async/await

它減少了圍繞promise的引用,並打破了Promise — “不要打斷鏈式呼叫”的限制。

當Promise在ES2015中引入時,它的本意是來解決非同步程式碼的問題,它也確實做到了,但在ES2015和ES2017間隔的這兩年中,大家意識到:Promise不是解決問題的終極方案

Promise是為了解決著名的回撥地獄而被引入的,但它本身也帶來了使用複雜性和語法複雜性。

Promise是很好的原生特性,圍繞著它開發人員可以探索出更好的語法,因此當時機成熟後,我們得到了async函式

async函式使程式碼看起來像是同步函式一樣,但其背後卻是非同步和非堵塞的。

它如何工作

一個async函式會返回一個promise,如下例:

const doSomethingAsync = () => {
  return new Promise(resolve => {
    setTimeout(() => resolve('I did something'), 3000)
  })
}
複製程式碼

當你想要呼叫該函式時,你在前面加上了一個wait,這樣呼叫就會被停止,直到該promise進行resolve或reject,需注意的是:外層函式必須定義為async,這是例子:

const doSomething = async () => {
  console.log(await doSomethingAsync())
}
複製程式碼

一個上手示例

這是一個使用async/await進行非同步函式的簡單示例:

const doSomethingAsync = () => {
  return new Promise(resolve => {
    setTimeout(() => resolve('I did something'), 3000)
  })
}
const doSomething = async () => {
  console.log(await doSomethingAsync())
}
console.log('Before')
doSomething()
console.log('After')
複製程式碼

上面的程式碼將會在瀏覽器的console中列印出如下結果:

Before
After
I did something //after 3s
複製程式碼

關於 Promise

async 關鍵字標記在任何函式上,意味著這個函式都將返回一個 Promise,即使這個函式沒有顯式的返回,它在內部也會返回一個 Promise,這就是下面這份程式碼有效的原因:

const aFunction = async () => {
  return 'test'
}
aFunction().then(alert) // This will alert 'test'
複製程式碼

下面的例子也一樣:

const aFunction = async () => {
  return Promise.resolve('test')
}
aFunction().then(alert) // This will alert 'test'
複製程式碼

更易於閱讀的程式碼

正如上述的例子,我們將它與普通回撥函式或鏈式函式進行比較,我們的程式碼看起來非常的簡單。

這是一個很簡單的例子,當程式碼足夠複雜時,它會產生更多的收益。

例如,使用 Promise 來獲取 JSON 資源並解析它:

const getFirstUserData = () => {
  return fetch('/users.json') // get users list
    .then(response => response.json()) // parse JSON
    .then(users => users[0]) // pick first user
    .then(user => fetch(`/users/${user.name}`)) // get user data
    .then(userResponse => response.json()) // parse JSON
}
getFirstUserData()
複製程式碼

這是使用 async/await 實現相同功能的例子:

const getFirstUserData = async () => {
  const response = await fetch('/users.json') // get users list
  const users = await response.json() // parse JSON
  const user = users[0] // pick first user
  const userResponse = await fetch(`/users/${user.name}`) // get user data
  const userData = await user.json() // parse JSON
  return userData
}
getFirstUserData()
複製程式碼

序列多個非同步功能

async 函式非常容易,並且它的語法比 Promise 更易讀。

const promiseToDoSomething = () => {
  return new Promise(resolve => {
    setTimeout(() => resolve('I did something'), 10000)
  })
}
const watchOverSomeoneDoingSomething = async () => {
  const something = await promiseToDoSomething()
  return something + ' and I watched'
}
const watchOverSomeoneWatchingSomeoneDoingSomething = async () => {
  const something = await watchOverSomeoneDoingSomething()
  return something + ' and I watched as well'
}
watchOverSomeoneWatchingSomeoneDoingSomething().then(res => {
  console.log(res)
})
複製程式碼

列印結果:

I did something and I watched and I watched as well
複製程式碼

更簡單的除錯

除錯 Promise 就很困難,因為偵錯程式無法跨越非同步程式碼,但除錯 async/await 就非常的簡單,偵錯程式會像除錯同步程式碼一樣來處理它。

共享記憶體和原子

WebWorkers 可以在瀏覽器中建立多執行緒程式。

它們通過事件的方式來傳遞訊息,從 ES2017 開始,你可以使用 SharedArrayBuffer 在每一個 Worker 中和它們的建立者之間共享記憶體陣列.

由於不知道寫入記憶體部分需要多長的週期來廣播,因此在讀取值時,任何型別的寫入操作都會完成,Atomics 可以避免競爭條件的發生。

關於它的更多細節可以在proposal中找到。


這是 ES2017,接下來我將介紹 ES2018 的功能。


Rest/Spread Properties

ES2015 引入瞭解構陣列的方法,當你使用時:

const numbers = [1, 2, 3, 4, 5]
[first, second, ...others] = numbers
複製程式碼

and 展開引數:

const numbers = [1, 2, 3, 4, 5]
const sum = (a, b, c, d, e) => a + b + c + d + e
const sum = sum(...numbers)
複製程式碼

ES2018 為物件引入了同樣的功能。

解構:

const { first, second, ...others } = { first: 1, second: 2, third: 3, fourth: 4, fifth: 5 }
first // 1
second // 2
others // { third: 3, fourth: 4, fifth: 5 }
複製程式碼

展開屬性 允許通過組合在展開運算子之後傳遞的物件屬性而建立新物件:

const items = { first, second, ...others }
items //{ first: 1, second: 2, third: 3, fourth: 4, fifth: 5 }
複製程式碼

非同步迭代器

for-await-of 允許你使用非同步可迭代物件做為迴圈迭代:

for await (const line of readLines(filePath)) {
  console.log(line)
}
複製程式碼

因為它使用的了 await,因此你只能在 async 函式中使用它。

Promise.prototype.finally()

當一個 Promise 是 fulfilled 時,它會一個接一個的呼叫 then。

如果在這個過程中發生了錯誤,則會跳過 then 而執行 catch

finally() 允許你執行一些程式碼,無論是成功還是失敗:

fetch('file.json')
  .then(data => data.json())
  .catch(error => console.error(error))
  .finally(() => console.log('finished'))
複製程式碼

正規表示式改進

ES2018 對正規表示式引入了許多改進,這些都可以在 flaviocopes.com/javascript-… 上找到。

以下是關於 ES2018 正規表示式改進的具體補充:

RegExp lookbehind assertions: 根據前面的內容匹配字串

這是一個 lookahead: 你可以使用 ?= 來匹配字串,後面跟隨一個特定的字串:

/Roger(?=Waters)/
/Roger(?= Waters)/.test('Roger is my dog') //false
/Roger(?= Waters)/.test('Roger is my dog and Roger Waters is a famous musician') //true
複製程式碼

?! 可以執行逆操作,如果匹配的字串是no而不是在此後跟隨特定的子字串的話:

/Roger(?!Waters)/
/Roger(?! Waters)/.test('Roger is my dog') //true
/Roger(?! Waters)/.test('Roger Waters is a famous musician') //false
複製程式碼

Lookaheads 使用 ?= Symbol,它們已經可以用了。

Lookbehinds, 是一個新功能使用?<=.

/(?<=Roger) Waters/
/(?<=Roger) Waters/.test('Pink Waters is my dog') //false
/(?<=Roger) Waters/.test('Roger is my dog and Roger Waters is a famous musician') //true
複製程式碼

如果一個 lookbehind 是否定,那麼使用 ?>!:

/(?<!Roger) Waters/
/(?<!Roger) Waters/.test('Pink Waters is my dog') //true
/(?<!Roger) Waters/.test('Roger is my dog and Roger Waters is a famous musician') //false
複製程式碼

Unicode屬性轉義 \p{…} and \P{…}

在正規表示式模式中,你可以使用 \d 來匹配任意的數字,\s 來匹配任意不是空格的字串,\w 來匹配任意字母數字字串,以此類推。

This new feature extends this concept to all Unicode characters introducing \p{} and is negation \P{}.

這個新功能擴充套件了unicode字元,引入了 \p{} 來處理

任何 unicode 字元都有一組屬性,例如 script 確認語言,ASCII 是一個布林值用於檢查 ASCII 字元。你可以將此屬性方在() 中,正規表示式將來檢查是否為真。

/^\p{ASCII}+$/u.test('abc')   //✅
/^\p{ASCII}+$/u.test('ABC@')  //✅
/^\p{ASCII}+$/u.test('ABC?') //❌
複製程式碼

ASCII_Hex_Digit 是另一個布林值,用於檢查字串是否包含有效的十六進位制數字:

/^\p{ASCII_Hex_Digit}+$/u.test('0123456789ABCDEF') //✅
/^\p{ASCII_Hex_Digit}+$/u.test('h')                //❌
複製程式碼

此外,還有很多其它的屬性。你可以在()中新增它們的名字來檢查它們,包括 Uppercase, Lowercase, White_Space, Alphabetic, Emoji等等:

/^\p{Lowercase}$/u.test('h') //✅
/^\p{Uppercase}$/u.test('H') //✅
/^\p{Emoji}+$/u.test('H')   //❌
/^\p{Emoji}+$/u.test('??') //✅
複製程式碼

除了二進位制屬性外,你還可以檢查任何 unicode 字元屬性以匹配特定的值,在這個例子中,我檢查字串是用希臘語還是拉丁字母寫的:

/^\p{Script=Greek}+$/u.test('ελληνικά') //✅
/^\p{Script=Latin}+$/u.test('hey') //✅
複製程式碼

閱讀github.com/tc39/propos… 獲取使用所有屬性的詳細資訊。

Named capturing groups

In ES2018 a capturing group can be assigned to a name, rather than just being assigned a slot in the result array:

const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
const result = re.exec('2015-01-02')
// result.groups.year === '2015';
// result.groups.month === '01';
// result.groups.day === '02';
複製程式碼

The s flag for regular expressions

The s flag, short for single line, causes the . to match new line characters as well. Without it, the dot matches regular characters but not the new line:

/hi.welcome/.test('hi\nwelcome') // false
/hi.welcome/s.test('hi\nwelcome') // true
複製程式碼

ESNext

什麼是 ESNext ?

ESNext 是一個始終指向下一個版本 JavaScript 的名稱。

當前的 ECMAScript 版本是 ES2018,它於2018年6月被髮布。

歷史上 JavaScript 標準化的版本都是在夏季被髮布,因此我們可以預期 ECMAScript 2019 將於 2019 年的夏季被髮布。

所以在編寫本文時 ES2018 已經被髮布,因此 ESNext 指的是 ES2019。

ECMAScript 標準的提案是分階段組織的,第一到第三階段屬於功能性的孵化,第四階段的功能才最終確定為新標準的一部分。

在編寫本文時主要瀏覽器都實現了第四階段大部分的功能,因此我將在本文中介紹它們。

其中一些變化主要在內部使用,但知道發生了什麼這也很好。

第三階段還有一些其它功能,可能會在接下來的幾個月內升級到第四階段,你可以在這個 Github 倉庫中檢視它們:github.com/tc39/propos…

Array.prototype.{flat,flatMap}

flat() 是一個新的陣列例項方法,它可以將多維陣列轉化成一維陣列。

例子:

['Dog', ['Sheep', 'Wolf']].flat()
//[ 'Dog', 'Sheep', 'Wolf' ]
複製程式碼

預設情況下它只能將二維的陣列轉化成一維的陣列,但你可以新增一個引數來確定要展開的級別,如果你將這個引數設定為 Infinity 那麼它將展開無限的級別到一維陣列:

['Dog', ['Sheep', ['Wolf']]].flat()
//[ 'Dog', 'Sheep', [ 'Wolf' ] ]
['Dog', ['Sheep', ['Wolf']]].flat(2)
//[ 'Dog', 'Sheep', 'Wolf' ]
['Dog', ['Sheep', ['Wolf']]].flat(Infinity)
//[ 'Dog', 'Sheep', 'Wolf' ]
複製程式碼

如果你熟悉陣列的 map 方法,那麼你就知道使用它可以對陣列的每個元素執行一個函式。

flatMap() 是一個新的陣列例項方法,它將 flat()map 結合了起來,當你期望在map函式中做一些處理時這非常有用,同時又希望結果如同 flat

['My dog', 'is awesome'].map(words => words.split(' '))
//[ [ 'My', 'dog' ], [ 'is', 'awesome' ] ]
['My dog', 'is awesome'].flatMap(words => words.split(' '))
//[ 'My', 'dog', 'is', 'awesome' ]
複製程式碼

Optional catch binding

有時候我們並不需要將引數繫結到 try/catch 中。

在以前我們不得不這樣做:

try {
  //...
} catch (e) {
  //handle error
}
複製程式碼

即使我們從來沒有通過 e 來分析錯誤,但現在我們可以簡單的省略它:

try {
  //...
} catch {
  //handle error
}
複製程式碼

Object.fromEntries()

Objects have an entries() method, since ES2017.

從 ES2017 開始 Object將有一個 entries() 方法。

它將返回一個包含所有物件自身屬性的陣列的陣列,如[key, value]

const person = { name: 'Fred', age: 87 }
Object.entries(person) // [['name', 'Fred'], ['age', 87]]
複製程式碼

ES2019 引入了一個新的 Object.fromEntries() 方法,它可以從上述的屬性陣列中建立一個新的物件:

const person = { name: 'Fred', age: 87 }
const entries = Object.entries(person)
const newPerson = Object.fromEntries(entries)

person !== newPerson //true
複製程式碼

String.prototype.{trimStart,trimEnd}

這些功能已經被 v8/Chrome 實現了近一年的時間,它將在 ES2019 中實現標準化。

trimStart()

刪除字串首部的空格並返回一個新的字串:

'Testing'.trimStart() //'Testing'
' Testing'.trimStart() //'Testing'
' Testing '.trimStart() //'Testing '
'Testing'.trimStart() //'Testing'
複製程式碼

trimEnd()

刪除字串尾部的空格並返回一個新的字串:

'Testing'.trimEnd() //'Testing'
' Testing'.trimEnd() //' Testing'
' Testing '.trimEnd() //' Testing'
'Testing '.trimEnd() //'Testing'
複製程式碼

Symbol.prototype.description

現在你可以使用 description 來獲取 Symbol 的值,而不必使用 toString() 方法:

const testSymbol = Symbol('Test')
testSymbol.description // 'Test'
複製程式碼

JSON improvements

在此之前 JSON 字串中不允許使用分隔符(\u2028)和分隔符(\u2029)。

使用 JSON.parse 時,這些字元會導致一個 SyntaxError 錯誤,但現在它們可以正確的解析並如 JSON 標準定義的那樣。

Well-formed JSON.stringify()

修復 JSON.stringify() 在處理 UTF-8 code points (U+D800 to U+DFFF)。

在修復之前,呼叫 JSON.stringify() 將返回格式錯誤的 Unicode 字元,如(a "�")。

現在你可以安全放心的使用 JSON.stringify() 轉成字串,也可以使用 JSON.parse() 將它轉換回原始表示的形態。

Function.prototype.toString()

函式總會有一個 toString 方法,它將返回一個包含函式程式碼的字串。

ES2019 對返回值做了修改,以避免剝離註釋和其它字串(如:空格),將更準確的表示函式的定義。

If previously we had

以前也許我們這樣過:

function /* this is bar */ bar () {}
複製程式碼

當時的行為:

bar.toString() //'function bar() {}
複製程式碼

現在的行為:

bar.toString(); // 'function /* this is bar */ bar () {}'
複製程式碼

總結一下,我希望這篇文章可以幫助你瞭解一些最新的 JavaScript 以及我們在 2019 年即將看見的內容。

Click here to get a PDF / ePub / Mobi version of this post to read offline

Copyright

版權宣告: 閃電礦工翻譯組 譯文僅用於學習、研究和交流。版權歸 閃電礦工翻譯組、文章作者和譯者所有,歡迎非商業轉載。轉載前請聯絡譯者或 管理員 獲取授權,並在文章開頭明顯位置註明本文出處、譯者、校對者和閃電礦工翻譯組的完整連結,違者必究。

相關文章