從ES6到ES10的新特性萬字大總結(不得不收藏)

陳大魚頭發表於2020-07-01

介紹

ECMAScript是一種由Ecma國際(前身為歐洲計算機制造商協會)在標準ECMA-262中定義的指令碼語言規範。這種語言在全球資訊網上應用廣泛,它往往被稱為JavaScript或JScript,但實際上後兩者是ECMA-262標準的實現和擴充套件。

歷史版本

至發稿日為止有九個ECMA-262版本發表。其歷史版本如下:

  1. 1997年6月:第一版
  2. 1998年6月:修改格式,使其與ISO/IEC16262國際標準一樣
  3. 1999年12月:強大的正規表示式,更好的詞法作用域鏈處理,新的控制指令,異常處理,錯誤定義更加明確,資料輸出的格式化及其它改變
  4. 2009年12月:新增嚴格模式("use strict")。修改了前面版本模糊不清的概念。增加了getters,setters,JSON以及在物件屬性上更完整的反射。
  5. 2011年6月:ECMAScript標5.1版形式上完全一致於國際標準ISO/IEC 16262:2011。
  6. 2015年6月:ECMAScript 2015(ES2015),第 6 版,最早被稱作是 ECMAScript 6(ES6),新增了類和模組的語法,其他特性包括迭代器,Python風格的生成器和生成器表示式,箭頭函式,二進位制資料,靜態型別陣列,集合(maps,sets 和 weak maps),promise,reflection 和 proxies。作為最早的 ECMAScript Harmony 版本,也被叫做ES6 Harmony。
  7. 2016年6月:ECMAScript 2016(ES2016),第 7 版,多個新的概念和語言特性。
  8. 2017年6月:ECMAScript 2017(ES2017),第 8 版,多個新的概念和語言特性。
  9. 2018年6月:ECMAScript 2018 (ES2018),第 9 版,包含了非同步迴圈,生成器,新的正規表示式特性和 rest/spread 語法。
  10. 2019年6月:ECMAScript 2019 (ES2019),第 10 版。

發展標準

TC39(Technical Committee 39)是一個推動JavaScript發展的委員會,它的成語來自各個主流瀏覽器的代表成語。會議實行多數決,每一項決策只有大部分人同意且沒有強烈反對才能去實現。

TC39成員制定著ECMAScript的未來。

每一項新特性最終要進入到ECMAScript規範裡,需要經歷5個階段,這5個階段如下:

  • Stage 0: Strawperson

    只要是TC39成員或者貢獻者,都可以提交想法

  • Stage 1: Proposal

    這個階段確定一個正式的提案

  • Stage 2: draft

    規範的第一個版本,進入此階段的提案大概率會成為標準

  • Stage 3: Candidate

    進一步完善提案細則

  • Stage 4: Finished

    表示已準備好將其新增到正式的ECMAScript標準中

由於ES6以前的屬性誕生年底久遠,我們使用也比較普遍,遂不進行說明,ES6之後的語言風格跟ES5以前的差異比較大,所以單獨拎出來做個記錄。

ES6(ES2015)

ES6是一次重大的革新,比起過去的版本,改動比較大,本文僅對常用的API以及語法糖進行講解。

Let 和 Const

在ES6以前,JS只有var一種宣告方式,但是在ES6之後,就多了letconst這兩種方式。用var定義的變數沒有塊級作用域的概念,而letconst則會有,因為這三個關鍵字建立是不一樣的。

區別如下:

{
    var a = 10
    let b = 20
    const c = 30
}
a // 10
b // Uncaught ReferenceError: b is not defined
c // c is not defined
let d = 40
const e = 50
d = 60
d // 60
e = 70 // VM231:1 Uncaught TypeError: Assignment to constant variable.
var let const
變數提升 × ×
全域性變數 × ×
重複宣告 × ×
重新賦值 ×
暫時死區 ×
塊作用域 ×
只宣告不初始化 ×

類(Class)

在ES6之前,如果我們要生成一個例項物件,傳統的方法就是寫一個建構函式,例子如下:

function Person(name, age) {
    this.name = name
    this.age = age
}
Person.prototype.information = function () {
    return 'My name is ' + this.name + ', I am ' + this.age
}

但是在ES6之後,我們只需要寫成以下形式:

class Person {
    constructor(name, age) {
        this.name = name
        this.age = age
    }
    information() {
        return 'My name is ' + this.name + ', I am ' + this.age
    }
}

箭頭函式(Arrow function)

箭頭函式表示式的語法比函式表示式更簡潔,並且沒有自己的thisargumentssupernew.target。這些函式表示式更適用於那些本來需要匿名函式的地方,並且它們不能用作建構函式。

在ES6以前,我們寫函式一般是:

var list = [1, 2, 3, 4, 5, 6, 7]
var newList = list.map(function (item) {
    return item * item
})

但是在ES6裡,我們可以:

const list = [1, 2, 3, 4, 5, 6, 7]
const newList = list.map(item => item * item)

看,是不是簡潔了不少

函式引數預設值(Function parameter defaults)

在ES6之前,如果我們寫函式需要定義初始值的時候,需要這麼寫:

function config (data) {
    var data = data || 'data is empty'
}

這樣看起來也沒有問題,但是如果引數的布林值為falsy時就會出問題,例如我們這樣呼叫config:

config(0)
config('')

那麼結果就永遠是後面的值

如果我們用函式引數預設值就沒有這個問題,寫法如下:

const config = (data = 'data is empty') => {}

模板字串(Template string)

在ES6之前,如果我們要拼接字串,則需要像這樣:

var name = 'kris'
var age = 24
var info = 'My name is ' + this.name + ', I am ' + this.age

但是在ES6之後,我們只需要寫成以下形式:

const name = 'kris'
const age = 24
const info = `My name is ${name}, I am ${age}`

解構賦值(Destructuring assignment)

我們通過解構賦值, 可以將屬性/值從物件/陣列中取出,賦值給其他變數。

