iT邦幫忙

2022 iThome 鐵人賽

DAY 21
0

Abstract

我是阿傑,既然前面介紹了這麼多 找東西 的咩色 (findfilterindexOf...),我們今天就來介紹一個最新最時尚的咩色 - findLast(),顧名思義,它會從陣列的結尾開始尋找。

我剛看到它的時候真的滿頭問號,它跟 find() 做的事情不是一樣嗎?撇除名字不看,它們的實際語義是不是重複了!?這跟下面有什麼差別...

手上的掌上明珠

為了解開這個疑惑,我點開了 TC39 github repo 內的 proposal-array-find-from-last,找到了他們提案的動機,我簡單畫一下重點:

  • 查找陣列元素是一個出現頻率極高的操作,這個提案的核心目的就是讓其更加地 語義化,也就是可以清楚地表明我現在想要執行的操作。
  • 可以增進一些效能,避免一些不必要的性能開銷,在某些場景會有用處,例如 React 的 render function。
  • 為了解決 [...arr].reverse().find() 這個替代方案產生的 issue:
    • reverse() 造成了陣列非必要的變動
    • 為了避免變動而產生了對陣列非必要的複製

接著,它又提示了適合使用的情境:

  • 當你清楚知道從陣列結尾查找會有更好效能
  • 你在乎元素的順序 (陣列中可能有重複的元素),例如數字陣列中的最後一個奇數,你不想為了找到它而重新排序陣列的順序

綜上所述,創造這個咩色的目的看起來不外乎就是 語義化性能

整篇會分成以下幾個部分:

  • 使用時機
  • 語法
  • 說明
  • 範例
  • 注意事項
  • ECMAScript
  • 結論

findLast() 這個 method 的全寫應該是 Array.prototype.findLast,有興趣可以看 Day 2 的介紹,這邊會直接使用 findLast() 作為替代。

範例的 callback 都會使用箭頭函式做介紹,如果尚不熟悉的話可以參考 MDN 的介紹。

最後會透過分析 ECMAScript 來驗證是否有吻合,如果覺得 ECMAScript 有點艱澀難懂,我們在 Day 4 、Day 5 有介紹其相關術語可以幫助閱讀。


語法

Arrow function (直接定義箭頭函式)

findLast((element, index, array) => {
	/* 測試條件 */
* })

callback (直接傳入回呼函式)

findLast(callbackFn, thisArg)

Inline Callback (直接定義匿名函式)

findLast(function(element, index, array){
	/* 測試條件 */ 
}, thisArg)

參數

findLast() 的第 1 個參數為 callback, 第 2 個參數為可選的 (optional) thisArg

callback

這個 callback 又稱為 testing functionpredicate (規範用語),顧名思義它會被拿來測試某些條件,再準確一點應該稱作斷言 (assert) ,因為它最後回傳的值會被強制轉換成布林值 (truefalse)!

當這個 callback 被呼叫時會帶入 elementindexarray 三個參數。

findLast() 會按照陣列元素的順序依次 (升冪) 呼叫這個 callback,直到這個 callback 回傳 true 或當陣列元素已被遍歷完畢即停止,換句話說,如果這個陣列有 5 個元素,那這個 callback 最多會被呼叫 5 次。

  • element
    陣列當前的元素 (element),callback 的第 1 個參數,為 findLast() 當前遍歷到的元素,也就表示 element 會依陣列的順序動態變化。

  • index
    陣列當前元素的索引值 (index),callback 的第 2 個參數,為 findLast() 當前遍歷到的元素其索引值,也就表示 index 會依陣列的順序動態變化。

  • array
    呼叫 findLast() 的陣列本身 (被遍歷的陣列本身), callback 的第 3 個參數,不論 findLast() 當下遍歷到哪個元素上, array 都會指向被遍歷的陣列本身,也就是呼叫 findLast() 的陣列本身。

thisArg

findLast() 的第 2 個選擇性參數,它會被傳入 callback 並作為其 this 的值,否則就會是 undefined

請注意,如果 callback 使用箭頭函式的話則沒有作用!可以參考範例的 Example - 4。

Return Value

回傳第 1 個從陣列結尾找到的元素, 如果沒有找到則回傳 undefined

Mutability

不會變動到原陣列。


說明

findLast() 會從陣列結尾開始尋找,當找到第 1 個符合條件 (callbalck) 的元素,則回傳該元素,如果遍歷完整個陣列都沒找到則回傳 undefinded

findLast會跟據陣列長度決定 callback 的最大呼叫次數,也就是說當 callback 只要一回傳 true 便會中止 findLast() 的執行,因此之後的 callback 便不會再被呼叫,如果都是回傳 false 便等於遍歷完整個陣列。

findLast() 可以用來判別稀疏陣列 (sparse array) 的 empty slot,其值會是 undefined,但由於 findLast() 在找不到符合條件的元素時也是回傳 undefined,我們沒有辦法分辨它是回傳 的值還是並未找到,因此意義不大。


範例

Example 1 - 基礎用法

const names = ['Emma', 'Alejo', 'Pedro', 'Cate', 'Arpad']

const pickedName = names.findLast(name => name === 'Cate')

console.log(pickedName)
// Cate

從陣列結尾開始尋找,回傳第 1 個為 'Cate' 的元素。

Example 2 - 使用 idnex 參數

const names = ['Tyler', 'Coby', 'Kingsley', 'Amber', 'Emma', 'Pedro']

const pickedName = names.findLast((name, index) => name.length > 4 && index > 1);

console.log(pickedName)
// "Pedro"

從陣列結尾開始尋找,回傳第 1 個長度大於 4 且 索引大於 1 的元素。

Example 3 - 陣列包含多個物件時

