細解JavaScript ES7 ES8 ES9 新特性

拓跋zhleven發表於2018-11-28

導言:ECMAScript的演化不會停止,但是我們完全沒必要害怕。除了ES6這個史無前例的版本帶來了海量的資訊和知識點以外,之後每年一發的版本都僅僅帶有少量的增量更新,一年更新的東西花半個小時就能搞懂了,完全沒必要畏懼。本文將帶您花大約一個小時左右的時間,迅速過一遍ES7,ES8,ES9的新特性。

想追求更好的閱讀體驗,請移步原文地址

題記:本文提供了一個線上PPT版本,方便您瀏覽 細解JAVASCRIPT ES7 ES8 ES9 新特性 線上PPT ver

本文的大部分內容譯自作者Axel Rauschmayer博士的網站,想了解更多關於作者的資訊,可以瀏覽Exploring JS: JavaScript books for programmers

那些與ECMAScript有關的事情

誰在設計ECMAScript?

TC39 (Technical Committee 39)。

TC39 是推進 JavaScript 發展的委員會。其會員都是公司(其中主要是瀏覽器廠商)。TC39 定期召開會議,會議由會員公司的代表與特邀專家出席。會議紀錄都可在網上檢視,可以讓你對 TC39 如何工作有一個清晰的概念。

很有意思的是,TC39 實行的是協商一致的原則:通過一項決議必須得到每一位會員(公司代表)的贊成。

ECMAScript的釋出週期

在2015年釋出的 ECMAScript(ES6)新增內容很多,在 ES5 釋出近 6 年(2009-11 至 2015-6)之後才將其標準化。兩個釋出版本之間時間跨度如此之大主要有兩大原因:

  • 比新版率先完成的特性,必須等待新版的完成才能釋出。
  • 那些需要花長時間完成的特性,也頂著很大的壓力被納入這一版本,因為如果推遲到下一版本釋出意味著又要等很久,這種特性也會推遲新的釋出版本。

因此,從 ECMAScript 2016(ES7)開始,版本釋出變得更加頻繁,每年釋出一個新版本,這麼一來新增內容也會更小。新版本將會包含每年截止時間之前完成的所有特性。

ECMAScript的釋出流程

每個 ECMAScript 特性的建議將會從階段 0 開始, 然後經過下列幾個成熟階段。其中從一個階段到下一個階段必須經過 TC39 的批准。

  1. stage-0 - Strawman: just an idea, possible Babel plugin. 任何討論、想法、改變或者還沒加到提案的特性都在這個階段。只有TC39成員可以提交。

當前的stage 0列表可以檢視這裡 --> Stage 0 Proposals

  1. stage-1 - Proposal: this is worth working on.

    **什麼是 Proposal?**一份新特性的正式建議文件。提案必須指明此建議的潛在問題,例如與其他特性之間的關聯,實現難點等。

  2. stage-2 - Draft: initial spec.

    **什麼是 Draft?**草案是規範的第一個版本。其與最終標準中包含的特性不會有太大差別。

草案之後,原則上只接受增量修改。這個階段開始實驗如何實現,實現形式包括polyfill, 實現引擎(提供草案執行本地支援),或者編譯轉換(例如babel)

  1. stage-3 - Candidate: complete spec and initial browser implementations.

候選階段,獲得具體實現和使用者的反饋。此後,只有在實現和使用過程中出現了重大問題才會修改。至少要在一個瀏覽器中實現,提供polyfill或者babel外掛。

  1. stage-4 - Finished: will be added to the next yearly release.

已經準備就緒,該特性會出現在下個版本的ECMAScript規範之中。

當前的stage 1-3列表可以檢視這裡 --> ECMAScript proposals

已經正式釋出的特性索引

