Why underscore

(觉得这部分眼熟的可以直接跳到下一段了...)

最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了我的 2016 计划中。

阅读一些著名框架类库的源码,就好像和一个个大师对话,你会学到很多。为什么是 underscore?最主要的原因是 underscore 简短精悍(约 1.5k 行),封装了 100 多个有用的方法,耦合度低,非常适合逐个方法阅读,适合楼主这样的 JavaScript 初学者。从中,你不仅可以学到用 void 0 代替 undefined 避免 undefined 被重写等一些小技巧 ,也可以学到变量类型判断、函数节流&函数去抖等常用的方法,还可以学到很多浏览器兼容的 hack,更可以学到作者的整体设计思路以及 API 设计的原理(向后兼容)。

之后楼主会写一系列的文章跟大家分享在源码阅读中学习到的知识。

欢迎围观~ (如果有兴趣,欢迎 star & watch~)您的关注是楼主继续写作的动力

题外话

先说点题外话。

自从 5 月 16 日开始 underscore 系列解读文章,目前已经收获了 160+ star,在这里子迟也感谢大家的支持,并将继续努力分享源码里的干货。有朋友私信我说好几天没看到更新,在此也请大家原谅,毕竟我把它当成了今年的计划之一,而且平时也要上班工作,只能利用闲暇时间,而且楼主本人对文章的质量要求比较高,如果是一律的流水文章,读者学不到什么东西,自己的那关都过不了。其实如果有心,应该能发现 underscore-1.8.3 源码全文注释 一直有在更新(注释行数已经快破 1000 了)。

Main

