Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript专题之学underscore在数组中查找指定元素 #37

Open
mqyqingfeng opened this issue Jul 26, 2017 · 35 comments
Open

JavaScript专题之学underscore在数组中查找指定元素 #37

mqyqingfeng opened this issue Jul 26, 2017 · 35 comments

Comments

@mqyqingfeng
Copy link
Owner

mqyqingfeng commented Jul 26, 2017

前言

在开发中,我们经常会遇到在数组中查找指定元素的需求,可能大家觉得这个需求过于简单,然而如何优雅的去实现一个 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 - 1; 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(func, context) {
       if (context === void 0) return func;
       return function() {
           return func.apply(context, arguments);
      };
}

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 并不支持。

@FrontToEnd
Copy link

厉害了 大神

@mqyqingfeng
Copy link
Owner Author

mqyqingfeng commented Jul 28, 2017

@FrontToEnd []~( ̄▽ ̄)~*

@xiaobinwu
Copy link

方法都好妙

@HowardTangHw
Copy link

🤒 之前看过loadsh的源码解读,感觉实现的方式都是差不多的~

@mqyqingfeng
Copy link
Owner Author

@HowardTangHw 确实如此,毕竟两者“本是同根生”~

@bailinlin
Copy link

好了,看了你的专题,我要开始读源码了

@GopherJ
Copy link

GopherJ commented Dec 5, 2017

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;
}

这里i的初始笔误了么

@mqyqingfeng
Copy link
Owner Author

@GopherJ 这是一个倒序循环,所以 i 是从 length 开始,i 的值依此递减

@GopherJ
Copy link

GopherJ commented Dec 7, 2017

@mqyqingfeng 是我糊涂了么233?还是没懂,有 arr[arr.length] 这个项么?

@mqyqingfeng
Copy link
Owner Author

@GopherJ 哈哈,尴尬了😂 感谢指出~

@GopherJ
Copy link

GopherJ commented Dec 8, 2017

@mqyqingfeng 哈哈我就说肯定笔误了。感谢分享啊!写的非常不错,有些地方我估计还得来来回回看几次,期待新的文章!

@dowinweb
Copy link

楼主能不能解释下为什么参数中经常会写context,而实际并未传入呀,基本上都是undefined

@mqyqingfeng
Copy link
Owner Author

@dowinweb 像 ES5 的 map、reduce、forEach、find 等等函数,第二个参数都是 context,这是需要开发者写的时候传入的,这其实还算是个蛮常见的场景,举个例子:

function Greet() {
  this.prefix = 'Hello'
  this.friends = ['dowinweb', 'kevin']
}

Greet.prototype.sayHello = function(){
  var self = this;
  var res = this.friends.map(function(item){
        return self.prefix + ' ' +  item
  })
  return res
}

var greet = new Greet();
console.log(greet.sayHello()) // [ "Hello dowinweb", "Hello kevin" ]

一般 map 里的函数,如果要使用外部 this 的值,需要通过 var self = this 这种形式。

其实我们可以写的更加优雅,就是利用 context 这个参数:

function Greet() {
  this.prefix = 'Hello'
  this.friends = ['dowinweb', 'kevin']
}

Greet.prototype.sayHello = function(){
  var res = this.friends.map(function(item){
        return this.prefix + ' ' +  item
  }, this)
  return res
}

var greet = new Greet();
console.log(greet.sayHello())  //  // [ "Hello dowinweb", "Hello kevin" ]

这个算是一个小技巧吧~

@dowinweb
Copy link

@mqyqingfeng 嗯嗯,学习了,谢谢

@Qinghuanhehuan
Copy link

倒序查找下标函数里,“item == 1”的返回值不应该是3吗?
console.log(findLastIndex([1, 2, 3, 4], function(item, index, array){
if (item == 1) return true;
})) // 0

@delayk
Copy link

delayk commented Jan 19, 2018

@Qinghuanhehuan 你这个逻辑想错了呀。给你一个数组[1,2,3,4],让你找出最后一个1出现的index,你说这个index应该是多少?明显应该是0嘛。。

@HowardTangHw
Copy link

HowardTangHw commented Jan 19, 2018

@Qinghuanhehuan 虽然是从后面找,可是下标还是从前面开始的,无论从前面开始找,还是后面开始找,最终返回的是元素在数组下的下标,而不是第几位

@delayk
Copy link