Proposal Author Champion(s) TC39 meeting notes Expected Publication Year
Array.prototype.includes Domenic Denicola Domenic Denicola
Rick Waldron
November 2015 2016
Exponentiation operator Rick Waldron Rick Waldron January 2016 2016
Object.values/Object.entries Jordan Harband Jordan Harband March 2016 2017
String padding Jordan Harband Jordan Harband
Rick Waldron
May 2016 2017
Object.getOwnPropertyDescriptors Jordan Harband
Andrea Giammarchi
Jordan Harband
Andrea Giammarchi
May 2016 2017
Trailing commas in function parameter lists and calls Jeff Morrison Jeff Morrison July 2016 2017
Async functions Brian Terlson Brian Terlson July 2016 2017
Shared memory and atomics Lars T Hansen Lars T Hansen January 2017 2017
Lifting template literal restriction Tim Disney Tim Disney March 2017 2018
s (dotAll) flag for regular expressions Mathias Bynens Brian Terlson
Mathias Bynens
November 2017 2018
RegExp named capture groups Gorkem Yakin
Daniel Ehrenberg
Daniel Ehrenberg
Brian Terlson
Mathias Bynens
November 2017 2018
Rest/Spread Properties Sebastian Markbåge Sebastian Markbåge January 2018 2018
RegExp Lookbehind Assertions Gorkem Yakin
Nozomu Katō
Daniel Ehrenberg
Daniel Ehrenberg
Mathias Bynens
January 2018 2018
RegExp Unicode Property Escapes Mathias Bynens Brian Terlson
Daniel Ehrenberg
Mathias Bynens
January 2018 2018
Promise.prototype.finally Jordan Harband Jordan Harband January 2018 2018
Asynchronous Iteration Domenic Denicola Domenic Denicola January 2018 2018
Optional catch binding Michael Ficarra Michael Ficarra May 2018 2019
JSON superset Richard Gibson Mark Miller
Mathias Bynens
May 2018 2019

ES7新特性(ECMAScript 2016)

ECMAScript 2016

ES7在ES6的基礎上主要新增了兩項內容:

  • Array.prototype.includes()方法
  • 求冪運算子(**)

Array.prototype.includes()方法

includes() 方法用來判斷一個陣列是否包含一個指定的值,根據情況,如果包含則返回 true,否則返回false。

var array = [1, 2, 3];

console.log(array.includes(2));
// expected output: true

var pets = ['cat', 'dog', 'bat'];

console.log(pets.includes('cat'));
// expected output: true

console.log(pets.includes('at'));
// expected output: false
複製程式碼

Array.prototype.includes()方法接收兩個引數:

  • 要搜尋的值
  • 搜尋的開始索引。

當第二個引數被傳入時,該方法會從索引處開始往後搜尋(預設索引值為0)。若搜尋值在陣列中存在則返回true,否則返回false。 且看下面示例:

['a', 'b', 'c', 'd'].includes('b')         // true
['a', 'b', 'c', 'd'].includes('b', 1)      // true
['a', 'b', 'c', 'd'].includes('b', 2)      // false
複製程式碼

乍一看,includes的作用跟陣列的indexOf重疊,為什麼要特意增加這麼一個api呢?主要區別有以下幾點:

  • 返回值。看一個函式,先看他們的返回值。indexOf的返回數是值型的,includes的返回值是布林型,所以在if條件判斷的時候includes要簡單得多,而indexOf 需要多寫一個條件進行判斷。
var ary = [1];
if (ary.indexOf(1) !== -1) {
    console.log("陣列存在1")
}
if (ary.includes(1)) {
    console.log("陣列存在1")
}
複製程式碼
  • NaN的判斷。如果陣列中有NaN,你又正好需要判斷陣列是否有存在NaN,這時你使用indexOf是無法判斷的,你必須使用includes這個方法。
var ary1 = [NaN];
console.log(ary1.indexOf(NaN))//-1
console.log(ary1.includes(NaN))//true
複製程式碼
  • 當陣列的有空的值的時候,includes會認為空的值是undefined,而indexOf不會。
var ary1 = new Array(3);
console.log(ary1.indexOf(undefined));//-1
console.log(ary1.includes(undefined))//true
複製程式碼

求冪運算子(**)

加/減法我們通常都是用其中綴形式,直觀易懂。在ECMAScript2016中,我們可以使用**來替代Math.pow。

4 ** 3           // 64
複製程式碼

效果等同於

Math.pow(4,3)
複製程式碼

值得一提的是,作為中綴運算子,**還支援以下操作

let n = 4;
n **= 3;
// 64
複製程式碼

ES8新特性(ECMAScript 2017)

ECMAScript 2017

在2017年1月的TC39會議上,ECMAScript 2017的最後一個功能“Shared memory and atomics”推進到第4階段。這意味著它的功能集現已完成。

ECMAScript 2017特性一覽

主要新功能:

  • 非同步函式 Async Functions(Brian Terlson)
  • 共享記憶體和Atomics(Lars T. Hansen)