言归正传,上一章 中我们结束了 Object 扩展方法部分,今天开始来解读 Array 部分的扩展方法。其实 JavaScript 中的数组是我最喜欢的类型,能模拟栈、队列等数据结构,还能随意插入元素(splice),非常的灵活,这点做过 leetcode 的应该都深有体会(这里也顺便安利下我的 leetcode 题解 Repo https://github.com/hanzichi/leetcode)。

今天要讲的是,如何在数组中寻找元素,对应 underscore 中的 _.findIndex,_.findLastIndex,_.indexOf,_.lastIndexOf 以及 _.sortIndex 方法。

等等,是不是有点眼熟,没错,JavaScript 中已经部署了 indexOf 方法(ES5)以及 findIndex 方法(ES6),这点不介绍了,大家可以自行学习。

我们先来看 _.findIndex 和 _.findLastIndex 函数。如果了解过 Array.prototype.findIndex() 方法,会非常容易。_.findIndex 的作用就是从一个数组中找到第一个满足某个条件的元素,_.findLastIndex 则是找到最后一个(或者说倒序查找)。

举个简单的例子:

var arr = [1, 3, 5, 2, 4, 6];

var isEven = function(num) {
  return !(num & 1);
};

var idx = _.findIndex(arr, isEven);
// => 3

直接看源码,注释已经写的非常清楚了。这里要注意这个 predicate 函数,其实就是把数组中的元素传入这个参数,返回一个布尔值。如果返回 true,则表示满足这个条件,如果 false 则相反。

// Generator function to create the findIndex and findLastIndex functions
// dir === 1 => 从前往后找
// dir === -1 => 从后往前找
function createPredicateIndexFinder(dir) {
  // 经典闭包
  return function(array, predicate, context) {
    predicate = cb(predicate, context);

    var length = getLength(array);

    // 根据 dir 变量来确定数组遍历的起始位置
    var index = dir > 0 ? 0 : length - 1;

    for (; index >= 0 && index < length; index += dir) {
      // 找到第一个符合条件的元素
      // 并返回下标值
      if (predicate(array[index], index, array)) return index;
    }

    return -1;
  };
}

// Returns the first index on an array-like that passes a predicate test
// 从前往后找到数组中 `第一个满足条件` 的元素,并返回下标值
// 没找到返回 -1
// _.findIndex(array, predicate, [context])
_.findIndex = createPredicateIndexFinder(1);

// 从后往前找到数组中 `第一个满足条件` 的元素,并返回下标值
// 没找到返回 -1
// _.findLastIndex(array, predicate, [context])
_.findLastIndex = createPredicateIndexFinder(-1);

接下来看 _.sortIndex 方法,这个方法无论使用还是实现都非常的简单。如果往一个有序数组中插入元素,使得数组继续保持有序,那么这个插入位置是?这就是这个方法的作用,有序,很显然用二分查找即可。不多说,直接上源码。

// _.sortedIndex(list, value, [iteratee], [context])
_.sortedIndex = function(array, obj, iteratee, context) {
  // 注意 cb 方法
  // iteratee 为空 || 为 String 类型(key 值)时会返回不同方法
  iteratee = cb(iteratee, context, 1);

  // 经过迭代函数计算的值
  var value = iteratee(obj);

  var low = 0, high = getLength(array);

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

  return low;
};

最后我们说说 _.indexOf 和 _.lastIndexOf 方法。

ES5 引入了 indexOf 和 lastIndexOf 方法,但是 IE < 9 不支持,面试时让你写个 Polyfill,你会怎么做(可以把 underscore 的实现看做 Polyfill)?如何能让面试官满意?首先如果分开来写,即两个方法相对独立地写,很显然代码量会比较多,因为两个方法功能相似,所以可以想办法调用一个方法,将不同的部分当做参数传入,减少代码量。其次,如果数组已经有序,是否可以用更快速的二分查找算法?这点会是加分项。

源码实现:

  // Generator function to create the indexOf and lastIndexOf functions
  // _.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);
  // _.lastIndexOf = createIndexFinder(-1, _.findLastIndex);
  function createIndexFinder(dir, predicateFind, sortedIndex) {

    // API 调用形式
    // _.indexOf(array, value, [isSorted])
    // _.indexOf(array, value, [fromIndex])
    // _.lastIndexOf(array, value, [fromIndex])
    return function(array, item, idx) {
      var i = 0, length = getLength(array);

      // 如果 idx 为 Number 类型
      // 则规定查找位置的起始点
      // 那么第三个参数不是 [isSorted]
      // 所以不能用二分查找优化了
      // 只能遍历查找
      if (typeof idx == 'number') {
        if (dir > 0) { // 正向查找
          // 重置查找的起始位置
          i = idx >= 0 ? idx : Math.max(idx + length, i);
        } else { // 反向查找
          // 如果是反向查找,重置 length 属性值
          length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;
        }
      } else if (sortedIndex && idx && length) {
        // 能用二分查找加速的条件
        // 有序 & idx !== 0 && length !== 0

        // 用 _.sortIndex 找到有序数组中 item 正好插入的位置
        idx = sortedIndex(array, item);

        // 如果正好插入的位置的值和 item 刚好相等
        // 说明该位置就是 item 第一次出现的位置
        // 返回下标
        // 否则即是没找到,返回 -1
        return array[idx] === item ? idx : -1;
      }

      // 特判,如果要查找的元素是 NaN 类型
      // 如果 item !== item
      // 那么 item => NaN
      if (item !== item) {
        idx = predicateFind(slice.call(array, i, length), _.isNaN);
        return idx >= 0 ? idx + i : -1;
      }

      // O(n) 遍历数组
      // 寻找和 item 相同的元素
      // 特判排除了 item 为 NaN 的情况
      // 可以放心地用 `===` 来判断是否相等了
      for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {
        if (array[idx] === item) return idx;
      }

      return -1;
    };
  }

  // Return the position of the first occurrence of an item in an array,
  // or -1 if the item is not included in the array.
  // If the array is large and already in sort order, pass `true`
  // for **isSorted** to use binary search.
  // _.indexOf(array, value, [isSorted])
  // 找到数组 array 中 value 第一次出现的位置
  // 并返回其下标值
  // 如果数组有序,则第三个参数可以传入 true
  // 这样算法效率会更高(二分查找)
  // [isSorted] 参数表示数组是否有序
  // 同时第三个参数也可以表示 [fromIndex] (见下面的 _.lastIndexOf)
  _.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);

  // 和 _indexOf 相似
  // 反序查找
  // _.lastIndexOf(array, value, [fromIndex])
  // [fromIndex] 参数表示从倒数第几个开始往前找
  _.lastIndexOf = createIndexFinder(-1, _.findLastIndex);

这里有一点要注意,_.indexOf 方法的第三个参数可以表示 [fromIndex] 或者 [isSorted],而 _.lastIndexOf 的第三个参数只能表示 [fromIndex],我们从代码中便可以轻易看出:

_.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);
_.lastIndexOf = createIndexFinder(-1, _.findLastIndex);

关于这点我也百思不得其解,不知道做这个限制是为了什么考虑,欢迎探讨~

最后给出本文涉及的五个方法的源码位置 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/src/underscore-1.8.3.js#L613-L673

