本文解读lodash源码版本为4.0.0
介绍
lodash中的groupBy
方法是将集合按照某个规则分类,并返回一个分类之后的对象的方法,看一下官网的解释:
_.groupBy(collection, [iteratee=_.identity])
创建一个对象,key
是 iteratee
遍历 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源码解读之GetbaseProperty
方法和basePropertyDeep
方法都是返回的一个函数,这个函数接收一个object
参数,其执行结果就是返回object
中的path
属性,也就是object[path]
,如果object中不存在此属性,就返回undefined
值得注意得是如果path
为length
的话,得到的是数组的或则字符串的长度
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);
)