Skip to content

underscore 中的三大重要方法 #6

@ilcherry

Description

@ilcherry

概览

optimizeCb、cb 和 restArguments 三个方法是 Underscore.js 中重要的三个内部函数。

optimizeCb

由一道题引发的思考。

const subjects = ["math", "biology", "English"];

const result = subjects.map(
  function(subject, index) {
    return this[index];
  },
  [1, 2, 3]
);

console.log(result); // [1, 2, 3]

查看 MDN 中 map 的用法可以得知, map(callback(currentValue[, index[, array]]) [, thisArg])
thisArg 是执行 callback 函数时 this 的指向。在 Underscore.js 里,库的创作者也要考虑
这种情况。

// underscore.js  v1.9.2

var optimizeCb = function(func, context, argCount) {
  /* 
    绝大多数情况下,不传 context, 直接返回原原本本传入的函数。
    不会走下面的 switch 流控制语句

  */
  if (context === void 0) return func;
  /* 
    当 context 不为 undefined 时
  */
  switch (argCount == null ? 3 : argCount) {
    /* 
      用在 _.times 方法中
      _.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;
      };
      只有传入的第一个参数有用
    */
    case 1:
      return function(value) {
        return func.call(context, value);
      };
    // The 2-argument case is omitted because we’re not using it.

    /* 
      一般都是这种情况,像 map、filter等等
    
    */
    case 3:
      return function(value, index, collection) {
        return func.call(context, value, index, collection);
      };
    /* 
      
        用在 reduce 方法里
      */
    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 参数为函数类型的情况。

Underscore.js 里对 context 分了五种情况处理

  • 没有传入 context 时,直接返回原原本本传入的函数;
  • argCount 为 1 时,改变方法的上下文,只接受传入的第一个参数,只为 _.times 方法服务
  • 不传 argCount,改变方法的上下文,接受三个参数,用于 map、filter等方法
  • argCount 为 4 时,改变方法的上下文,接受三个参数,用于 reduce 等方法
  • 其他情况,单纯改变方法的上下文

cb

cb 函数与 optimizeCb 都起优化传入的方法的作用。
optimizeCb 函数主要优化传入的方法,并绑定方法的执行上下文;
cb 函数负责处理传入的值所有情况。

// underscore.js  v1.9.2

_.map = _.collect = function(obj, iteratee, context) {
  /*
    map 方法首次用到了 cb 这个内部函数
  */
  iteratee = cb(iteratee, context);
  var keys = !isArrayLike(obj) && _.keys(obj),
    length = (keys || obj).length,
    results = Array(length);
  /* 
    此处的 for 循环是 _.map 方法 核心
    为 Array、ArrayLike, 那么 result = array[index]
    为 Object, 那么 result = object[key]
  */
  for (var index = 0; index < length; index++) {
    var currentKey = keys ? keys[index] : index;
    results[index] = iteratee(obj[currentKey], currentKey, obj);
  }
  return results;
};

仅仅看这行代码,我们不妨猜测 cb 函数的作用:

iteratee = cb(iteratee, context);

cb 函数对传入的 iteratee 进行处理,最后将返回的结果赋值给 iteratee 变量。

在 JavaScript 中, 一个变量的值可能是下面八种数据类型。

Number String Boolean undefined null BigInt Symbol Object

我们来看看 cb 函数的源码。

// underscore.js  v1.9.2

var cb = function(value, context, argCount) {
  if (_.iteratee !== builtinIteratee) return _.iteratee(value, context);
  /* 
    _.identity = value => value
    若 value 为 null、undefined, 则返回一个函数
  */
  if (value == null) return _.identity;
  /* 
    通常情况下,如果 value 是函数类型,则丢给 optimizeCb 方法专门处理
  */
  if (_.isFunction(value)) return optimizeCb(value, context, argCount);
  /* 
    若 value 是 Object 类型而不是 Array 类型
  */
  if (_.isObject(value) && !_.isArray(value)) return _.matcher(value);
  /* 
    如果传入的是字符串或数组
    返回对象的属性值

   const books = [{name: 'qin'}, {name: 'xiao'}];

   console.log(_.map(books, "name")); ['qin', 'xiao']
  */
  return _.property(value);
};

cb 方法用来优化传入的迭代、断言等函数。对于有的方法,比如 _.each 只能传递一个函数,
而 _.map、_.filter 等方法除了传递函数外,还可以传递字符串或数组参数。

restArguments

// underscore.js  v1.9.2

var restArguments = function(func, startIndex) {
  /* 
    func.length 即为函数参数的个数
  */
  startIndex = startIndex == null ? func.length - 1 : +startIndex;
  /* 
    返回一个函数
  */
  return function() {
    var length = Math.max(arguments.length - startIndex, 0),
      /* 
      剩余参数组成一个数组
    */
      rest = Array(length),
      index = 0;
    for (; index < length; index++) {
      rest[index] = arguments[index + startIndex];
    }
    switch (startIndex) {
      case 0:
        return func.call(this, rest);
      case 1:
        return func.call(this, arguments[0], rest);
      case 2:
        return func.call(this, arguments[0], arguments[1], rest);
    }

    var args = Array(startIndex + 1);
    for (index = 0; index < startIndex; index++) {
      args[index] = arguments[index];
    }
    args[startIndex] = rest;
    return func.apply(this, args);
  };
};

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions