Underscore 源码学习 (二) - 迭代

Underscore 源码的学习落下了好几天,因为前几天一直正在重构项目和搞 React,不过这几天应该会花较多时间在 Underscore 上面了。
这次主要说下 Underscore 两个比较重要的函数吧,一个是optimizeCb,另一个是cb,这两个花了我挺长时间看的,而且是整个 Underscore 非常重要的函数,后面很多地方都使用到了它。

optimizeCb 函数

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 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);
    };
};

这个地方 switch 只是一个性能的优化,其实简化来看就是这样的

var optimizeCb = function(func, context, argCount) {
    if (context === void 0) return func;
    return function() {
      return func.apply(context, arguments);
    };
};

之所以有那段 switch 前面一篇已经有提到了,只是一个优化而已。使用 call 快于 apply。不过好像最新的 Chrome 已经可以自己优化这个过程,但为了提升性能,加上也无妨。
解释下段代码的意思,字如起名 optimizeCb 优化回调。这个函数传入三个参数依次是函数,上下文,参数个数。如果没有指定上下文则返回函数本身,如果有,则对该上下文绑定到传入的函数,根据传入的参数个数,在做一个性能优化。这个函数就是这个意思。我们看下他的使用。

  _.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 {
      var keys = _.keys(obj);
      for (i = 0, length = keys.length; i < length; i++) {
        iteratee(obj[keys[i]], keys[i], obj);
      }
    }
    return obj;
  };

这个函数是用来实现数组或者对象的遍历的,他是怎么做到呢?
首先是

iteratee = optimizeCb(iteratee, context);

这个是优化 iteratee 这个函数,如果指定了上下文(context)则做绑定。一开始没理解 iteratee 这东西,其实他就是一个函数而已,比如

var stu = {
  'age': 20,
  'school': 'SCNU',
  'sex': 'male'
};
_.each(stu, function(value, key, obj) {
    console.log(key + ' : ' + value); 
});
// console
age : 20
school : SCNU
sex : male

我们传入了一个函数,这个函数可以有三个回调参数分别是 value, key, obj 分别表示键值,键名,迭代对象。
重新看回 each 这个函数,isArrayLike 函数判断 obj 是不是数组,如果是的话,一个循环分别把 obj[i], i, obj 分别传入这个 iteratee 这个传入来的函数,比如上面的 function(value, key, obj){} 里面,一一对应到 value, key, obj。从而实现迭代。在下面的是对象的处理,没什么好说的。
然后我们就讲完了 optimizeCb 这个函数了,其实也挺好理解的。

cb 函数

var cb = function(value, context, argCount) {
    // 如果改变了iteratee的行为,则返回自定义的iteratee
    if (_.iteratee !== builtinIteratee) return _.iteratee(value, context);
    // 没有传入value,返回当前迭代元素自身,比如var results = _.map([1,2,3]) => results: [1,2,3]
    if (value == null) return _.identity;
    // 是函数返回优化回调函数,比如var results = _.map([1,2,3], function(value, index, obj) {...})
    if (_.isFunction(value)) return optimizeCb(value, context, argCount);
    // 是对象返回一个能判断对象是否相等的函数,比如
    // var results = _.map([{name:'qq'},{name:'w',age:13}], {name:'w'}) => results: [false, true]
    if (_.isObject(value)) return _.matcher(value);
    // 返回获取对象属性的函数,比如
    // var results = _.map([{name: 'qq'}, {name: 'ww'}], 'name') => results: ['qq', 'ww']
return _.property(value);
};

首先是 buildinIteratee 这东西,这要结合下面这个来看

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

这个是给用户自定义迭代规则用的。怎么自定义呢,比如这样

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

还有 identity 其实就这样,返回一个返回自身的函数...

// Keep the identity function around for default iteratees.
_.identity = function(value) {
return value;
};

我们自定义的这个迭代规则,如果 value 不为空而且是对象,则返回一个可以返回自身的函数。注意我们改变的只是 iteratee 函数,builtinIteratee 存的是默认规则,在 cb 函数中如果发现 iteratee 的行为更改了,则使用更改的行为来处理,否则往下默认处理,上面已经备注的很清楚了,自己看吧。
我们举个例子说下 cb 函数的用法,例如

_.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 的,比如

var results = _.map([1, 2, 3], function(value, index, obj) {
  return '['+obj+']' + '\'s '+index+' position is '+value;
});

我们使用 map 传入两个参数,一个是迭代对象,这里是 [1, 2, 3],第二个参数是迭代函数,这里是 function (value, index, obj){...}。这个函数在 map 内部也就是 iteratee,然后我们再来看

iteratee = cb(iteratee, context)

iteratee 是一个函数,使用上面这个句子返回了 optimizeCb(value, context, argCount),这里的 value 就对应了我们的 function(value, index, obj){...} 函数。接着回到 map,他对对象进行遍历依次把通过调用 iteratee 也就是我们传入的函数得到的结果复制给 result,最后返回了 result
所以上面例子的结果是

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"
];

一定要理解 cb 和 optimizeCb 这两个的用法,他们在后面多次用到。好了,完了~