写在前面
最近想提高一下自己的编程能力,另一方面是为了培养自己规律写作的习惯,所以开了这个专栏,督促自己每天写几个 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']]_.chunk(['a', 'b', 'c', 'd'], 2); // => [['a', 'b'], ['c', 'd']] _.chunk(['a', 'b', 'c', 'd'], 3); // => [['a', 'b', 'c'], ['d']]_.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 的长度就应该是 3let res = new Array(Math.ceil(len / size));// 切割的开始位置,结束的位置是 index + sizelet index = 0;// 结果数组的索引,切割一次 resIndex 增加一次let resIndex = 0;while(index < len){res[resIndex++] = arr.slice(index,index+=size)}return res}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 }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]_.compact([0, 1, false, 2, '', 3]); // => [1, 2, 3]_.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}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 }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]var array = [1]; var other = _.concat(array, 2, [3], [[4]]); console.log(other); // => [1, 2, 3, [4]] console.log(array); // => [1]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; // 4while (--index) {args[index - 1] = arguments[index];}// 倒序排列console.log(args,"args") // [2,[3],[[4]]]return arrayPush(Array.isArray(array) ?copyArray(array) : [array],baseFlatten(args, 1));}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)); }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 = -1const length = source.length// 如果有传入 array,使用传入参数,否则就自己构建array || (array = new Array(length))// index 从 -1 开始,所以使用 ++indexwhile (++index < length) {array[index] = source[index]}return array}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 }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]// 第二个参数传入数组,保留部分 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]// 第二个参数传入数组,保留部分 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.toStringfunction 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}// 是对象并且是 Argumentsfunction 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 == 1result.push(...value)}} else if (!isStrict) {// depth < 0 或者 value 不可以展开// 如果不是严格模式,直接往后追加元素result[result.length] = value}}return result}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; 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]]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]]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"]var x = [1, 2, 3]; var fakeArray = { [Symbol.isConcatSpreadable]: true, length: 2, 0: "hello", 1: "world" } x.concat(fakeArray); // [1, 2, 3, "hello", "world"]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.toStringfunction getTag(value) {if (value == null) {return value === undefined ? '[object Undefined]' : '[object Null]'}return toString.call(value)}const toString = Object.prototype.toString function getTag(value) { if (value == null) { return value === undefined ? '[object Undefined]' : '[object Null]' } return toString.call(value) }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;}function arrayPush(array, values) { var index = -1, // 要合并的长度 length = values.length, // 原数组的长度 offset = array.length; while (++index < length) { array[offset + index] = values[index]; } return array; }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));return arrayPush( Array.isArray(array) ? copyArray(array) : [array], baseFlatten(args, 1));return arrayPush( Array.isArray(array) ? copyArray(array) : [array], baseFlatten(args, 1));
最后
今天是第一天写 lodash
,有一些收获,lodash
本身不难,而且代码结构很清晰,一个方法一个文件,对于提高编程能力有帮助,希望自己可以每天坚持写 lodash
打卡:2023/6/20