Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

内部方法 createAssigner 详解 #4

Open
lessfish opened this issue May 24, 2016 · 8 comments
Open

内部方法 createAssigner 详解 #4

lessfish opened this issue May 24, 2016 · 8 comments

Comments

@lessfish
Copy link
Owner

lessfish commented May 24, 2016

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 枚举除去第一个参数外的其他参数,将它们的键值对覆盖到第一个参数对象上,具体可以看我的源码注释。

@lessfish lessfish added bug and removed bug labels May 24, 2016
@zhoucumt
Copy link

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

@lessfish
Copy link
Owner Author

@zhoucumt

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

@changxiupeng
Copy link

@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 中传入的集合进行迭代。

@aleen42
Copy link

aleen42 commented Feb 8, 2017

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

@ooooevan
Copy link

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

@aswind7
Copy link

aswind7 commented Jan 3, 2018

@ooooevan 不行。

@wangliang1124
Copy link

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

@shenzhim
Copy link

shenzhim commented Jul 4, 2018

@anotherleon caller 并不在 arguments对象上的

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants