lodash 源码解析 -- countBy

1,085 阅读3分钟

前言

countBy 函数会创建一个对象,用于有条件的统计第一个参数集合(collection,包含数组、类数组、对象等可遍历的集合)内的出现的某些种类型的值,对象的 key 值是第二个参数 iteratee 处理后的值的类型,对应的 value 值是第一个参数每个值经过第二个参数 iteratee 处理后返回的 key 的次数

思路分析

about countBy.png

源码分析

1. countBy

1. 传入参数

collection 传入一个集合,类型可以是数组、类数组、普通对象等 iteratee 迭代器,这里可以理解为 countBy 聚合进行处理的条件

2. 源码分析

调用 createAggregator ,传入计数函数,result 判断不是已有 key 就用 baseAssignValue 初始化其 value1,判断是已有 key 就将该 key 对应的 value 值加 1

var countBy = createAggregator(function(result, value, key) {
  if (hasOwnProperty.call(result, key)) {
    ++result[key];
  } else {
    baseAssignValue(result, key, 1);
  }
});

2. createAggregator

1. 传入参数
  • setteraccumulator 累加器进行处理的函数,
  • initalizer 初始化函数,初始化一个用于记录的累加器,默认是一个对象
2. 源码分析

创建了一个聚合器函数,这里的没有传入 initalizer 因此默认创建一个新对象作为数据的记录。使用 setter 计数 这里的聚合器函数内部判断了集合是否是 array 。是数组就用 arrayAggregator,否则用 baseAggregator 这里我其实有个疑问,就是为什么不直接判断 isArrayLike,这样就不需要后面 baseAggregator 再判断一次,而 arrayAggregator 完全可以胜任

function createAggregator(setter, initializer) {
  return function(collection, iteratee) {
    var func = isArray(collection) ? arrayAggregator : baseAggregator,
        accumulator = initializer ? initializer() : {};
    return func(collection, setter, getIteratee(iteratee, 2), accumulator);
  };
}

3. getIteratee

1. 传入参数
  • value 需要被迭代器处理的值
  • arity 所创建迭代对象的数量
2. 源码分析

获取 lodash.iteratee ,或者使用传入的 iteratee,使用 lodash 的内置迭代器 iteratee 处理外部的迭代器,并将其返回,iteratee 这个函数是 lodash 的一个很大的亮点,也是 lodash 内最复杂的函数之一,以后会讲讲用法

function getIteratee() {
  var result = lodash.iteratee || iteratee;
  result = result === iteratee ? baseIteratee : result;
  return arguments.length ? result(arguments[0], arguments[1]) : result;
}

4. arrayAggregator

1. 传入参数
  • array 普通数组
  • setter 处理 accumulator 的函数
  • iteratee 迭代器,和 countBy 的迭代器一致
  • accumulator 累加器,记录最终结果
2. 源码分析

使用 setter 计数,accmulator 是用于计数的对象,key 值是 iteratee(value) 的返回值,最终返回累加器 accmulator 函数内部以 length 为条件,使用 while 循环

function arrayAggregator(array, setter, iteratee, accumulator) {
  var index = -1,
      length = array == null ? 0 : array.length;
  while (++index < length) {
    var value = array[index];
    setter(accumulator, value, iteratee(value), array);
  }
  return accumulator;
}

5. baseAggregator

1. 传入参数
  • collection 类数组对象或是对象
  • setter 处理 accumulator 的函数
  • iteratee 迭代器,和 countBy 的迭代器一致
  • accumulator 累加器,记录最终结果
2. 源码分析

baseEach 会判断 collection 是否是类数组,如果不是,会使用 createBaseFor 取出 collection 里所有的 key 及其总数,并进行 while 循环遍历所有的 key 取到 value,最后返回计数器

function baseAggregator(collection, setter, iteratee, accumulator) {
  baseEach(collection, function(value, key, collection) {
    setter(accumulator, value, iteratee(value), collection);
  });
  return accumulator;
}

6. baseAssignValue

1. 传入参数
  • object 一个对象
  • key 需要合并的 key
  • value 前一个参数 key 值对应的 value
2. 源码分析

利用原生对象上的 defineProperty 合并 __proto__ 的属性,直接用 [] 合并对象的其他值

function baseAssignValue(object, key, value) {
  if (key == '__proto__' && defineProperty) {
    defineProperty(object, key, {
      'configurable': true,
      'enumerable': true,
      'value': value,
      'writable': true
    });
  } else {
    object[key] = value;
  }
}

应用场景

类似如下,统计了数组里整数部分相同的数字,以及相同长度的字符串出现的次数

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

总结

countBy 函数通过传入 iteratee 能够根据一定的条件筛选出合适的 key 值补并进行计数,是函数式编程的一种展现。传统的统计方式可能就需要创建新的函数去专门遍历并使用 iteratee ,最后再传入 count 中去统计,而利用函数式编程的思想,可以利用 countBy 直接传入函数 iteratee 作为条件,直接用一个函数完成统计