lessfish/underscore-analysis

内部方法 createAssigner 详解

lessfish opened this issue · 8 comments

Why underscore

最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了我的 2016 计划中。

阅读一些著名框架类库的源码,就好像和一个个大师对话,你会学到很多。为什么是 underscore?最主要的原因是 underscore 简短精悍(约 1.5k 行),封装了 100 多个有用的方法,耦合度低,非常适合逐个方法阅读,适合楼主这样的 JavaScript 初学者。从中,你不仅可以学到用 void 0 代替 undefined 避免 undefined 被重写等一些小技巧 ,也可以学到变量类型判断、函数节流&函数去抖等常用的方法,还可以学到很多浏览器兼容的 hack,更可以学到作者的整体设计思路以及 API 设计的原理(向后兼容)。

之后楼主会写一系列的文章跟大家分享在源码阅读中学习到的知识。

欢迎围观~ (如果有兴趣,欢迎 star & watch~)您的关注是楼主继续写作的动力

createAssigner

(PS:本文有点水,没多少干货,主要想跟大家分享下这里的闭包)

今天要跟大家聊的是 underscore 源码中一个重要的内部方法,createAssigner。

这个方法是用来干嘛的呢?该方法涉及的 api 包括 _.extend & _.extendOwn & _.defaults,那么这三个 api 又是用来干嘛的呢?ok,先简单介绍下这三个 api 的用处。

首先思考这样一个场景,有 a,b 两个对象,将 b 所有的键值对都添加到 a 上面去,返回 a,如何写这个方法?恩,应该不难,刷刷刷写下如下代码:

function extend(a, b) {
  for (var key in b)
    a[key] = b[key];

  return a;
}

我擦,这么少,这是真的吗?是真的,除了没有兼容 IE < 9 下某些 key 不能被 for ... in 枚举到的 bug(这个可以参考 前文)。你不禁开始怀疑人生,讲这个有毛的意义?

事实上,_.extend 大概就是用来干上面 extend 函数的事情的;而 _.extendOwn 则只会取 b 对象的 own properties,大同小异; _.defaults 呢?跟 _.extend 类似,但是如果 key 相同,后面的不会覆盖前面的,取第一次出现某 key 的 value,为 key-value 键值对。除此之外,三个方法都能接受 >= 1 个参数,以 .extend 为例,.extend(a, b, c) 将会将 b,c 两个对象的键值对分别覆盖到 a 上。

那么问题来了,如何设计这三个用途相似的 api?我们来看看 underscore 是怎么做的。

_.extend = createAssigner(_.allKeys);
_.extendOwn = createAssigner(_.keys);
_.defaults = createAssigner(_.allKeys, true);

我们完整地看下带注释的 createAssigner 函数:

// An internal function for creating assigner functions.
// 有三个方法用到了这个内部函数
// _.extend & _.extendOwn & _.defaults
// _.extend = createAssigner(_.allKeys);
// _.extendOwn = _.assign = createAssigner(_.keys);
// _.defaults = createAssigner(_.allKeys, true);
var createAssigner = function(keysFunc, undefinedOnly) {
  // 返回函数
  // 经典闭包(undefinedOnly 参数在返回的函数中被引用)
  // 返回的函数参数个数 >= 1
  // 将第二个开始的对象参数的键值对 "继承" 给第一个参数
  return function(obj) {
    var length = arguments.length;
    // 只传入了一个参数(或者 0 个?)
    // 或者传入的第一个参数是 null
    if (length < 2 || obj == null) return obj;

    // 枚举第一个参数除外的对象参数
    // 即 arguments[1], arguments[2] ...
    for (var index = 1; index < length; index++) {
      // source 即为对象参数
      var source = arguments[index],
          // 提取对象参数的 keys 值
          // keysFunc 参数表示 _.keys 
          // 或者 _.allKeys
          keys = keysFunc(source),
          l = keys.length;

      // 遍历该对象的键值对
      for (var i = 0; i < l; i++) {
        var key = keys[i];
        // _.extend 和 _.extendOwn 方法
        // 没有传入 undefinedOnly 参数,即 !undefinedOnly 为 true
        // 即肯定会执行 obj[key] = source[key] 
        // 后面对象的键值对直接覆盖 obj
        // ==========================================
        // _.defaults 方法,undefinedOnly 参数为 true
        // 即 !undefinedOnly 为 false
        // 那么当且仅当 obj[key] 为 undefined 时才覆盖
        // 即如果有相同的 key 值,取最早出现的 value 值
        // *defaults 中有相同 key 的也是一样取首次出现的
        if (!undefinedOnly || obj[key] === void 0) 
          obj[key] = source[key];
      }
    }

    // 返回已经继承后面对象参数属性的第一个参数对象
    return obj;
  };
};

函数返回函数,并且返回的函数引用了外面的一个变量,这不正是经典的闭包?因为变量的个数可以 >=1,于是我们用 arguments 去获取变量。当变量个数为 1,或者第一个参数是 null 时,这时我们不需要做任何 "extend",直接返回第一个参数。之后,我们便可以用 arguments 枚举除去第一个参数外的其他参数,将它们的键值对覆盖到第一个参数对象上,具体可以看我的源码注释。

再请教一个问题,.each和.map中,为什么前者的迭代函数是iteratee = optimizeCb(iteratee, context);而后者的却是iteratee = cb(iteratee, context);

@zhoucumt

好问题,这点我也十分诧异,个人觉得两者作用相同,可以互换。类似的还有 _.each 源码中用了 if else 结构,而 _.map 中没有用,我觉得也是一样的。唯一可以想到的原因是,可能为了测试 optimizeCbcb 两个内部方法的正确性?

@zhoucumt
cb 会检查 iteratee 是否为函数,只有当 iteratee 是函数时,才会在 cb 内部调用 optimizeCb 对 iteratee 进行优化(optimizeCb 只能优化函数)。
因为不像 each 那样 iteratee 肯定是函数,map 中 iteratee 可以是对象或字符串等:
var results = _.map([{name:'cxp'},{name:'comma'}],'name'); // => results: ['cxp', 'comma'];
如果 iteratee 不是函数,cb 就不会调用 optimizeCb, 而是返回其他函数对 map 中传入的集合进行迭代。

可否采用 undefinedOnly === void 0 来判断,而非 arguments.length

如果在严格模式,这里用的arguments还行吗?

@ooooevan 不行。

@sqfbeijing 为什么不行,可以的啊,严格模式只是淘汰了arguments.callee 和 arguments.caller

@anotherleon caller 并不在 arguments对象上的