> 翻譯:閃電礦工翻譯組
> 作者:Flavio?Copes
> 原文:https://medium.freecodecamp.org/es5-to-esnext-heres-every-feature-added-to-javascript-since-2015-d0c255e13c6e
>
這篇文章的出發點是為了幫助前端開發者串聯 ES6前后的 JavaScript 知識,并且可以快速了解 JavaScript 語言的最新進展。
JavaScript 在當下處于特權地位,因為它是唯一可以在瀏覽器中運行的語言,并且是被高度集成和優化過的。
JavaScript 在未來有著極好的發展空間,跟上它的變化不會比現在更加的困難。我的目標是讓你能夠快速且全面的了解這門語言可以使用的新內容。
#### 目錄
##### ECMAScript 簡介
##### ES2015
* let 和 const
* 箭頭函數
* 類
* 默認參數
* 模板字符串
* 解構賦值
* 增強的對象字面量
* For-of 循環
* Promises
* 模塊
* String 新方法
* Object 新方法
* 展開運算符
* Set
* Map
* Generators
##### ES2016
* Array.prototype.includes()
* 求冪運算符
##### ES2017
* 字符串填充
* Object.values()
* Object.entries()
* Object.getOwnPropertyDescriptors()
* 尾逗號
* 共享內存 and 原子操作
##### ES2018
* Rest/Spread Properties
* Asynchronous iteration
* Promise.prototype.finally()
* 正則表達式改進
##### ESNext
* Array.prototype.{flat,flatMap}
* try/catch 可選的參數綁定
* Object.fromEntries()
* String.prototype.{trimStart,trimEnd}
* Symbol.prototype.description
* JSON improvements
* Well-formed JSON.stringify()
* Function.prototype.toString()
#### 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 來定義版本號。
下圖比較清晰的展示了版本號與年份的關聯:

