ES2015+ 常用新特性一口氣看個夠

by.Genesis發表於2021-06-22

ES2015 也叫 ES6,區別只是一個是以釋出的年份來命名,一個是以版本號來命名

從那以後組織每年都會釋出一個新版本,根據這個規則,ES2016 === ES7... ES2020 === ES11

但通常我習慣將 ES2015 及其後續版本統稱為 ES2015+

變數宣告

ES2015 增加了兩個宣告變數識別符號的關鍵字,letconst,兩者都支援塊級作用域,並且在宣告之前不能訪問

凡是不需要重新賦值的變數識別符號都可以使用 const 關鍵字來宣告

其餘需要重新賦值的變數就使用 let 關鍵字來宣告,像迴圈計數器之類的

{
  const arr = ['a', 'b', 'c', 'd', 'e']
  const length = arr.length

  for (let i = 0; i < length; i++) {
    //
  }
}
// 塊之外無法訪問

物件字面量

物件字面量的簡寫形式以及計算屬性

const foo = 1

const obj = {
  foo, // 屬性簡寫,等同於 foo: foo,
  bar() {
    // 方法簡寫
  },
  // 計算屬性
  ['na' + 'me']: 'by.Genesis',
  __proto__: 原型
}

這些特性都可以簡化原本的程式碼

__proto__ 用來 get/set 原型,不過並不推薦使用,應該使用 Object.getPrototypeOf(o)Object.setPrototypeOf(o, proto)

箭頭函式

箭頭(=>)就是一種函式簡寫方式,同時提供一些有用的特性

// 當函式有且僅有一個引數的時候可以省略引數的圓括號
;[1, 2, 3].forEach(item => { console.log(item) })

// 當函式體內只有一條語句的時候可以省略函式體的花括號,同時隱式返回該條語句
const sum = (x, y) => x + y

// 如果隱式返回的是一個物件字面量,為了消除歧義,可以使用一對圓括號包裹物件字面量
const pos = (x, y) => ({ x: x + 1, y: y * 2 })

// 詞法 this
const obj = {
  name: 'by.Genesis',
  showName() {
    setTimeout(() => {
      console.log(this.name) // obj.showName() this === obj
    }, 300)
  }
}

// 立即執行箭頭函式表示式
;(() => {
  alert(101)
})()

class

類(class)就是傳統的建構函式基於原型繼承的語法糖

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }

  update() {
    // 方法
  }
}

// 繼承
class Student extends Person {
  constructor(name, age, grade) {
    super(name, age)
    this.grade = grade
  }

  update() {
    // 呼叫父類的方法
    super.update()
  }
  get foo() {
    // getter
  }
  set foo() {
    // setter
  }
  static baz() {
    // 靜態方法通過 Student.baz() 呼叫
  }
}

const s1 = new Student('by.Genesis', 20, 2)

同函式一樣,類也可以作為表示式賦值給一個變數,或者作為引數傳給函式,甚至從函式中返回

const Person = class {
  //
}

無論是用類宣告還是表示式,都需要先定義,然後再使用,不會提升,不會提升

Symbol

符號(Symbol)是一種新的原始型別,其沒有字面量形式

符號可以分為3類,普通符號,全域性符號和眾所周知的符號(well-known Symbol)

// 建立 Symbol,不需要 new
// 傳入的引數作為該 Symbol 的描述符
const name = Symbol('name')

// 將 Symbol 用作物件的 key
// 只能使用可計算屬性名的方式
const o = {
  [name]: 'by.Genesis'
}

// 通過 typeof 操作符判斷值型別
typeof Symbol('name') === 'symbol'

// 符號值是唯一的,就算在建立時傳入了相同的引數,得到的符號也不是同一個
Symbol('name') !== Symbol('name')

// 不過在全域性符號登錄檔中同一個 key 返回的是同一個符號
Symbol.for('name') === Symbol.for('name')

// 獲取符號描述符
Symbol('name').description === 'name'

物件的符號屬性無法通過傳統的方法遍歷出來,需要的時候可以使用 Object.getOwnPropertySymbols() 方法獲取引數物件中所有符號屬性組成的陣列

Object.getOwnPropertySymbols(o) // [Symbol(name)]

除此之外,還有一些 眾所周知的符號(well-known Symbol),這類符號的作用是暴露一些 JavaScript 內部操作

// 當陣列作為 concat 引數時,預設會被展開
const arr = [4, 5, 6]
;[1, 2, 3].concat(arr) // [1, 2, 3, 4, 5, 6]

