有关于切片的方法分别是:
注意, while 切片并不是过滤,而是删除或获取连续满足条件的元素直到不满足回调结果为止,下面以从头删除元素是偶数的元素为例:
let nums = [ 2,5,4,6,8 ];
console.log( lodash.dropWhile(nums, o => o % 2 == 0 ) )
//=> [ 5, 4, 6, 8 ]
nums = [ 2,4,6,8 ];
console.log( lodash.dropWhile(nums, o => o % 2 == 0 ) )
//=> []
数组切片也是就数组的截取,这几个方法都是由#baseSlice
生成,只是各自都有各自实现的逻辑
#baseWhile
该方法可以我的做到从头或从末尾是截取元素,下面用一个例子来分析,首先分为两个步骤:
index 为结束索引,当删除时,截取出来的元素要包括 index 位置元素,所以这个元素是最后一个不被满足条件元素,其它地方一样,见下面例子:
let numbers = [2, 5, 4, 6, 8]
const odd = o => o % 2 == 0
// let users = [ 2,5,4,6,8 ];
lodash.dropWhile(numbers, odd )
lodash.takeWhile(numbers, odd )
lodash.dropRightWhile(numbers, odd )
lodash.takeRightWhile(numbers, odd )
/*
[ 5, 4, 6, 8 ]
[ 2 ]
[ 2, 5 ]
[ 4, 6, 8 ]
*/
const baseWhile = (array, predicate, isDrop, fromRight) => {
// 确定遍历方向
// 从头则 index 从 -1 开始
// 从末尾 index 从 array.length 开始
let length = array.length,
index = fromRight ? length - 1 : 0;
// 确定截取结束位置
for(; index < length ; fromRight ? index-- : ++index ){
if( !predicate(array[index], index, array) ){
break;
}
}
// 计算截取范围
// 如果是从右截取 index 应该是开始位置
return isDrop
// drop
? array.slice( fromRight ? 0 : index , fromRight ? index + 1 : length )
// take
: array.slice( fromRight ? index + 1 : 0 , fromRight ? length : index );
}
// 从左至右 drop
console.log( baseWhile(numbers, odd, true ) )
//=> [ 5, 4, 6, 8 ]
// 从左至右 take
console.log( baseWhile(numbers, odd ) )
//=> [ 2 ]
// 从右至左 drop
console.log( baseWhile(numbers, odd, true, true ) )
//=> [ 2, 5 ]
// 从右至左 take
console.log( baseWhile(numbers, odd, false, true) )
//=> [ 4, 6, 8 ]
下面是#baseWhile
源码:
function baseWhile(array, predicate, isDrop, fromRight) {
var length = array.length,
index = fromRight ? length : -1;
while ((fromRight ? index-- : ++index < length) &&
predicate(array[index], index, array)) {}
return isDrop
? baseSlice(array, (fromRight ? 0 : index), (fromRight ? index + 1 : length))
: baseSlice(array, (fromRight ? index + 1 : 0), (fromRight ? length : index));
}
不要被删除和获取名字误导了,删除就是截取剩余部分,获取就是截取删除部分,都是做截取操作,过滤可以使用 Lodash.filter
有关于切片的方法分别是:
Lodash.chunk(array, size=1) 以 size 为单位分割数组,默认情况下会外排将每一个元素单独分割并返回一个新数组
Lodash.flatten 可以做 chunk 的逆运算
以下是 chunk 源码:
function chunk(array, size, guard) {
//? 暂时还不知道 guard 能有什么用
if ((guard ? isIterateeCall(array, size, guard) : size === undefined)) {
size = 1;
} else {
// 利用 Math.max 验证 size 有效性
// 也就是其要大于等于 0
size = nativeMax(toInteger(size), 0);
}
var length = array == null ? 0 : array.length;
if (!length || size < 1) {
return [];
}
var index = 0,
resIndex = 0,
// 最大可能拆分
result = Array(nativeCeil(length / size));
while (index < length) {
result[resIndex++] = baseSlice(array, index, (index += size));
}
return result;
}
源码中因注意以下几点:
Math.max
和 toInteger 两个方法验证数字有有效值,不需要在进行是否大于小0,如果小于0 给默认值在这里就不需要,这样的做法省去了对数字的判断Math.ceil
对长度与拆分块长度取最大可能,计算出新数组应该有多少个元素#baseToPairs
toPairs toPairsIn fromPairs该方法生成 Lodash.toPairs 和 Lodash.toPairsIn 两个方法,生成函数接收一个对象作为参数,因为是对象属性
数组,那么就包括可枚举和不可枚举属性
Lodash.toPairs 使用 Object.keys 遍历属性;Lodash.toPairsIn 使用in
运算符遍历属性
方法很简单,接收属性数组,#arrayMap
遍历出新的数组,下面附上源码:
function createToPairs(keysFunc) {
return function(object) {
var tag = getTag(object);
if (tag == mapTag) {
return mapToArray(object);
}
if (tag == setTag) {
return setToPairs(object);
}
return baseToPairs(object, keysFunc(object));
};
}
function baseToPairs(object, props) {
return arrayMap(props, function(key) {
return [key, object[key]];
});
}
var toPairs = createToPairs(keys);
var toPairsIn = createToPairs(keysIn);
接下来就是 toPairs 的逆运算, Lodash.fromPairs, 因为是做逆运算,那么也就表示他们两个方法的返回值应该互为他们的接收值,当然这不是参绝对的,fromPairs 接收的是 toPairs 组成的数组键值对,所以只需要遍历该数组返回一个新对象即可,下面看其源码和例子:
function fromPairs(pairs) {
var index = -1,
length = pairs == null ? 0 : pairs.length,
result = {};
while (++index < length) {
var pair = pairs[index];
result[pair[0]] = pair[1];
}
return result;
}
let obj = { money: [100, 500], name: 'Qlover' }
let pairs = lodash.toPairs(obj)
console.log( pairs )
// 添加一个其它方法
pairs.push(['toString', {}])
// => [ [ 'money', [ 100, 500 ] ], [ 'name', 'Qlover' ] ]
console.log( lodash.fromPairs(pairs) )
// => { money: [ 100, 500 ], name: 'Qlover', toString: {} }
建议不要使用该方法操作比较敏感的对象
如果做过 php,就会有这样一个方法爱不释手, array_combine , 因为这个方法可以将一个数组的当成键,另一个数组当用值,组合与一个新的数组,这样的数组组合思想很好
Lodash 也有这样的思想:
console.log( lodash.zip([1,2], [3,4], [true, false]) )
//=> [ [ 1, 3, true ], [ 2, 4, false ] ]
console.log( lodash.unzip([ [ 1, 3, true ], [ 2, 4, false ] ]) )
//=> [ [ 1, 2 ], [ 3, 4 ], [ true, false ] ]
let fields = [ 'money', 'name' ]
let values = [ [200, 300], 'Lee' ]
let pairs = lodash.zip(fields, values)
console.log( pairs )
// => [ [ 'money', [ 200, 300 ] ], [ 'name', 'Lee' ] ]
let obj = lodash.fromPairs(pairs)
console.log( obj )
// => { money: [ 200, 300 ], name: 'Lee' }
console.log( lodash.zipObject(fields, values) )
//=> { money: [ 200, 300 ], name: 'Lee' }
zip 操作是 rest 参数,那么就离不开 baseRest 方法, 而 unzip 操作只接收一个数组,返回的结果互为接收的参数,然后仔细观察它们的返回值,数组结构是一致的,这个特点很重要,那么一个方法就可以了,而处理这两种情况的方法是#unzip
对就是 lodash.unzip 方法…
但是 unzip 有一个设计非常巧妙的地方,如果再仔细观察上面例子中的头两行的 zip 和 unzip 操作,会发现,每一次的操作参数都是一个二维数组,并且是将每列元素遍历出来组合成的新数组,不管是 zip 还是 unzip,这种数据结构像矩阵
如果发现了这一点下面就是 unzip 方法的两个要点:
就是获取出每一列的集合,这里可以参考一下 getIteratee 部分的 baseIteratee 值的转换这一部分,先看下面两个例子:
对象集合的列数组:
let users = [
{ money: [100, 500], name: 'Qlover' },
{ money: [500, 50], name: 'Lee' },
{ money: [350, 100], name: 'Fred' }
]
// 获取对象的属性值
const baseProperty = key => obj => obj ? obj[key] : undefined
// 传递给 iteratee 的值是 money|name 是一个 key 返回一个回调
const getName = baseProperty('name')
// 返回的回调,给定对象就返回对象的 key
// 这个 key 是被记忆起来的为 money
console.log( getName(users[2]) )
// => Fred
// 这是获取指定的一个对象的 name 键值
// 那么如果是列,一个对象集合只需要对它进行遍历
console.log( lodash.map( users, getName ) )
//=> [ 'Qlover', 'Lee', 'Fred' ]
数组集合的列数组:
const baseProperty = key => obj => obj ? obj[key] : undefined
let users = [ [100, 500], [500, 50], [350, 100] ]
// 指定元素数组的第1个元素
const getName = baseProperty(0)
console.log( getName(users[2]) ) // => 350
console.log( lodash.map( users, getName ) )
//=> [ 'Qlover', 'Lee', 'Fred' ]
// 如果是每一列全部的集合,只需要更改 baseProperty 方法指定的索引值
// 每行矩阵最多只有 2 个元素
let index = -1,
length = 2,
result = [];
while ( ++index < length ) {
result.push(lodash.map( users, baseProperty(index) ))
}
console.log( result )
//=> [ [ 100, 500, 350 ], [ 500, 50, 100 ] ]
这可能要仔细的花点时间揣摩上面的两个例子,关键点就再一使用 baseProperty 这个高阶函数和 map 配合
这一点只需要观察 zip 和 unzip 两个方法返回的结果值, zip 矩阵每一行的长度决定 unzip 结果元素总长度; unzip 元素总长度决定 zip 矩阵每一行的长度
下面就直接附上两个方法的源码:
// lodash.zip
var zip = baseRest(unzip)
// lodash.unzip
function unzip(array) {
if (!(array && array.length)) {
return [];
}
var length = 0;
// 过滤掉非类数组的同时得到了 length 的最大值
array = arrayFilter(array, function(group) {
if (isArrayLikeObject(group)) {
length = nativeMax(group.length, length);
return true;
}
});
return baseTimes(length, function(index) {
return arrayMap(array, baseProperty(index));
});
}
baseRest 起一个作用就是将 rest 参数组合成了 unzip 可以接收的一个矩阵值,也就是个二维数组
不得不说 Lodash 设计的很好,将函数为单位独立出来,试想 zip 已经是将数组进行压缩组合,而 zipWith 将 zip 的结果用回调遍历一次,不就达到了 zipWith 的方法的初衷了吗?
这样一来,不要多写其它多余方法,只需要在对外多暴露一个接口而已,当然这都是看了源码才悟出的道理,那么下面就附上其源码:
function unzipWith(array, iteratee) {
if (!(array && array.length)) {
return [];
}
// 个人觉得将这一行删掉看起来视觉上更好
// 没有一个额外申请的变量
// var result = unzip(array);
if (iteratee == null) {
// return result;
return unzip(array);
}
// return arrayMap(result, function(group) {
return arrayMap(unzip(array), function(group) {
return apply(iteratee, undefined, group);
});
}
var zipWith = baseRest(function(arrays) {
var length = arrays.length,
iteratee = length > 1 ? arrays[length - 1] : undefined;
iteratee = typeof iteratee == 'function' ? (arrays.pop(), iteratee) : undefined;
return unzipWith(arrays, iteratee);
});
#baseZipObject
zipObject zipObjectDeep该方法用于生成 Lodash.zipObject 和 Lodash.zipObjectDeep,主要作用将源数组值当作键,目标数组值当作值组成一个新对象
方法组合流程也很简单遍历源数组,将值取出来循环赋值上目标数组的值,只是做赋值操作的方法不同 zipObject 直接使用#assignValue
,而 zipObjectWith 使用的是#baseSet
baseSet 和 baseGet 两个方法分别是设置访问路径属性值和设置访问路径属性值,具体可参考 baseSet 部分,下面附两个方法的源码:
function baseZipObject(props, values, assignFunc) {
var index = -1,
length = props.length,
valsLength = values.length,
result = {};
while (++index < length) {
var value = index < valsLength ? values[index] : undefined;
assignFunc(result, props[index], value);
}
return result;
}
function zipObjectDeep(props, values) {
return baseZipObject(props || [], values || [], baseSet);
}
function zipObject(props, values) {
return baseZipObject(props || [], values || [], assignValue);
}
#createAggregator
聚合 countBy groupBy partition keyBy聚合函数对一组值执行计算,并返回单个值,也被称为组函数
下面是聚合函数使用用例:
// orders 为用户的下单信息
// id 为用户id,oid 为订单id
let orders = [
{ id: 2, oid: 1},
{ id: 3, oid: 2},
{ id: 1, oid: 3},
{ id: 2, oid: 4},
{ id: 1, oid: 5},
{ id: 1, oid: 6},
]
// 统计用户订单数
console.log( lodash.countBy(orders, 'id') )
//=> { '1': 3, '2': 2, '3': 1 }
// 将用户下单信息以用户id分组查询
console.log( lodash.groupBy(orders, 'id') )
/*
{ '1': [ { id: 1, oid: 3 }, { id: 1, oid: 5 }, { id: 1, oid: 6 } ],
'2': [ { id: 2, oid: 1 }, { id: 2, oid: 4 } ],
'3': [ { id: 3, oid: 2 } ] }
*/
countBy,groupBy,partition 和 keyBy 四个方法都是由 createAggregator 这个基函数生成,该方法是一个聚合器,有下面两个特点:
首先从 countBy 角度分析一次 createAggregator, countBy 会将对象数组或一个数组中重复出现的元素用该元素或该值的键记录该重复值的次数,下面是一个可以实现该方法的 sql 语句:
SELECT elem, count(id) FROM table group by id
所以可以直接考虑的第一件事就是,将 createAggregator 暴露的 setter 定义为键值自增 1,首次出现则为 1, countBy 没有累加值
知道了如何 setter,下面就直接附上 createAggregator 源码:
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);
};
}
源码很简单,可以接收两个参数,参数一必须是一个 setter 操作函数,参数二可以是个函数返回的初始累加值,默认为空对象,#arrayAggregator
和#baseAggregator
就是遍历集合做 setter 操作
除了 partition 默认为返回两个元素组成的空数组,其余三个方法都没有初始累加值
由上面几点,可以得出,基函数 createAggregator 只做一件事件,循环对累加值做 setter 操作:
const aggregator = (array, setter, iteratee, initializer) => {
let index = -1,
length = array.length,
value, acc = initializer ? initializer() : {}
// 循环做 setter 操作
while ( ++index < length) {
value = array[index]
setter(acc, value, iteratee(value), array)
}
return acc
}
// count by
let arr = [1,2,1,3,2]
console.log( lodash.countBy(arr))
// => { '1': 2, '2': 2, '3': 1 }
let res = aggregator(arr, (acc, value, iv, array) => {
if( acc.hasOwnProperty(value) ){
++acc[value]
} else {
acc[value] = 1
}
}, value => value)
// getIteratee 方法如果回调值为 undefined 则回调方法是 identity
console.log( res )
// => { '1': 2, '2': 2, '3': 1 }
countBy 的 setter 会使用 Object 原型的 hasOwnProperty 方法判断累加器是否包含该值键,如果有则自增,没有则初始为1,就这样简单,下面附上其源码:
var countBy = createAggregator(function(result, value, key) {
if (hasOwnProperty.call(result, key)) {
++result[key];
} else {
baseAssignValue(result, key, 1);
}
});
groupBy 就是将重复键出现的值组成一个数组,其 setter 如下:
// group by
// 比如以名字长度为分组条件
let users = [
{ money: [100, 500], name: 'Qlover' },
{ money: [500, 50], name: 'Lee' },
{ money: [200, 150], name: 'Edward' },
{ money: [350, 100], name: 'Fred' }
]
console.log( lodash.groupBy(users, 'name.length') )
/*
{ '3': [ { money: [Array], name: 'Lee' } ],
'4': [ { money: [Array], name: 'Fred' } ],
'6':
[ { money: [Array], name: 'Qlover' },
{ money: [Array], name: 'Edward' } ] }
*/
console.log( aggregator(users, (acc, value, key, array) => {
if( acc.hasOwnProperty(key) ){
acc[key].push(value)
} else {
acc[key] = [value]
}
}, value => value['name'].length) )
// 指定回调返回 name 值的长度
/*
{ '3': [ { money: [Array], name: 'Lee' } ],
'4': [ { money: [Array], name: 'Fred' } ],
'6':
[ { money: [Array], name: 'Qlover' },
{ money: [Array], name: 'Edward' } ] }
*/
由于 Lodash 源码中有专门处理回调的 getIteratee 方法,这里只是做个模拟,下面附上其源码:
var groupBy = createAggregator(function(result, value, key) {
if (hasOwnProperty.call(result, key)) {
result[key].push(value);
} else {
baseAssignValue(result, key, [value]);
}
});
同样的对于 keyBy setter 处理的是键的最后一个值,那么简单点也就是遍历一次,就给对应键赋值:
// key by
users = [
{ money: 100, name: 'Qlover' },
{ money: 500, name: 'Lee' },
{ money: 200, name: 'Qlover' },
{ money: 350, name: 'Fred' }
]
console.log( lodash.keyBy(users, 'name') )
/*
{ Qlover: { money: 200, name: 'Qlover' },
Lee: { money: 500, name: 'Lee' },
Fred: { money: 350, name: 'Fred' } }
*/
console.log( aggregator(users, (acc, value, key, array) => {
acc[key] = value
}, value => value['name']) )
/*
{ Qlover: { money: 200, name: 'Qlover' },
Lee: { money: 500, name: 'Lee' },
Fred: { money: 350, name: 'Fred' } }
*/
其源码:
var keyBy = createAggregator(function(result, value, key) {
baseAssignValue(result, key, value);
});
baseAssignValue 就是直接做赋值操作的基函数,可参考核心部分
该方法与前面的三个方法有一个不同的地方就是,返回的是个二维数组,如果没值不会返回空对象,而是两个元素组成的空数组,其源码和其 setter 如下:
let users = [
{ money: 100, name: 'Qlover' },
{ money: 500, name: 'Lee' },
{ money: 200, name: 'Qlover' },
{ money: 350, name: 'Fred' }
]
console.log(lodash.partition(users, value => value.money > 200))
/*
[ [ { money: 500, name: 'Lee' }, { money: 350, name: 'Fred' } ],
[ { money: 100, name: 'Qlover' },
{ money: 200, name: 'Qlover' } ] ]
*/
console.log( aggregator(users, (acc, value, key, array) => {
// 真值在元素一里面
// 假值在元素二里面
acc[key ? 0 : 1].push(value)
}, value => value.money > 200, () => [[], []] ) )
/*
[ [ { money: 500, name: 'Lee' }, { money: 350, name: 'Fred' } ],
[ { money: 100, name: 'Qlover' },
{ money: 200, name: 'Qlover' } ] ]
*/
// 源码
var partition = createAggregator(function(result, value, key) {
result[key ? 0 : 1].push(value);
}, function() { return [[], []]; });