iT邦幫忙

2022 iThome 鐵人賽

DAY 23
0

Abstract

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

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

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

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

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


使用時機

當你想要確認某個元素是否存在於陣列之中,並得到它在陣列中的索引 (index)。

lastIndexOf() 會從陣列結尾開始搜索,如果有找到便回傳第一個匹配到的元素索引,否則回傳 -1


語法

lastIndexOf(searchElement)

lastIndexOf(searchElement, fromIndex)

參數

第 1 個參數為 searchElement

第 2 個參數為 fromIndex (可選的)。

searchElement

想要確認的元素,可以是任一型別的值。

fromIndex (可選的)

搜索的起始點,如果不提供則預設為陣列的結尾點,也就是陣列的長度 - 1 (array.length - 1),也就表示整個陣列會被搜索。

其值可以是正負正數或小數:

  • 如果值大於等於陣列長度則為陣列長度 -1 (array.length - 1)
  • 如果值為負數表示起始點要從陣列結尾開始計算,要注意的是這不表示要改變搜索的方向,只是會從陣列結尾計算起始點的索引
  • 如果值為負數且加上陣列長度仍小於 0 的話 (array.length + fromIndex),陣列則不會被搜索,並回傳 -1
  • 如果為小數會先被轉成正數, 例如 1.1 -> 1-1.1 -> -1

Return Value

lastIndexOf() 會回傳陣列中第 1 個被找到的元素索引,如果都沒有找到則回傳 -1

Mutability

不會改動到原陣列。


說明

lastIndexOf() 會從陣列結尾開始尋找,當找到第 1 個完全相等的元素,則回傳該元素的索引,如果遍歷完整個陣列都沒找到則回傳 -1

如果 fromIndex 為負數的話, lastIndexOf() 會將其加上陣列長度也就是 array.length + fromIndex,例如以下:

const array = [1, 2, 3, 4]

console.log(array.lastIndexOf(2, -2))
// 1

// fromIndex + array.length => -2 + 4 = 2
// 等同以下

console.log(array.lastIndexOf(2, 2))
// 1

lastIndexOf() 找不到相對的元素會回傳 -1 (包括給的索引超過範圍),我們可以利用這個 -1 來做很多事,可參考範例的 Example 2、Example 3。

lastIndexOf() 會使用 嚴格相等 (strict equality,===)searchElement 跟陣列的每個元素作比較。

lastIndexOf() 不會去比對稀疏陣列 (sparse array) 中的 empty slot。


範例

Example 1 - 基礎用法

const numbers = [2, 3, 5, 7, 5]

console.log(numbers.lastIndexOf(2))
// 0

console.log(numbers.lastIndexOf(7))
// 3

console.log(numbers.lastIndexOf(5))
// 4

console.log(numbers.lastIndexOf(5, 3))
// 2

console.log(numbers.lastIndexOf(5, -1))
// 4

Example 2 - 利用 -1 + reduce() 來更新陣列

const names = ['Anita', 'Collin', 'Pedro']

const newNames = ['Anita', 'Emma', 'Arpad']
// 欲添加的名字,重複的我們不要

const updatedNames = newNames.reduce((updatedNames, newName) => {
	if (updatedNames.lastIndexOf(newName) !== -1)
	return [...updatedNames]
	// 如果名字已經存在,便直接複製一個新陣列往下傳遞

	return [...updatedNames, newName]
	//如果名字不存在,便複製一個新陣列並將新名稱添加後往下傳遞
}, names)

console.log(updatedNames) 
// ['Anita', 'Collin', 'Pedro', 'Emma', 'Arpad']

這邊使用到 reduce(),可能會稍嫌複雜,如果不熟悉可以參考 Day 13 的文章。

這邊考慮到 mutability,因此都是回傳新陣列,而非改動原陣列。

Example 3 - 找出陣列中跟 searchElement 匹配的全部元素索引 (參考自 MDN)

const array = [0, 1, 0, 1, 1, 0, 1]

