underscore源码解析(三)Array的部分扩展方法

219 阅读2分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

这是我写的第三篇了,这一篇来写Array的相关扩展方法,今天主要来写如何在数组中寻找元素、去重(uniq)和扁平化(flatten),对应 underscore 中的 findIndex**, findLastIndexindexOf**, lastIndexOf 以及sortIndex 等方法。

findIndex和findLastIndex

findIndex* 的作用就是从一个数组中找到第一个满足某个条件的元素,*findLastIndex 则是找到最后一个(或者说倒序查找)。当我们来到源码中,可以看到是这样的。

// Returns the first index on an array-like that passes a truth test.
var findIndex = createPredicateIndexFinder(1);
  
// Returns the last index on an array-like that passes a truth test.
var findLastIndex = createPredicateIndexFinder(-1);

我们发现两者都用了createPredicateIndexFinder函数,通过传入1-1来返回不同的返回方法。直接看源码,注释已经写的非常清楚了。

// Internal function to generate `_.findIndex` and `_.findLastIndex`.
// dir === 1 => 从前往后找 dir === -1 => 从后往前找
function createPredicateIndexFinder(dir) {
    // 返回一个函数,该函数接受三个参数,数组、筛选函数、上下文
    return function (array, predicate, context) {
        // cb 函数先不说,就是处理传入的 predicate,根据 predicate的值 返回不同的处理函数
        predicate = cb(predicate, context);
        // 获取传入数组的长度
        var length = getLength(array);
        var index = dir > 0 ? 0 : length - 1; // 初始化 index,根据 dir 不同的值,确定起始位置
        // index >= 0 && index < length 组合判断,适用两种情况
        for (; index >= 0 && index < length; index += dir) {
            // 调用 predicate 函数,返回值为真时(满足用户的条件),立即返回 当前索引(index)
            if (predicate(array[index], index, array)) return index;
        }
        // 没找到就返回 -1
        return -1;
    };
}

sortIndex

sortIndex 方法,这个方法无论使用还是实现都非常的简单。如果往一个有序数组中插入元素,使得数组继续保持有序,那么这个插入位置是?这就是这个方法的作用,有序,很显然用二分查找即可。我们先来看看它所要传入的参数_.sortedIndex(array, value, [iteratee], [context]),然后直接看源码

// Use a comparator function to figure out the smallest index at which
// an object should be inserted so as to maintain order. Uses binary search.

// _.sortedIndex([10, 20, 30, 40, 50], 35); => 3
// var stooges = [{name: 'moe', age: 40}, {name: 'curly', age: 60}];
// _.sortedIndex(stooges, {name: 'larry', age: 50}, 'age');
// => 1
function sortedIndex(array, obj, iteratee, context) {
    // 处理传入的 iteratee 函数
    // 一般我们没传,采用内部的 identity 函数,该函数就是传入什么值,返回什么值
    iteratee = cb(iteratee, context, 1);
    var value = iteratee(obj); //   // 经过迭代函数计算出 将要插入的值
    var low = 0, high = getLength(array);
    while (low < high) {
        var mid = Math.floor((low + high) / 2);
        if (iteratee(array[mid]) < value) low = mid + 1; else high = mid;
    }
    return low;
}

indexOf和lastIndexOf

接下来看看indexOflastIndexOf两个方法,indexOf作用是返回 value 在该 array 中的索引值,如果 value 不存在 array 中就返回-1。其需要参数是_.indexOf(array, value, [isSorted])lastIndexOf作用是返回 value 在该 array 中的从最后开始的索引值,如果 value 不存在 array 中就返回-1。其需要参数是_.lastIndexOf(array, value, [fromIndex])

在ES6中引入了这两个方法,所以其实这个实现就可以当作是Polyfill了,那么我们该如何实现呢?

// Return the position of the first occurrence of an item in an array,
// or -1 if the item is not included in the array.
// If the array is large and already in sort order, pass `true`
// for **isSorted** to use binary search.
// _.indexOf(array, value, [isSorted])
// 找到数组 array 中 value 第一次出现的位置
// 并返回其下标值
// 如果数组有序,则第三个参数可以传入 true
// 这样算法效率会更高(二分查找)
// [isSorted] 参数表示数组是否有序
// 同时第三个参数也可以表示 [fromIndex] (见下面的 _.lastIndexOf)
var indexOf = createIndexFinder(1, findIndex, sortedIndex);

// Return the position of the last occurrence of an item in an array,
// or -1 if the item is not included in the array.
// 和 _indexOf 相似
// 反序查找
// _.lastIndexOf(array, value, [fromIndex])
// [fromIndex] 参数表示从倒数第几个开始往前找
var lastIndexOf = createIndexFinder(-1, findLastIndex);

是不是似曾相识?对,和上面两个一样但是是使用了createIndexFinder函数,那么我们明白了这个函数怎么实现的就是知道了indexOflastIndexOf怎么做的,啊哈哈哈真聪明。

// Internal function to generate the `_.indexOf` and `_.lastIndexOf` functions.
function createIndexFinder(dir, predicateFind, sortedIndex) {
    // 返回一个函数,该函数接受三个参数,数组、将要查找的那个值、从给定索引开始搜索或是否有序
    return function (array, item, idx) {
        var i = 0, length = getLength(array);
        // 给定索引 查找,不能使用二分查找优化
        if (typeof idx == 'number') {
            if (dir > 0) {
                i = idx >= 0 ? idx : Math.max(idx + length, i);
            } else {
                length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;
            }
        } else if (sortedIndex && idx && length) {
            // indexOf 支持 数组是否有序
            // 如果正好插入的位置的值和 item 刚好相等 ,说明该位置就是 item 第一次出现的位置,
            // 返回下标,否则即是没找到,返回 -1
            idx = sortedIndex(array, item);
            return array[idx] === item ? idx : -1;
        }
        // 如果要查找的元素是 NaN 类型
        if (item !== item) {
            // findIndex、findLastIndex 进行查找
            idx = predicateFind(slice.call(array, i, length), isNaN$1);
            return idx >= 0 ? idx + i : -1;
        }
        for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {
            if (array[idx] === item) return idx;
        }
        return -1;
    };
}

range

range函数是一个用来创建整数灵活编号的列表的函数,_.range([start], stop, [step])(左闭右开)使用方法见这:www.underscorejs.com.cn/range。实现起来也是比较容易的。

// Generate an integer Array containing an arithmetic progression. A port of
// the native Python `range()` function. See
// [the Python documentation](https://docs.python.org/library/functions.html#range).
function range(start, stop, step) {
    // stop 不存在(就传入了一个参数),就把 start 赋值给 stop,然后 start = 0
    if (stop == null) {
        stop = start || 0;
        start = 0;
    }
    // 没有指定间隔,那么 默认是 1 或 -1
    if (!step) {
        step = stop < start ? -1 : 1;
    }

    // 向上取整, 因为包含 起始值start
    var length = Math.max(Math.ceil((stop - start) / step), 0);
    var range = Array(length);

    for (var idx = 0; idx < length; idx++, start += step) {
        range[idx] = start;
    }

    return range;
}

unique

_.uniq(array, [isSorted], [iteratee])返回 array 去重后的副本

// Produce a duplicate-free version of the array. If the array has already
// been sorted, you have the option of using a faster algorithm.
// The faster algorithm will not work with an iteratee if the iteratee
// is not a one-to-one function, so providing an iteratee will disable
// the faster algorithm.
// 数组去重,如果第二个参数 `isSorted` 为 true, 则说明事先已经知道数组有序
// 程序会跑一个更快的算法(一次线性比较,元素和数组前一个元素比较即可)
// 如果有第三个参数 iteratee,则对数组每个元素迭代,对迭代之后的结果进行去重
// 返回去重后的数组(array 的子数组)
// _.uniq(array, [isSorted], [iteratee])
function uniq(array, isSorted, iteratee, context) {
    // isSorted 不是 boolean 值的时候
    if (!isBoolean(isSorted)) {
        context = iteratee;
        iteratee = isSorted;
        isSorted = false;
    }
    // 存在迭代函数,就对这个 迭代函数 进行处理返回正确的迭代函数
    if (iteratee != null) iteratee = cb(iteratee, context);
    var result = [];
    var seen = [];
    for (var i = 0, length = getLength(array); i < length; i++) {
        var value = array[i], // 当前值
            // 如果指定了迭代函数,则对数组每一个元素进行迭代
            // 根据 当前值 计算出来的值,要是对象的时候可能是根据某个属性值去重,那么这 computed 就是 属性值
            computed = iteratee ? iteratee(value, i, array) : value;
        // 是有序的并且没有指定迭代函数
        if (isSorted && !iteratee) {
            // i===0 或者 相邻两个不相等的时候(有序的,相邻不相等说明不是重复的),第一个直接填加进去
            if (!i || seen !== computed) result.push(value);
            seen = computed; // 保存前一个值
        } else if (iteratee) {
            // 存在迭代函数,判断 seen 是否有该值,没有就添加 ==》 seen 保存的是 属性值
            if (!contains(seen, computed)) {
                seen.push(computed);
                result.push(value);
            }
        } else if (!contains(result, value)) {
            // 默认情况 result 中没有 value,就添加
            result.push(value);
        }
    }
    return result;
}