const actors = [
	{ name: "Denzel Washington", age: 67 },
	{ name: "Tom Hardy", age: 45 },
	{ name: "Brad pitt", age: 58 },
	{ name: "Michael Fassbender", age: 45 },
	{ name: "Jake Gyllenhaal", age: 41 },
	{ name: "Collin Farrel", age: 46 }
]

const result = actors.findLast((actor, index) => actor.age >= 45 && index < 4)

console.log(result)
// { name: "Michael Fassbender", age: 45 }

從陣列結尾開始尋找,回傳第 1 個屬性 age 大於等於 45 且 索引小於 4 的物件元素。

Example 4 - 使用 thisArg 參數

const stars = ["Charlize", "Emma", "Jake", "Collin"]

const ages = [35, 16, 12, 41]

const childStar = stars.findLast(function(_star, index) {
	return this[index] < 18;
}, ages)

console.log(childStar)
// 'Jake'

這邊將 ages 作為 findLast() 的第 2 個參數傳入,因此裡面的 callback 之 this 會指向 ages 這個陣列。

我們利用 index 來取用 ages 相對應索引的元素,並判斷其值是否小於 18。


注意事項

雖然 findIndex() 會自動對回傳的值做布林轉換,但可以考慮讓 callback 直接回傳布林值,這樣比較不會有轉型上的失誤,閱讀起來也較直覺。

請注意 callback 定義時的參數順序,依序應為 elementindexarray,假設你只想使用 index 而不使用 element,你仍需定義 element,可以增加底線以利閱讀,例如這樣:

array.findLast((_element, index) => { /* ... */ })

有一點值得注意的是,雖然 findLast() 不會變動到原陣列,但我們傳進去的 callback 卻有可能 ,而陣列元素被遍歷的範圍在第一次呼叫 callback 前就已經確立好了 (也就是 findLast() 被呼叫後但 callback 尚未被呼叫),因此有可能會發生以下的狀況:

  • 在這之後才被添加 (appended) 進去的元素不會被 callback 遍歷到
  • 如果原先存在的元素被變動到了, 那當 callback 遍歷到此元素時會使用它最新的值,而非原先的值
  • 如果尚未被遍歷到的元素被刪除了,那它還是會被遍歷到,如果還是找不到便會往 prototype 尋找

上述這種高併發 (concurrent) 的更動會導致程式碼非常難以閱讀,非常不建議使用 (除非有特殊的情境)。


ECMAScript

21.2

findLast() 的演算法並沒有要求呼叫它的物件必須是一個陣列,可以從步驟 1 跟 Note 2 得知,為了方便解釋,這邊一律使用陣列來說明;我們先來驗證一下它是否被做成了通用的咩色:

21.3

演算法的前 3 個步驟都是用來做一些前置處理,包括轉型、確認長度、確認參數是否為一個 function 等...。

步驟 4 為最關鍵的一步 - 它將初始的計數 k 設為 len -1,也就是陣列中的長度 - 1 (陣列的最後一個索引),這表示它將從陣列的結尾開始遍歷整個陣列的元素。

步驟 5 會看到遍歷的次數在 callback 第 1 次呼叫前就已經決定好了,驗證了上面注意事項所說。

於遍歷的期間,每次都會將原物件相對應屬性的值取出, 這個值會在 callback 呼叫時當作第 1 個參數帶入;而遍歷當前的計數會被當作第 2 個參數帶入;這驗證了上面所說的 callback 之 element、及 index 會在遍歷的途中動態變化。

步驟 5-c 會對 callback 回傳的結果進行布林轉換,只要為 true 便會立即中止 findLast(),並回傳當前的元素,如果所有的結果都是 flase,則回傳 undefined

有一個地方很值得注意,就是為什麼 findLast() 可以判斷稀疏陣列的 empty slot,這是因為其演算法並沒有使用 HasProperty() 這個抽象操作,因此 findLast 在呼叫 callback 之前並不會檢查陣列是否有這個屬性,所以 callback 的 element 參數會拿到一個 undefined, 而不少 method 都有使用 HasProperty(),因此它們不會遍歷到 empty slot (例如 some())。

出現 ? 的地方代表有可能會丟出錯誤,所以整個演算法有 4 處有機會丟出錯誤,例如步驟 1 的 ToObject(),當你傳入 Undefined 或 Null 即會丟出一個 TypeError 的錯誤,我們來驗證一下:

21.4

如果出現 ! 則代表這個 abstract operation 絕對不會丟出錯誤,例如步驟 5 - a 的 ToString() 它會在參數是一個 Symbol 時丟出一個 TypeError,但我們確定丟進去的是一個 Number (F(k)),因此不會有丟出錯誤的可能。

從 ECMAScript 的演算法來看,尚未找到與 JavaScript 實作的不同之處。


結論

另外要注意的是 findLast() 目前還處在提案的 stage 4,也就是說它雖然尚未出現在正式的 ECMAScript 2022 裡,但已經被納入 TC39 的 ECMAScript 2023 草案裡!這意味著它應該已經有被瀏覽器實作出來,我們可以點進這張 相容性表格 來看看,會發現 4 大瀏覽器的較新版本皆已支援 (IE 一如既往的沒有呢 XD),我們來確認一下吧:

21.1

最後,希望大家可以開心地使用各種咩色,體驗它帶給你的便利,祝大家歸剛沒煩惱。


參考資源


上一篇
Day 20 咩色用得好 - Array.prototype.findIndex
下一篇
Day 22 咩色用得好 - Array.prototype.findLastIndex
系列文
咩色用得好,歸剛沒煩惱 - 從 ECMAScript 偷窺 JavaScript Array method30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言