比如我們需要交換兩個變數的值,在ES6之前我們可能需要:

var a = 10
var b = 20
var temp = a
a = b
b = temp

但是在ES6裡,我們有:

let a = 10
let b = 20
[a, b] = [b, a]

是不是方便很多

模組化(Module)

在ES6之前,JS並沒有模組化的概念,有的也只是社群定製的類似CommonJS和AMD之類的規則。例如基於CommonJS的NodeJS:

// circle.js
// 輸出
const { PI } = Math
exports.area = (r) => PI * r ** 2
exports.circumference = (r) => 2 * PI * r

// index.js
// 輸入
const circle = require('./circle.js')
console.log(`半徑為 4 的圓的面積是 ${circle.area(4)}`)

在ES6之後我們則可以寫成以下形式:

// circle.js
// 輸出
const { PI } = Math
export const area = (r) => PI * r ** 2
export const circumference = (r) => 2 * PI * r

// index.js
// 輸入
import {
    area
} = './circle.js'
console.log(`半徑為 4 的圓的面積是: ${area(4)}`)

擴充套件操作符(Spread operator)

擴充套件操作符可以在函式呼叫/陣列構造時, 將陣列表示式或者string在語法層面展開;還可以在構造字面量物件時, 將物件表示式按key-value的方式展開。

比如在ES5的時候,我們要對一個陣列的元素進行相加,在不使用reduce或者reduceRight的場合,我們需要:

function sum(x, y, z) {
    return x + y + z;
}
var list = [5, 6, 7]
var total = sum.apply(null, list)

但是如果我們使用擴充套件操作符,只需要如下:

const sum = (x, y, z) => x + y + z
const list = [5, 6, 7]
const total = sum(...list)

非常的簡單,但是要注意的是擴充套件操作符只能用於可迭代物件

如果是下面的情況,是會報錯的:

var obj = {'key1': 'value1'}
var array = [...obj] // TypeError: obj is not iterable

物件屬性簡寫(Object attribute shorthand)

在ES6之前,如果我們要將某個變數賦值為同樣名稱的物件元素,則需要:

var cat = 'Miaow'
var dog = 'Woof'
var bird = 'Peet peet'

var someObject = {
  cat: cat,
  dog: dog,
  bird: bird
}

但是在ES6裡我們就方便很多:

let cat = 'Miaow'
let dog = 'Woof'
let bird = 'Peet peet'

let someObject = {
  cat,
  dog,
  bird
}

console.log(someObject)

//{
//  cat: "Miaow",
//  dog: "Woof",
//  bird: "Peet peet"
//}

非常方便

Promise

Promise 是ES6提供的一種非同步解決方案,比回撥函式更加清晰明瞭。

Promise 翻譯過來就是承諾的意思,這個承諾會在未來有一個確切的答覆,並且該承諾有三種狀態,分別是:

  1. 等待中(pending)
  2. 完成了 (resolved)
  3. 拒絕了(rejected)

這個承諾一旦從等待狀態變成為其他狀態就永遠不能更改狀態了,也就是說一旦狀態變為 resolved 後,就不能再次改變

new Promise((resolve, reject) => {
  resolve('success')
  // 無效
  reject('reject')
})

當我們在構造 Promise 的時候,建構函式內部的程式碼是立即執行的

new Promise((resolve, reject) => {
  console.log('new Promise')
  resolve('success')
})
console.log('finifsh')
// new Promise -> finifsh

Promise 實現了鏈式呼叫,也就是說每次呼叫 then 之後返回的都是一個 Promise,並且是一個全新的 Promise,原因也是因為狀態不可變。如果你在 then 中 使用了 return,那麼 return 的值會被 Promise.resolve() 包裝

Promise.resolve(1)
  .then(res => {
    console.log(res) // => 1
    return 2 // 包裝成 Promise.resolve(2)
  })
  .then(res => {
    console.log(res) // => 2
  })

當然了,Promise 也很好地解決了回撥地獄的問題,例如:

ajax(url, () => {
    // 處理邏輯
    ajax(url1, () => {
        // 處理邏輯
        ajax(url2, () => {
            // 處理邏輯
        })
    })
})

可以改寫成:

ajax(url)
  .then(res => {
      console.log(res)
      return ajax(url1)
  }).then(res => {
      console.log(res)
      return ajax(url2)
  }).then(res => console.log(res))

for...of

for...of語句在可迭代物件(包括 Array,Map,Set,String,TypedArray,arguments 物件等等)上建立一個迭代迴圈,呼叫自定義迭代鉤子,併為每個不同屬性的值執行語句。

例子如下:

const array1 = ['a', 'b', 'c'];

for (const element of array1) {
      console.log(element)
}

// "a"
// "b"
// "c"

Symbol

symbol 是一種基本資料型別,Symbol()函式會返回symbol型別的值,該型別具有靜態屬性和靜態方法。它的靜態屬性會暴露幾個內建的成員物件;它的靜態方法會暴露全域性的symbol註冊,且類似於內建物件類,但作為建構函式來說它並不完整,因為它不支援語法:"new Symbol()"。

每個從Symbol()返回的symbol值都是唯一的。一個symbol值能作為物件屬性的識別符號;這是該資料型別僅有的目的。

例子如下:

const symbol1 = Symbol();
const symbol2 = Symbol(42);
const symbol3 = Symbol('foo');

console.log(typeof symbol1); // "symbol"
console.log(symbol3.toString()); // "Symbol(foo)"
console.log(Symbol('foo') === Symbol('foo')); // false

迭代器(Iterator)/ 生成器(Generator)

迭代器(Iterator)是一種迭代的機制,為各種不同的資料結構提供統一的訪問機制。任何資料結構只要內部有 Iterator 介面,就可以完成依次迭代操作。

一旦建立,迭代器物件可以通過重複呼叫next()顯式地迭代,從而獲取該物件每一級的值,直到迭代完,返回{ value: undefined, done: true }

