浅谈 underscore 内部方法 group 的设计原理

前言

真是天一热什么事都不想干,这个月只产出了一篇文章,赶紧写一篇压压惊!

前文(https://github.com/hanzichi/underscore-analysis/issues/15)说到楼主开始解读 underscore.js 中的 Collection Functions 部分,看了一遍这部分的源码,很多方法都是一看便懂,不需要楼主过分解读。本文来聊聊内部方法 group 的设计,这会是 Collection Functions 部分的第二篇文章,也是最后一篇文章,关于这部分其他方法的实现,可以看我的 全文源码注释

_.groupBy & _.indexBy & _.countBy

group 是 underscore.js 中的一个内部方法,顾名思义,为了分组而用。有三个 API 用到了这个方法,分别是 _.groupBy,_.indexBy,_.countBy。

来看看这三个 API 的作用。

_.groupBy,可以对一个数组的元素进行分组,如何分组?可以将元素传入一个迭代函数,根据迭代后的值进行分组,也可以传入一个字符串表示元素属性,根据该属性值进行分组。

_.groupBy([1.3, 2.1, 2.4], function(num){ return Math.floor(num); });
=> {1: [1.3], 2: [2.1, 2.4]}

_.groupBy(['one', 'two', 'three'], 'length');
=> {3: ["one", "two"], 5: ["three"]}

可以看到,返回的结果是一个对象,key 为经过迭代函数迭代后的值,或者属性值,value 为一个数组,保存迭代结果或者属性值一样的元素。

再来看 _.indexBy

var stooges = [{name: 'moe', age: 40}, {name: 'larry', age: 50}, {name: 'curly', age: 60}];
_.indexBy(stooges, 'age');
=> {
  "40": {name: 'moe', age: 40},
  "50": {name: 'larry', age: 50},
  "60": {name: 'curly', age: 60}
}

跟 _.groupBy 差不多,不同的是,_.indexBy 的结果,每个 key 值对应的是一个元素(传入 _.indexBy 方法的数组中的元素),而不是一个数组(Just like groupBy, but for when you know your keys are unique.)。如果原来数组中,有两个元素,经过迭代后(或者元素属性值)相同,那么在结果对象中,后者会覆盖前者。(所以最好确认数组中的元素经过迭代后的值没有相同的,或者属性值没有相同的)

var tmp = _.indexBy(['one', 'two', 'three'], 'length');
=> Object {3: "two", 5: "three"}

最后来看 _.countBy。还是返回一个结果对象,它的 key 值意思还是和 _.groupBy 以及 _.indexBy 相同,而 value 值为迭代结果(或者属性值)是该 key 值的元素的个数。

_.countBy([1, 2, 3, 4, 5], function(num) {
  return num % 2 == 0 ? 'even': 'odd';
});
=> {odd: 3, even: 2}

group

这三个 API 功能相近,难道要写三个独立的方法?如果独立写,大概会是这样。

_.groupBy = function(obj, iteratee, context) {
  // 返回结果是一个对象
  var result = {};
  // 根据 iteratee 值确定迭代函数
  iteratee = cb(iteratee, context);
  // 遍历元素
  _.each(obj, function(value, index) {
    // 经过迭代,获取结果值,存为 key
    var key = iteratee(value, index, obj);

    // TODO
    // ...
  });
  // 返回结果对象
  return result;
};

_.indexBy = function(obj, iteratee, context) {
  // 返回结果是一个对象
  var result = {};
  // 根据 iteratee 值确定迭代函数
  iteratee = cb(iteratee, context);
  // 遍历元素
  _.each(obj, function(value, index) {
    // 经过迭代,获取结果值,存为 key
    var key = iteratee(value, index, obj);

    // TODO
    // ...
  });
  // 返回结果对象
  return result;
};

_.countBy = function(obj, iteratee, context) {
  // 返回结果是一个对象
  var result = {};
  // 根据 iteratee 值确定迭代函数
  iteratee = cb(iteratee, context);
  // 遍历元素
  _.each(obj, function(value, index) {
    // 经过迭代,获取结果值,存为 key
    var key = iteratee(value, index, obj);

    // TODO
    // ...
  });
  // 返回结果对象
  return result;
};

大堆功能相似的代码,简直不能忍!每当这个时候,就要想到闭包,函数嵌套函数!

首先定义个函数 group,返回一个函数,为以上三个方法能调用的函数。听起来有点拗口,其实就是用 group 做个中间层,上代码体会下。

var group = function() {
  return function(obj, iteratee, context) {
    // 返回结果是一个对象
    var result = {};
    // 根据 iteratee 值确定迭代函数
    iteratee = cb(iteratee, context);
    // 遍历元素
    _.each(obj, function(value, index) {
      // 经过迭代,获取结果值,存为 key
      var key = iteratee(value, index, obj);

      // TODO
      // ...
    });
    // 返回结果对象
    return result;
  };
};

_.groupBy = group();
_.indexBy = group();
_.countBy = group();

其实三个方法主要的操作,就是对上面的 group 中 result 对象的操作,我们可以传入一个方法,利用该方法对 result 对象进行操作。

以 _.groupBy 方法为例:

var group = function(behavior) {
  return function(obj, iteratee, context) {
    // 返回结果是一个对象
    var result = {};
    // 根据 iteratee 值确定迭代函数
    iteratee = cb(iteratee, context);
    // 遍历元素
    _.each(obj, function(value, index) {
      // 经过迭代,获取结果值,存为 key
      var key = iteratee(value, index, obj);
      // operate
      behavior(result, value, key);
    });
    // 返回结果对象
    return result;
  };
};

var behavior = function(result, value, key) {
  if (_.has(result, key))
    result[key].push(value);
  else result[key] = [value];
}

_.groupBy = group(behavior);

将对象当做参数传入函数,能在函数内改变对象值,正是利用了这个特点。而 _.indexBy 和 _.countBy,无非是改一下 behavior 函数的事了。

酱油了一篇解读,下文开始讲扩展函数了,其实最开始有解读 underscore 的欲望,正是因为函数部分的节流和去抖。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值