接下來,我們來深入了解 JavaScript 自 ES5 以來增加的特性。
#### let和const
ES2015 之前, var 是唯一可以用來聲明變量的語句。
~~~
var a = 0
~~~
上面語句如果你遺漏了 var,那么你會把這個值(0)賦給一個未聲明的變量,其中聲明和未聲明變量之間存在一些差異。
在現代瀏覽器開啟嚴格模式時,給未聲明的變量賦值會拋出 ReferenceError 異常,在較老的瀏覽器(或者禁用嚴格模式)的情況下,未聲明的變量在執行賦值操作時會隱式的變為全局對象的屬性。
當你聲明一個變量卻沒有進行初始化,那么它的值直到你對它進行賦值操作之前都是 undefined 。
~~~
var a //typeof a === 'undefined'
~~~
你可以對一個變量進行多次重新聲明,并覆蓋它:
~~~
var a = 1var a = 2
~~~
你也可以在一條聲明語句中一次聲明多個變量:
~~~
var a = 1, b = 2
~~~
作用域是變量可訪問的代碼部分。
在函數之外用 var 聲明的會分配給全局對象,這種變量可以在全局作用域中被訪問到。而在函數內部聲明的變量只能在函數局部作用域被訪問到,這類似于函數參數。
在函數中定義的局部變量名如何跟全局變量重名,那么局部變量的優先級更高,在函數內無法訪問到同名的全局變量。
需要注意的是,var 是沒有塊級作用域(標識符是一對花括號)的,但是 var 是有函數作用域的,所以在新創建的塊級作用域或者是函數作用域里面聲明變量會覆蓋全局同名變量,因為 var 在這兩種情況下沒有創建新的作用域。
在函數內部,其中定義的任何變量在所有函數代碼中都是可見的,因為JavaScript在執行代碼之前實際上將所有變量都移到了頂層(被稱為懸掛的東西)。 在函數的內部定義的變量在整個函數作用域中都是可見(可訪問),即使變量是在函數體末尾被聲明,但是仍然可以再函數體開頭部分被引用,因為 JavaScript存在變量提升機制。為避免混淆,請在函數開頭聲明變量,養成良好的編碼規范。
#### Using let
let 是ES2015中引入的新功能,它本質上是具有塊級作用域的 var 。它可以被當前作用域(函數以及塊級作用域)以及子級作用域訪問到。
現代 JavaScript 開發者在 let 和 var 的選擇中可能會更傾向于前者。
> 如果 let 看起來是一個很抽象的術語,當你閱讀到 let color = ‘red’ 這一段,因為使用 let 定義了color 為紅色,那么這一切就變的有意義了。
在任何函數之外用 let 聲明變量,和 var相反的是 它并不會創建全局變量。
#### Using const
使用變量 var 或 let 聲明的變量可以被重新賦值。 使用 const 聲明的變量一經初始化,它的值就永遠不能再改變,即不可重新被賦值。
~~~
const a = 'test'
~~~
我們不能再為 a 進行賦值操作。然而,a 如果它是一個具有屬性或者方法的對象,那么我們可以改變它的屬性或者方法。
const 并不意味著具有不可變性,只是保證用 const 聲明的變量的引用地址不被變更。
類似于 let,const 也具有塊級作用域。
現代 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方法,并將參數傳遞給此方法。
類聲明語句中也可以增加類需要的一些原型方法。在這種情況下 hello 是 Person 類的一個原型方法,可以在這個類的對象實例上調用:
~~~
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'
~~~
模板字符串使得定義多行字符串變得更加簡便。
一個模板字符串由一個反引號開始,你只需要按下回車鍵來創建新的一行,不需要插入特殊符號,最終的渲染效果如下所示:
~~~
const string = `Heythisstringis awesome!`
~~~
需要特別留意空格在這里是有特殊意義的,如果這樣做的話:
~~~
const string = `First Second`
~~~
那么它會創建出像下面的字符串:
~~~
First Second
~~~
有一個簡單的方法可以修復這個問題,只需要將第一行置為空,然后添加了右邊的翻譯好后調用一個 trim() 方法,就可以消除第一個字符前的所有空格:
~~~
const string = `FirstSecond`.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.button和gql模板標簽其實都是函數:
~~~
function gql(literals, ...expressions) {}
~~~
這個函數返回一個字符串,可以是任意類型的計算結果。
字面量(literals)是一個包含了表達式插值的模板字面量的序列。 表達式(expressions)包含了所有的插值。
舉個例子:
~~~
const string = `something ${1 + 2 + 3}`
~~~
這個例子里面的字面量是由2個部分組成的序列。第1部分就是something,也就是第一個插值位置(${})之前的字符串,第2部分就是一個空字符串,從第1個插值結束的位置直到字符串的結束。
這個例子里面的表達式就是只包含1個部分的序列,也就是6。
舉一個更復雜的例子:
~~~
const string = `somethinganother ${'x'}new line ${1 + 2 + 3}test`
~~~
這個例子里面的字面量的序列里面,第1個部分是:
~~~
;`somethinganother `
~~~
第2部分是:
~~~
;`new line `
~~~
第3部分是:
~~~
;`test`
~~~
這個例子里面的表達式包含了2個部分:x和6。
拿到了這些值的函數就可以對其做任意處理,這就是這個特性的威力所在。
比如最簡單的處理就是字符串插值,把字面量和表達式拼接起來:
~~~
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
~~~
name和age就包含了對應的值。
這個語法同樣可以用到數組當中:
~~~
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 valuefor (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中,比如:
* 電池API
* Fetch API
* Service Workers
在現代的JavaScript中,不使用promise是不太可能的,所以我們來深入研究下promise吧。
##### 創建一個promise
Promise API暴露了一個Promise構造函數,可以通過new Promise()來初始化:
~~~
let done = trueconst 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。
通過resolve和reject,我們可以得到一個返回值,返回值可以是字符串也可以是對象。
##### 使用一個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)})// ornew 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.jsexport 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 = 1const b = 2const c = 3export { 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 模塊的文章,可以看一下:?https://glitch.com/edit/#!/flavio-es-modules-example?path=index.html
#### 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:
* a 和 b 是同一個對象
* a 和 b 是相等的字符串(用同樣的字符組合在一起的字符串是相等的)
* a 和 b 是相等的數字
* a 和 b 都是 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 //Fiestacopied.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__ = animalmammal.isAnimal //trueconst dog = Object.create(animal)dog.isAnimal //trueconsole.log(dog.isMammal) //undefinedObject.setPrototypeOf(dog, mammal)dog.isAnimal //truedog.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 + econst sum = sum(...numbers)
~~~
ES2018 引入了 剩余屬性 ,同樣的操作符但是只能用在對象上。
#### 剩余屬性(Rest properties):
~~~
const { first, second, ...others } = { first: 1, second: 2, third: 3, fourth: 4, fifth: 5}first // 1second // 2others // { 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') //trues.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 } 對象的形式返回每一個元素,直到迭代結束,這時 done 是 true。
你也可以調用 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()]// orconst a = [...s.values()]
~~~
##### WeakSet
一個 WeakSet 是一個特殊的 Set.
在 set 中,元素不會被 gc(垃圾回收)。一個 weakSet 讓它的所有元素都是可以被 gc 的。weakSet 中的每個鍵都是一個對象。當這個對象的引用消失的時候,對應的值就可以被 gc 了。
下面是主要的不同點:
* WeakSet 不可迭代
* 你不能清空 weakSet 中的所有元素
* 不能夠得到 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']) //redconsole.log(car.color) //redconsole.log(car.owner) //Flavioconsole.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) //testconst 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的主要不同處:
* 你不可以在WeakMap上迭代keys值和values值(或者key-value鍵值對)
* 你不可以從WeakMap上清除所有條目
##### 你不可以獲取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等其它多種語言)。