雖然自定義的迭代器是一個有用的工具,但由於需要顯式地維護其內部狀態,因此需要謹慎地建立。生成器函式提供了一個強大的選擇:它允許你定義一個包含自有迭代演算法的函式, 同時它可以自動維護自己的狀態。 生成器函式使用 function*語法編寫。 最初呼叫時,生成器函式不執行任何程式碼,而是返回一種稱為Generator的迭代器。 通過呼叫生成器的下一個方法消耗值時,Generator函式將執行,直到遇到yield關鍵字。

可以根據需要多次呼叫該函式,並且每次都返回一個新的Generator,但每個Generator只能迭代一次。

所以我們可以有以下例子:

function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
    for (let i = start; i < end; i += step) {
        yield i;
    }
}
var a = makeRangeIterator(1,10,2)
a.next() // {value: 1, done: false}
a.next() // {value: 3, done: false}
a.next() // {value: 5, done: false}
a.next() // {value: 7, done: false}
a.next() // {value: 9, done: false}
a.next() // {value: undefined, done: true}

Set/WeakSet

Set 物件允許你儲存任何型別的唯一值,無論是原始值或者是物件引用。

所以我們可以通過Set實現陣列去重

const numbers = [2,3,4,4,2,3,3,4,4,5,5,6,6,7,5,32,3,4,5]
console.log([...new Set(numbers)]) 
// [2, 3, 4, 5, 6, 7, 32]

WeakSet 結構與 Set 類似,但區別有以下兩點:

  • WeakSet 物件中只能存放物件引用, 不能存放值, 而 Set 物件都可以。
  • WeakSet 物件中儲存的物件值都是被弱引用的, 如果沒有其他的變數或屬性引用這個物件值, 則這個物件值會被當成垃圾回收掉. 正因為這樣, WeakSet 物件是無法被列舉的, 沒有辦法拿到它包含的所有元素。

所以程式碼如下:

var ws = new WeakSet()
var obj = {}
var foo = {}

ws.add(window)
ws.add(obj)

ws.has(window) // true
ws.has(foo)    // false, 物件 foo 並沒有被新增進 ws 中 

ws.delete(window) // 從集合中刪除 window 物件
ws.has(window)    // false, window 物件已經被刪除了

ws.clear() // 清空整個 WeakSet 物件

Map/WeakMap

Map 物件儲存鍵值對。任何值(物件或者原始值) 都可以作為一個鍵或一個值。

例子如下,我們甚至可以使用NaN來作為鍵值:

var myMap = new Map();
myMap.set(NaN, "not a number");

myMap.get(NaN); // "not a number"

var otherNaN = Number("foo");
myMap.get(otherNaN); // "not a number"

WeakMap 物件是一組鍵/值對的集合,其中的鍵是弱引用的。其鍵必須是物件,而值可以是任意的。

Map的區別與SetWeakSet的區別相似,具體程式碼如下:

var wm1 = new WeakMap(),
    wm2 = new WeakMap(),
    wm3 = new WeakMap();
var o1 = {},
    o2 = function(){},
    o3 = window;

wm1.set(o1, 37);
wm1.set(o2, "azerty");
wm2.set(o1, o2); // value可以是任意值,包括一個物件
wm2.set(o3, undefined);
wm2.set(wm1, wm2); // 鍵和值可以是任意物件,甚至另外一個WeakMap物件
wm1.get(o2); // "azerty"
wm2.get(o2); // undefined,wm2中沒有o2這個鍵
wm2.get(o3); // undefined,值就是undefined

wm1.has(o2); // true
wm2.has(o2); // false
wm2.has(o3); // true (即使值是undefined)

wm3.set(o1, 37);
wm3.get(o1); // 37
wm3.clear();
wm3.get(o1); // undefined,wm3已被清空
wm1.has(o1);   // true
wm1.delete(o1);
wm1.has(o1);   // false

Proxy/Reflect

Proxy 物件用於定義基本操作的自定義行為(如屬性查詢,賦值,列舉,函式呼叫等)。

Reflect 是一個內建的物件,它提供攔截 JavaScript 操作的方法。這些方法與 Proxy 的方法相同。Reflect不是一個函式物件,因此它是不可構造的。

ProxyReflect是非常完美的配合,例子如下:

const observe = (data, callback) => {
      return new Proxy(data, {
            get(target, key) {
                return Reflect.get(target, key)
            },
            set(target, key, value, proxy) {
                  callback(key, value);
                  target[key] = value;
                    return Reflect.set(target, key, value, proxy)
            }
      })
}

const FooBar = { open: false };
const FooBarObserver = observe(FooBar, (property, value) => {
  property === 'open' && value 
          ? console.log('FooBar is open!!!') 
          : console.log('keep waiting');
});
console.log(FooBarObserver.open) // false
FooBarObserver.open = true // FooBar is open!!!

當然也不是什麼都可以被代理的,如果物件帶有configurable: falsewritable: false 屬性,則代理失效。

Regex物件的擴充套件

正則新增符號

  • i 修飾符

    // i 修飾符
    /[a-z]/i.test('\u212A') // false
    /[a-z]/iu.test('\u212A') // true
  • y修飾符

    // y修飾符
    var s = 'aaa_aa_a';
    var r1 = /a+/g;
    var r2 = /a+/y;
    
    r1.exec(s) // ["aaa"]
    r2.exec(s) // ["aaa"]
    
    r1.exec(s) // ["aa"]
    r2.exec(s) // null
  • String.prototype.flags

    // 檢視RegExp建構函式的修飾符
    var regex = new RegExp('xyz', 'i')
    regex.flags // 'i'
  • unicode模式

    var s = '?'
    /^.$/.test(s) // false
    /^.$/u.test(s) // true
  • u轉義

    // u轉義
    /\,/ // /\,/
    /\,/u // 報錯 沒有u修飾符時,逗號前面的反斜槓是無效的,加了u修飾符就報錯。
  • 引用

    const RE_TWICE = /^(?<word>[a-z]+)!\k<word>$/;
    RE_TWICE.test('abc!abc') // true
    RE_TWICE.test('abc!ab') // false
    
    const RE_TWICE = /^(?<word>[a-z]+)!\1$/;
    RE_TWICE.test('abc!abc') // true
    RE_TWICE.test('abc!ab') // false

