博客 / 詳情

返回

兩千字助你理解for of原理,擴展for of完美解決遍歷對象問題

ES6 推出的 for of 語句非常強大,遠超曾經的所有遍歷方式。

for of 可以很輕鬆地遍歷數組、集合、映射,寫法也十分簡潔。

在我的項目中,除了需要獲取特定返回值的時候還採用數組的 map filter reduce 方法,其餘遍歷都由 for of 代勞。

本文我將逐層深入地介紹 for of 語句的用法與注意事項,並刨析其原理——迭代器和生成器,最後在對象與數字類型上擴展 for of 的功能。

語法與優勢

for of 語句的語法如下:

for (let variable of iterable) {
    //statements
}
  • iterable 是要被遍歷的目標,一般是數組、字符串、集合、映射,或者是其他實現迭代器接口的類數組對象,比如函數的參數列表 arguments、DOM 的節點列表 NodeList
  • variable 是自己定義的一個變量,用來存儲每次迭代中迭代器的返回值。

可以看看下面的示例,能更好的理解怎麼使用。

迭代數組,變量存儲的是數組的值。

let iterable = [10, 20, 30];

for (let value of iterable) {
    console.log(value);
}
// 10
// 20
// 30

迭代字符串,變量存儲的是單個字符。

let iterable = "boo";

for (let value of iterable) {
  console.log(value);
}
// "b"
// "o"
// "o"

迭代集合,變量存儲的是集合的值。

let iterable = new Set([1, 1, 2, 2, 3, 3]);

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

迭代映射,變量存儲的是一個鍵值對的數組,一般會通過解構賦值來使用。

let iterable = new Map([["a", 1], ["b", 2], ["c", 3]]);

for (let entry of iterable) {
  console.log(entry);
}
// ["a", 1]
// ["b", 2]
// ["c", 3]

for (let [key, value] of iterable) {
  console.log(key, value);
}
// a 1
// b 2
// c 3

相較於傳統的循環語句,for of 語句更簡潔,傳統的循環是無法遍歷集合與映射的,因為它們不具有索引。

for of 中可以使用 break continue 操作符結束或終止迭代,這使其超越 forEach 方法。

注意事項

for of 雖然好用,但要注意下面的幾個問題:

  • 不能直接遍歷對象。對象沒有實現迭代器接口,直接遍歷會拋出異常。如果想遍歷對象的屬性,可以先通過 Object.keys() 方法獲取對象的屬性列表,然後再遍歷。
  • 不能實現數組的賦值for of 遍歷數組時並沒有提供索引,無法直接修改數組。如果打算改變數組,建議使用其他遍歷方法。
  • 不要提前修改未迭代的項目。如果你在遍歷途中修改後面項的值,在之後的迭代中獲取的是新的值。
  • 不要在遍歷途中增刪項目。如果你在遍歷途中刪除了未迭代的項目,會導致迭代次數的減少;如果你在遍歷途中添加了新項,它們也將會被迭代。

前兩個問題在 擴展 for of 一節中將得到完美解決,另外兩個問題我們在遍歷時理應遵守的規範,如果不遵守將導致代碼邏輯的混亂。

預備知識

想要弄清楚 for of 語句的原理,要先認識3個概念:迭代協議、迭代器、生成器。

迭代協議

MDN 對迭代協議的介紹比較複雜,我簡單概括一下,詳情可點擊連接。

一個對象要想能夠被迭代,需要實現一個迭代接口,其值應該是一個符合規定的迭代器

在 JS 中,對象的迭代接口通過屬性 Symbol.iterator 暴露給了開發者。

迭代器

迭代器是一個對象,它具有一個 next 方法,該方法會返回一個對象,包含 valuedone 兩個屬性。value 表示這次迭代的值; done 表示是否已經迭代到序列中的最後一個。

迭代器對象可以重複調用 next() 方法,該過程稱迭代一個迭代器,又稱消耗了這個迭代器,因為它通常只能迭代一次。 在產生終止值之後,對 next() 的額外調用只會返回 {value: 最終值, done:true}

迭代器的寫法較為複雜,在這裏只展示一個實例,該函數用於生成迭代器對象。

不需要對其過多研究,因為我們不用手寫迭代器,JS 向我們提供了便捷的生成器用來生成迭代器對象。

// 數字從start開始,每次迭代增加step,直到大於end
function makeRangeIterator(start = 0, end = Infinity, step = 1) {
  let nextIndex = start

  const rangeIterator = {
    next: function () {
      let result
      if (nextIndex <= end) {z
        result = { value: nextIndex, done: false }
        nextIndex += step
      } else {
        result = { value: undefined, done: true }
      }
      return result
    },
  }

  return rangeIterator
}

生成器

生成器是一個函數,我將從語法和調用兩個方面詳細説明生成器函數。

語法:
生成器函數的語法有一定規則,該函數要使用 function* 語法編寫,在其內部可以使用 yield 關鍵字指定每一次迭代產出的值,也可以使用 return 關鍵字作為迭代器的終值。

調用:調用生成器函數只會返回一個迭代器對象,不會執行函數體中的代碼。通過調用生成器的 next() 方法,才會執行函數體中的內容,直到遇到 yield 關鍵字或執行完畢。

只看文字不好理解,看個例子吧

function* generator() {
  console.log('第一次調用')
  yield 'a'
  console.log('第二次調用')
  yield 'b'
  console.log('第三次調用')
  return 'c'
}

let iterator = generator()
console.log('創建迭代器')

console.log('next1:', iterator.next())
console.log('next2:', iterator.next())
console.log('next3:', iterator.next())
console.log('next4:', iterator.next())

控制枱的打印如下:

image.png

生成器函數的內容是分步調用的,每次迭代只運行到下一個 yield 的位置,將 yield 關鍵字後的表達式作為本次迭代的值產出。當遇到 return 或執行完函數後,返回對象的 done 屬性會被設置為 true,表示這個迭代器被完全消耗了。

將之前的例子改用生成器的寫法,代碼十分簡潔:

// 數字從start開始,每次迭代增加step,直到大於end
function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
  for (let i = start; i <= end; i += step) {
    yield i
  }
}

next 方法是可以傳參的,參數將以 yield 關鍵字的返回值的形式被使用,第一次 next 調用傳遞的參數將被忽略。

看看下面這個無限累加器,傳遞0會將其重置:

function* generator(start) {
  let cur = start
  while (true) {
    let num = yield cur
    if (num == 0) {
      console.log('迭代器被重置')
      cur = start
    } else {
      cur += num
    }
  }
}

let iterator = generator(10)
console.log('創建迭代器')
console.log('next1:', iterator.next().value)
console.log('next2:', iterator.next(2).value)
console.log('next3:', iterator.next(4).value)
console.log('next4:', iterator.next(5).value)
console.log('next5:', iterator.next(0).value)
console.log('next6:', iterator.next(5).value)
console.log('next7:', iterator.next(10).value)

控制枱輸出如下

image.png

原理與實現

for of 的原理,就是調用目標的迭代接口(生成器函數)獲取一個迭代器,然後不斷迭代這個迭代器,將返回對象的 value 屬性賦值給變量,直到返回對象的 done 屬性為 true

通過函數的方式簡單實現一下:

/**
 * @description: for of 方法實現
 * @param {object} iteratorObj 可迭代對象
 * @param {Function} fn 回調函數
 * @return {void}
 */
function myForOf(iteratorObj, fn) {
  // 如果傳入的對象不具備迭代接口,拋出異常
  if (typeof iteratorObj[Symbol.iterator] != 'function') {
    throw new TypeError(`${iteratorObj} is not iterable`)
  }
  // 獲取迭代器
  let iterator = iteratorObj[Symbol.iterator]()
  // 遍歷迭代器
  let i
  while (!(i = iterator.next()).done) {
    fn(i.value)
  }
}

const arr = [10, 20, 30]

myForOf(arr, (item) => {
  console.log(item)
})

let map = new Map([
  ['a', 1],
  ['b', 2],
  ['c', 3],
])

myForOf(map, ([key, value]) => {
  console.log(key, value)
})

控制枱輸入如下:

image.png

for offorEach 方法原理一致,上述代碼稍作修改就能實現 forEach 方法

擴展 for of

我們知道,for of 美中不足的一點是沒法直接遍歷對象的屬性

我們只要實現 Object 原型對象上的迭代接口,將其定義為一個返回包含對象所有屬性的生成器

實現如下:

Object.prototype[Symbol.iterator] = function* () {
  const keys = Object.keys(this)
  for (let i = 0; i < keys.length; i++) {
    yield keys[i]
  }
}

const obj = { a: 1, b: 2, c: 3 }

for (const key of obj) {
  console.log(key, obj[key])
}
// a 1
// b 2
// c 3

我們在生成器中提前獲取了對象的屬性數組,在迭代器中不斷產生就好了

令人驚喜的是,這個行為不會影響到 ... 操作符的對象淺拷貝功能,百利無一害。

console.log({ ...obj }) // {a: 1, b: 2, c: 3}
console.log([...obj]) // ['a', 'b', 'c']
使用 ... 淺拷貝對象,實際上調用的 Object.assign 方法

for of 另一點不足就是取不到索引,沒法修改數組,可以通過實現 Number 原型對象上的迭代接口解決

代碼如下:

Number.prototype[Symbol.iterator] = function* () {
  const num = this.valueOf()
  for (let i = 0; i < num; i++) {
    yield i
  }
}

const arr = [...5]

console.log(arr) // [0, 1, 2, 3, 4]

for (const index of arr.length) {
  arr[index] *= 2
}

console.log(arr) // [0, 2, 4, 6, 8]

如果是在 ts 中擴展 for of 後,使用時會提示錯誤,加入下列接口聲明就好了

declare interface Object {
  [Symbol.iterator]: any
}
declare interface Number {
  [Symbol.iterator]: any
}

結語

如果文中有錯誤或不嚴謹的地方,請務必給予指正,十分感謝。

內容整理不易,如果喜歡或者有所啓發,希望能點贊關注,鼓勵一下作者。

user avatar 1023 頭像 guizimo 頭像 yaofly 頭像 esunr 頭像 columsys 頭像 201926 頭像 yilezhiming 頭像 huanjinliu 頭像 b_a_r_a_n 頭像 light_5cfbb652e97ce 頭像 nihaojob 頭像 tofrankie 頭像
37 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.