这是underscore实现的去重方法,对象也可以用,如果说就单纯数组去重的的话还有好几种思路大家可以想一想。

flatten

_.flatten(array, [shallow])将一个嵌套多层的数组 array(数组) (嵌套可以是任何层数)转换为只有一层的数组。 如果你传递 shallow(boolean、number) 参数,数组将只减少一维或指定维数的嵌套。

// _.flatten([1, [2], [3, [[4]]]]); => [1, 2, 3, 4];
// _.flatten([1, [2], [3, [[4]]]], true);  => [1, 2, 3, [[4]]];

// Flatten out an array, either recursively (by default), or up to `depth`.
// Passing `true` or `false` as `depth` means `1` or `Infinity`, respectively.
function flatten(array, depth) {
  return flatten$1(array, depth, false);
}

这里又引入了一个重要函数flatten$1,还有其他函数也用到了,我们就直接看代码注释吧

// Internal implementation of a recursive `flatten` function.
// flatten(array, depth) 使用 flatten$1(array, depth, false)
/**
 *  strict: 能过滤 input 参数元素中的非数组元素
    var ans = flatten([5, 6, [1, 2], [3, 4]], true, true);
    console.log(ans); // => [1, 2, 3, 4]
 */
function flatten$1(input, depth, strict, output) {
    output = output || [];
    // false 或者不传 的时候,全部展开    // '' null undefined NaN 
    if (!depth && depth !== 0) {
        depth = Infinity;
    } else if (depth <= 0) {
        // 不展开,直接返回
        return output.concat(input);
    }
    // 从哪往后加,因为展开后 output.length > input.length, 递归过程中添加到 output 的哪个位置
    var idx = output.length;
    // 递归遍历 input
    for (var i = 0, length = getLength(input); i < length; i++) {
        var value = input[i]; // 每一项
        if (isArrayLike(value) && (isArray(value) || isArguments$1(value))) {
            // Flatten current level of array or arguments object.
            if (depth > 1) {
                flatten$1(value, depth - 1, strict, output);
                idx = output.length;
            } else {
                // depth === 1 或者 depth === true 一层的时候,直接把 value 展开
                var j = 0, len = value.length;
                while (j < len) output[idx++] = value[j++];
            }
        } else if (!strict) {
            /**
             * 进入该分支的条件: value 不是数组,并且 strict === false 的时候 ===> flatten传入的就是false
             * 不是数组,就直接把这一项的值添加进 output
             */
            output[idx++] = value;
        }
    }
    return output;
}

union

_.union(*arrays)返回传入的 **arrays(数组)**并集:按顺序返回,返回数组的元素是唯一的,可以传入一个或多个 arrays (数组)。

// _.union([1, 2, 3], [101, 2, 1, 10], [2, 1]); => [1, 2, 3, 101, 10]

// Produce an array that contains the union: each distinct element from all of
// the passed-in arrays.
var union = restArguments(function(arrays) {
  // 只展开一层,然后去重
  return uniq(flatten$1(arrays, true, true));
});

Array的扩展方法我就介绍到这里了,还有一些其他的方法都比较简单,大家可以直接去看看源代码,都有注释。