delayk commented Jan 19, 2018

@Qinghuanhehuan 你只是倒序查找这个数组,并不是把数组倒序,然后正向查找

@Qinghuanhehuan
Copy link

@delayk @HowardTangHw 嗯嗯,懂了,谢谢!

@mqyqingfeng
Copy link
Owner Author

@delayk @HowardTangHw 感谢回答哈~ @Qinghuanhehuan ,其实你可以这样想哈,我们倒序查找也是为了找出该元素的下标,然后通过 arr[下标值] 来获取元素,如果 [1, 2, 3, 4] 查找 item === 1,返回值是 3 的话,我们通过下标获取该元素,arr[3] 不就取成了 4 吗?

@iiicon
Copy link

iiicon commented Jan 23, 2018

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

传入一个函数,返回另一个函数,这属于函数柯里化吗?
平时写的时候总是抽象不到这个层面。

@zjp6049
Copy link

zjp6049 commented Jan 24, 2018

@iiicon 所以我也感觉写的so nb,脑洞太大

@mqyqingfeng
Copy link
Owner Author

mqyqingfeng commented Jan 24, 2018

@iiicon 函数柯里化是指将一个 n 元函数转化为 n 个一元函数,我觉得这个例子叫做高阶函数更好些,高阶函数就是传入一个函数,返回一个函数。

说起来这篇文章并没有讲到这样的例子呀~

关于平时写的时候抽象不到这个层次的问题,实际上,这种方法很多时候是"被迫"要去做的,你比如说函数防抖:

document.getElementById('test').onmouseover = fn

每当 mouseover 的时候,就会去执行 fn 函数,如果我们要做防抖,我们可以直接在 fn 函数内部去做,但这样就会跟 fn 的代码连在一起,如果我们要给多个函数都做防抖,岂不是每次都要写一遍,所以我们才会单独抽离写一个 debounce 函数:

var debounceFn = debounce(fn);
document.getElementById('test').onmouseover = debounceFn

你看赋给 onmouserover 的值必须要是一个函数,所以你写 debounce 函数的时候,不就是要传入一个函数,返回另一个函数吗?

关于你写的这个 cb 函数,我最一开始以为是 underscore 中的 cb 函数,后来觉得这个有点像 bind 函数的模拟实现:

fucntion bind(fn, context){
         return function(){
               return fn.apply(context,arguments);
         }
}

bind 函数其实也是同样的道理,我们只是想将一个函数的 this 指向一个固定的 context,如果我们直接使用 fn.call(context) ,就执行了 fn 函数,其实我们现在还并不想执行这个函数,所以为了不让他立刻执行,就可以返回一个函数,里面”包裹“着执行 fn 函数的代码。

嗯,说了这么多,还是要多看多思考……

最后,

记得早点睡觉哈~ 😀

@iiicon
Copy link

iiicon commented Jan 24, 2018

@mqyqingfeng 我其实是拷贝的你的 sortedIndex 第二版的 iteratee 函数,我感觉和 bind 关系不大,如果不抽象一般就 iteratee(array[mid], fn, context), 我一般就这样

还有就是在南方这边工作就是要中午睡一大觉,晚上不熬个夜感觉贼浪费,不过你说的很对,得爱惜自己的身体
古耐~~

@mqyqingfeng
Copy link
Owner Author

@iiicon 哈哈,我竟然没有发现……😂 这个 cb 函数是我当时简写 underscore 的 cb 函数得来的……现在看来,写法上有点莫名其妙,给你造成了困扰,我很抱歉,完整的 cb 函数还要参考 underscore 系列之内部函数 cb 和 optimizeCb

如果我现在写的话,应该是写成:

function cb(func, context) {
       if (context === void 0) return func;
       return function() {
           return func.apply(context, arguments);
      };
}

我还是修改一下吧~

@Tan90Qian
Copy link

@mqyqingfeng 在具有fromIndex参数的实现版本里,没有考虑传入的参数为NaN的问题,因为typeof NaN为“number”,也会走进if语句的代码体内,导致参数被污染,而原生的indexOf没有这个问题[1,2,3,1].lastIndexOf(1,NaN) //0以及[1,2,3,1].indexOf(1,NaN) // 0,虽然不知道为什么都返回了0。。。

@mqyqingfeng
Copy link
Owner Author

@Tan90Qian 感谢指出~ 确实有这个问题~