【跟着子迟品 underscore】如何优雅地写一个『在数组中寻找指定元素』的方法的更多相关文章

  1. 【跟着子迟品 underscore】Array Functions 相关源码拾遗 & 小结

    Why underscore 最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了我的 2016 计划中. 阅读一些著名框架类库的源码,就好像和一个个大师对 ...

  2. 【跟着子迟品 underscore】JavaScript 数组展开以及重要的内部方法 flatten

    Why underscore (觉得这一段眼熟的童鞋可以直接跳到正文了...) 最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了我的 2016 计划中. ...

  3. 【跟着子迟品 underscore】Object Functions 相关源码拾遗 & 小结

    Why underscore 最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了我的 2016 计划中. 阅读一些著名框架类库的源码,就好像和一个个大师对 ...

  4. 【跟着子迟品 underscore】for ... in 存在的浏览器兼容问题你造吗

    Why underscore 最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了我的 2016 计划中. 阅读一些著名框架类库的源码,就好像和一个个大师对 ...

  5. 【跟着子迟品 underscore】常用类型判断以及一些有用的工具方法

    Why underscore 最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了我的 2016 计划中. 阅读一些著名框架类库的源码,就好像和一个个大师对 ...

  6. 【跟着子迟品 underscore】JavaScript 中如何判断两个元素是否 "相同"

    Why underscore 最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了我的 2016 计划中. 阅读一些著名框架类库的源码,就好像和一个个大师对 ...

  7. 【跟着子迟品underscore】从用 `void 0` 代替 `undefined` 说起

    Why underscore 最近开始看 underscore源码,并将 underscore源码解读 放在了我的 2016计划 中. 阅读一些著名框架类库的源码,就好像和一个个大师对话,你会学到很多 ...

  8. 学underscore在数组中查找指定元素

    前言 在开发中,我们经常会遇到在数组中查找指定元素的需求,可能大家觉得这个需求过于简单,然而如何优雅的去实现一个 findIndex 和 findLastIndex.indexOf 和 lastInd ...

  9. 如何优雅的写一个Vue 的弹框

    写Vue或者是react 都会遇见弹框的问题.也尝试了多种办法来写弹框,一直都不太满意,今天特地看了一下 Element UI 的源码,模仿着写了一个简易版. 大概有一下几个问题: 1.弹框的层级问题 ...

随机推荐

  1. jquery定时滑出可最小化的底部提示层

    效果预览:http://keleyi.com/keleyi/phtml/jqtexiao/index.htm当打开页面或者刷新页面后等待两秒钟,会在底部滑出可最小化的提示层.滑出层半透明,可关闭再现. ...

  2. 渗透测试-奇技淫巧(一)--源IP地址隐藏

    切记,切记.本文只作为技术交流,提醒各位注意网络安全,请勿用于其它用途,否则后果自付. 在很多时候,某某不希望不愿意有人溯源他的地址.他们是如何隐藏IP的? 今天来浅析下,如何隐藏源地址. 用到的工具 ...

  3. [C#6] 6-表达式形式的成员函数

    0. 目录 C#6 新增特性目录 1. 老版本的代码 internal class Person { public string FirstName { get; set; } public stri ...

  4. Thinking in Java 笔记

    大二就买了这本书,如今再看这本书,看到了一些以前没看的细节,也有了不同的体会.本文使用第4版,整理每章的笔记心得.老外的书有个特点,他会花费大量的文字去阐述一个概念,这比堆代码强多了. 第 1 章 对 ...

  5. Linux命令学习总结: file命令

    命令简介: 该命令用来识别文件类型,也可用来辨别一些文件的编码格式.它是通过查看文件的头部信息来获取文件类型,而不是像Windows通过扩展名来确定文件类型的. 执行权限 :All User 指令所在 ...

  6. JVM之Parallel Scavenge收集器

    新生代收集器,复制算法,并行收集,面向吞吐量要求(吞吐量优先收集器). 吞吐量=用户代码运行时间/(用户代码运行时间+垃圾回收时间) -XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间 ...

  7. HTML5 data-* 自定义属性

    在jQuery的attr与prop提 到过在IE9之前版本中如果使用property不当会造成内存泄露问题,而且关于Attribute和Property的区别也让人十分头痛,在 HTML5中添加了da ...

  8. SVN 提交失败: permission denied - txn-current-lock

    执行以下命令即可 sudo chown -R www-data:subversion myproject sudo chmod -R g+rws myproject

  9. Struts与Struts2的区别

    Struts与Struts2的区别 首先看一张Struts2的发展路线图:       从Struts2的发展过程来看,Struts2继承了Struts与Webwork的特性,形成了新的框架.但是它的 ...

  10. phpqrcode 生成二维码

    这个项目需要根据信息来自动生成二维码,到网上搜了下,发现php有生成二维码的类库phpqrcode,所以打算就用这个来生成二维码 从官网下载类库源码http://sourceforge.net/pro ...