前言
在之前開發 Python 時,最常使用 for in 去遍歷物件;仔細想想,自己好像被 forEach 寵壞了,都忘記 JavaScript 也有相關的語法,實際使用下來,發現有部分觀念需要釐清,且某些情境可能不是這麼好用,需要搭配其他方法才能完成目的。此篇重點圍繞在 for、for/in、for/of、forEach 的使用情境與差別。
筆記重點
- 遍歷物件基本操作
- 使用情境 - 非數值屬性
- 使用情境 - 陣列的空元素
- 使用情境 - this 的指向
- 結論
遍歷物件基本操作
此篇文章將會針對下列 4 種迴圈語法做介紹,讓我們先來看看各語法的基本操作:
- for (let index = 0; index < array.length; index += 1) {}
- for (const key in object) {}
- for (const interator of object) {}
- array.forEach((item, index, array) => {} )
處理陣列:
1 | let arr = ['Eric', 'Allen', 'Owen']; |
處理物件:
1 | let obj = { |
for
作用對象:陣列
遍歷對象:無
遍歷陣列
1 | for (let i = 0; i < arr.length; i++) { |
for in
作用對象:陣列、物件
遍歷對象:鍵(key)
遍歷陣列
1 | for (const key in arr) { |
遍歷物件
1 | for (const key in obj) { |
繼承屬性物件問題:hasOwnProperty
1 | Array.prototype.newType = 'newValue'; |
for of
作用對象:陣列
遍歷對象:值(value)
遍歷陣列
1 | for (const value of arr) { |
遍歷物件:搭配 Object.values() - ES8 新增
1 | for (const value of Object.values(obj)) { |
遍歷物件:搭配 Object.entries() - ES8 新增
1 | for (const [key, value] of Object.entries(obj)) { |
遍歷陣列:搭配 Array.prototype.entries() - ES6 新增
1 | for (const iterator of arr.entries()) { |
forEach
作用對象:陣列
遍歷對象:鍵(key)、值(value)、作用對象(array)
遍歷陣列
1 | arr.forEach((item, index, array) => { |
遍歷物件:搭配 Object.entries() - ES8 新增
1 | Object.entries(obj).forEach((item) => { |
使用情境 - 非數值屬性
事實上 JavaScript 的陣列是類似列表的物件,這就意味著我們可以直接給陣列新增屬性:
1 | let arr = ['red', 'blue', 'yellow']; |
需要注意的是,遍歷相關語法對於非數值屬性的處理方式是不一樣的,主要分為兩種:
不會忽略非數字屬性:for in
1 | let arr = ['red', 'blue', 'yellow']; |
會忽略非數字屬性:for、for of、forEach
1 | let arr = ['red', 'blue', 'yellow']; |
由上面測試可得知,使用 for in 時,會連同非數字屬性也一起遍歷,其他 3 種則不會,正常來講,不應該連同非數字屬性一起遍歷才對,遍歷陣列時,應該避免使用 for in,轉而使用其他三種遍歷語法。
使用情境 - 陣列的空元素
JavaScript 中的陣列是允許有空元素的,如下範例:
1 | let arr = ['red', , 'blue']; |
奇怪的地方在於,遍歷相關語法對於空元素的處理方式卻是不一樣的,主要分為兩種:
跳過空元素:for in、forEach
1 | let arr = ['red', , 'blue']; |
不會跳過空元素:for、for of
1 | let arr = ['red', , 'blue']; |
額外補充:JSON 也不支援空元素
1 | /* --- JSON.stringify --- */ |
由上面測試可得知,for in、forEach 遇到空元素會直接跳過,for、for of 則不會,取決於你遍歷的目的是什麼,選擇相對應的方法,同時也得注意 JSON 是否支援等問題。
使用情境 - this 的指向
在 JavaScript 中各遍歷語法對於 this 的指向都是大同小異的,基本上都是指向外部的 window 物件,唯獨 forEach 最特別,如下範例:
保留外部作用域:for、for in、for of
1 | let arr = ['red']; |
指向特定的對象:forEach
- 非嚴謹模式
1 | let arr = ['red']; |
- 嚴謹模式
1 | // JavaScript 嚴謹模式 ; |
由上例可看出,forEach 所指向的 this 是根據第 2 個 thisArg 參數所提供,相反的,如果 thisArg 參數未定義或為 null,this 將根據設定模式指向對應的對象,嚴謹模式下為 undefined,非嚴謹模式下為 window,盡可能的要求所有 callback function 必須使用箭頭函式。
使用情境 - 中斷迴圈
在一般遍歷語法中,使用 break、return 中斷迴圈是再正常不過的事情,但這兩個語法使用在 forEach 上是行不通的,相關範例如下:
中斷迴圈成功:for、for in、for of
1 | let arr = ['red', 'blue', 'black']; |
中斷迴圈失敗:forEach
1 | let arr = ['red', 'blue', 'black']; |
其他遍歷方法:every、some
1 | let arr = ['red', 'blue', 'black']; |
由上例可看出,forEach 使用 break 會發生錯誤,使用 return 最多只能中斷當前遍歷項目,最後依然會遍歷後面的項目,個人建議,如果有中斷迴圈需求,請使用 for、for in、for of 方法,或者利用 every、some 依序判斷項目特性來完成操作。
結論
經過上面的討論,你會發現 for of 是遍歷陣列最可靠的方式,它比 for 語法簡潔,並且沒有 for in 與 forEach 那麼多奇怪的特例,唯二的缺點是取得索引值需要搭配其他方法才能完成,以及無法像 forEach 一樣鏈式操作物件;在 Airbnb 的 Style Guide 中,禁止使用 for 相關的遍歷方法,推薦使用 forEach 高階函數來完成遍歷,其主要原因為較容易推論結果,其實也蠻有道理的,除非遇到上述所講的特殊情境,使用 for of 較為容易,不然在一般情境中 forEach 或許是你更好的選擇。