次要新功能:

  • Object.values / Object.entries(Jordan Harband)
  • String padding(Jordan Harband,Rick Waldron)
  • Object.getOwnPropertyDescriptors() (Jordan Harband,Andrea Giammarchi)
  • 函式引數列表和呼叫中的尾逗號(Jeff Morrison)

Async Functions

Async Functions也就是我們常說的Async/Await,相信大家對於這個概念都已經不陌生了。Async/Await是一種用於處理JS非同步操作的語法糖,可以幫助我們擺脫回撥地獄,編寫更加優雅的程式碼。

通俗的理解,async關鍵字的作用是告訴編譯器對於標定的函式要區別對待。當編譯器遇到標定的函式中的await關鍵字時,要暫時停止執行,帶到await標定的函式處理完畢後,再進行相應操作。如果該函式fulfiled了,則返回值是fulfillment value,否則得到的就是reject value。

下面通過拿普通的promise寫法來對比,就很好理解了:

async function asyncFunc() {
    const result = await otherAsyncFunc();
    console.log(result);
}

// Equivalent to:
function asyncFunc() {
    return otherAsyncFunc()
    .then(result => {
        console.log(result);
    });
}
複製程式碼

按順序處理多個非同步函式的時候優勢更為明顯:

async function asyncFunc() {
    const result1 = await otherAsyncFunc1();
    console.log(result1);
    const result2 = await otherAsyncFunc2();
    console.log(result2);
}

// Equivalent to:
function asyncFunc() {
    return otherAsyncFunc1()
    .then(result1 => {
        console.log(result1);
        return otherAsyncFunc2();
    })
    .then(result2 => {
        console.log(result2);
    });
}
複製程式碼

並行處理多個非同步函式:

async function asyncFunc() {
    const [result1, result2] = await Promise.all([
        otherAsyncFunc1(),
        otherAsyncFunc2(),
    ]);
    console.log(result1, result2);
}

// Equivalent to:
function asyncFunc() {
    return Promise.all([
        otherAsyncFunc1(),
        otherAsyncFunc2(),
    ])
    .then([result1, result2] => {
        console.log(result1, result2);
    });
}
複製程式碼

處理錯誤:

async function asyncFunc() {
    try {
        await otherAsyncFunc();
    } catch (err) {
        console.error(err);
    }
}

// Equivalent to:
function asyncFunc() {
    return otherAsyncFunc()
    .catch(err => {
        console.error(err);
    });
}
複製程式碼

Async Functions若是要展開去講,可以佔用很大段的篇幅。鑑於本文是一篇介紹性文章,再次不再進行深入。

SharedArrayBuffer和Atomics

,如果之前您沒有接觸過ArrayBuffer相關知識的話,建議您從記憶體管理速成教程系列漫畫解說入門,強推: A crash course in memory management A cartoon intro to ArrayBuffers and SharedArrayBuffers Avoiding race conditions in SharedArrayBuffers with Atomics


ECMAScript 2017 特性 SharedArrayBuffer 和 atomics”,由Lars T. Hansen設計。它引入了一個新的建構函式 SharedArrayBuffer 和 具有輔助函式的名稱空間物件 Atomics。

在我們開始之前,讓我們澄清兩個相似但截然不同的術語:並行(Parallelism) 和 併發(Concurrency) 。他們存在許多定義,我使用的定義如下

  • 並行(Parallelism) (parallel 並行 vs. serial 序列):同時執行多個任務;
  • 併發(Concurrency) (concurrent 併發 vs. sequential 連續):在重疊的時間段內(而不是一個接一個)執行幾個任務。

JS並行的歷史

  • JavaScript 在單執行緒中執行。某些任務可以非同步執行:瀏覽器通常會在單執行緒中執行這些任務,然後通過回撥將結果重新加入到單執行緒中。
  • Web workers 將任務並行引入了 JavaScript :這些是相對重量級的程式。每個 workers 都有自己的全域性環境。預設情況下,不共享任何內容。 workers 之間的通訊(或在 workers 和主執行緒之間的通訊)發展:
    • 起初,你只能傳送和接收字串。
    • 然後,引入結構化克隆:可以傳送和接收資料副本。結構化克隆適用於大多數資料(JSON 資料,TypedArray,正規表示式,Blob物件,ImageData物件等)。它甚至可以正確處理物件之間的迴圈引用。但是,不能克隆 error 物件,function 物件和 DOM 節點。
    • 可在 workers 之間的轉移資料:當接收方獲得資料時,傳送方失去訪問許可權。
  • 通過 WebGL 使用 GPU 計算(它傾向於資料並行處理)