字串方法的實現改為呼叫RegExp方法

  • String.prototype.match 呼叫 RegExp.prototype[Symbol.match]
  • String.prototype.replace 呼叫 RegExp.prototype[Symbol.replace]
  • String.prototype.search 呼叫 RegExp.prototype[Symbol.search]
  • String.prototype.split 呼叫 RegExp.prototype[Symbol.split]

正則新增屬性

  • RegExp.prototype.sticky 表示是否有y修飾符

    /hello\d/y.sticky // true
  • RegExp.prototype.flags獲取修飾符

    /abc/ig.flags // 'gi'

Math物件的擴充套件

  • 二進位制表示法 : 0b或0B開頭表示二進位制(0bXX0BXX)
  • 二進位制表示法 : 0b或0B開頭表示二進位制(0bXX0BXX)
  • 八進位制表示法 : 0o或0O開頭表示二進位制(0oXX0OXX)
  • Number.EPSILON : 數值最小精度
  • Number.MIN_SAFE_INTEGER : 最小安全數值(-2^53)
  • Number.MAX_SAFE_INTEGER : 最大安全數值(2^53)
  • Number.parseInt() : 返回轉換值的整數部分
  • Number.parseFloat() : 返回轉換值的浮點數部分
  • Number.isFinite() : 是否為有限數值
  • Number.isNaN() : 是否為NaN
  • Number.isInteger() : 是否為整數
  • Number.isSafeInteger() : 是否在數值安全範圍內
  • Math.trunc() : 返回數值整數部分
  • Math.sign() : 返回數值型別(正數1負數-1零0)
  • Math.cbrt() : 返回數值立方根
  • Math.clz32() : 返回數值的32位無符號整數形式
  • Math.imul() : 返回兩個數值相乘
  • Math.fround() : 返回數值的32位單精度浮點數形式
  • Math.hypot() : 返回所有數值平方和的平方根
  • Math.expm1() : 返回e^n - 1
  • Math.log1p() : 返回1 + n的自然對數(Math.log(1 + n))
  • Math.log10() : 返回以10為底的n的對數
  • Math.log2() : 返回以2為底的n的對數
  • Math.sinh() : 返回n的雙曲正弦
  • Math.cosh() : 返回n的雙曲餘弦
  • Math.tanh() : 返回n的雙曲正切
  • Math.asinh() : 返回n的反雙曲正弦
  • Math.acosh() : 返回n的反雙曲餘弦
  • Math.atanh() : 返回n的反雙曲正切

Array物件的擴充套件

  • Array.prototype.from:轉換具有Iterator介面的資料結構為真正陣列,返回新陣列。

    console.log(Array.from('foo')) // ["f", "o", "o"]
    console.log(Array.from([1, 2, 3], x => x + x)) // [2, 4, 6]
  • Array.prototype.of():轉換一組值為真正陣列,返回新陣列。

    Array.of(7)       // [7] 
    Array.of(1, 2, 3) // [1, 2, 3]
    
    Array(7)          // [empty, empty, empty, empty, empty, empty]
    Array(1, 2, 3)    // [1, 2, 3]
  • Array.prototype.copyWithin():把指定位置的成員複製到其他位置,返回原陣列

    const array1 = ['a', 'b', 'c', 'd', 'e']
    
    console.log(array1.copyWithin(0, 3, 4)) // ["d", "b", "c", "d", "e"]
    
    console.log(array1.copyWithin(1, 3)) // ["d", "d", "e", "d", "e"]
  • Array.prototype.find():返回第一個符合條件的成員

    const array1 = [5, 12, 8, 130, 44]
    
    const found = array1.find(element => element > 10)
    
    console.log(found) // 12
  • Array.prototype.findIndex():返回第一個符合條件的成員索引值

    const array1 = [5, 12, 8, 130, 44]
    
    const isLargeNumber = (element) => element > 13
    
    console.log(array1.findIndex(isLargeNumber)) // 3
  • Array.prototype.fill():根據指定值填充整個陣列,返回原陣列

    const array1 = [1, 2, 3, 4]
    
    console.log(array1.fill(0, 2, 4)) // [1, 2, 0, 0]
    
    console.log(array1.fill(5, 1)) // [1, 5, 5, 5]
    
    console.log(array1.fill(6)) // [6, 6, 6, 6]
  • Array.prototype.keys():返回以索引值為遍歷器的物件

    const array1 = ['a', 'b', 'c']
    const iterator = array1.keys()
    
    for (const key of iterator) {
          console.log(key)
    }
    
    // 0
    // 1
    // 2
  • Array.prototype.values():返回以屬性值為遍歷器的物件

    const array1 = ['a', 'b', 'c']
    const iterator = array1.values()
    
    for (const key of iterator) {
          console.log(key)
    }
    
    // a
    // b
    // c
  • Array.prototype.entries():返回以索引值和屬性值為遍歷器的物件

    const array1 = ['a', 'b', 'c']
    const iterator = array1.entries()
    
    console.log(iterator.next().value) // [0, "a"]
    console.log(iterator.next().value) // [1, "b"]
  • 陣列空位:ES6明確將陣列空位轉為undefined或者empty

    Array.from(['a',,'b']) // [ "a", undefined, "b" ]
    [...['a',,'b']] // [ "a", undefined, "b" ]
    Array(3) //  [empty × 3]
    [,'a'] // [empty, "a"]

ES7(ES2016)

Array.prototype.includes()

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

程式碼如下:

const array1 = [1, 2, 3]
console.log(array1.includes(2)) // true