此外,关于 [1,2,3,1].lastIndexOf(1,NaN) 结果为 0,是因为 fromIndex 如果为 NaN , fromIndex 的值相当于被设置为 0,这点在 MDN 的 lastIndexOf 的模拟实现中可以看出:

default

@habc0807
Copy link

最近我在手动实现数组的常用方法,同事推荐看你的这篇文章,受益匪浅。

@Dannyzn
Copy link

Dannyzn commented Sep 29, 2020

indexOf ---->. var index = dir > 0 ? 0 : length - 1;
for (; index >= 0 && index < length; index += dir) 
不管是正序遍历还是逆序遍历都是满足 于这个 循环遍历的条件
 

@Dannyzn
Copy link

Dannyzn commented Sep 29, 2020

学习笔记分析
function createIndexOf(dir) {
  return function(array, item, idx) {
    var length = array.length;

    var i = 0;
 
    // [0,1,1,2,4]
    // idx 正数  0, 1, 2, 3, 4
    // idx 负数  -5,-4,-3,-2,-1
    
    // idx 的格式判断
    // dir > 0 ===>  我们需要的是 i
    // dir < 0 ===>  我们需要的是 length
    // 和下面的for 循环 成为 一一对应的 关系

    //   dir > 0  从前向后查找  length 此时是数组的长度(需要遍历的次数)
    //  i 是指 从数组 array[i] 开始遍历 
    //  在 dir > 0  ===>  idx >= 0  i = idx
    //  在 dir > 0  ===>  idx <  0  i = Math.max(length + idx, 0)

    //   dir < 0  从后向前查找     i 此时是初始值  
    //  length  是指这当前数组可遍历的次数(长度) 
    //  dir < 0  ===> idx >= 0   length  Math.min(idx + 1, length)
    //  dir < 0  ===> idx < 0    length  idx + length + 1;
    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;
      }
    }

    // dir 1 和 -1 => 正向查找 he 逆向查找 

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

    return -1;
  }
}

@lqPrototype
Copy link

字符串的indexOf 有很大的学问,楼主可以看看。

@yinju123
Copy link

可能是我技术的问题,我感觉稍微有点乱。typeof idx == "number这个步骤只对后面的for循环有用吧,我觉得可以把它跟for循环写在一起。然后我有个问题sortedIndex && idx && length其中的 idx是什么意思,而且还是else if判断的,那么传入idx时,sortedIndex是无效的

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;

@dsphoebe
Copy link

@yinju123 https://github.com/jashkenas/underscore/blob/58df1085cdb05cb0888719c5fe5493948604ab69/test/arrays.js#L351-L353 underscore 的测试案例里面可以看出来,idx 是在非数字的情况下,才会触发 sortedIndex 的执行。

@zlh-GitHub
Copy link

感觉把idx 改为 fromIndexlength 改为 end 会比较好理解,然后逆向查找的判断里面 end 的计算可以不用再 +1,相当于计算查找范围的最后的位置,这样在 for 循环里的三元运算中的 end 也不用再减了,最后把 i < end - 1 改为 i <= end 就行

const createIndexOfFinder = dir => {
  return (arr, item, fromIndex) => {
    let end = arr.length;
    let i = 0;
    if (typeof fromIndex === 'number') {
      if (dir > 0) { // 正向查找
        ......
      } else { // 逆向查找
        end = fromIndex >= 0 ? Math.min(fromIndex, end) : fromIndex + end;
      }
    }
    for (i = dir > 0 ? i : end; i >= 0 && i <= end; i += dir) {
      ......
    }
    return -1;
  }
}

@bobo-only-cv
Copy link

bobo-only-cv commented Mar 19, 2023

针对于lastIndexOf,关于NaN的查找哪里有点bug
根据‘’其绝对值大于数组长度,则方法返回 -1,即数组不会被查找。‘’
这句话
console.log(lastIndexOf([NaN,1,2],NaN,-5)) 应该返回-1的;
实际调用文中的方法会返回0
原因是因为slice去截取的时候length作为负数也是会从数组末尾向前的偏移

可以增加一条判断
if (typeof idx == "number") { if (dir > 0) { i = idx >= 0 ? idx : Math.max(length + idx, 0); } else { //此处可以增加一条判断 if(Math.abs(idx)>length) return -1; length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1; } }

不知道说的对不对

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests