JavaScript專題之學underscore在陣列中查詢指定元素

冴羽發表於2019-03-01

JavaScript專題系列第十篇,講解如何從陣列中查詢指定元素,並且跟著 undersocre 實現 findIndex 和 findLastIndex、sortedIndex、indexOf 和 lastIndexOf

前言

在開發中,我們經常會遇到在陣列中查詢指定元素的需求,可能大家覺得這個需求過於簡單,然而如何優雅的去實現一個 findIndex 和 findLastIndex、indexOf 和 lastIndexOf 方法卻是很少人去思考的。本文就帶著大家一起參考著 underscore 去實現這些方法。

在實現前,先看看 ES6 的 findIndex 方法,讓大家瞭解 findIndex 的使用方法。

findIndex

ES6 對陣列新增了 findIndex 方法,它會返回陣列中滿足提供的函式的第一個元素的索引,否則返回 -1。

舉個例子:

function isBigEnough(element) {
  return element >= 15;
}

[12, 5, 8, 130, 44].findIndex(isBigEnough);  // 3複製程式碼

findIndex 會找出第一個大於 15 的元素的下標,所以最後返回 3。

是不是很簡單,其實,我們自己去實現一個 findIndex 也很簡單。

實現findIndex

思路自然很明瞭,遍歷一遍,返回符合要求的值的下標即可。

function findIndex(array, predicate, context) {
    for (var i = 0; i < array.length; i++) {
        if (predicate.call(context, array[i], i, array)) return i;
    }
    return -1;
}

console.log(findIndex([1, 2, 3, 4], function(item, i, array){
    if (item == 3) return true;
})) // 2複製程式碼

findLastIndex

findIndex 是正序查詢,但正如 indexOf 還有一個對應的 lastIndexOf 方法,我們也想寫一個倒序查詢的 findLastIndex 函式。實現自然也很簡單,只要修改下迴圈即可。

function findLastIndex(array, predicate, context) {
    var length = array.length;
    for (var i = length; i >= 0; i--) {
        if (predicate.call(context, array[i], i, array)) return i;
    }
    return -1;
}

console.log(findLastIndex([1, 2, 3, 4], function(item, index, array){
    if (item == 1) return true;
})) // 0複製程式碼

createIndexFinder

然而問題在於,findIndex 和 findLastIndex 其實有很多重複的部分,如何精簡冗餘的內容呢?這便是我們要學習的地方,日後面試問到此類問題,也是加分的選項。

underscore 的思路就是利用傳參的不同,返回不同的函式。這個自然是簡單,但是如何根據引數的不同,在同一個迴圈中,實現正序和倒序遍歷呢?

讓我們直接模仿 underscore 的實現:

function createIndexFinder(dir) {
    return function(array, predicate, context) {

        var length = array.length;
        var index = dir > 0 ? 0 : length - 1;

        for (; index >= 0 && index < length; index += dir) {
            if (predicate.call(context, array[index], index, array)) return index;
        }

        return -1;
    }
}

var findIndex = createIndexFinder(1);
var findLastIndex = createIndexFinder(-1);複製程式碼

sortedIndex

findIndex 和 findLastIndex 的需求算是結束了,但是又來了一個新需求:在一個排好序的陣列中找到 value 對應的位置,保證插入陣列後,依然保持有序的狀態。

假設該函式命名為 sortedIndex,效果為:

sortedIndex([10, 20, 30], 25); // 2複製程式碼

也就是說如果,注意是如果,25 按照此下標插入陣列後,陣列變成 [10, 20, 25, 30],陣列依然是有序的狀態。

那麼這個又該如何實現呢?

既然是有序的陣列,那我們就不需要遍歷,大可以使用二分查詢法,確定值的位置。讓我們嘗試著去寫一版:

// 第一版
function sortedIndex(array, obj) {

    var low = 0, high = array.length;

    while (low < high) {
        var mid = Math.floor((low + high) / 2);
        if (array[mid] < obj) low = mid + 1;
        else high = mid;
    }

    return high;
};

console.log(sortedIndex([10, 20, 30, 40, 50], 35)) // 3複製程式碼

現在的方法雖然能用,但通用性不夠,比如我們希望能處理這樣的情況:

// stooges 配角 比如 三個臭皮匠 The Three Stooges
var stooges = [{name: `stooge1`, age: 10}, {name: `stooge2`, age: 30}];

var result = sortedIndex(stooges, {name: `stooge3`, age: 20}, function(stooge){
    return stooge.age
});

console.log(result) // 1複製程式碼

所以我們還需要再加上一個引數 iteratee 函式對陣列的每一個元素進行處理,一般這個時候,還會涉及到 this 指向的問題,所以我們再傳一個 context 來讓我們可以指定 this,那麼這樣一個函式又該如何寫呢?

// 第二版
function cb(fn, context) {
    return function(obj) {
        return fn ? fn.call(context, obj) : obj;
    }
}

function sortedIndex(array, obj, iteratee, context) {

    iteratee = cb(iteratee, context)

    var low = 0, high = array.length;
    while (low < high) {
        var mid = Math.floor((low + high) / 2);
        if (iteratee(array[mid]) < iteratee(obj)) low = mid + 1;
        else high = mid;
    }
    return high;
};複製程式碼