const pets = ['cat', 'dog', 'bat']
console.log(pets.includes('cat')) // true
console.log(pets.includes('at')) // false

冪運算子**

冪運算子**,具有與Math.pow()一樣的功能,程式碼如下:

console.log(2**10) // 1024
console.log(Math.pow(2, 10)) // 1024

模板字串(Template string)

自ES7起,帶標籤的模版字面量遵守以下轉義序列的規則:

  • Unicode字元以"u"開頭,例如\u00A9
  • Unicode碼位用"u{}"表示,例如\u{2F804}
  • 十六進位制以"x"開頭,例如\xA9
  • 八進位制以""和數字開頭,例如\251

這表示類似下面這種帶標籤的模版是有問題的,因為對於每一個ECMAScript語法,解析器都會去查詢有效的轉義序列,但是隻能得到這是一個形式錯誤的語法:

latex`\unicode`
// 在較老的ECMAScript版本中報錯(ES2016及更早)
// SyntaxError: malformed Unicode character escape sequence

ES8(ES2017)

async/await

雖然Promise可以解決回撥地獄的問題,但是鏈式呼叫太多,則會變成另一種形式的回撥地獄 —— 麵條地獄,所以在ES8裡則出現了Promise的語法糖async/await,專門解決這個問題。

我們先看一下下面的Promise程式碼:

fetch('coffee.jpg')
    .then(response => response.blob())
    .then(myBlob => {
          let objectURL = URL.createObjectURL(myBlob)
          let image = document.createElement('img')
          image.src = objectURL
          document.body.appendChild(image)
    })
    .catch(e => {
          console.log('There has been a problem with your fetch operation: ' + e.message)
    })

然後再看看async/await版的,這樣看起來是不是更清晰了。

async function myFetch() {
      let response = await fetch('coffee.jpg')
      let myBlob = await response.blob()

      let objectURL = URL.createObjectURL(myBlob)
      let image = document.createElement('img')
      image.src = objectURL
      document.body.appendChild(image)
}

myFetch()

當然,如果你喜歡,你甚至可以兩者混用

async function myFetch() {
      let response = await fetch('coffee.jpg')
      return await response.blob()
}

myFetch().then((blob) => {
      let objectURL = URL.createObjectURL(blob)
      let image = document.createElement('img')
      image.src = objectURL
      document.body.appendChild(image)
})

Object.values()

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

程式碼如下:

const object1 = {
      a: 'somestring',
      b: 42,
      c: false
}
console.log(Object.values(object1)) // ["somestring", 42, false]

Object.entries()

Object.entries()方法返回一個給定物件自身可列舉屬性的鍵值對陣列,其排列與使用 for...in 迴圈遍歷該物件時返回的順序一致(區別在於 for-in 迴圈還會列舉原型鏈中的屬性)。

程式碼如下:

const object1 = {
      a: 'somestring',
      b: 42
}

for (let [key, value] of Object.entries(object1)) {
      console.log(`${key}: ${value}`)
}

// "a: somestring"
// "b: 42"

padStart()

padStart() 方法用另一個字串填充當前字串(重複,如果需要的話),以便產生的字串達到給定的長度。填充從當前字串的開始(左側)應用的。

程式碼如下:

const str1 = '5'
console.log(str1.padStart(2, '0')) // "05"

const fullNumber = '2034399002125581'
const last4Digits = fullNumber.slice(-4)
const maskedNumber = last4Digits.padStart(fullNumber.length, '*') 
console.log(maskedNumber) // "************5581"

padEnd()

padEnd() 方法會用一個字串填充當前字串(如果需要的話則重複填充),返回填充後達到指定長度的字串。從當前字串的末尾(右側)開始填充。

const str1 = 'Breaded Mushrooms'
console.log(str1.padEnd(25, '.')) // "Breaded Mushrooms........"
const str2 = '200'
console.log(str2.padEnd(5)) // "200  "

函式引數結尾逗號(Function parameter lists and calls trailing commas)

在ES5裡就新增了物件的尾逗號,不過並不支援函式引數,但是在ES8之後,便開始支援這一特性,程式碼如下:

// 引數定義
function f(p) {}
function f(p,) {} 

(p) => {}
(p,) => {}

class C {
  one(a,) {},
  two(a, b,) {},
}

var obj = {
  one(a,) {},
  two(a, b,) {},
};

// 函式呼叫
f(p)
f(p,)

Math.max(10, 20)
Math.max(10, 20,)

但是以下的方式是不合法的:

僅僅包含逗號的函式引數定義或者函式呼叫會丟擲 SyntaxError。 而且,當使用剩餘引數的時候,並不支援尾後逗號,例子如下:

function f(,) {} // SyntaxError: missing formal parameter
(,) => {}       // SyntaxError: expected expression, got ','
f(,)             // SyntaxError: expected expression, got ','

function f(...p,) {} // SyntaxError: parameter after rest parameter
(...p,) => {}        // SyntaxError: expected closing parenthesis, got ','

在解構裡也可以使用,程式碼如下:

// 帶有尾後逗號的陣列解構
[a, b,] = [1, 2]

// 帶有尾後逗號的物件解構
var o = {
  p: 42, 
  q: true,
}
var {p, q,} = o

同樣地,在使用剩餘引數時,會丟擲 SyntaxError,程式碼如下:

var [a, ...b,] = [1, 2, 3] // SyntaxError: rest element may not have a trailing comma

ShareArrayBuffer(因安全問題,暫時在Chrome跟FireFox中被禁用)

SharedArrayBuffer 物件用來表示一個通用的,固定長度的原始二進位制資料緩衝區,類似於 ArrayBuffer 物件,它們都可以用來在共享記憶體(shared memory)上建立檢視。與 ArrayBuffer 不同的是,SharedArrayBuffer 不能被分離。

程式碼如下:

let sab = new SharedArrayBuffer(1024) // 必須例項化
worker.postMessage(sab)

