lodash中flatten方法,就是将二维数组打平的一个方法(注意这里只能打平二维数组,多维数组有另外的方法),在lodash的flatten.js文件中,就只有几行代码,如下:
function flatten(array) {
const length = array == null ? 0 : array.length
return length ? baseFlatten(array, 1) : []
}
其意思就是如果未传入array或则传入的array是null类型的,那么就返回一个空的数组,否则就返回baseFlatten方法的执行结果。
看下baseFlatten.js文件,文件导出了一个名为baseFlatten的方法,其代码如下:
import isFlattenable from './isFlattenable.js'
/**
* The base implementation of `flatten` with support for restricting flattening.
*
* @private
* @param {Array} array The array to flatten.
* @param {number} depth The maximum recursion depth.
* @param {boolean} [predicate=isFlattenable] The function invoked per iteration.
* @param {boolean} [isStrict] Restrict to values that pass `predicate` checks.
* @param {Array} [result=[]] The initial result value.
* @returns {Array} Returns the new flattened array.
*/
function baseFlatten(array, depth, predicate, isStrict, result) {
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
result.push(...value)
}
} else if (!isStrict) {
result[result.length] = value
}
}
return result
}
export default baseFlatten
baseFlatten实则接受五个参数,但是在flattent中调用baseFlatten时只传入了两个参数,所以我们这里只按照两个参数解读
从代码中可以看到,无论predicate和result是否有传,都会被重新赋值,所以predicate被赋值为一个名为isFlattenable的函数,isFlattenable的内部不用看了,就是判断数组/类数组/Symbol.isConcatSpreadable来确定对象是否可以被解构(下面会提到,因为这里面涉及到一些规范的问题);
result被赋值为空数组
如果array为null,则返回空数组,这的这个判断跟flatten中的判断重复的原因主要是baseFlatten函数不止在flatten函数中用到,所以为了保险,在这里又判断了一次。
下面就是打平数组的主要代码了,很平常的采用for循环加递归的方式实现的,使用for of循环,从性能上考虑要比for in循环更优,因为for in循环会遍历对象的原型上的属性
depth表示数组是几纬的(也就是数组的嵌套层级有多深);可以看到在flatten中调用baseFlatten时传入的是1,所以flatten方法只能打平二维数组;
当depth大于1并且predicate(value)为true时,继续判断depth是否大于1,大于1就表示数组不止嵌套两层,就会进入到递归中,否则就采用解构赋值的形式push进result。
如果条件depth > 0 && predicate(value)不满足,并且不是在严格模式下(isStrict表示是否在严格模式下),result[result.length] = value;结束
现在数组的新增了很多方法,在不考虑老旧版本的情况下,flatten函数还可以自己使用reduce方法去实现:
//利用reduce实现
function flatten(arr) {
return arr.reduce(function (prev, next) {
return prev.concat(Array.isArray(next) ? flatten(next) : next)
}, [])
}
这样实现就比较简单粗暴了,并没有像lodash中那这样去考虑数组是否知道嵌套层级,知道和不知道有不同的处理(flatten、flattenDeep、flattenDepth方法),也没有考虑不同场景(例如:arr是数组可以使用concat函数,Symbol.isConcatSpreadable 会直接影响到concat函数的返回结果,也就是会影响到数组的展开,所以,数组在使用concat方法之后,不一定能得到你想要的结果)
这里额外说一下 isFlattenable 里面涉及到的一些东西
const a = [1, 2];
const b = [3, 4].concat(a);
console.log(b) // [3,4,1,2]
使用Symbol.isConcatSpreadable之后:
const a = [1, 2];
a[Symbol.isConcatSpreadable] = false;
const b = [3, 4].concat(a);
console.log(b) //[3,4,[1,2]]
这里就可以看到在使用concat方法的时候,并没有将数组a展开了
看看lodash中的isFlattenable
import isArguments from '../isArguments.js'
/** Built-in value reference. */
const spreadableSymbol = Symbol.isConcatSpreadable
/**
* Checks if `value` is a flattenable `arguments` object or array.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is flattenable, else `false`.
*/
function isFlattenable(value) {
return Array.isArray(value) || isArguments(value) ||
!!(value && value[spreadableSymbol])
}
export default isFlattenable
这里也是用到的Symbol.isConcatSpreadable来判断数组是否可展开,但是在lodash中并没有使用concat函数在展开数组,而是使用解构...(result.push(...value)),那为什么lodash中还使用数组的Symbol.isConcatSpreadable属性来判断数组是否可以展开呢?明明Symbol.iterator才会影响到解构赋值。
这是因为 isFlattenable 是给 flatten 族函数用的。而 flatten 的实现是按照 tc39/flat 提案实现的,在规范中 flat 中并没有使用 @@iterator。这就会导致一些问题,因为字符串也是可以迭代的,最简单的就是导致
['hello', 'world'].flat() //输出的结果是 ['h', 'e', 'l', 'l', ...]
看着这输出的结果是不是很怪异,不是自己想要的,所以lodash做了一层判断,但是,现在的规范已经出现了@@iterator,所以这里lodash这样判断的
但是,这个函数还是有问题的,就是这个函数的实现和规范并不兼容,但是问题不是在于 @@iterator,而是在于 @@isConcatSpreadable。根据规范,flat 函数是不检查 @@isConcatSpreadable 的。