console.log(findLastIndices(array, 1))
[6, 4, 3, 1]

function findLastIndices(array, searchElement) {
	const indices = []
	
	let index = array.lastIndexOf(searchElement)
	// 先找出第 1 個匹配的 index

	while(index !== -1) {
	// 只要元素有找到便做以下的事
		indices.push(index)

		index = array.lastIndexOf(searchElement, index - 1)
		// 接著再找下一個匹配的 index,但這次從上次被找到的地方開始
	}
	return indices
}

lastIndexOf() 只會回傳第一個匹配到的索引,但我們可以利用這個索引再設置新的起始點。


注意事項

如果有給 fromIndex 只是表示要從這個位置開始找,並不會改變元素本身所處的位置,例如:

const names = ['Anita', 'Emma', 'Pedro', 'Collin']

console.log(names.lastIndexOf('Emma', 2))
// 1
// 回傳的仍是 `Emma` 原本的位置,不會因為 `fromIndex` 的改變而變成回傳相對位置

fromIndex 可以使用小數,因為它會先被轉成整數,但請注意它被轉換的方式可能跟預期不同,可參考下方 ECMAScript 的說明。


ECMAScript

23.1

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

23.2

演算法的前 2 個步驟都是用來做一些前置處理,包括轉型、確認長度。

步驟 3 可以看到如果陣列長度為 0,則直接回傳 -1 表示不會做任何的搜索。

步驟 4 蠻值得注意的,這個 ToIntegerOrInfinityindex 是小數仍然可以正常使用,而且因為它在轉換的過程使用了 絕對值,導致轉成整數的結果可能會跟預期的不一樣。

而且步驟 4 為這個咩色很重要的一步,它將遍歷的起始點設為陣列長度 - 1,因此 lastIndexOf() 將會從陣列結尾開始遍歷陣列。

步驟 6 表明了當 fromIndex 大於等於陣列長度則會直接使用陣列長度 - 1 (array.length - 1) 取代,驗證了上面的參數說明。

步驟 7 表明了當 fromIndex 為負數時,會將其加上陣列的長度,讓其可以從陣列結尾處計算起始點的索引,驗證了上面的參數說明。

步驟 8 開始遍歷陣列並逐一比對,可以看到其步驟 a 使用了 HasProperty 來檢查是否有相對索引的屬性存在,如果有才會去比對,沒有則會跳過,這也驗證了它會忽略稀疏陣列的 empty slot。

這邊有一個很值得注意的是 IsStrictlyEqual(x, y), 它的實際語義就是我們常用的 - 比較 2 個值是否嚴格相等 (===),所以可以看到其演算法的步驟 1 直接剔除型別不相等的值,而接下來它又把 Number、非 Number 分開來處理:

  • Number::equal(x, y) - 從步驟 1、2 可以看到只要 xy 其中一個為 NaN 則回傳 false,這驗證了 JavaScript 最讓人迷惑的地方之一 「NaN 不等於 NaN」,接著只要值相等就會相等
  • SameValueNonNumber(x,y) - 其步驟 1 直接斷言 (Assert) x, y 的型別相等,表示在執行這個 abstract operation 時傳進去的參數應該要是型別相同的值,接著需要特別注意的便是物件型別,它比較的會是物件的參照 (reference) 而非內容!

出現 ? 的地方代表有可能會丟出錯誤,所以整個演算法有 5 處有機會丟出錯誤,例如步驟 4 的 ToIntegerOrInfinity(),當你傳入 Symbol 或 BigInt 即會從內部的 ToNumber()丟出一個 TypeError 的錯誤,我們來驗證一下:

23.3

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

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


結論

雖然 lastIndexOf() 使用起來跟 indexOf() 的感受差別不大,但你會發現,ECMAScript 做了不少從後方開始開始搜索的咩色 (例如 findLastfindLastIndex),這也間接地表示語義跟效能節省的重要性。

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


參考資源


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

尚未有邦友留言

立即登入留言