资讯专栏INFORMATION COLUMN

探究underscore源码(二)

jeyhan / 2646人阅读

摘要:源码如下通过来判断到底通过来区分对象以及数组。传入回调函数的参数分别为对象键值对中的值或者数组中的序号值对象键值对中的键或者数组中的相应序号举个例子,传入回调的参数依次为如果是数组,则传入参数依次为三这几个方法都是利用一个核心函数。

一、_.each

一开始我并没有以为_.each这个方法会有多大的用处,不就是一个遍历嘛~

但当我利用自己测试这个函数的时候,发现了一件“大事”

underscore的初始化时怎么做的?你是不是跟我一样都以为underscore的初始化就是在_这个对象上面加上一堆属性?

Naive!

underscore的真是做法是通过一系列的函数编程实现初始化的“自动化”

看下面的代码:

// Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError, isMap, isWeakMap, isSet, isWeakSet.
  _.each(["Arguments", "Function", "String", "Number", "Date", "RegExp", "Error", "Symbol", "Map", "WeakMap", "Set", "WeakSet"], function(name) {
    _["is" + name] = function(obj) {
      return toString.call(obj) === "[object " + name + "]";
    };
  });

underscore通过如上的代码实现数据类型的判断

类似的,作者通过调用_.each方法来实现