共享陣列緩衝區(Shared Array Buffers)

共享陣列緩衝區是更高併發抽象的基本構建塊。它們允許您在多個 workers 和主執行緒之間共享 SharedArrayBuffer 物件的位元組(該緩衝區是共享的,用於訪問位元組,將其封裝在一個 TypedArray 中)這種共享有兩個好處:

你可以更快地在 workers 之間共享資料。 workers 之間的協調變得更簡單和更快(與 postMessage() 相比)。

// main.js
const worker = new Worker('worker.js');

// 要分享的buffer
const sharedBuffer = new SharedArrayBuffer( // (A)
    10 * Int32Array.BYTES_PER_ELEMENT); // 10 elements

// 使用Worker共用sharedBuffer
worker.postMessage({sharedBuffer}); // clone

// 僅限本地使用
const sharedArray = new Int32Array(sharedBuffer); // (B)
複製程式碼

建立一個共享陣列緩衝區(Shared Array Buffers)的方法與建立普通的陣列緩衝區(Array Buffer)類似:通過呼叫建構函式,並以位元組的形式指定緩衝區的大小(行A)。你與 workers 共享的是 緩衝區(buffer) 。對於你自己的本地使用,你通常將共享陣列緩衝區封裝在 TypedArray 中(行B)。

workers的實現如下所列。

// worker.js
self.addEventListener('message', function (event) {
    const {sharedBuffer} = event.data;
    const sharedArray = new Int32Array(sharedBuffer); // (A)
    // ···
});
複製程式碼

sharedArrayBuffer 的 API

建構函式:

  • new SharedArrayBuffer(length) 建立一個 length 位元組的 buffer(緩衝區)。

靜態屬性:

  • get SharedArrayBuffer[Symbol.species] 預設情況下返回 this。 覆蓋以控制 slice() 的返回。

例項屬性:

  • get SharedArrayBuffer.prototype.byteLength() 返回 buffer(緩衝區) 的位元組長度。

  • SharedArrayBuffer.prototype.slice(start, end) 建立一個新的 this.constructor[Symbol.species] 例項,並用位元組填充從(包括)開始到(不包括)結束的索引。

Atomics: 安全訪問共享資料

舉一個例子

// main.js
sharedArray[1] = 11;
sharedArray[2] = 22;
複製程式碼

在單執行緒中,您可以重新排列這些寫入操作,因為在中間沒有讀到任何內容。 對於多執行緒,當你期望以特定順序執行寫入操作時,就會遇到麻煩:

// worker.js
while (sharedArray[2] !== 22) ;
console.log(sharedArray[1]); // 0 or 11
複製程式碼

Atomics 方法可以用來與其他 workers 進行同步。例如,以下兩個操作可以讓你讀取和寫入資料,並且不會被編譯器重新排列:

  • Atomics.load(ta : TypedArray, index)
  • Atomics.store(ta : TypedArray, index, value : T)

這個想法是使用常規操作讀取和寫入大多數資料,而 Atomics 操作(load ,store 和其他操作)可確保讀取和寫入安全。通常,您將使用自定義同步機制,例如鎖,其實現基於Atomics。

這是一個非常簡單的例子,它總是有效的:

// main.js
console.log('notifying...');
Atomics.store(sharedArray, 0, 123);

// worker.js
while (Atomics.load(sharedArray, 0) !== 123) ;
console.log('notified');
複製程式碼

Atomics 的 API

Atomic 函式的主要運算元必須是 Int8Array ,Uint8Array ,Int16Array ,Uint16Array ,Int32Array 或 Uint32Array 的一個例項。它必須包裹一個 SharedArrayBuffer 。

所有函式都以 atomically 方式進行操作。儲存操作的順序是固定的並且不能由編譯器或 CPU 重新排序。

載入和儲存

  • Atomics.load(ta : TypedArray, index) : T 讀取和返回 ta[index] 上的元素,返回陣列指定位置上的值。
  • Atomics.store(ta : TypedArray, index, value : T) : T 在 ta[index] 上寫入 value,並且返回 value。
  • Atomics.exchange(ta : TypedArray, index, value : T) : T 將 ta[index] 上的元素設定為 value ,並且返回索引 index 原先的值。
  • Atomics.compareExchange(ta : TypedArray, index, expectedValue, replacementValue) : T 如果 ta[index] 上的當前元素為 expectedValue , 那麼使用 replacementValue 替換。並且返回索引 index 原先(或者未改變)的值。

簡單修改 TypeArray 元素

以下每個函式都會在給定索引處更改 TypeArray 元素:它將一個操作符應用於元素和引數,並將結果寫回元素。它返回元素的原始值。

  • Atomics.add(ta : TypedArray, index, value) : T 執行 ta[index] += value 並返回 ta[index] 的原始值。
  • Atomics.sub(ta : TypedArray, index, value) : T 執行 ta[index] -= value 並返回 ta[index] 的原始值。
  • Atomics.and(ta : TypedArray, index, value) : T 執行 ta[index] &= value 並返回 ta[index] 的原始值。
  • Atomics.or(ta : TypedArray, index, value) : T 執行 ta[index] |= value 並返回 ta[index] 的原始值。
  • Atomics.xor(ta : TypedArray, index, value) : T 執行 ta[index] ^= value 並返回 ta[index] 的原始值。

等待和喚醒

  • Atomics.wait(ta: Int32Array, index, value, timeout=Number.POSITIVE_INFINITY) : ('not-equal' | 'ok' | 'timed-out') 如果 ta[index] 的當前值不是 value ,則返回 'not-equal'。否則繼續等待,直到我們通過 Atomics.wake() 喚醒或直到等待超時。 在前一種情況下,返回 'ok'。在後一種情況下,返回'timed-out'。timeout 以毫秒為單位。記住此函式執行的操作:“如果 ta[index] 為 value,那麼繼續等待” 。
  • Atomics.wake(ta : Int32Array, index, count) 喚醒等待在 ta[index] 上的 count workers。

Object.values and Object.entries

Object.values() 方法返回一個給定物件自己的所有可列舉屬性值的陣列,值的順序與使用for...in迴圈的順序相同 ( 區別在於for-in迴圈列舉原型鏈中的屬性 )。

obj引數是需要待操作的物件。可以是一個物件,或者一個陣列(是一個帶有數字下標的物件,[10,20,30] -> {0: 10,1: 20,2: 30})。

const obj = { x: 'xxx', y: 1 };
Object.values(obj); // ['xxx', 1]

const obj = ['e', 's', '8']; // 相當於 { 0: 'e', 1: 's', 2: '8' };
Object.values(obj); // ['e', 's', '8']

// 當我們使用數字鍵值時,返回的是數字排序
// 根據鍵值排序
const obj = { 10: 'xxx', 1: 'yyy', 3: 'zzz' };
Object.values(obj); // ['yyy', 'zzz', 'xxx']

Object.values('es8'); // ['e', 's', '8']
複製程式碼

Object.entries 方法返回一個給定物件自身可遍歷屬性 [key, value] 的陣列, 排序規則和 Object.values 一樣。這個方法的宣告比較瑣碎:

const obj = { x: 'xxx', y: 1 };
Object.entries(obj); // [['x', 'xxx'], ['y', 1]]

const obj = ['e', 's', '8'];
Object.entries(obj); // [['0', 'e'], ['1', 's'], ['2', '8']]

const obj = { 10: 'xxx', 1: 'yyy', 3: 'zzz' };
Object.entries(obj); // [['1', 'yyy'], ['3', 'zzz'], ['10': 'xxx']]

Object.entries('es8'); // [['0', 'e'], ['1', 's'], ['2', '8']]
複製程式碼

String padding

為 String 物件增加了 2 個函式:padStart 和 padEnd。

像它們名字那樣,這幾個函式的主要目的就是填補字串的首部和尾部,為了使得到的結果字串的長度能達到給定的長度。你可以通過特定的字元,或者字串,或者預設的空格填充它。下面是函式的宣告:

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

這些函式的第一個引數是 targetLength(目標長度),這個是結果字串的長度。第二個引數是可選的 padString(填充字元),一個用於填充到源字串的字串。預設值是空格。

'es8'.padStart(2);          // 'es8'
'es8'.padStart(5);          // '  es8'
'es8'.padStart(6, 'woof');  // 'wooes8'
'es8'.padStart(14, 'wow');  // 'wowwowwowwoes8'
'es8'.padStart(7, '0');     // '0000es8'

'es8'.padEnd(2);            // 'es8'
'es8'.padEnd(5);            // 'es8  '
'es8'.padEnd(6, 'woof');    // 'es8woo'
'es8'.padEnd(14, 'wow');    // 'es8wowwowwowwo'
'es8'.padEnd(7, '6');       // 'es86666'
複製程式碼