Atomics物件

Atomics物件 提供了一組靜態方法用來對 SharedArrayBuffer 物件進行原子操作。

方法如下:

  • Atomics.add() :將指定位置上的陣列元素與給定的值相加,並返回相加前該元素的值。
  • Atomics.and():將指定位置上的陣列元素與給定的值相與,並返回與操作前該元素的值。
  • Atomics.compareExchange():如果陣列中指定的元素與給定的值相等,則將其更新為新的值,並返回該元素原先的值。
  • Atomics.exchange():將陣列中指定的元素更新為給定的值,並返回該元素更新前的值。
  • Atomics.load():返回陣列中指定元素的值。
  • Atomics.or():將指定位置上的陣列元素與給定的值相或,並返回或操作前該元素的值。
  • Atomics.store():將陣列中指定的元素設定為給定的值,並返回該值。
  • Atomics.sub():將指定位置上的陣列元素與給定的值相減,並返回相減前該元素的值。
  • Atomics.xor():將指定位置上的陣列元素與給定的值相異或,並返回異或操作前該元素的值。
  • Atomics.wait():檢測陣列中某個指定位置上的值是否仍然是給定值,是則保持掛起直到被喚醒或超時。返回值為 "ok"、"not-equal" 或 "time-out"。呼叫時,如果當前執行緒不允許阻塞,則會丟擲異常(大多數瀏覽器都不允許在主執行緒中呼叫 wait())。
  • Atomics.wake():喚醒等待佇列中正在陣列指定位置的元素上等待的執行緒。返回值為成功喚醒的執行緒數量。
  • Atomics.isLockFree(size):可以用來檢測當前系統是否支援硬體級的原子操作。對於指定大小的陣列,如果當前系統支援硬體級的原子操作,則返回 true;否則就意味著對於該陣列,Atomics 物件中的各原子操作都只能用鎖來實現。此函式面向的是技術專家。

Object.getOwnPropertyDescriptors()

Object.getOwnPropertyDescriptors() 方法用來獲取一個物件的所有自身屬性的描述符。程式碼如下:

const object1 = {
  property1: 42
}

const descriptors1 = Object.getOwnPropertyDescriptors(object1)

console.log(descriptors1.property1.writable) // true

console.log(descriptors1.property1.value) // 42

// 淺拷貝一個物件
Object.create(
  Object.getPrototypeOf(obj), 
  Object.getOwnPropertyDescriptors(obj) 
)

// 建立子類
function superclass() {}
superclass.prototype = {
  // 在這裡定義方法和屬性
}
function subclass() {}
subclass.prototype = Object.create(superclass.prototype, Object.getOwnPropertyDescriptors({
  // 在這裡定義方法和屬性
}))

ES9(ES2018)

for await...of

for await...of 語句會在非同步或者同步可迭代物件上建立一個迭代迴圈,包括 StringArrayArray-like 物件(比如arguments 或者NodeList),TypedArrayMapSet和自定義的非同步或者同步可迭代物件。其會呼叫自定義迭代鉤子,併為每個不同屬性的值執行語句。

配合迭代非同步生成器,例子如下:

async function* asyncGenerator() {
      var i = 0
      while (i < 3) {
            yield i++
      }
}

(async function() {
      for await (num of asyncGenerator()) {
            console.log(num)
      }
})()
// 0
// 1
// 2

模板字串(Template string)

ES9開始,模板字串允許巢狀支援常見轉義序列,移除對ECMAScript在帶標籤的模版字串中轉義序列的語法限制。

不過,非法轉義序列在"cooked"當中仍然會體現出來。它們將以undefined元素的形式存在於"cooked"之中,程式碼如下:

function latex(str) { 
 return { "cooked": str[0], "raw": str.raw[0] }
} 

latex`\unicode` // { cooked: undefined, raw: "\\unicode" }

正規表示式反向(lookbehind)斷言

首先我們得先知道什麼是斷言(Assertion)

斷言(Assertion)是一個對當前匹配位置之前或之後的字元的測試, 它不會實際消耗任何字元,所以斷言也被稱為“非消耗性匹配”或“非獲取匹配”。

正規表示式的斷言一共有 4 種形式:

  • (?=pattern) 零寬正向肯定斷言(zero-width positive lookahead assertion)
  • (?!pattern) 零寬正向否定斷言(zero-width negative lookahead assertion)
  • (?<=pattern) 零寬反向肯定斷言(zero-width positive lookbehind assertion)
  • (?<!pattern) 零寬反向否定斷言(zero-width negative lookbehind assertion)

在ES9之前,JavaScript 正規表示式,只支援正向斷言。正向斷言的意思是:當前位置後面的字串應該滿足斷言,但是並不捕獲。例子如下:

'fishHeadfishTail'.match(/fish(?=Head)/g) // ["fish"]

反向斷言和正向斷言的行為一樣,只是方向相反。例子如下:

'abc123'.match(/(?<=(\d+)(\d+))$/) //  ["", "1", "23", index: 6, input: "abc123", groups: undefined]

正規表示式 Unicode 轉義

正規表示式中的Unicode轉義符允許根據Unicode字元屬性匹配Unicode字元。 它允許區分字元型別,例如大寫和小寫字母,數學符號和標點符號。

部分例子程式碼如下:

// 匹配所有數字
const regex = /^\p{Number}+$/u;
regex.test('²³¹¼½¾') // true
regex.test('㉛㉜㉝') // true
regex.test('ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫ') // true

// 匹配所有空格
\p{White_Space}

// 匹配各種文字的所有字母,等同於 Unicode 版的 \w
[\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]

// 匹配各種文字的所有非字母的字元,等同於 Unicode 版的 \W
[^\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]

// 匹配 Emoji
/\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu

// 匹配所有的箭頭字元
const regexArrows = /^\p{Block=Arrows}+$/u;
regexArrows.test('←↑→↓↔↕↖↗↘↙⇏⇐⇑⇒⇓⇔⇕⇖⇗⇘⇙⇧⇩') // true

