内部方法 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 设计的原理(向后兼容)。
之后楼主会写一系列的文章跟大家分享在源码阅读中学习到的知识。
- underscore-1.8.3 源码解读项目地址 https://github.com/hanzichi/underscore-analysis
- underscore-1.8.3 源码全文注释 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/underscore-1.8.3-analysis.js
- underscore-1.8.3 源码解读系列文章 https://github.com/hanzichi/underscore-analysis/issues
欢迎围观~ (如果有兴趣,欢迎 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);
好问题,这点我也十分诧异,个人觉得两者作用相同,可以互换。类似的还有 _.each
源码中用了 if else
结构,而 _.map
中没有用,我觉得也是一样的。唯一可以想到的原因是,可能为了测试 optimizeCb
和 cb
两个内部方法的正确性?
@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还行吗?
@sqfbeijing 为什么不行,可以的啊,严格模式只是淘汰了arguments.callee 和 arguments.caller
@anotherleon caller 并不在 arguments对象上的