整篇會分成以下幾個部分:
forEach
這個 method 的全寫應該是 Array.prototype.forEach
,有興趣可以看 Day 2 的介紹,這邊會直接使用 forEach()
作為替代。
Array method 有不少會使用到 callback function,如果尚不熟悉的話,可以看 Day 2 的介紹。
範例使用的 callback 都會使用箭頭函式做介紹,如果尚不熟悉的話可以參考 MDN 的介紹。
最後會透過分析 ECMAScript 來驗證是否有吻合,如果覺得 ECMAScript 有點艱澀難懂,我們在 Day 4 、Day 5 有介紹其相關術語可以幫助閱讀。
可以遍歷一個陣列並重複執行自定義的函式 (callback)。
當你不想用 for-loop
的時候。
forEach()
會依照陣列長度決定 callback 的呼叫次數,每次都會帶入陣列當前遍歷到的元素、索引跟陣列本身這 3 個參數。
forEach((element, index, array) => {
/* 執行內容 */
})
forEach(callbackFn,thisArg)
forEach(function(element, index, array) {
/* 執行內容 */
}, thisArg)
forEach()
的第 1 個參數為 callback, 第 2 個參數為選擇性 (optional) 的 thisArg
。
callback
forEach()
會按照陣列元素的順序依次 (升冪) 呼叫這個 callback,直到陣列元素被遍歷完畢,換句話說,如果這個陣列有 5 個元素,那這個 callback 至多會被呼叫 5 次。
當這個 callback 被呼叫時會帶入 element
、index
、array
三個參數。
這個 callback 永遠都會回傳 undefined
element
陣列當前的元素 (element),callback 的第 1 個參數,為 forEach()
當前遍歷到的元素,也就表示 element
會依陣列的順序動態變化。
index
陣列當前元素的索引值 (index),callback 的第 2 個參數,為 forEach()
當前遍歷到的元素其索引值,也就表示 index
會依陣列的順序動態變化。
array
呼叫 forEach()
的陣列本身 (被遍歷的陣列本身), callback 的第 3 個參數,不論 forEach()
當下遍歷到哪個元素上, array
都會指向被遍歷的陣列本身,也就是呼叫 forEach()
的陣列本身。
thisArg
為 forEach()
的第 2 個選擇性參數,它會被傳入 callback 並作為其 this
的值,否則就會是 undefined
。
請注意,如果 callback 使用箭頭函式的話則沒有作用!
可以參考範例的 Example - 4 寫法。
不論如何都會回傳 undefined
。
不會改動到原陣列。
const names = ['Arpad', 'Ross', 'Damien', 'Tyler']
names.forEach(name => console.log(name))
// 'Arpad'
// 'Ross'
// 'Damien'
// 'Tyler'
names
names
呼叫 forEach()
時會順著原型鏈拿到放在 Array.prototype
指向的 prototype 物件上的 forEach()
methodforEach()
呼叫時需傳入了一個 callback (testing function),因此我們直接在 forEach()
的參數裡定義一個帶有 name
參數的箭頭函式forEach()
被呼叫時, forEach()
會執行 callback,並帶入 element
、index
、array
等 3 個參數,其中的 element
會被指派給 method
這個參數name
forEach
最後會回傳 undefined
index
參數const names = ['Arpad', 'Ross', 'Damien', 'Tyler']
names.forEach((name, index) => {
console.log(`The person ${index + 1} is ${name}`)
})
// The person 1 is Arpad
// The person 2 is Ross
// The person 3 is Damien
// The person 4 is Tyler
index
參數, callback 必須依照順序定義參數,不能略過前面的參數forEach
vs. for-loop
const numbers = [1, 2, 3, 4, 5];
const numbersPlusOne = [];
// for-loop
for (let i = 0; i < numbers.length; i++) {
const newNumber = numbers[i] + 1
numbersPlusOne.push(newNumber);
}
// forEach()
numbers.forEach(number => {
const newNumber = number + 1
numbersPlusOne.push(newNumber)
})
forEach
可讀性較高thisArg
參數:const names = ['Arpad', 'Ross', 'Damien', 'Tyler']
const ages = [25, 31, 18, 47]
names.forEach(function(name, index) {
console.log(`${name} is ${this[index]} years old`)
}, ages)
// Arpad is 25 years old
// Ross is 31 years old
// Damien is 18 years old
// Tyler is 47 years old
ages
被作為 forEach()
的第 2 個參數傳入this
會指向 ages
請注意 callback 定義時的參數順序,依序應為 element
、index
、array
,但不可隨意略過,就算你只想使用 index
而不需要 element
, 可參考範例的 Example 2 寫法。
我們無法透過在 callback 裡使用 return
或 break
來終止 forEach
的執行,只能透過 throw
錯誤來達到終止的目的,這點可以從 ECMAscript 的演算法窺知一二,並且要稍微瞭解 function 在 stack 的運作原理:
forEach
的演算法裡並沒有使用到 for
這個陳述式 (statement),因此在 callback 裡使用 break
不會有任何作用,而且也不是合法的return
終止的是當前被呼叫的這個 callback,而非 forEach
本身, 只要陣列還有元素沒有被遍歷到,forEach
便會繼續呼叫下一次的 callback,我們來驗證看看:const arr = ['item 1', 'item 2', 'item 3']
const result = arr.forEach((item, index) => {
console.log(`index: ${index}`)
if (index === 1) return 'error'
console.log(`item: ${item}`)
})
// index: 0
// item: item 1
// index: 1
// index: 2
// item: item 3
console.log(result)
// undefined
forEach
的 callback 必須是同步的函式,它不會等待 promise 物件回傳後才繼續執行,這邊之後會再討論,可以先安心跳過。
有一點值得注意的是,雖然 forEach()
不會變動到原陣列,但我們傳進去的 callback 卻有可能 ,而陣列元素被遍歷的範圍在第一次呼叫 callback 前就已經確立好了 (也就是 forEach()
被呼叫後但 callback 尚未被呼叫),因此有可能會發生以下的狀況:
undefined
上述這種高併發 (concurrent) 的更動會導致程式碼非常難以閱讀,非常不建議使用 (除非有不得已的特殊情境)。
Array.prototype.forEach(callbackfn[,thisArg])
this
轉型成 object 後指派給 O
O
的長度並指派給 len
callbackfn
是不可呼叫的 object 則丟出一個 TypeErrork
k
< len
時,重複以下步驟k
轉型成 Number 再轉型成 String 後指派給 Pk
O
是否有 Pk
這個屬性並將結果指派給 kPresent
kPresent
為 true,則進行以下步驟O
的屬性 Pk
的值,並指派給 kPresent
callbackfn
並帶入 '« kValue, ?(k), O »' 這個 argumentList,如果 thisArg
存在,則會用其取代 callbackfn
的 this
k
= k
+ 1undefined
演算法的前 3 個步驟都是用來做一些前置處理,包括轉型、確認長度、確認參數是否為一個 function 等...。
步驟 5 開始執行迴圈,最大圈數為陣列長度,計數從 0 開始,重複以下步驟:
callbackfn
並帶入被取出的 元素、 當前計數、 陣列本身 這 3 個參數整個演算法不管結果如何,最終都會回傳 undefined
而出現 ?
的地方代表有可能會丟出錯誤,所以整個演算法有 5 處有機會丟出錯誤。
我們來看看演算法裡面的 Call()
,點進去可以看到當傳入的物件不是一個可呼叫 (callable) 的物件,例如 function,便會丟出一個 TypeError ,但這個可能已在 forEach
的步驟 3 時便被過濾掉,因此這裡的 Call
有機會丟出錯誤的地方便落在了其步驟 3 - 呼叫 [[Call]]
這個 internal method 的時候!
當呼叫 [[Call]]
的這個物件,其 internal slot - [[IsClassConstructor]]
為 true 時,即表示這個物件是一個 class constructor,所以 [[Call]] 便丟出一個 TypeError,這也表示 class cunstructor 無法單純使用 [[Call]]
這個 internal method 來呼叫,這可能也是為什麼有人認為 class 不僅僅是語法糖,因爲它跟 function 之間存在一些本質上的差異。
如果出現 !
,則代表這個 abstract operation 絕對不會丟出錯誤,例如步驟 5 -> 步驟a 的 ToString()
它會在參數是一個 Symbol 時丟出一個 TypeError,但我們確定丟進去的是一個 Number (F(k)
),因此不會有丟出錯誤的可能。
ECMAScript 其實並沒有規定使用 forEach()
的物件必須是一個陣列,從步驟 1 跟 Note 2 可以看得出來,我們試著來驗證看看:
有人會說 forEach
是進階版的 for-loop
,從結果來看是沒錯的,但如果試著去瞭解 Array method 的演算法,會發現 forEach
並不是 for-loop
的延伸再包裝版本,它們有本質上的不同,這樣一來便能清楚地知道什麼時候該用哪個方式來遍歷我們的陣列。
最後,希望大家可以開心地使用各種咩色,體驗它帶給你的便利,祝大家歸剛沒煩惱。
Array.prototype.find - MDN
Draft ECMA 262 - TC39