深入理解ES6 ---- 迭代器

漓漾li發表於2019-04-08

迭代器

遍歷器(Iterator)是一種介面,為各種不同的資料結構提供統一的訪問機制。任何資料結構只要部署 Iterator 介面,就可以完成遍歷操作(即依次處理該資料結構的所有成員)。

(1) Iterator 的作用

  • 為各種資料結構,提供一個統一的、簡便的訪問介面
  • 使得資料結構的成員能夠按某種次序排列
  • ES6 創造了一種新的遍歷命令for...of迴圈,Iterator 介面主要供for...of消費。

(2) Iterator的迭代過程

所有的迭代器物件都會有一個next方法,每次呼叫都會返回一個物件:{done: boolean, value: any}value表示當前成員的值,done表示是否還有更多的資料。迭代器內部會維護一個指標,指向當前成員的位置,每次呼叫next都會指向下一個成員。

// es5實現迭代器
function creatIterator(arr){
    let index = 0;
    return {
      next: () =>{
        return {
          done: index > arr.length - 1,
          value: this.done ? undefined : arr[index++]
        }
      }
    }
}
const iterator = creatIterator([1, 2, 3])
console.log(a.next())  // { done: false, value: 1 }
console.log(a.next())  // { done: false, value: 2 }
console.log(a.next())  // { done: false, value: 3 }
console.log(a.next())  // { done: true, value: undefined }
// 之後的呼叫都會返回相同的內容
console.log(a.next())  // { done: true, value: undefined }
複製程式碼

生成器

生成器是一種返回迭代器的函式。通過function後面的*來表明它是一個生成器。

  • yield關鍵字是es6的新特性,它可以指定呼叫next方法時的返回值以及呼叫順序
  • 每當執行完yield語句,函式就會停止執行,直到再次呼叫next方法才會繼續執行
  • yield關鍵字只能在生成器內部使用,其他地方會導致語法錯誤
function * createIterator(){
   yield 1
   yield 2
}
// 生成器返回的是一個迭代器
const iterator = createIterator()
console.log(iterator.next())  // { done: false, value: 1 }
console.log(iterator.next())  // { done: false, value: 2 }
console.log(iterator.next())  // { done: true, value: undefined }
複製程式碼

使用函式表示式:const createIterator = function *(){ yield 1 }但不能使用箭頭函式來建立生成器。

這裡僅僅介紹了生成器可以返回迭代器,但它主要的作用是流程管理,因此可以使用同步的方式書寫非同步程式碼generator詳情

可迭代物件

(1) Symbol.iterator屬性

一個資料結構只要具有Symbol.iterator屬性,就可以認為是可迭代的

  • 所有的集合物件array、set、map以及string都是可迭代物件,都有預設的迭代器,即Symbol.iterator屬性
  • 生成器生成的物件就是可迭代的,生成器會預設為他們的Symbol.iterator屬性賦值
  • 通過執行Symbol.iterator方法 來獲取物件的迭代器

訪問陣列的預設迭代器:

const arr = [1, 2, 3]
// 通過執行陣列的 `Symbol.iterator`方法來獲取 `arr` 的迭代器
const arrIterator = arr[Symbol.iterator]()

console.log(arrIterator.next())  // { value: 1, done: false }
console.log(arrIterator.next())  // { value: 2, done: false }
console.log(arrIterator.next())  // { value: 3, done: false }
console.log(arrIterator.next())  // { value: undefined, done: true }
複製程式碼

(2) for...of

for...of封裝了next的重複呼叫過程,來自動執行迭代器,遍歷訪問可迭代物件的成員。它通過物件的Symbol.iterator方法來獲取迭代器,並在每次迴圈中呼叫可迭代物件的next方法,將返回值根據迭代器的規則賦給中間變數,一直迴圈到done屬性為true。即

const arr = [1, 2, 3]
for(let item of arr){
    console.log(item)
}
// 1 2 3
複製程式碼

(3) 建立可迭代物件

可以通過給一個不可迭代物件設定Symbol.iterator屬性,使它變成可迭代的。Symbol.iterator必須是迭代器生成函式,否則使用for...of會報錯。

// es5
const list = {
  items: [1,2,3],
  [Symbol.iterator](){
    let index = 0;
    return {
      next: () =>{
        return {
          done: index > this.items.length - 1,
          value: this.done ? undefined : this.items[index++]
        }
      }
    }
  }
}
for (const item of list) {
  console.log(item)
}
// 1 2 3

// es6
const list = {
  items: [1,2,3],
  *[Symbol.iterator](){
    for(item of this.items){
      yield item
    }
  }
}
for (const item of list) {
  console.log(item)
}
// 1 2 3
複製程式碼

(3) 內建迭代器

entries、values、keys都是es6新增的迭代器。下表為各種集合型別的各種迭代器在for...of迴圈中的中間變數:

集合型別/迭代器 entries values keys
array [[index, value]] [value] [index]
map [[key, value]] [value] [key]
set [[value, value]] [value] [value]

每個集合型別都有預設的迭代器,在for... of迴圈中,如果沒有顯式的指定,則使用預設的迭代器。上表中的✔表示各集合型別的for... of預設迭代器

const arr = [1,2,3]
// 沒有顯示指定迭代器,則使用array預設的迭代器,即values迭代器
for (const item of arr) {
  console.log(item)
}
// 1 2 3

// 使用顯示指定的entries迭代器
for (const item of arr.entries()) {
  console.log(item)
}
// [0,1]  [1,2]  [2,3]
複製程式碼

需要注意的是:entries、values、keys請勿與Object.entries、Object.values、Obejct.keys混淆。前者返回的是迭代器,用於for...of迴圈;後者返回的是根據原物件格式化後的陣列

(4) 字串迭代器

es5中也可以使用for迴圈遍歷字串,但卻以編碼為單元而非字元,因此無法正確訪問雙位元組字元es6中全面支援unicode,所以**for...of迴圈就可以正確遍歷字串中的雙位元組字元**

const str = '?1'
for (let i = 0; i < text.length; i++) {
  console.log(text[i]);
}
// " "
// " "
// "1"

// 可以正確識別雙位元組字元
for (let i of text) {
  console.log(i);
}
// "?"
// "1"
複製程式碼

(5) NodeList迭代器

DOM標準中有一個NodeList型別,用於表示頁面文件元素的集合。它含有length屬性;可以通過[number]訪問元素,跟陣列的格式和操作方法很類似,但其實它是物件,也就是我們俗稱的偽陣列,比如:{0: domObject, 1: domObject, 2: domObject, length:3}es6NodeList型別也擁有了預設迭代器,可以使用for...of迭代

const nodeList = document.getElementsByClassName('abc')
for (const node of nodeList) {
    console.log(node)
}
複製程式碼

(6) 展開運算子

展開運算子可以操作所有可迭代物件,並根據預設迭代器來選取要引用的值,然後讀取所有值

const nodeList = document.getElementsByClassName('abc')
console.log([...nodeList])   
// [node, node, node]

const map = new Map()
map.set('name','li yang')
map.set('age', 18)
console.log([...map])  // 呼叫了map的預設迭代器 entries
// [["name", "li yang"], ["age", 18]]
複製程式碼

由於展開運算子可以作用於任意可迭代物件,因此要將可迭代物件裝換成陣列,這是最簡單的方法

系列文章

相關文章