這些都是2016年引入的特性,現在讓我們進入2017年。
#### 字符串填充
字符串填充的目的是給字符串添加字符,以使其達到指定長度。
ES2017引入了兩個String方法:padStart()和padEnd()。
~~~
padStart(targetLength [, padString])padEnd(targetLength [, padString])
~~~
使用例子:

#### 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中打印出如下結果:
~~~
BeforeAfterI 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 + econst sum = sum(...numbers)
~~~
ES2018 為對象引入了同樣的功能。
解構:
~~~
const { first, second, ...others } = { first: 1, second: 2, third: 3, fourth: 4, fifth: 5 }first // 1second // 2others // { 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 對正則表達式引入了許多改進,這些都可以在?https://flaviocopes.com/javascript-regular-expressions/?上找到。
以下是關于 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') //?
~~~
閱讀https://github.com/tc39/proposal-regexp-unicode-property-escapes?獲取使用所有屬性的詳細信息。
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 expressionsThe 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 倉庫中查看它們:https://github.com/tc39/proposals。
~~~
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 //trueString.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 年即將看見的內容。
- 以專業工程師的標準要求自己
- JS
- 函數的this
- 函數的argument
- 函數的apply、bind、call方法
- 創建對象
- 構造函數創建對象
- 原型結合構造函數創建對象
- 原型
- 繼承
- 閉包
- 正則表達式
- Ajax
- 設計模式
- ES6
- es6的模塊化
- 定義變量的新方式
- 函數擴展
- 數組擴展
- 性能與工程化
- 關于http與緩存
- 重排(reflow、layout)與重繪
- 頁面性能
- gulp
- webpack
- 一些項目回顧總結
- 移動端&微信H5游戲
- 微信小程序
- Vue.js
- 隨手記錄
- 如何通過前端技能獲取2018世界杯門票
- jsonp
- es6 javascript對象方法Object.assign()
- 一份不錯的基礎面試題
- vscode常用插件
- koroFileHeader
- 構建自己的Js工具庫
- H5 game
- Phaser從入坑到放棄再入坑
- 1.游戲的創建
- 2.資源的加載
- 3.phaser中的舞臺,世界和攝像機
- 4.游戲縮放控制,移動端的適配
- 5.phaser中的顯示對象
- 1.概述
- 2.phaser中的圖片,圖形,和按鈕
- 3.phaser中的精靈
- 4.文字
- 5.組
- 6.phaser中的動畫
- 7.粒子和瓦片地圖
- 8.瓦片地圖
- lodash
- ES5 to ESNext?—?here’s every feature added to JavaScript since 2015
- 防抖(debounce) 和 節流(throttling)