lodash源码解读之 groupBy

1,618 阅读8分钟

本文解读lodash源码版本为4.0.0

介绍

lodash中的groupBy方法是将集合按照某个规则分类,并返回一个分类之后的对象的方法,看一下官网的解释:
_.groupBy(collection, [iteratee=_.identity])
创建一个对象,keyiteratee 遍历 collection(集合) 中的每个元素返回的结果。 分组值的顺序是由他们出现在 collection(集合) 中的顺序确定的。每个键对应的值负责生成 key 的元素组成的数组。iteratee 调用 1 个参数: (value)。
简单的说就是按照iteratee内的规则对collection进行分组

  • collection:集合,进行分组的对象,一般是数组
  • iteratee:分组的规则,可以是一个函数,也可以是一个字符串

用法

_.groupBy([6.1, 4.2, 6.3], Math.floor);
// => { '4': [4.2], '6': [6.1, 6.3] }
 
_.groupBy(['one', 'two', 'three'], 'length');
// => { '3': ['one', 'two'], '5': ['three'] }

_.groupBy([6.1, 4.2, 6.3], [1, 2]);
// => { false: [6.1, 4.2, 6.3] }

_.groupBy([6.1, 4.2, 6.3], {'1': 4.1});
// => { false: [6.1, 4.2, 6.3] }

源码

var groupBy = createAggregator(function(result, value, key) {
  if (hasOwnProperty.call(result, key)) {
  // 如果result内已经有key属性,就直接往key属性末尾添加,result内的属性,只能是数组,也只会是数组
    result[key].push(value);
  } else {
  // 如果result没有key属性,就为其添加key属性,其值是数组类型,因为result初始为空数组,所以result的每一个属性,都是数组
    result[key] = [value];
  }
});

先看下createAggregator方法内传入的函数,其功能就是判断result自己属性中是否包含key,如果包含就直接在result[key]末尾添加value,否则result[key] = [value],简单点讲就是result是个对象,而自身的每一个属性都是一个数组,这也是调用groupBy方法之后返回的数据的格式,大致长这样:

{key1: [1,2], key2: [3,4]}

是不是就是我们上面例子中打印出来的数据格式

baseIteratee

在看createAggregator函数之前,我们先来看一个函数baseIteratee,可以先说一下baseIteratee函数的作用,它其实就是返回一个供我们对目标进行分组的函数,如果我们传入是一个函数,那就直接使用传入的函数,如果是字符串等类型呢?就需要使用lodash自己的基础函数了:

function baseIteratee(value) {
  var type = typeof value;
  if (type == 'function') {
    return value;
  }
  if (value == null) {
  // identity是lodash的基础函数,没有做任何事,就是一个返回传入值的函数
    return identity;
  }
  if (type == 'object') {
    return isArray(value)
    // 如果是数组,就使用baseMatchesProperty的执行结果
      ? baseMatchesProperty(value[0], value[1])
      // 如果是对象,但不是数组,就使用baseMatches的执行结果
      : baseMatches(value);
  }
  // 针对字符串、数字等类型,就使用property的执行结果
  return property(value);
}

这里注意是用typeof判断的,

  • 如果value是函数类型,就直接返回value
  • 如果是null或则undefiend就返回identity函数
  • 如果是对象,就进行判断
  • 如果都不满足,如字符串、数字等类型,就返回property(value)

baseMatchesProperty

baseMatchesProperty方法是当调用groupBy方法时,传入的第二个参数为数组的情况,看我们一开始的例子,是可以接收数组、对象等数据类型的,那如果是数组的话,是怎么进行处理的呢?

