在javascript的数组原型对象上,有forEachmapreducereduceRight这几个方法,underscore将这几个方法拓展到了对象上。

each

如果你试图polyfill过数组原型对象上的forEach方法,将其拓展到对象上也没什么特别的难度,无非是索引从数字索引变成了对象的键。

_.each = _.forEach = function(obj, iteratee, context) {
    iteratee = optimizeCb(iteratee, context);
    var i, length;
    if (isArrayLike(obj)) {
        // 数组、类数组,按照数字下标遍历
        for (i = 0, length = obj.length; i < length; i++) {
            iteratee(obj[i], i, obj);
        }
    } else {
        // 对象,按照Object.keys返回的键的顺序进行遍历
        var keys = _.keys(obj);
        for (i = 0, length = keys.length; i < length; i++) {
            iteratee(obj[keys[i]], keys[i], obj);
        }
    }
    return obj;
};

underscore默认each方法传入的iteratee是一个函数形式,毕竟each方法仅仅负责遍历而已,其核心业务逻辑在于iteratee函数内部,而不需要考虑每一次迭代的返回值。

map

map方法是根据原数组将其映射为一个新数组,它关注的是每一次迭代的返回值。

_.map = _.collect = function(obj, iteratee, context) {
    iteratee = cb(iteratee, context);
    var keys = !isArrayLike(obj) && _.keys(obj),
        length = (keys || obj).length,
        results = Array(length);
    for (var index = 0; index < length; index++) {
        var currentKey = keys ? keys[index] : index;
        results[index] = iteratee(obj[currentKey], currentKey, obj);
    }
    return results;
};

map方法从实现上和each方法非常类似,它比each多做的是把每一次迭代的结果按序放入到了一个数组中。

我很好奇的一点是,为什么each方法不安map方法的格式来写?写成这样:

_.each = function(obj, iteratee, context) {
    iteratee = optimizeCb(iteratee, context);
    var keys = !isArrayLike(obj) && _.keys(obj),
        length = (keys || obj).length,
    for (var index = 0; index < length; index++) {
        var currentKey = keys ? keys[index] : index;
        iteratee(obj[currentKey], currentKey, obj);
    }
    return obj;
};

或许是考虑到了每次判断是否有keys对性能的损耗吧,那既然如此map方法为什么不按照each方法的格式来写呢?

reduce reduceRight

有了上面的示例,其实实现reduce或者reduceRight没有什么技术上的问题,underscore高明在没有去生写这两个方法,而是抽象出这两个方法的共同点和差异点,利用一个高阶函数返回这两个具体的方法。

function createReduce(dir) {
    function iterator(obj, iteratee, memo, keys, index, length) {
        // 按序遍历规约处理
        for (; index >= 0 && index < length; index += dir) {
            // 针对数组和对象确定不同的索引
            var currentKey = keys ? keys[index] : index;
            memo = iteratee(memo, obj[currentKey], currentKey, obj);
        }
        return memo;
    }

    return function(obj, iteratee, memo, context) {
        iteratee = optimizeCb(iteratee, context, 4);
        var keys = !isArrayLike(obj) && _.keys(obj),
        length = (keys || obj).length,
        index = dir > 0 ? 0 : length - 1;
        // 默认起始规约值是第一个要迭代的元素
        if (arguments.length < 3) {
            memo = obj[keys ? keys[index] : index];
            index += dir;
        }
        return iterator(obj, iteratee, memo, keys, index, length);
    };
}

reduce和reduceRight方法唯一的差别是迭代处理的方向,分别用1和-1表示,通过这种表示方法我们也可以轻松获得下一个要迭代元素的索引。

invoke pluck

这两个方法其实是对map方法的运用。

// method可以是具体的方法,也可以是方法名
_.invoke = function(obj, method) {
    var args = slice.call(arguments, 2);
    var isFunc = _.isFunction(method);
    return _.map(obj, function(value) {
        // 如果传入的是方法名,会调用被迭代元素的对应方法
        var func = isFunc ? method : value[method];
        return func == null ? func : func.apply(value, args);
    });
};

_.pluck = function(obj, key) {
    return _.map(obj, _.property(key));
};

invoke获取的新数组,是由旧数组每个元素经过传入方法处理构成的。pluck获得的新数组,是由旧数组每一项特定属性构成的。

results matching ""

    No results matching ""