Lodash系列 - 1. chunk,compact,concat
写在前面
最近想提高一下自己的编程能力,另一方面是为了培养自己规律写作的习惯,所以开了这个专栏,督促自己每天写几个 Lodash
的几个 Api
今天带来三个 Api
,分别是 chunk, compact, concat
Api
chunk
先看官网对于 chunk 的介绍
_.chunk(array, [size=1])
Creates an array of elements split into groups the length of
size
. Ifarray
can't be split evenly, the final chunk will be the remaining elements.
Arguments
array
(Array) : The array to process.[size=1]
(number) : The length of each chunk
Returns
(Array) : Returns the new array of chunks.
Example
_.chunk(['a', 'b', 'c', 'd'], 2);
// => [['a', 'b'], ['c', 'd']]
_.chunk(['a', 'b', 'c', 'd'], 3);
// => [['a', 'b', 'c'], ['d']]
根据提供的例子,参数是一个数组,一个要分割数组的长度,返回一个分割后的数组,如果不能平均分割,最后一个元素被单独放置在一个新的数组里
分析
本质就是切割数组,切割的长度是后面传入的参数 size
,可以使用数组原生方法 slice
方法进行切割,同时 slice
规定: 如果被切割的数组长度小于 size,仅仅返回仅剩的数组元素,所以使用 slice
切割是最合适不过的了,最后把切割后的结果放入新的数组
type TChunk = <T>(arr:Array<T> | null | undefined,size?:number) =>T[]
const chunk:TChunk = (arr,size = 1)=>{
if(arr == null) return [];
let len = arr.length;
// 创建一个结果数组,数组的长度是 原数组总长度 / 切割长度 ,并且向上取整
// len = 5, size = 2, res 的长度就应该是 3
let res = new Array(Math.ceil(len / size));
// 切割的开始位置,结束的位置是 index + size
let index = 0;
// 结果数组的索引,切割一次 resIndex 增加一次
let resIndex = 0;
while(index < len){
res[resIndex++] = arr.slice(index,index+=size)
}
return res
}
compact
先看官网对于 compact(紧凑) 的介绍
_.compact(array)
Creates an array with all falsey values removed. The values
false
,null
,0
,""
,undefined
, andNaN
are falsey.
Arguments
array
(Array) : The array to compact.
Returns
(Array) : Returns the new array of filtered values.
Example
_.compact([0, 1, false, 2, '', 3]);
// => [1, 2, 3]
根据提供的例子,参数是一个数组,返回一个只有 真值 的数组
分析
类似于 es6
中的 Filter
+ Boolean
的效果,可以使用 Array.filter(Boolean)
代替
type Falsey = null | undefined | false | "" | 0;
type TCompact = <T>(arr:Array<T | Falsey>) =>T[];
const compact:TCompact = (array)=>{
let resIndex = 0;
let result:any[] = [];
if (array == null) {
return result
}
for (const value of array) {
// 在 这个地方阻断 falsy 值
if(value){
result[resIndex++] = value
}
}
return result
}
concat
先看官网对于 concat 的介绍
_.concat(array, [values])
Creates a new array concatenating(连接)
array
with any additional arrays and/or values.
Arguments
array
(Array) : The array to concatenate.[values]
(...)* : The values to concatenate.
Returns
(Array) : Returns the new concatenated array.
Example
var array = [1];
var other = _.concat(array, 2, [3], [[4]]);
console.log(other);
// => [1, 2, 3, [4]]
console.log(array);
// => [1]
根据提供的例子,接受两个参数,第一个参数是接收一个数组,后面可以传入 n
多个参数,如果参数中有数组嵌套,还会拍平第一层数组, 返回一个拼接后的数组,原数组不变
分析
类似于 es6
中的 concat
+ flat
的效果,首先要保证原数组不受污染,其次要接收任意多种类型,最后如果类型中有数组嵌套,还要拍平第一层数组
源码中这个方法有很大的借鉴意义
以参数 concat([1],2,[3],[[4]])
为例
function concat() {
var length = arguments.length;
if (!length) {
return [];
}
var args = Array(length - 1),
array = arguments[0],
index = length; // 4
while (--index) {
args[index - 1] = arguments[index];
}
// 倒序排列
console.log(args,"args") // [2,[3],[[4]]]
return arrayPush(
Array.isArray(array) ?
copyArray(array) : [array],
baseFlatten(args, 1));
}
在 最后的 return
中,判断了 array = arguments[0]
是否是一个数组,如果是一个数组,则进行copyArray
,如果不是,则包装成一个数组,同时使用 baseFlatten
对后面的参数进行拍平,最后使用 arrayPush
把拍平后的数组放入第一个参数中
根据代码执行顺序进行依次展开 copyArray->baseFlatten->arrayPush
copyArray
function copyArray(source, array) {
let index = -1
const length = source.length
// 如果有传入 array,使用传入参数,否则就自己构建
array || (array = new Array(length))
// index 从 -1 开始,所以使用 ++index
while (++index < length) {
array[index] = source[index]
}
return array
}
比如
// 第二个参数传入数组,保留部分
console.log("🚀 ~ file:", copyArray([1,2,3],[4,5,6,7])); // [1,2,3,7]
// 不传入数组
console.log("🚀 ~ file:", copyArray([1,2,3])); // [1,2,3]
baseFlatten
const spreadableSymbol = Symbol.isConcatSpreadable;
const toString = Object.prototype.toString
function getTag(value) {
// 如果 value 不传值的话,是undefined
// 如果直接使用 toString.call的话,必须要传入一个参数
if (value == null) {
return value === undefined ? '[object Undefined]' : '[object Null]'
}
return toString.call(value)
}
// 判断是否是 对象
function isObjectLike(value) {
return typeof value === 'object' && value !== null
}
// 是对象并且是 Arguments
function isArguments(value) {
return isObjectLike(value) && getTag(value) == '[object Arguments]'
}
// 可以展开的数据结构
function isFlattenable(value) {
return Array.isArray(value) || isArguments(value) ||
!!(value && value[spreadableSymbol])
}
function baseFlatten(array, depth, predicate, isStrict, result) {
// predicate 断言 是一个函数
predicate || (predicate = isFlattenable)
// 并没有使用全局变量,使用了闭包
result || (result = [])
if (array == null) {
return result
}
for (const value of array) {
// 说明可以展开
if (depth > 0 && predicate(value)) {
if (depth > 1) {
// Recursively flatten arrays (susceptible to call stack limits).
// 递归展平数组
baseFlatten(value, depth - 1, predicate, isStrict, result)
} else {
// depth == 1
result.push(...value)
}
} else if (!isStrict) {
// depth < 0 或者 value 不可以展开
// 如果不是严格模式,直接往后追加元素
result[result.length] = value
}
}
return result
}
可能有人对这个const spreadableSymbol = Symbol.isConcatSpreadable;
有疑问
MDN关于 Symbol.isConcatSpreadable 的介绍
内置的 Symbol.isConcatSpreadable 符号用于配置某对象作为 Array.prototype.concat() 方法的参数时是否展开其数组元素。
const alpha = ['a', 'b', 'c'];
const numeric = [1, 2, 3];
let alphaNumeric = alpha.concat(numeric);
console.log(alphaNumeric);
// Expected output: Array ["a", "b", "c", 1, 2, 3]
numeric[Symbol.isConcatSpreadable] = false;
alphaNumeric = alpha.concat(numeric);
console.log(alphaNumeric);
// Expected output: Array ["a", "b", "c", Array [1, 2, 3]]
对于类数组 (array-like) 对象,默认不展开。期望展开其元素用于连接,需要设置 Symbol.isConcatSpreadable
为 true:
var x = [1, 2, 3];
var fakeArray = {
[Symbol.isConcatSpreadable]: true,
length: 2,
0: "hello",
1: "world"
}
x.concat(fakeArray); // [1, 2, 3, "hello", "world"]
因为 loadash中的 concat 是要和数组的 concat 对齐,当然要抹平差异,如果一个元素的Symbol.isConcatSpreadable为 false,那么就不应该合并,主要是考虑到 「类数组对象」
还有这个 getTag
方法
getTag
const toString = Object.prototype.toString
function getTag(value) {
if (value == null) {
return value === undefined ? '[object Undefined]' : '[object Null]'
}
return toString.call(value)
}
为啥要判断 value == null
呢,直接使用 toString.call
好了,toString.call(undefned)
也是 [object Undefined]
,toString.call(null)
是 [object Null]
,为啥要多此一步呢?
是因为
getTag()
当不传入参数的时候,也应该是 undefined,但是 toString.call
必须要传入参数,所以做了一个判断
arrayPush
function arrayPush(array, values) {
var index = -1,
// 要合并的长度
length = values.length,
// 原数组的长度
offset = array.length;
while (++index < length) {
array[offset + index] = values[index];
}
return array;
}
直接在原数组的的基础上不断添加,没有使用push,或者展开运算符,可能性能会好一点?
concat 总结
应该对这段代码有不一样的理解,Lodash
把基础方法拆解的很细,而且大多使用的 es5或者es3 的语法,特别是对于一些细节处理,值得学习
return arrayPush(
Array.isArray(array) ?
copyArray(array) : [array],
baseFlatten(args, 1));
最后
今天是第一天写 lodash
,有一些收获,lodash
本身不难,而且代码结构很清晰,一个方法一个文件,对于提高编程能力有帮助,希望自己可以每天坚持写 lodash
打卡:2023/6/20
转载自:https://juejin.cn/post/7246581605769543737