// 这个path和srcValue的值就是我们所传数组的第0项和第1项,从baseIteratee内调用此函数时的传参可以看到
function baseMatchesProperty(path, srcValue) {
  return function(object) {
  // get函数就是lodash的get函数,根据path获取object内相应的值
    var objValue = get(object, path);
    // 当object内没有path属性时,objValue就是undefined,此时如果再没有srcValue值,就满足条件,
    return (objValue === undefined && objValue === srcValue)
    // hasIn就是lodasj的hasIn方法,判断path是否是object的直接属性或则继承属性,返回boolean值
      ? hasIn(object, path)
      // baseIsEqual大致就是判断srcValue, objValue是否相等,返回值也是boolean值
      : baseIsEqual(srcValue, objValue, undefined, UNORDERED_COMPARE_FLAG | PARTIAL_COMPARE_FLAG);
  };
}

小结:可以看到,如果是传入的是数组的话,最多取数组的前两项作为path判断,其结果返回是boolean

  • 如果传入的数组只有一项,并且分组对象的属性中没有此属性,则返回false
  • 如果传入的数组只有一项,并且分组对象的属性中,有此属性且不为undefined,为false,否则为true
  • 如果传入的数组有两项([arr[0],arr[1]]),但是object中的arr[0]的值不等于arr[1],返回false;反之返回true 例:
const arr = [
    { 'test': 'test' },
    { 'test': 'test1', '1': '1' },
    { 'test': undefiend, '2':'2'}
]

_.groupBy(arr,['test1'])
// => {false: [ { 'test': 'test' }, { 'test': 'test1', '1': '1' }, {'2':'2'} ]}

_.groupBy(arr,['test'])
// => {false: [ { 'test': 'test' }, { 'test': 'test1', '1': '1' }], true: [{'2':'2', 'test': undefined}]}

_.groupBy(arr,['test','test2'])
// => {false: [ { 'test': 'test' }, { 'test': 'test1', '1': '1' }, {'2':'2'} ]}

_.groupBy(arr,['test','test'])
// => {false: [{ 'test': 'test1', '1': '1' }, {'2':'2'}], true:[{ 'test': 'test' }]}

baseMatches函数

baseMatches函数就是当参数是对象的时候,生成分组函数的一个方法

function baseMatches(source) {
    // matchData是将传入的对象拆分,是个数组,长度为对象属性的个数,数组的每一项又是一个数组[key,value,boolean],
    //key代表对象的键,vakue代表对象当前键对应的值,boolean代表value时候是一个对象
  var matchData = getMatchData(source);
  // 如果传入的对象有且只有一个属性,而且value不为null、undefined
  if (matchData.length == 1 && matchData[0][2]) {
    var key = matchData[0][0],
        value = matchData[0][1];
    // 这个函数就是将目标对象中的object项找出,返回true,其余的返回false
    return function(object) {
      if (object == null) {
        return false;
      }
      return object[key] === value &&
        (value !== undefined || (key in Object(object)));
    };
  }
  // 如果传入的对象不止一个属性等情况
  return function(object) {
    // baseIsMatch函数是判断object中是否包含source中的所有属性,
    return object === source || baseIsMatch(object, source, matchData);
  };
}

分析baseMatches函数可以知道:

  • 当传入的是个空对象的时候,分组将只分为一组,其键为true
  • 当传入的对象只有一项或则多项的时候,包含对象所有属性的目标元素的项,分为一组,另外的分为一组 通过例子加深理解:
const arr = [
    { 'test': 'test' },
    { 'test': 'test1', '1': '1' },
    {'test': 'test1', '2':'2'}
]
console.log(_.groupBy(arr, { }))
// => { true: [{'test': "test"}, {'1': "1", 'test': "test1"}, {'2': "2", 'test': "test1"}]}

console.log(_.groupBy(arr, { 'test': 'test1', '1': '1' }))
// => { true: [ {'1': "1", 'test': "test1"}], false: [{{ 'test': "test"}, {'2': "2", test: "test1" }}]}

console.log(_.groupBy(arr, { 'test': 'test' }))
// => { true: [{"test": "test"}], false: [{"test": "test1", "1": "1", "test": "test1", "2": "2"}]}

property

最后是property(value)执行之后是个什么东西呢?看下property函数代码:

function property(path) {
  return isKey(path) ? baseProperty(path) : basePropertyDeep(path);
}

很简单的代码

  • isKey方法就是判断path是否是一个key值,这个方法在定义的时候是接收两个参数的,没有传第二个参数的话,就是使用特殊的正则匹配去判断path是否是key值,方法具体的代码在前面介绍get方法时有解释 -- lodash源码解读之Get
  • baseProperty方法和basePropertyDeep方法都是返回的一个函数,这个函数接收一个object参数,其执行结果就是返回object中的path属性,也就是object[path],如果object中不存在此属性,就返回undefined 值得注意得是如果pathlength的话,得到的是数组的或则字符串的长度

baseIteratee函数小结

经过前面的解读,大致可以知道baseIteratee函数的作用了,就是我们在使用groupBy方法是,传入的第二个参数不是一个函数的时候,根据不同的数据类型,生成不同的分组函数,如果是一个函数,就直接使用这个函数

createAggregator

看完baseIteratee函数之后,再看createAggregator函数的代码:

function createAggregator(setter, initializer) {
  return function(collection, iteratee) {
    // 在调用createAggregator没有传入initializer,所以result是一个空对象
    var result = initializer ? initializer() : {};
    // 这里的iteratee就是baseIteratee(iteratee, undefined)的值
    iteratee = getIteratee(iteratee);
    // 判断是否是数组,是数组就直接循环,否则走baseEach方法遍历集合
    if (isArray(collection)) {
      var index = -1,
          length = collection.length;

      while (++index < length) {
        var value = collection[index];
        // setter就是上面调用createAggregator传入的函数,只接收三个参数
        setter(result, value, iteratee(value), collection);
      }
    } else {
    // 遍历对象、类数组等集合,在循环内调用setter方法,将iteratee(value)作为键,添加到result上,其值为数组([value]或则push(value))
      baseEach(collection, function(value, key, collection) {
        setter(result, value, iteratee(value), collection);
      });
    }
    return result;
  };
}

在此函数中,循环目标对象的每一项,如果是数组,就直接while循环其length,如果是对象、类数组的等,使用baseEach方法遍历,这个方法在之前解读forEach时有介绍;循环内部,调用setter方法,将目标数据的每一项添加到result中,并返回result -- lodash源码解读之ForEach

总结

总结一下,groupBy就是将目标元素按照一定的规则进行分组,这个规则可以是我们自己传入的一个函数,也可以是其他的类型,然后lodash根据不同的类型使用不同的规则函数;大致可以分为一下几种情况: _.groupBy(collection, [iteratee=_.identity])

  • collection为数组,iteratee为函数,就使用iteratee函数的返回值作为键为collection进行分组,分组依据为iteratee(collection[i]) === iteratee(collection[j])
  • collection为数组,iteratee为字符串,就根据collection中的每一项的iteratee属性进行分组,相同的为一组,不包含iteratee属性的分为一组
  • collection为数组,iteratee为对象,就根据collection的每一项中是否包含iteratee属性进行分组,包含分到true一组;不包含分到false一组
  • collection为数组,iteratee为数组,根据iteratee的第一项为键,第二项为值,查找collection中是否包含该属性,包含分为true一组,不包含分到false一组
  • collection为对象,iteratee为函数,规则同collection为数组,iteratee为函数一致
  • collection为对象,iteratee为字符串,检测collection中的每一个属性是否包含iteratee属性,不包含的分为一组,包含的且值相同的分为一组
  • collection为对象,iteratee为对象,检测collection的每个属性是否包含iteratee中的所有属性,包含的分为一组,不包含的分为一组
  • collection为对象,iteratee为数组时,检测collection的每个属性的iteratee第一项的值是否等于iteratee的第二项,是的分为true一组,否的分为false一组(注意:取collection的每项属性中的iteratee第一项的值时,是使用的get方法取值,详见baseMatchesProperty方法中的objValue = get(object, path);