解析underscore源码-JavaScript 数组展开以及 underscore 重要的内部方法 flatten 详解

2016-10-16

2016-10-10 摘录自 - JavaScript 数组展开以及 underscore 重要的内部方法 flatten 详解

flatten

今天要讲的是数组展开以及和数组展开息息相关的一个重要的内部方法 flatten
什么是数组展开?简单的说就是将嵌套的数组“铺平”,还是举几个简单的例子吧。

[[[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 方法的调用方式。

var flatten = function(input, shallow, strict, startIndex){
    // ...
};

第一个参数 input 即为需要展开的数组,所以 flatten 方法中传入的第一个参数肯定是数组(或者 arguments) ;第二个参数 shallow 是个布尔值,如果为 false ,则表示数组是深度展开,如果为 true 则表示只展开一层;第四个参数表示 input 展开的起始位置,即从 input 数组中第几个元素开始展开。

var ans = flatten([[1,2][3,4]], false, false, 1);
console.log(ans); // => [3,4]

从第1项开始展开数组,即忽略了数组的第0项([1,2])。
以上三个参数还是比较容易理解的,相对来说费劲的是第三个参数 strictstrict 也是个布尔值,当 shallowtrue 时,能过滤 input 参数元素中的非数组元素。好难理解啊!我们举个栗子。

var ans = flatten([5,6,[1,2],[3,4]], true, true);
console.log(ans); // => [1,2,3,4]

5和6是 input 参数中的非数组元素,直接过滤掉了。如果 stricttrue 并且 shallowfalse ,那么调用 flatten 方法的结果只能是 [] 。所以我们会看到源码里如果 stricttrue ,那么 shallow 也一定是 true
直接来看源码,加了非常多的注释。

var flatten = function(input, shallow, strcit, 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 bushishuzu,是基本类型时
            // 肯定会走到这个 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 的前三个参数。

_.flatten = function(array, shallow){
    // array => 需要展开的数组
    // shallow => 是否只展开一层
    // false 为 flatten 方法 strict 变量
    return flatten(array, shllow, false);
};

前面说了,stricttrue 只和 shallowtrue 一起使用,所以没有特殊情况的话 strict 默认为 false
_.union 方法同样用到了 flatten ,这个方法的作用是传入多个数组,然后对数组去重。

var ans = _.union([[1],[1,2],3,4]);
console.log(ans); // => [[1],1,2]

首先并不需要对数组深度展开,其次 _.union 传入的是数组,对于非数组元素可以直接忽略。这两点直接对应了 shallow 参数和 strict 参数均为 true (都不用做容错处理了)。对于一个数组的去重,最后调用 _.unqiue 即可。

_.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 调用,如何设计的合理,优雅,如何兼顾到各种情况,真的需要强大的实践以及代码能力,这点还需要日后多加摸索。