自己拓展的函数跟库提供函数的合并(详见_.mixin

Array方法的自动初始化(line1588+line1599

二、_.map

_.map源码的精华之处在于将Array以及Object合在一起处理。

源码如下:

_.map = _.collect = function(obj, iteratee, context) {
    debugger
    iteratee = cb(iteratee, context);
    var keys = !isArrayLike(obj) && _.keys(obj),
        length = (keys || obj).length,
        results = Array(length);
    for (var index = 0; index < length; index++) {
      // 通过key来判断到底
      var currentKey = keys ? keys[index] : index;
      results[index] = iteratee(obj[currentKey], currentKey, obj);
    }
    return results;
};

通过var keys = !isArrayLike(obj) && _.keys(obj)来区分对象以及数组。如果为对象,则keystrue,否则为false。传入回调函数的参数分别为:

对象键值对中的值或者数组中的序号值

对象键值对中的键或者数组中的相应序号

举个例子,{one:1, two:2, three:3}:
传入回调的参数依次为:

1, one

2, two

3, three

如果是数组[1, 2, 3],则传入参数依次为:

1, 0

2, 1

3, 2

三、_.reduce _.foldl _.inject _.reduceRight _.foldr

这几个方法都是利用一个核心函数createReduce

createReduce利用了高阶函数的方式进行封装, 通过传入的参数来确定是正序遍历还是倒叙遍历。下面来看源码:

  // 创建一个遍历函数,从左到右或者从右向左
  // param: dir  >0(从左向右遍历),dir < 0(从右向左遍历)
  var createReduce = function(dir) {
    // 包装代码,在一个多带带的函数中分配参数变量,而不是访问`arguments.length",以避免发生issue #1991
    var reducer = function(obj, iteratee, memo, initial) {
      var keys = !isArrayLike(obj) && _.keys(obj),
          length = (keys || obj).length,
          index = dir > 0 ? 0 : length - 1;
      if (!initial) {
        // 未初始化memo参数的情况下,设置memo值为传入参数的第一个值(有可能是倒序的第一个值也有可能是正序的第一个值)
        memo = obj[keys ? keys[index] : index];
        index += dir;
      }
      // 利用 for 循环 迭代用户传入的参数
      for (; index >= 0 && index < length; index += dir) {
        var currentKey = keys ? keys[index] : index;
        memo = iteratee(memo, obj[currentKey], currentKey, obj);
      }
      return memo;
    };

    return function(obj, iteratee, memo, context) {
      var initial = arguments.length >= 3;
      return reducer(obj, optimizeCb(iteratee, context, 4), memo, initial);
    };
  };

看来这个主要函数,来看看这个函数的调用方式:

  _.reduce = _.foldl = _.inject = createReduce(1);

  _.reduceRight = _.foldr = createReduce(-1);

主要是在初始化时,先运行createReduce形成一个闭包,从而可以在运行时可以很好的区分是从左向右遍历还是从右向左遍历。

四、_.findIndex|_.findKey|_.find|_.findLastIndex

_.find基于_.findIndex以及_.findKey,所以我们先来看后两者。

先来看_.findIndex。查看findIndex源码会发现,它与findLastIndex原理一直,都是调用一个名为createPredicateIndexFinder

// 利用此函数来生成findIndex以及findLastIndex
  var createPredicateIndexFinder = function(dir) {
    // 判断 dir 参数的正负来确认是findIndex还是findLastIndex
    return function(array, predicate, context) {
      predicate = cb(predicate, context);
      var length = getLength(array);
      var index = dir > 0 ? 0 : length - 1;
      // 遍历来寻找符合要求的index
      for (; index >= 0 && index < length; index += dir) {
        // 传入参数给回调函数
        if (predicate(array[index], index, array)) return index;
      }
      // 如果未找到符合要求的index则返回 -1
      return -1;
    };
  };

看懂上面的函数,则findIndex以及findLatIndex就浅显了:

_.findIndex = createPredicateIndexFinder(1);
_.findLastIndex = createPredicateIndexFinder(-1);

看懂findIndex之后,来了解_.findKey,这个函数又牵扯到了另一个函数_.keys了(别那样的表情,函数式编程就是这样。。。)

  _.keys = function(obj) {
    // 容错处理,判断是否为对象
    if (!_.isObject(obj)) return [];
    // 如果能调用 ES5 的方法,则调用 Object.keys 方法
    if (nativeKeys) return nativeKeys(obj);
    var keys = [];
    // 遍历key值
    for (var key in obj) if (_.has(obj, key)) keys.push(key);
    // Ahem, IE < 9.(IE9以下的处理没太看懂。。。)
    if (hasEnumBug) collectNonEnumProps(obj, keys);
    return keys;
  };

上面源码的处理有几个点值得我们注意以下:

  _.isObject = function(obj) {
    var type = typeof obj;
    // function 属于 object,typeof null = "object",需要通过!!obj排除null这种情况
    return type === "function" || type === "object" && !!obj;
  };
  _.has = function(obj, key) {
    // 通过原生的hasOwnProperty进行属性的判断
    return obj != null && hasOwnProperty.call(obj, key);
  };
  // IE < 9 ,有些key值不会被遍历,导致key的遍历缺失bug
  // hasEnumBug 用来辨识是否在IE < 9 的环境下
  var hasEnumBug = !{toString: null}.propertyIsEnumerable("toString");
  var nonEnumerableProps = ["valueOf", "isPrototypeOf", "toString",
                      "propertyIsEnumerable", "hasOwnProperty", "toLocaleString"];

  var collectNonEnumProps = function(obj, keys) {
    var nonEnumIdx = nonEnumerableProps.length;
    var constructor = obj.constructor;
    var proto = _.isFunction(constructor) && constructor.prototype || ObjProto;

    // Constructor is a special case.
    var prop = "constructor";
    if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);

    while (nonEnumIdx--) {
      prop = nonEnumerableProps[nonEnumIdx];
      if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {
        keys.push(prop);
      }
    }
  };
五、_.filter

上源码:

  // 返回所有通过测试的元素
  // 别名为`select`
  _.filter = _.select = function(obj, predicate, context) {
    var results = [];
    // 改变predicate的函数指向
    predicate = cb(predicate, context);
    // 遍历每个值,如果通过测试则放入要返回的数组中
    _.each(obj, function(value, index, list) {
      if (predicate(value, index, list)) results.push(value);
    });
    return results;
  };

主要采用了内部函数_.each,这个函数没有什么好说的~

六、_.reject | _.negate

_.reject这个函数内部使用了_.negate,所以,我们先从_.negate看起.

  _.negate = function(predicate) {
    return function() {
      return !predicate.apply(this, arguments);
    };
  };

这个函数的意思就是运行传入进来的cb函数之后,对其运行的结果取反。

下面来看_.reject

  _.reject = function(obj, predicate, context) {
    return _.filter(obj, _.negate(cb(predicate)), context);
  };

不难理解,_.reject就是通过_.filter来实现的。

七、_.every | _.some

如果你有兴趣查看代码的话,你会发现这两个函数除了判断部分,其它地方基本一模一样。也不难理解,两个函数只是逻辑稍微不同而已。

  // 判断是否所有元素都符合条件
  // 别名为 `all`.
  _.every = _.all = function(obj, predicate, context) {
    predicate = cb(predicate, context);
    var keys = !isArrayLike(obj) && _.keys(obj),
        length = (keys || obj).length;
    for (var index = 0; index < length; index++) {
      var currentKey = keys ? keys[index] : index;
      if (!predicate(obj[currentKey], currentKey, obj)) return false;
    }
    return true;
  };

  // 判断至少又一个元素符合条件
  // 别名为 `any`.
  _.some = _.any = function(obj, predicate, context) {
    predicate = cb(predicate, context);
    var keys = !isArrayLike(obj) && _.keys(obj),
        length = (keys || obj).length;
    for (var index = 0; index < length; index++) {
      var currentKey = keys ? keys[index] : index;
      if (predicate(obj[currentKey], currentKey, obj)) return true;
    }
    return false;
  };
八、_.indexOf | _.lastIndexOf

这两个函数与findIndex以及findLastIndex在写法上面有异曲同工之处。

_.indexOf以及_.lastIndexOf都引用的一个公共函数,参数中都传入标识符,用来表示是从头到尾寻找还是从尾到头寻找。

我们先来看这个公共函数createIndexFinder

  /**
   * dir           Number    1表示正序查找,-1表示倒叙查找
   * predicateFind Function  _.findIndex 或者 _.findLastIndex
   * sortedIndex   Function  _.sortedIndex
   */
  var createIndexFinder = function(dir, predicateFind, sortedIndex) {
    return function(array, item, idx) {
      var i = 0, length = getLength(array);
      if (typeof idx == "number") {
        // 正序查找,重置i
        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 = sortedIndex(array, item);
        return array[idx] === item ? idx : -1;
      }
      // item为NaN的情况处理
      if (item !== item) {
        idx = predicateFind(slice.call(array, i, length), _.isNaN);
        return idx >= 0 ? idx + i : -1;
      }
      // NaN处理之后,可以放心的使用for循环迭代了
      for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {
        if (array[idx] === item) return idx;
      }
      // 如果上面的都不存在,则返回 -1
      return -1;
    };
  };

基本上看懂上面的那个函数,_.findIndex_.findLastIndex就可以理解啦

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

这个方法咋眼一看对我而言还是有点难理解的。所以我先看它的api描述。

使用二分查找确定value在list中的位置序号,value按此序号插入能保持list原有的排序。如果提供iterator函数,iterator将作为list排序的依据,包括你传递的value 。iterator也可以是字符串的属性名用来排序(比如length)

看源码如下:

_.sortedIndex = function(array, obj, iteratee, context) {
    // 如果是一个数组元素为数字,则直接以数组元素大小为查找条件
    // 如果是数组元素为对象,则以 iteratee 为查找条件
    // 通过 cb 来查找出 数组元素中的 iterate 属性值 (利用 _.property ) 
    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;
  };

总体而言,理解二分法以及其对数组元素为对象这种情况的处理方式即可以很好的理解这个方法了。

十、_.range
/**
 * start: 返回数组的起始值
 * stop: 返回数组的终止值
 * step: 返回数组中间的间隔
 */
_.range = function(start, stop, step) {
    // 如果不存在stop,则默认设置stop为0,start也为0
    if (stop == null) {
      stop = start || 0;
      start = 0;
    }
    // 如果不存在 step 值,则默认设置为 1 或者 -1
    if (!step) {
      step = stop < start ? -1 : 1;
    }
    
    var length = Math.max(Math.ceil((stop - start) / step), 0);
    var range = Array(length);
    
    // 遍历生成数组
    for (var idx = 0; idx < length; idx++, start += step) {
      range[idx] = start;
    }

    return range;
};

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/80931.html

相关文章

  • 探究underscore源码(一)

    摘要:什么鬼结合上面的函数,貌似可以看到每次调用函数时都会判断一次是否等于。主要原理是利用回调函数来处理调用方法传入的参数。 本文基于underscore v1.8.3版本 源头 一直想学习一下类库的源码,jQuery刚刚看到选择器那块,直接被那一大块正则搞懵逼了。经过同事的推荐,选择了underscore来作为类库研究的起点。 闭包 所有函数都在一个闭包内,避免污染全局变量,这没什么特殊的...

    CloudwiseAPM 评论0 收藏0
  • JavaScript专题系列20篇正式完结!

    摘要:写在前面专题系列是我写的第二个系列,第一个系列是深入系列。专题系列自月日发布第一篇文章,到月日发布最后一篇,感谢各位朋友的收藏点赞,鼓励指正。 写在前面 JavaScript 专题系列是我写的第二个系列,第一个系列是 JavaScript 深入系列。 JavaScript 专题系列共计 20 篇,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里...

    sixleaves 评论0 收藏0
  • javascript知识点

    摘要:模块化是随着前端技术的发展,前端代码爆炸式增长后,工程化所采取的必然措施。目前模块化的思想分为和。特别指出,事件不等同于异步,回调也不等同于异步。将会讨论安全的类型检测惰性载入函数冻结对象定时器等话题。 Vue.js 前后端同构方案之准备篇——代码优化 目前 Vue.js 的火爆不亚于当初的 React,本人对写代码有洁癖,代码也是艺术。此篇是准备篇,工欲善其事,必先利其器。我们先在代...

    Karrdy 评论0 收藏0
  • 你要看看这些有趣的函数方法吗?

    前言 这是underscore.js源码分析的第六篇,如果你对这个系列感兴趣,欢迎点击 underscore-analysis/ watch一下,随时可以看到动态更新。 下划线中有非常多很有趣的方法,可以用比较巧妙的方式解决我们日常生活中遇到的问题,比如_.after,_.before,_.defer...等,也许你已经用过他们了,今天我们来深入源码,一探究竟,他们到底是怎么实现的。 showIm...

    melody_lql 评论0 收藏0

发表评论

0条评论

jeyhan

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<