flatten
端午休息三天,睡了两天,是该有点产出了。
今天要讲的是数组展开以及和数组展开息息相关的一个重要的内部方法 flatten。
什么是数组展开?简单的说就是将嵌套的数组 “铺平”,还是举几个简单的例子吧。
1 2 |
[[[1, 2], [1, 2, 3]], [1, 2]] => [1, 2, 1, 2, 3, 1, 2] [[[1, 2], [1, 2, 3]], [1, 2]] => [[1, 2], [1, 2, 3], 1, 2] |
以上两种都是数组展开,第一种我们认为是深度展开,即打破所有嵌套数组,将元素提取出来放入一个数组中;第二种只展开了一层,即只把数组内嵌套的一层数组展开,而没有递归展开下去。
我们首先来看看 flatten 方法的调用形式。
1 2 3 |
var flatten = function(input, shallow, strict, startIndex) { // ... }; |
第一个参数 input 即为需要展开的数组,所以 flatten 方法中传入的第一个参数肯定是数组(或者 arguments);第二个参数 shallow 是个布尔值,如果为 false,则表示数组是深度展开,如果为 true 则表示只展开一层;第四个参数表示 input 展开的起始位置,即从 input 数组中第几个元素开始展开。
1 2 |
var ans = flatten([[1, 2], [3, 4]], false, false, 1); console.log(ans); // => [3, 4] |
从第 1 项开始展开数组,即忽略了数组的第 0 项([1, 2])。
以上三个参数还是比较容易理解的,相对来说费劲的是第三个参数 strict。strict 也是个布尔值,当 shallow 为 true 并且 strict 也为 true 时,能过滤 input 参数元素中的非数组元素。好难理解啊!我们举个简单的例子。
1 2 |
var ans = flatten([5, 6, [1, 2], [3, 4]], true, true); console.log(ans); // => [1, 2, 3, 4] |
5 和 6 是 input 参数中的非数组元素,直接过滤掉了。如果 strict 为 true 并且 shallow 为 false,那么调用 flatten 方法的结果只能是 []。所以我们会看到源码里如果 strict 为 true,那么 shallow 也一定是 true。
直接来看源码,加了非常多的注释。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
var flatten = function(input, shallow, strict, startIndex) { // output 数组保存结果 // 即 flatten 方法返回数据 // idx 为 output 的累计数组下标 var output = [], idx = 0; // 根据 startIndex 变量确定需要展开的起始位置 for (var i = startIndex || 0, length = getLength(input); i < length; i++) { var value = input[i]; // 数组 或者 arguments if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) { // flatten current level of array or arguments object // (!shallow === true) => (shallow === false) // 则表示需深度展开 // 继续递归展开 if (!shallow) // flatten 方法返回数组 // 将上面定义的 value 重新赋值 value = flatten(value, shallow, strict); // 递归展开到最后一层(没有嵌套的数组了) // 或者 (shallow === true) => 只展开一层 // value 值肯定是一个数组 var j = 0, len = value.length; // 这一步貌似没有必要 // 毕竟 JavaScript 的数组会自动扩充 // 但是这样写,感觉比较好,对于元素的 push 过程有个比较清晰的认识 output.length += len; // 将 value 数组的元素添加到 output 数组中 while (j < len) { output[idx++] = value[j++]; } } else if (!strict) { // (!strict === true) => (strict === false) // 如果是深度展开,即 shallow 参数为 false // 那么当最后 value 不是数组,是基本类型时 // 肯定会走到这个 else-if 判断中 // 而如果此时 strict 为 true,则不能跳到这个分支内部 // 所以 shallow === false 如果和 strict === true 搭配 // 调用 flatten 方法得到的结果永远是空数组 [] output[idx++] = value; } } return output; }; |
总的来说,就是持续递归调用 flatten,直到不能展开为止。给出 flatten 方法的实现源码位置 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/src/underscore-1.8.3.js#L489-L507。
接着我们来看看源码中有用到这个内部方法的 API。
首先是 _.flatten 方法,非常简单,用了 flatten 的前三个参数。
1 2 3 4 5 6 |
_.flatten = function(array, shallow) { // array => 需要展开的数组 // shallow => 是否只展开一层 // false 为 flatten 方法 strict 变量 return flatten(array, shallow, false); }; |
前面说了,strict 为 true 只和 shallow 为 true 一起使用,所以没有特殊情况的话 strict 默认为 false。
_.union 方法同样用到了 flatten,这个方法的作用是传入多个数组,然后对数组元素去重。
1 2 |
var ans = _.union([[1]], [1, 2], 3, 4); console.log(ans); // => [[1], 1, 2] |
首先并不需要对数组深度展开,其次 _.union 传入的是数组,对于非数组元素可以直接忽略。这两点直接对应了 shallow 参数和 strict 参数均为 true(都不用做容错处理了)。对于一个数组的去重,最后调用 _.unique 即可。
1 2 3 4 5 6 7 8 9 10 |
_.union = function() { // 首先用 flatten 方法将传入的数组展开成一个数组 // 然后就可以愉快地调用 _.uniq 方法了 // 假设 _.union([1, 2, 3], [101, 2, 1, 10], [2, 1]); // arguments 为 [[1, 2, 3], [101, 2, 1, 10], [2, 1]] // shallow 参数为 true,展开一层 // 结果为 [1, 2, 3, 101, 2, 1, 10, 2, 1] // 然后对其去重 return _.uniq(flatten(arguments, true, true)); }; |
而 _.difference,_.pick,_.omit 方法,大家可以自己进源码去看,都大同小异,没什么特别要注意的点。(注意下 startIndex 参数即可)
对于内部方法 flatten,我要总结的是,可能某个内部方法会被多个 API 调用,如何设计地合理,优雅,如何兼顾到各种情况,真的需要强大的实践以及代码能力,这点还需要日后多加摸索。
源码解读完整系列文章请至 https://github.com/hanzichi/underscore-analysis/issues