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

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

Open
lessfish opened this issue Jul 18, 2016 · 3 comments
Open

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

lessfish opened this issue Jul 18, 2016 · 3 comments

Comments

@lessfish
Copy link
Owner

lessfish commented Jul 18, 2016

前言

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

前文(#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 的欲望,正是因为函数部分的节流和去抖。

@lessfish
Copy link
Owner Author

当然,传入参数不一定是数组,还可以是对象,只是觉得没数组有用

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

@seaskymonster
Copy link

闭包的运用简直是js最美的地方。

@djlxiaoshi
Copy link

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

这句话是点睛之笔

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

3 participants