// 可以修改此行為讓陣列引數不展開
arr[Symbol.isConcatSpreadable] = false
;[1, 2, 3].concat(arr) // [1, 2, 3, [4, 5, 6]]

// 也可以讓類陣列物件展開
;[1, 2, 3].concat({
  [Symbol.isConcatSpreadable]: true,
  length: 3,
  0: 4,
  1: 5,
  2: 6
}) // [1, 2, 3, 4, 5, 6]

Promise

Promise 主要用來表示一個未來值

// 建立一個 promise
const p = new Promise((resolve, reject) => {
  setTimeout(resolve, 3000, 'by.Genesis')
})

// promise resolve 時執行
p.then(res => {
  console.log(res) // 'by.Genesis'
})

// 總是會執行,無論 resolve 還是 reject
p.finally(() => console.log('finally'))

// 立即建立一個 fulfilled 的 promise
const p2 = Promise.resolve('101真狗')

// 立即建立一個 rejected 的 promise
const p3 = Promise.reject(404)

// promise reject 時執行
p3.catch(err => {
  console.log(err) // 404
})

// 等待一組 promise 全部 resolve
// 一旦有一個為 rejected 則立即 reject
Promise.all([p2, p3])

// 獲取一組 promise 中最快的那一個,無論 resolve 還是 reject
Promise.race([p2, p3])

// 等待一組 promise 全部 settled,無論 resolve 還是 reject
Promise.allSettled([p2, p3])

// 獲取一組 promise 中最快 resolve 的那一個
// 只有當全部都為 rejected 的時候 reject
Promise.any([p2, p3])

迭代器和生成器

當一個物件擁有一個 next 方法,並且呼叫該方法時可以得到一個包含 donevalue 兩個屬性的結果物件,那麼這就是一個迭代器(Iterator)

iterator = {
  next() {
    return {
      done: false,
      value: 10
    }
  }
}

其中 done 為 Boolean 型別,表示該迭代器是否已經迭代完畢

生成器(Generator)是一種特殊函式,宣告的時候在 function 關鍵字和函式名中間多了個星號(*)

生成器內通過 yield 關鍵字返回值

function *g() {
  yield 1
  yield 2
  yield 3
}

// 呼叫生成器可以得到一個迭代器
iterator = g()

// 呼叫迭代器的 next 方法執行生成器內部程式碼並得到結果物件
iterator.next() // { value: 1, done: false }

當一個物件具有特殊的符號 [Symbol.iterator] 方法,並且該方法返回一個迭代器的時候,那麼這個物件就是一個可迭代物件(Iterable)

iterable = {
  *[Symbol.iterator]() { // 這裡同時使用了物件方法簡寫,計算屬性以及生成器
    yield 1
    yield 2
    yield 3
  }
}

String,Array,Map,Set,NodeList 等等都是可迭代物件,迭代器自身也是可迭代物件,迭代器的 [Symbol.iterator] 方法返回自身

生成器可以通過 yield* 委託給其它可迭代物件

iterable = {
  *[Symbol.iterator]() {
    yield 1
    yield* [2, 3]
  }
}

可迭代物件可以使用 for of 語法遍歷

for (let v of iterable) {
  console.log(v) // 1 2 3
}

非同步函式

非同步函式(Async function)在函式前面新增一個 async 關鍵字,其內部可以使用 await 關鍵字

await 表示式可以將其後面的 Promise resolve 的值提取出來

async function fn() {
  const x = await Promise.resolve(101)
  return x
}

// 執行非同步函式也返回一個 Promise
fn().then(res => console.log(res)) // 101

非同步函式就是生成器和 Promise 語法糖

非同步迭代

當一個迭代器的 next 方法返回一個 Promise,並且該 Promise resolve 後可以得到一個包含 donevalue 兩個屬性的結果物件,那麼這個迭代器就是一個非同步迭代器(Async Iterator)

asyncIterator = {
  next() {
    return Promise.resolve({
      done: false,
      value: 10
    })
  }
}

將非同步函式和生成器結合到一起,就是非同步生成器(Async Generator),其內部可以同時使用 awaityield 關鍵字

async function *g() {
  yield 1
  const a = await new Promise(resolve => {
    setTimeout(resolve, 3000, 2)
  })
  yield a
  yield Promise.resolve(a + 1)
}

// 執行非同步生成器返回一個非同步迭代器
asyncIterator = g()

當一個物件具有特殊的符號 [Symbol.asyncIterator] 方法,並且該方法返回一個非同步迭代器的時候,那麼這個物件就是一個非同步可迭代物件(Async Iterable)

asyncIterable = {
  async *[Symbol.asyncIterator]() {
    yield 1
    const a = await new Promise(resolve => {
      setTimeout(resolve, 3000, 2)
    })
    yield a
    yield Promise.resolve(a + 1)
  }
}

非同步可迭代物件使用 for await of 語法遍歷

;(async () => {
  for await (let v of asyncIterable) {
    console.log(v) // 1 2 3
  }
})()

await 應該放到非同步函式中

Map & Set

Map 是包含鍵值對(key-value)的有序集合,其中 key 可以是 任意型別 (是任意型別,包括引用型別甚至 DOM 元素都可以作為 Map 的 key)

// 建立一個 Map
const m = new Map([['a', 1], ['b', 2]])

// 新增值,如果已存在就是修改
m.set('c', 3)

// 獲取值
m.get('b') // 2

// 判斷值
m.has('b') // true

// 獲取長度
m.size // 3

// 刪除值
m.delete('b')
m.has('b') // false
m.size // 2

// 清空
m.clear()
m.size // 0

Set 就是一組不重複值的有序集合

// 建立一個 Set
const s = new Set([1, 2])

// 新增值
s.add(3)
s.add(1) // 該值已存在,集合保持不變

// 除了沒有獲取值的方法,剩下的和 Map 一致

Map 和 Set 都可以通過 forEach 方法遍歷其中的值

set.forEach(handler => handler())

可以把 Set 看作是 key 和 value 為同一個值的特殊 Map,也可以認為 Set 是隻有 key

Map 和 Set 遍歷順序和新增時的順序是一致的,因此都是有序集合

WeakSet & WeakMap

弱版本只能用來存放引用型別

WeakMap 只對其 key 有型別要求,而 value 可以是任意型別

弱版本不是可迭代物件,不能遍歷,也沒有 size 屬性,也不能用 clear 方法清空集合,只具備最基本的新增,刪除等方法

弱版本是弱引用,其優勢就是利於垃圾回收

解構

按照一定模式從物件或者可迭代物件中提取值

// 可迭代物件解構
// let 宣告對變數 a, b, c 都生效
let [a, b, c] = [1, 2]
a === 1
b === 2
c === undefined

// 交換值
;[a, b] = [b, a] // a = 2, b = 1

// 陣列可以解構任意可迭代物件,包括字串
// 解構時也可以跳過一些不需要的值
;[, , c] = '123' // c = '3'

// 物件屬性解構
{ x, y, z: { w } } = { x: 3, y: 4, z: { w: 5 } }
a === 3
b === 4
w === 5

預設值

在宣告函式引數或者解構的時候都可以指定一個預設值,當對應的值為 undefined 的時候,就會使用這個預設值

// 函式引數預設值
const sum = (x, y = 4) => x + y
sum(3) === 7

// 迭代器解構的預設值
const [a, b = 2] = [1]
a === 1
b === 2

// 物件解構的預設值
const { name = 'by.Genesis' } = { age: 18 }
name === 'by.Genesis'

// 函式引數和物件解構一起使用
const fn = ({ height = 18, width = 36 } = {}) => {}
fn() // height = 18, width = 36
fn({ height: 36 }) // height = 36, width = 36
fn({ height: 36, width: 18 }) // height = 36, width = 18

Spread & Rest

可迭代物件均可使用展開(Spread)運算子(...)展開為獨立的值,這些值可以作為函式的引數或放到陣列中

// 展開可迭代物件作為函式引數
Math.max(...[5, 20, 10]) === 20

// 展開可迭代物件到一個陣列中
const arr = [...new Set([1, 2, 2, 3])] // [1, 2, 3]

// 展開可迭代物件到一個陣列中
const newArr = [1, ...[2, 3], ...'45'] // [1, 2, 3, '4', '5']

而普通物件也可以展開其屬性,放到另一個物件中,這和 Object.assign 方法作用類似

// 展開物件屬性到另一個物件中
const o = {
  a: 1,
  ...{
    b: 2,
    c: 3
  }
} // o = { a: 1, b: 2, c: 3 }

const o2 = Object.assign({ a: 1 }, { b: 2, c: 3 })

和展開相反,多個值可以使用收集(Rest)運算子(...)打包成一個陣列,或者多個物件屬性打包成一個物件

// 函式剩餘引數打包成一個陣列
const fn = (x, ...y) => y.length
fn(2, 5, 7, 11) === 3 // x = 2, y = [5, 7, 11]

// 可迭代物件剩餘值打包成一個陣列
const [a, ...b] = new Set([2, 5, 7, 11])
a === 2
// b = [5, 7, 11]

// 物件剩餘屬性打包成一個物件
const { a, ...o } = {
  a: 1,
  b: 2,
  c: 3
} // o = { b: 2, c: 3 }

收集運算子只能用於最後一個識別符號

模板字串

模板字串就是功能更強大的字串,它支援多行以及插值

在模板字串中插值使用 ${} 花括號裡面可以插入表示式,表示式甚至可以是另一個模板字串

const str = `
<ul>
  ${lists.map(item => {
    return `<li>${item.user} is ${item.age} years old.</li>`
  }).join('')}
</ul>
`

標籤模板

const username = 'by.Genesis'
const age = 18
const str = tag`${username} is ${age} years old.`

// tag 就是一個函式
// 第一個引數為字串按插值分割而成的陣列
// 後面的引數為插值表示式的值
// 可以自行處理字串邏輯
function tag(template, ...substitutions) {
  console.log(template) // ['', ' is ', ' years old.']
  console.log(substitutions) // ['by.Genesis', 18]
  return substitutions[0] + template[1] + 'handsome'
}

str === 'by.Genesis is handsome'

代理和反射

代理(Proxy)就是為一個目標物件生成一個代理,當對這個代理物件執行一些操作的時候,就會觸發對應的攔截器,在攔截器中可以自行定義操作和返回的值,或者用反射(Reflect)執行元操作,每個代理方法都有對應的反射方法

const obj = {}
const proxy = new Proxy(obj, {
  get(target, key) {
    // 屬性取值
    if (key === 'name') {
      // 自定義返回值
      return 'by.Genesis'
    } else {
      // 用反射還原操作
      return Reflect.get(target, key)
    }
  },
  set() { 屬性賦值 },
  has() { in 操作符 },
  deleteProperty() { 刪除屬性 },
  getPrototypeOf() { 獲取原型 },
  setPrototypeOf() { 設定原型 },
  defineProperty() { Object.defineProperty },
  getOwnPropertyDescriptor() { Object.getOwnPropertyDescriptor },
  preventExtensions() { Object.preventExtensions },
  isExtensible() { Object.isExtensible },
  ownKeys() { Object.keys, Object.getOwnPropertyNames, Object.getOwnPropertySymbols, Object.assign },
  enumerable() { for in 迴圈 },
  apply() { 函式普通呼叫 },
  construct() { new 方式呼叫函式 }
})
proxy.name === 'by.Genesis'
obj.name === undefined

以上是這些攔截器以及對應的觸發條件

邏輯運算

Nullish coalescing Operator

JavaScript 裡面的假值(Falsy)有 null, undefined, 0, '', NaN, false,除假值外都為真值(Truthy)

而空值(Nullish)只有 nullundefined,當該運算子左側為空值時返回右側

null ?? 1 // 1
undefined ?? 1 // 1

0 ?? 1 // 0
0 || 1 // 1

Optional chaining

鏈式操作時,當中間某個值是 null 或者 undefined 就會報錯,而這個操作符可以讓鏈式操作更安全

const o = {}
o.p.q // Uncaught TypeError: Cannot read property 'q' of undefined
o.p?.q // undefined

邏輯賦值

// 邏輯或賦值
a ||= b // 當 a 為假值時賦值
a || a = b

// 邏輯與賦值
a &&= b // 當 a 為真值時賦值
a && a = b

// 邏輯空賦值
a ??= b // 當 a 為空值時賦值
a ?? a = b

模組

在 ES 模組(Modules) 問世之前,已經有各種定義模組的規範了,比如 AMD,CommonJS 等,ES 模組提供語言層面的支援

使用 export 關鍵字匯出模組,使用 import 關鍵字匯入模組

// 可以同時匯出多個具名模組
export const sum = (x, y) => x + y
export const name = 'by.Genesis'

// 匯入具名模組時名稱必須和匯出時一致
// 另外可以使用 as 關鍵字指定別名
import { sum, name as username } from './example.js'

// 也可以先宣告再匯出,匯出時也可以使用 as 關鍵字指定別名
// 指定別名後,匯入的時候就需要使用這個別名了
export { sum, name as username }

// 一個模組只允許有一個預設匯出
export default { name: 'by.Genesis' }

// 匯入預設模組可以任意命名
import o from './example.js'

// 同時匯入預設模組和具名模組
import o, { sum } from './example.js'

// 全部匯入並指定一個別名,所有模組都會成為這個別名的屬性
import * as m from './example.js'
m.default // 預設模組是 default 屬性
m.sum // 具名模組就是自己的名字

