迭代!迭代!迭代

使用迭代,而不是循环

在函数式编程,更推荐使用 迭代

var results = _.map([1,2,3],function(elem){
  return elem*2;
}); // => [2,4,6]

而不是 循环

var results = [];
var elems = [1,2,3];
for(var i=0,length=elems.length;i<length;i++) {
  results.push(elems[i]*2);
} // => [2,4,6]

iteratee

对于一个迭代来说,他至少由如下两个部分构成:

  • 被迭代集合
  • 当前迭代过程

在 underscore 中,当前迭代过程是一个函数,他被称为 iteratee(直译为被迭代者),他将对当前的迭代元素进行处理。我们看到 _.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 的第二个参数就是一个 iteratee,他可能是函数,对象,甚至是字符串,underscore 会将其统一处理为一个函数。这个处理由 underscore 的内置函数 cb 来完成。下面我们看一下 cb 的实现:

var cb = function (value, context, argCount) {
    // 是否用自定义的iteratee
    if (_.iteratee !== builtinIteratee) return _.iteratee(value, context);
    // 针对不同的情况
    if (value == null) return _.identity;
    if (_.isFunction(value)) return optimizeCb(value, context, argCount);
    if (_.isObject(value)) return _.matcher(value);
    return _.property(value);
};

cb 将根据不同情况来为我们的迭代创建一个迭代过程 iteratee,服务于每轮迭代:

  • valuenull

如果传入的 valuenull,亦即没有传入 iteratee,则 iteratee 的行为只是返回当前迭代元素自身,比如:

var results = _.map([1,2,3]); // => results:[1,2,3]
  • value 为一个函数

如果传入 value 是一个函数,那么通过内置函数 optimizeCb 对其进行优化,optimizeCb 的作用放到之后讲,先来看个传入函数的例子:

var results = _.map([1,2,3], function(value,index,obj) {
  return '['+obj+']' + '\'s '+index+' position is '+value;
});
// => results:  [
//  "[1,2,3]'s 0 position is 1",
//  "[1,2,3]'s 1 position is 2",
//  "[1,2,3]'s 2 position is 3"
// ]
  • value 为一个对象

如果 value 传入的是一个对象,那么返回的 iteratee_.matcher)的目的是想要知道当前被迭代元素是否匹配给定的这个对象:

var results = _.map([{name:'yoyoyohamapi'},{name: 'wxj',age:13}], {name: 'wxj'});
// => results: [false,true]
  • value 是字面量,如数字,字符串等

如果以上情况都不是, 那么传入的 value 会是一个字面量(直接量),他指示了一个对象的属性 key,返回的 iteratee_.property)将用来获得该属性对应的值:

var results = _.map([{name:'yoyoyohamapi'},{name:'wxj'}],'name');
// => results: ['yoyoyohamapi', 'wxj'];

自定义 iteratee

cb 函数的代码中,我们也发现了 underscore 支持通过覆盖其提供的 _.iteratee 函数来自定义 iteratee,更确切的说,来自己决定如何产生一个 iteratee

var cb = function (value, context, argCount) {
  // ...
  if (_.iteratee !== builtinIteratee) return _.iteratee(value, context);
  // ...
}

我们看一下 iteratee 函数的实现:

_.iteratee = builtinIteratee = function (value, context) {
    return cb(value, context, Infinity);
};

默认的 _.iteratee 函数仍然是把生产 iteratee 的工作交给 cb 完成,并且通过变量 buildIteratee 保存了默认产生器的引用,方便之后我们覆盖了 _.iteratee 后,underscore 能够通过比较 _.iterateebuildIteratee 来知悉这次覆盖(也就知悉了用户想要自定义 iteratee 的生产过程)。

比如当传入的 value 是对象时,我们不想返回一个 _.matcher 来判断当前对象是否满足条件,而是返回当前元素自身(虽然这么做很无聊),就可以这么做:

_.iteratee = function(value, context) {
  // 现在,value为对象时,也是返回自身  
  if (value == null || _.isObject(value)) return _.identity;
  if (_.isFunction(value)) return optimizeCb(value, context, argCount);
  return _.property(value);
}

现在运行之前的例子,看一下有什么不同:

var results = _.map([{name:'yoyoyohamapi'},{name: 'wxj',age:13}], {name: 'wxj'});
// => results: [{name:'yoyoyohamapi'}, {name: 'wxj', age```:13}];

重置默认的_.iteratee改变迭代过程中的行为只在underscore最新的master分支支持, 发布版的1.8.3并不支持, 我们可以看到发布版的1.8.3中的cb代码如下,并没有判断_.iteratee是否被覆盖:

var cb = function (value, context, argCount) {
    if (value == null) return _.identity;
    if (_.isFunction(value)) return optimizeCb(value, context, argCount);
    if (_.isObject(value)) return _.matcher(value);
    return _.property(value);
};

optimizeCb

在上面的分析中,我们知道,我们知道,当传入的 value 是一个函数时,value 还要经过一个叫 optimizeCb 的内置函数才能获得最终的 iteratee