indexOf

sortedIndex 也完成了,現在我們嘗試著去寫一個 indexOf 和 lastIndexOf 函式,學習 findIndex 和 FindLastIndex 的方式,我們寫一版:

// 第一版
function createIndexOfFinder(dir) {
    return function(array, item){
        var length = array.length;
        var index = dir > 0 ? 0 : length - 1;
        for (; index >= 0 && index < length; index += dir) {
            if (array[index] === item) return index;
        }
        return -1;
    }
}

var indexOf = createIndexOfFinder(1);
var lastIndexOf = createIndexOfFinder(-1);

var result = indexOf([1, 2, 3, 4, 5], 2);

console.log(result) // 1複製程式碼

fromIndex

但是即使是陣列的 indexOf 方法也可以多傳遞一個引數 fromIndex,從 MDN 中看到 fromIndex 的講究可有點多:

設定開始查詢的位置。如果該索引值大於或等於陣列長度,意味著不會在陣列裡查詢,返回 -1。如果引數中提供的索引值是一個負值,則將其作為陣列末尾的一個抵消,即 -1 表示從最後一個元素開始查詢,-2 表示從倒數第二個元素開始查詢 ,以此類推。 注意:如果引數中提供的索引值是一個負值,仍然從前向後查詢陣列。如果抵消後的索引值仍小於 0,則整個陣列都將會被查詢。其預設值為 0。

再看看 lastIndexOf 的 fromIndex:

從此位置開始逆向查詢。預設為陣列的長度減 1,即整個陣列都被查詢。如果該值大於或等於陣列的長度,則整個陣列會被查詢。如果為負值,將其視為從陣列末尾向前的偏移。即使該值為負,陣列仍然會被從後向前查詢。如果該值為負時,其絕對值大於陣列長度,則方法返回 -1,即陣列不會被查詢。

按照這麼多的規則,我們嘗試著去寫第二版:

// 第二版
function createIndexOfFinder(dir) {

    return function(array, item, idx){
        var length = array.length;
        var i = 0;

        if (typeof idx == "number") {
            if (dir > 0) {
                i = idx >= 0 ? idx : Math.max(length + idx, 0);
            }
            else {
                length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;
            }
        }

        for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {
            if (array[idx] === item) return idx;
        }
        return -1;
    }
}

var indexOf = createIndexOfFinder(1);
var lastIndexOf = createIndexOfFinder(-1);複製程式碼

優化

到此為止,已經很接近原生的 indexOf 函式了,但是 underscore 在此基礎上還做了兩點優化。

第一個優化是支援查詢 NaN。

因為 NaN 不全等於 NaN,所以原生的 indexOf 並不能找出 NaN 的下標。

[1, NaN].indexOf(NaN) // -1複製程式碼

那麼我們該如何實現這個功能呢?

就是從陣列中找到符合條件的值的下標嘛,不就是我們最一開始寫的 findIndex 嗎?

我們來寫一下:

// 第三版
function createIndexOfFinder(dir, predicate) {

    return function(array, item, idx){

        if () { ... }

        // 判斷元素是否是 NaN
        if (item !== item) {
            // 在擷取好的陣列中查詢第一個滿足isNaN函式的元素的下標
            idx = predicate(array.slice(i, length), isNaN)
            return idx >= 0 ? idx + i: -1;
        }

        for () { ... }
    }
}

var indexOf = createIndexOfFinder(1, findIndex);
var lastIndexOf = createIndexOfFinder(-1, findLastIndex);複製程式碼

第二個優化是支援對有序的陣列進行更快的二分查詢。

如果 indexOf 第三個引數不傳開始搜尋的下標值,而是一個布林值 true,就認為陣列是一個排好序的陣列,這時候,就會採用更快的二分法進行查詢,這個時候,可以利用我們寫的 sortedIndex 函式。

在這裡直接給最終的原始碼:

// 第四版
function createIndexOfFinder(dir, predicate, sortedIndex) {

    return function(array, item, idx){
        var length = array.length;
        var i = 0;

        if (typeof idx == "number") {
            if (dir > 0) {
                i = idx >= 0 ? idx : Math.max(length + idx, 0);
            }
            else {
                length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;
            }
        }
        else if (sortedIndex && idx && length) {
            idx = sortedIndex(array, item);
            // 如果該插入的位置的值正好等於元素的值,說明是第一個符合要求的值
            return array[idx] === item ? idx : -1;
        }

        // 判斷是否是 NaN
        if (item !== item) {
            idx = predicate(array.slice(i, length), isNaN)
            return idx >= 0 ? idx + i: -1;
        }

        for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {
            if (array[idx] === item) return idx;
        }
        return -1;
    }
}

var indexOf = createIndexOfFinder(1, findIndex, sortedIndex);
var lastIndexOf = createIndexOfFinder(-1, findLastIndex);複製程式碼

值得注意的是:在 underscore 的實現中,只有 indexOf 是支援有序陣列使用二分查詢,lastIndexOf 並不支援。

相關文章