// 從另一個模組中匯入再匯出
export * from './another.js'

// 直接匯入,不指定任何命名
import './example.js'

// 甚至還可以不匯出任何東西,僅僅只是執行一些程式碼而已

數字

// 非無窮
Number.isFinite(101) // true
Number.isFinite(NaN) // false

// 安全整數
Number.isSafeInteger(Number.MAX_SAFE_INTEGER) === true

// 二進位制數字 0b 開頭(binary),八進位制數字 0o 開頭(octonary)
// 前面是數字零,後面是字母,大小寫都可以,但是為了便於區別,建議使用小寫
0b1001 === 0o11

// 新增指數運算子(兩個乘號),主要是給指數運算一個正兒八經的運算子,而不是去呼叫方法
2 ** 3 === Math.pow(2, 3)
2 ** 3 === 2 * 2 * 2

// 數字中可以新增下劃線分割數字,增加數字的可讀性
123_4567_8889 === 12345678889

BigInt

大整數用來表示安全整數範圍之外的整數,數字字面量後面新增一個 n

const num = 9007199254740992n
typeof num === 'bigint'

字串方法

// 重複幾次
'xyz'.repeat(3) // 'xyzxyzxyz'

// 判斷開頭
'http://xyz.io/'.startsWith('http') === true

// 判斷結尾
'avator.jpg'.endsWith('.jpg') === true

// 判斷包含
'xyz'.includes('yz') === true

// 首尾填充,第一個引數為填充後長度,第二個引數為填充字串
'2'.padStart(2, '0') // '02'
// 字串已經達到長度則不填充
'12'.padStart(2, '0') // '12'

// 首尾去空白
'  xyz  '.trimStart() === 'xyz  '
'  xyz  '.trimLeft() === 'xyz  '

'  xyz  '.trimEnd() === '  xyz'
'  xyz  '.trimRight() === '  xyz'

// 字串全部替換
'xyx'.replaceAll('x', 'z') === 'zyz'
// replace 方法只會替換一次
'xyx'.replace('x', 'z') === 'zyx'
// 多次替換需要使用全域性正則
'xyx'.replace(/x/g, 'z') === 'zyz'

陣列方法

靜態方法

// 建立只有一個數字值的陣列
Array.of(3) // [3]
// 建構函式只會建立長度為傳入數字的稀疏陣列
new Array(3) // [empty × 3]

// 將類陣列或者可迭代物件轉換為陣列
Array.from($('.modal'))
Array.from({
  length: 5
}).map((item, index) => index + 1) // [1, 2, 3, 4, 5]

例項方法

// 填充陣列,可以傳入一個起始索引
new Array(3).fill('x', 1) // [empty, 'x', 'x']

// 包含判斷,可以傳入一個起始索引
[NaN, 1, 2].includes(NaN) === true
[NaN, 1, 2].includes(NaN, 1) === false

// 陣列中查詢元素,返回找到的元素
[1, 2, NaN].find(item => item !== item) // NaN

// 查詢元素索引,返回找到元素的索引
['x', 'y', NaN].findIndex(item => item !== item) === 2

// 複製到指定位置
// 這是一個變異方法,直接在原陣列上進行修改
// Array#copyWithin(target, start, ?end)
[1, 2, 3, 4, 5, 6].copyWithin(3, 0, 3) // [1, 2, 3, 1, 2, 3]

// 扁平化
[1, [2, [3, [4]]]].flat(2) // [1, 2, 3, [4]]
[1, [2, [3, [4]]]].flat(Infinity) // [1, 2, 3, 4]

// flatMap 相當於 map + flat(1)
// 會自動扁平化一層
// 這個方法可以讓返回的陣列變長,這是普通 map 無法合理辦到的
[1, 2, 3, 4].flatMap(x => [x, x * x]) // [1, 1, 2, 4, 3, 9, 4, 16]

其它方法

// 物件比較
Object.is(NaN, NaN) // true
Object.is(0, -0) // false

// Object.keys() 補充方法
Object.values({ x: 1, y: 2 }) // [1, 2]
Object.entries({ x: 1, y: 2 }) // [['x', 1], ['y', 2]]
Object.fromEntries([['x', 1], ['y', 2]]) // { x: 1, y: 2 }

// 獲取物件全部自身屬性描述
Object.getOwnPropertyDescriptors({ x: 1, y: 2 }) // { x: { value: 1, writable: true, enumerable: true, configurable: true }, y: { value: 2, ... } }

相關文章