var cb = function (value, context, argCount) {
  // ...
  if (_.isFunction(value)) return optimizeCb(value, context, argCount);
  // ...
};

顾名思义, optimizeCb有优化回调的意思,所以他是一个对最终返回的iteratee进行优化的过程,我们看到他的源码:

/** 优化回调(特指函数中传入的回调)
 *
 * @param func 待优化回调函数
 * @param context 执行上下文
 * @param argCount 参数个数
 * @returns {function}
 */
var optimizeCb = function(func, context, argCount) {
  // 一定要保证回调的执行上下文存在
  if (context === void 0) return func;
  switch (argCount == null ? 3 : argCount) {
    case 1: return function(value) {
      return func.call(context, value);
    };
    case 2: return function(value, other) {
      return func.call(context, value, other);
    };
    case 3: return function(value, index, collection) {
      return func.call(context, value, index, collection);
    };
    case 4: return function(accumulator, value, index, collection) {
      return func.call(context, accumulator, value, index, collection);
    };
  }
  return function() {
    return func.apply(context, arguments);
  };
};

optimizeCb 的总体思路就是:传入待优化的回调函数 func,以及迭代回调需要的参数个数 argCount,根据参数个数分情况进行优化。

  • argCount == 1,即 iteratee 只需要 1 个参数

在 underscore 的 _.times 函数的实现中,_.times 的作用是执行一个传入的 iteratee 函数 n 次,并返回由每次执行结果组成的数组。它的迭代过程 iteratee 只需要 1 个参数 -- 当前迭代的索引:

// 执行 iteratee 函数 n 次,返回每次执行结果构成的数组
_.times = function (n, iteratee, context) {
    var accum = Array(Math.max(0, n));
    iteratee = optimizeCb(iteratee, context, 1);
    for (var i = 0; i < n; i++) accum[i] = iteratee(i);
    return accum;
};

看一个 _.times 的使用例子:

function getIndex(index) {
  return index;
}
var results = _.times(3, getIndex); // => [0,1,2]
  • argCount == 2,即 iteratee 需要 2 个参数

该情况在 underscore 没用使用,所以最新的 master 分支已经不再考虑这个参数个数为 2 的情况。

  • argCount == 3(默认),即 iteratee 需要 3 个参数

这 3 个参数是:

  • value:当前迭代元素的值
  • index:迭代索引
  • collection:被迭代集合

_.map, _.each, _.filter 等函数中,都是给 argCount 赋值了 3

_.each([1,2,3], function(value,index,collection) {
  console.log("被迭代的集合:"+collection+";迭代索引:"+index+";当前迭代的元素值:"+value);
});
// =>
// 被迭代的集合:1,2,3;迭代索引:0;当前迭代的元素值:1
// 被迭代的集合:1,2,3;迭代索引:1;当前迭代的元素值:2
// 被迭代的集合:1,2,3;迭代索引:2;当前迭代的元素值:3
  • argCount == 4,即 iteratee 需要 4 个参数

这 4 个参数分别是:

  • accumulator:累加器
  • value:迭代元素
  • index:迭代索引
  • collection:当前迭代集合

那么这个累加器是什么意思呢?在 underscore 中的内部函数 createReducer 中,就涉及到了 4 个参数的情况。该函数用来生成 reduce 函数的工厂,underscore 中的 _.reduce_.reduceRight 都是由它创建的:

/**
 * reduce 函数的工厂函数, 用于生成一个 reducer, 通过参数决定 reduce 的方向
 * @param dir 方向 left or right
 * @returns {function}
 */
var createReduce = function (dir) {
    var reducer = function (obj, iteratee, memo, initial) {
        var keys = !isArrayLike(obj) && _.keys(obj),
            length = (keys || obj).length,
            index = dir > 0 ? 0 : length - 1;
        // memo 用来记录最新的 reduce 结果
        // 如果 reduce 没有初始化 memo, 则默认为首个元素 (从左开始则为第一个元素, 从右则为最后一个元素)
        if (!initial) {
            memo = obj[keys ? keys[index] : index];
            index += dir;
        }
        for (; index>= 0 && index < length; index += dir) {
            var currentKey = keys ? keys[index] : index;
            // 执行 reduce 回调, 刷新当前值
            memo = iteratee(memo, obj[currentKey], currentKey, obj);
        }
        return memo;
    };

    return function (obj, iteratee, memo, context) {
        // 如果参数正常, 则代表已经初始化了 memo
        var initial = arguments.length >= 3;
        // reducer 因为引入了累加器, 所以优化函数的第三个参数传入了 4,
        // 这样, 新的迭代回调第一个参数就是当前的累加结果
        return reducer(obj, optimizeCb(iteratee, context, 4), memo, initial);
    };
};

我们可以看到,createReduce 最终创建的 reducer 就是需要一个累加器,该累加器需要被初始化,看一个利用 _.reduce 函数求和的例子:

var sum = _.reduce([1,2,3,4,5], function(accumulator, value, index, collection){
  return accumulator+value;
},0); // => 15;

results matching ""

    No results matching ""