具體的屬性列表可檢視:https://developer.mozilla.org...

正規表示式 s/dotAll 模式

在以往的版本里,JS的正則的.只能匹配emoji跟行終結符以外的所有文字,例如:

let regex = /./;

regex.test('\n');       // false
regex.test('\r');       // false
regex.test('\u{2028}'); // false
regex.test('\u{2029}'); // false

regex.test('\v');       // true
regex.test('\f');       // true
regex.test('\u{0085}'); // true

/foo.bar/.test('foo\nbar');     // false
/foo[^]bar/.test('foo\nbar');   // true

/foo.bar/.test('foo\nbar');     // false
/foo[\s]bar/.test('foo\nbar');   // true

但是在ES9之後,JS正則增加了一個新的標誌 s 用來表示 dotAll,這可以匹配任意字元。程式碼如下:

/foo.bar/s.test('foo\nbar');    // true

const re = /foo.bar/s;  //  等價於 const re = new RegExp('foo.bar', 's');
re.test('foo\nbar');    // true
re.dotAll;      // true
re.flags;       // "s"

正規表示式命名捕獲組

在以往的版本里,JS的正則分組是無法命名的,所以容易混淆。例如下面獲取年月日的例子,很容易讓人搞不清哪個是月份,哪個是年份:

const matched = /(\d{4})-(\d{2})-(\d{2})/.exec('2019-01-01')
console.log(matched[0]);    // 2019-01-01
console.log(matched[1]);    // 2019
console.log(matched[2]);    // 01
console.log(matched[3]);    // 01

ES9引入了命名捕獲組,允許為每一個組匹配指定一個名字,既便於閱讀程式碼,又便於引用。程式碼如下:

const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{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 RE_OPT_A = /^(?<as>a+)?$/;
const matchObj = RE_OPT_A.exec('');

matchObj.groups.as // undefined
'as' in matchObj.groups // true

物件擴充套件操作符

ES6中新增了陣列的擴充套件操作符,讓我們在運算元組時更加簡便,美中不足的是並不支援物件擴充套件操作符,但是在ES9開始,這一功能也得到了支援,例如:

var obj1 = { foo: 'bar', x: 42 };
var obj2 = { foo: 'baz', y: 13 };

var clonedObj = { ...obj1 };
// 克隆後的物件: { foo: "bar", x: 42 }

var mergedObj = { ...obj1, ...obj2 };
// 合併後的物件: { foo: "baz", x: 42, y: 13 }

上面便是一個簡便的淺拷貝。這裡有一點小提示,就是Object.assign() 函式會觸發 setters,而展開語法則不會。所以不能替換也不能模擬Object.assign()

如果存在相同的屬性名,只有最後一個會生效。

Promise.prototype.finally()

finally()方法會返回一個Promise,當promise的狀態變更,不管是變成rejected或者fulfilled,最終都會執行finally()的回撥。

例子如下:

fetch(url)
      .then((res) => {
        console.log(res)
      })
      .catch((error) => { 
        console.log(error)
      })
      .finally(() => { 
        console.log('結束')
    })

ES10(ES2019)

Array.prototype.flat() / flatMap()

flat() 方法會按照一個可指定的深度遞迴遍歷陣列,並將所有元素與遍歷到的子陣列中的元素合併為一個新陣列返回。

flatMap()map() 方法和深度為1的 flat() 幾乎相同.,不過它會首先使用對映函式對映每個元素,然後將結果壓縮成一個新陣列,這樣效率會更高。

例子如下:

var arr1 = [1, 2, 3, 4]

arr1.map(x => [x * 2]) // [[2], [4], [6], [8]]

arr1.flatMap(x => [x * 2]) // [2, 4, 6, 8]

// 深度為1
arr1.flatMap(x => [[x * 2]]) // [[2], [4], [6], [8]]

flatMap()可以代替reduce()concat(),例子如下:

var arr = [1, 2, 3, 4]
arr.flatMap(x => [x, x * 2]) // [1, 2, 2, 4, 3, 6, 4, 8]
// 等價於
arr.reduce((acc, x) => acc.concat([x, x * 2]), []) // [1, 2, 2, 4, 3, 6, 4, 8]

但這是非常低效的,在每次迭代中,它建立一個必須被垃圾收集的新臨時陣列,並且它將元素從當前的累加器陣列複製到一個新的陣列中,而不是將新的元素新增到現有的陣列中。

String.prototype.trimStart() / trimLeft() / trimEnd() / trimRight()

在ES5中,我們可以通過trim()來去掉字元首尾的空格,但是卻無法只去掉單邊的,但是在ES10之後,我們可以實現這個功能。

如果我們要去掉開頭的空格,可以使用trimStart()或者它的別名trimLeft()

同樣的,如果我們要去掉結尾的空格,我們可以使用trimEnd()或者它的別名trimRight()

例子如下:

const Str = '   Hello world!  '
console.log(Str) // '   Hello world!  '
console.log(Str.trimStart()) // 'Hello world!  '
console.log(Str.trimLeft()) // 'Hello world!  '
console.log(Str.trimEnd()) // '   Hello world!'
console.log(Str.trimRight()) // '   Hello world!'

不過這裡有一點要注意的是,trimStart()trimEnd()才是標準方法,trimLeft()trimRight()只是別名。

在某些引擎裡(例如Chrome),有以下的等式:

String.prototype.trimLeft.name === "trimStart"

String.prototype.trimRight.name === "trimEnd"

Object.fromEntries()

Object.fromEntries() 方法把鍵值對列表轉換為一個物件,它是Object.entries()的反函式。

例子如下:

const entries = new Map([
  ['foo', 'bar'],
  ['baz', 42]
])

const obj = Object.fromEntries(entries)

console.log(obj) // Object { foo: "bar", baz: 42 }

Symbol.prototype.description

description 是一個只讀屬性,它會返回Symbol物件的可選描述的字串。與 Symbol.prototype.toString() 不同的是它不會包含Symbol()的字串。例子如下:

Symbol('desc').toString();   // "Symbol(desc)"
Symbol('desc').description;  // "desc"
Symbol('').description;      // ""
Symbol().description;        // undefined

// 具名 symbols
Symbol.iterator.toString();  // "Symbol(Symbol.iterator)"
Symbol.iterator.description; // "Symbol.iterator"

//全域性 symbols
Symbol.for('foo').toString();  // "Symbol(foo)"
Symbol.for('foo').description; // "foo"

String.prototype.matchAll

matchAll() 方法返回一個包含所有匹配正規表示式的結果及分組捕獲組的迭代器。並且返回一個不可重啟的迭代器。例子如下:

var regexp = /t(e)(st(\d?))/g
var str = 'test1test2'

str.match(regexp) // ['test1', 'test2']
str.matchAll(regexp) // RegExpStringIterator {}
[...str.matchAll(regexp)] // [['test1', 'e', 'st1', '1', index: 0, input: 'test1test2', length: 4], ['test2', 'e', 'st2', '2', index: 5, input: 'test1test2', length: 4]]

Function.prototype.toString() 返回註釋與空格

在以往的版本中,Function.prototype.toString()得到的字串是去掉空白符號的,但是從ES10開始會保留這些空格,如果是原生函式則返回你控制檯看到的效果,例子如下:

function sum(a, b) {
      return a + b;
}

console.log(sum.toString())
// "function sum(a, b) {
//         return a + b;
//  }"

console.log(Math.abs.toString()) // "function abs() { [native code] }"

try-catch

在以往的版本中,try-catchcatch後面必須帶異常引數,例如:

    // ES10之前
try {
      // tryCode
} catch (err) {
      // catchCode
}

但是在ES10之後,這個引數卻不是必須的,如果用不到,我們可以不用傳,例如:

try {
      console.log('Foobar')
} catch {
      console.error('Bar')
}

BigInt

BigInt 是一種內建物件,它提供了一種方法來表示大於 253 - 1 的整數。這原本是 Javascript中可以用 Number 表示的最大數字。BigInt 可以表示任意大的整數。

可以用在一個整數字面量後面加 n 的方式定義一個 BigInt ,如:10n,或者呼叫函式BigInt()

在以往的版本中,我們有以下的弊端:

// 大於2的53次方的整數,無法保持精度
2 ** 53 === (2 ** 53 + 1)
// 超過2的1024次方的數值,無法表示
2 ** 1024 // Infinity

但是在ES10引入BigInt之後,這個問題便得到了解決。

以下操作符可以和 BigInt 一起使用: +*-**% 。除 >>> (無符號右移)之外的位操作也可以支援。因為 BigInt 都是有符號的, >>> (無符號右移)不能用於 BigIntBigInt 不支援單目 (+) 運算子。

/ 操作符對於整數的運算也沒問題。可是因為這些變數是 BigInt 而不是 BigDecimal ,該操作符結果會向零取整,也就是說不會返回小數部分。

BigIntNumber不是嚴格相等的,但是寬鬆相等的。

所以在BigInt出來以後,JS的原始型別便增加到了7個,如下:

  • Boolean
  • Null
  • Undefined
  • Number
  • String
  • Symbol (ES6)
  • BigInt (ES10)

globalThis

globalThis屬性包含類似於全域性物件 this值。所以在全域性環境下,我們有:

globalThis === this // true

import()

靜態的import 語句用於匯入由另一個模組匯出的繫結。無論是否宣告瞭 嚴格模式,匯入的模組都執行在嚴格模式下。在瀏覽器中,import 語句只能在宣告瞭 type="module"script 的標籤中使用。

但是在ES10之後,我們有動態 import(),它不需要依賴 type="module" 的script標籤。

所以我們有以下例子:

const main = document.querySelector("main")
for (const link of document.querySelectorAll("nav > a")) {
      link.addEventListener("click", e => {
            e.preventDefault()

            import('/modules/my-module.js')
              .then(module => {
                    module.loadPageInto(main);
              })
              .catch(err => {
                    main.textContent = err.message;
              })
      })
}

私有元素與方法

在ES10之前,如果我們要實現一個簡單的計數器元件,我們可能會這麼寫:

// web component 寫法
class Counter extends HTMLElement {
      get x() { 
              return this.xValue
      }
      set x(value) {
              this.xValue = value
              window.requestAnimationFrame(this.render.bind(this))
      }

      clicked() {
            this.x++
      }

      constructor() {
            super()
            this.onclick = this.clicked.bind(this)
            this.xValue = 0
      }

      connectedCallback() { 
              this.render()
      }

      render() {
            this.textContent = this.x.toString()
      }
}
window.customElements.define('num-counter', Counter)

但是在ES10之後我們可以使用私有變數進行元件封裝,如下:

class Counter extends HTMLElement {
      #xValue = 0

      get #x() { 
          return #xValue
      }
      set #x(value) {
            this.#xValue = value
            window.requestAnimationFrame(this.#render.bind(this))
      }

      #clicked() {
            this.#x++
      }

      constructor() {
            super();
            this.onclick = this.#clicked.bind(this)
      }

      connectedCallback() { 
              this.#render()
      }

      #render() {
            this.textContent = this.#x.toString()
      }
}
window.customElements.define('num-counter', Counter)

參考資料

  1. ECMAScript 6 入門
  2. 1.5萬字概括ES6全部特性
  3. MDN
  4. ES2018 新特徵之:非轉義序列的模板字串
  5. 正規表示式反向(lookbehind)斷言
  6. Unicode property escapes
  7. exnext提案
  8. ES7、ES8、ES9、ES10新特性大盤點
  9. Ecma TC39
  10. [[ECMAScript] TC39 process](https://www.jianshu.com/p/b08...
  11. The TC39 Process

相關文章