Object.getOwnPropertyDescriptors

getOwnPropertyDescriptors 方法返回指定物件所有自身屬性的描述物件。屬性描述物件是直接在物件上定義的,而不是繼承於物件的原型。ES2017加入這個函式的主要動機在於方便將一個物件深度拷貝給另一個物件,同時可以將getter/setter拷貝。宣告如下:

Object.getOwnPropertyDescriptors(obj)
複製程式碼

obj 是待操作物件。返回的描述物件鍵值有:configurable, enumerable, writable, get, set and value。

const obj = { 
  get es7() { return 777; },
  get es8() { return 888; }
};
Object.getOwnPropertyDescriptor(obj);
// {
//   es7: {
//     configurable: true,
//     enumerable: true,
//     get: function es7(){}, //the getter function
//     set: undefined
//   },
//   es8: {
//     configurable: true,
//     enumerable: true,
//     get: function es8(){}, //the getter function
//     set: undefined
//   }
// }
複製程式碼

結尾逗號

結尾逗號用程式碼展示非常明瞭:

// 引數定義時
function foo(
    param1,
    param2,
) {}

// 函式呼叫時
foo(
    'abc',
    'def',
);

// 物件中
let obj = {
    first: 'Jane',
    last: 'Doe',
};

// 陣列中
let arr = [
    'red',
    'green',
    'blue',
];
複製程式碼

這個改動有什麼好處呢?

  • 首先,重新排列專案更簡單,因為如果最後一項更改其位置,則不必新增和刪除逗號。
  • 其次,它可以幫助版本控制系統跟蹤實際發生的變化。例如,從:
[
    'foo'
]
複製程式碼

修改為

[
    'foo',
    'bar'
]
複製程式碼

導致線條'foo'和線條'bar'被標記為已更改,即使唯一真正的變化是後一條線被新增。

ES9新特性(ECMAScript 2018)

ECMAScript 2018

ES9的新特性索引如下:

主要新功能:

  • 非同步迭代(Domenic Denicola,Kevin Smith)
  • Rest/Spread 屬性(SebastianMarkbåge)

新的正規表示式功能:

  • RegExp named capture groups(Gorkem Yakin,Daniel Ehrenberg)
  • RegExp Unicode Property Escapes(Mathias Bynens)
  • RegExp Lookbehind Assertions(Gorkem Yakin,NozomuKatō,Daniel Ehrenberg)
  • s (dotAll) flag for regular expressions(Mathias Bynens)

其他新功能:

  • Promise.prototype.finally() (Jordan Harband)
  • 模板字串修改(Tim Disney)

非同步迭代

首先來回顧一下同步迭代器:

ES6引入了同步迭代器,其工作原理如下:

  • Iterable:一個物件,表示可以通過Symbol.iterator方法進行迭代。
  • Iterator:通過呼叫iterable [Symbol.iterator] ()返回的物件。它將每個迭代元素包裝在一個物件中,並通過其next()方法一次返回一個。
  • IteratorResult:返回的物件next()。屬性value包含一個迭代的元素,屬性done是true 後最後一個元素。

示例:

const iterable = ['a', 'b'];
const iterator = iterable[Symbol.iterator]();
iterator.next()
// { value: 'a', done: false }
iterator.next()
// { value: 'b', done: false }
iterator.next()
// { value: undefined, done: true }
複製程式碼

非同步迭代器

先前的迭代方式是同步的,並不適用於非同步資料來源。例如,在以下程式碼中,readLinesFromFile()無法通過同步迭代傳遞其非同步資料:

for (const line of readLinesFromFile(fileName)) {
    console.log(line);
}
複製程式碼

非同步迭代器和常規迭代器的工作方式非常相似,但是非同步迭代器涉及promise:

async function example() {
  // 普通迭代器:
  const iterator = createNumberIterator();
  iterator.next(); // Object {value: 1, done: false}
  iterator.next(); // Object {value: 2, done: false}
  iterator.next(); // Object {value: 3, done: false}
  iterator.next(); // Object {value: undefined, done: true}

  // 非同步迭代器:
  const asyncIterator = createAsyncNumberIterator();
  const p = asyncIterator.next(); // Promise
  await p;// Object {value: 1, done: false}
  await asyncIterator.next(); // Object {value: 2, done: false}
  await asyncIterator.next(); // Object {value: 3, done: false}
  await asyncIterator.next(); // Object {value: undefined, done: true}
}
複製程式碼

非同步迭代器物件的next()方法返回了一個Promise,解析後的值跟普通的迭代器類似。 用法:iterator.next().then(({ value, done })=> {//{value: ‘some val’, done: false}}

const promises = [
    new Promise(resolve => resolve(1)),
    new Promise(resolve => resolve(2)),
    new Promise(resolve => resolve(3)),
];

async function test() {
    for await (const p of promises) {
        console.log(p);
    }
}
test(); //1 ,2 3
複製程式碼

Rest/Spread 屬性

這個就是我們通常所說的rest引數和擴充套件運算子,這項特性在ES6中已經引入,但是ES6中的作用物件僅限於陣列:

restParam(1, 2, 3, 4, 5);

function restParam(p1, p2, ...p3) {
  // p1 = 1
  // p2 = 2
  // p3 = [3, 4, 5]
}

const values = [99, 100, -1, 48, 16];
console.log( Math.max(...values) ); // 100
複製程式碼

在ES9中,為物件提供了像陣列一樣的rest引數和擴充套件運算子:

const obj = {
  a: 1,
  b: 2,
  c: 3
}
const { a, ...param } = obj;
  console.log(a)     //1
  console.log(param) //{b: 2, c: 3}

function foo({a, ...param}) {
  console.log(a);    //1
  console.log(param) //{b: 2, c: 3}
}
複製程式碼

正規表示式命名捕獲組

編號的捕獲組

//正規表示式命名捕獲組
const RE_DATE = /([0-9]{4})-([0-9]{2})-([0-9]{2})/;

const matchObj = RE_DATE.exec('1999-12-31');
const year = matchObj[1]; // 1999
const month = matchObj[2]; // 12
const day = matchObj[3]; // 31
複製程式碼

通過數字引用捕獲組有幾個缺點:

  • 找到捕獲組的數量是一件麻煩事:必須使用括號。
  • 如果要了解組的用途,則需要檢視正規表示式。
  • 如果更改捕獲組的順序,則還必須更改匹配程式碼。

命名的捕獲組

ES9中可以通過名稱來識別捕獲組:(?<year>[0-9]{4})

在這裡,我們用名稱標記了前一個捕獲組year。該名稱必須是合法的JavaScript識別符號(認為變數名稱或屬性名稱)。匹配後,您可以通過訪問捕獲的字串matchObj.groups.year來訪問。

讓我們重寫前面的程式碼:

const RE_DATE = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;

const matchObj = RE_DATE.exec('1999-12-31');
const year = matchObj.groups.year; // 1999
const month = matchObj.groups.month; // 12
const day = matchObj.groups.day; // 31

// 使用解構語法更為簡便
const {groups: {day, year}} = RE_DATE.exec('1999-12-31');
console.log(year); // 1999
console.log(day); // 31
複製程式碼

可以發現,命名捕獲組有以下優點:

  • 找到捕獲組的“ID”更容易。
  • 匹配程式碼變為自描述性的,因為捕獲組的ID描述了正在捕獲的內容。
  • 如果更改捕獲組的順序,則無需更改匹配程式碼。
  • 捕獲組的名稱也使正規表示式更容易理解,因為您可以直接看到每個組的用途。

正規表示式 Unicode 轉義

該特性允許您使用\p{}通過提及大括號內的Unicode字元屬性來匹配字元,在正規表示式中使用標記 u (unicode) 設定。

/^\p{White_Space}+$/u.test('\t \n\r')
// true
/^\p{Script=Greek}+$/u.test('μετά')
// true
複製程式碼

新方法匹配中文字元

由於在Unicode裡面,中文字元對應的Unicode Script是Han,於是我們就可以用這個reg來匹配中文:

/\p{Script=Han}/u

這樣我們就可以不用記憶繁瑣又不好記的/[\u4e00-\u9fa5]/了,況且這個表示式已經有些年頭了,說實話,後來又新增的屬性為Han的字元並不在這個範圍內,因此這個有年頭reg並不一定好使。

我隨便從網上找了一個Unicode8.0新增的中文字元“錀”,我測了一下兩種reg的相容性:

oldReg=/[\u4e00-\u9fa5]/
newReg=/\p{Script=Han}/u

oldReg.test('abc')
// false
newReg.test('abc')
// false

oldReg.test('地平線')
// true
newReg.test('地平線')
// true

oldReg.test('錀')
// false
newReg.test('錀')
// true
複製程式碼

http://www.unicode.org/charts/PDF/U4E00.pdf

可以參考一下這個PDF,是Unicode的漢字全集,從524頁9FA6至526頁(最後一頁)用舊匹配方式都無法生效。

Unicode

一些對於Unicode的科普

  • Name:唯一名稱,由大寫字母,數字,連字元和空格組成。例如:

    • A: Name = LATIN CAPITAL LETTER A
    • ?: Name = GRINNING FACE
  • General_Category:對字元進行分類。例如:

    • X: General_Category = Lowercase_Letter
    • $: General_Category = Currency_Symbol
  • White_Space:用於標記不可見的間距字元,例如空格,製表符和換行符。例如:

    • \ T: White_Space = True
    • π: White_Space = False
  • Age:引入字元的Unicode標準版本。例如:歐元符號€在Unicode標準的2.1版中新增。

    • €: Age = 2.1
  • Script:是一個或多個書寫系統使用的字符集合。

    • 有些指令碼支援多種寫入系統。例如,拉丁文指令碼支援英語,法語,德語,拉丁語等書寫系統。
    • 某些語言可以用多個指令碼支援的多個備用寫入系統編寫。例如,土耳其語在20世紀初轉換為拉丁文字之前使用了阿拉伯文字。
    • 例子:
      • α: Script = Greek
      • Д: Script = Cyrillic

正規表示式的Unicode屬性轉義

  • 匹配其屬性prop具有值的所有字元value:

    \p{prop=value}

  • 匹配所有沒有屬性prop值的字元value:

    \P{prop=value}

  • 匹配二進位制屬性bin_prop為True的所有字元:

    \p{bin_prop}

  • 匹配二進位制屬性bin_prop為False的所有字元:

    \P{bin_prop}

  • 匹配空格:

/^\p{White_Space}+$/u.test('\t \n\r')
//true
複製程式碼

匹配字母:

/^\p{Letter}+$/u.test('πüé')
//true
複製程式碼

匹配希臘字母:

/^\p{Script=Greek}+$/u.test('μετά')
//true
複製程式碼

匹配拉丁字母:

/^\p{Script=Latin}+$/u.test('Grüße')
//true
複製程式碼

正規表示式反向斷言

先來看下正規表示式先行斷言是什麼:

如獲取貨幣的符號

const noReLookahead = /\D(\d+)/,
      reLookahead = /\D(?=\d+)/,
      match1 = noReLookahead.exec('$123.45'),
      match2 = reLookahead.exec('$123.45');
console.log(match1[0]); // $123   
console.log(match2[0]); // $
複製程式碼

在ES9中可以允許反向斷言:

const reLookahead = /(?<=\D)[\d\.]+/;
      match = reLookahead.exec('$123.45');
console.log(match[0]); // 123.45
複製程式碼

使用?<=進行反向斷言,可以使用反向斷言獲取貨幣的價格,而忽略貨幣符號。

正規表示式dotAll模式

正規表示式中點.匹配除回車外的任何單字元,標記s改變這種行為,允許行終止符的出現,例如:

/hello.world/.test('hello\nworld');  // false
/hello.world/s.test('hello\nworld'); // true
複製程式碼

Promise.prototype.finally()

這個基本沒什麼好講的,看名字就能看懂了。其用法如下:

promise
  .then(result => {···})
  .catch(error => {···})
  .finally(() => {···});
複製程式碼

finally的回撥總會被執行。

模板字串修改

ES2018 移除對 ECMAScript 在帶標籤的模版字串中轉義序列的語法限制。 之前,\u開始一個 unicode 轉義,\x開始一個十六進位制轉義,\後跟一個數字開始一個八進位制轉義。這使得建立特定的字串變得不可能,例如Windows檔案路徑 C:\uuu\xxx\111。

要取消轉義序列的語法限制,可在模板字串之前使用標記函式String.raw:

`\u{54}`
// "T"
String.raw`\u{54}`
// "\u{54}"
複製程式碼

尾聲

ECMAScript的演化不會停止,但是我們完全沒必要害怕。除了ES6這個史無前例的版本帶來了海量的資訊和知識點以外,之後每年一發的版本都僅僅帶有少量的增量更新,一年更新的東西花半個小時就能搞懂了,完全沒必要畏懼。

Stay hungry. Stay foolish.

相關文章