仅看 cb 和 optimizeCb 两个函数的名字,你可能想不到这是用来作什么的,尽管你可能想到 cb 是 callback 的缩写。javascript
若是直接讲解源码,你可能想不明白为何要这么写,因此咱们从 _.map 函数开始讲起。html
_.map 相似于 Array.prototype.map
,但更加健壮和完善。咱们看下 _.map 的源码:java
// 简化过,这里仅假设 obj 是数组 _.map = function (obj, iteratee, context) { iteratee = cb(iteratee, context); var length = obj.length, results = Array(length); for (var index = 0; index < length; index++) { results[index] = iteratee(obj[index], index, obj); } return results; };
map 方法除了传入要处理的数组以外,还有两个参数 iteratee 和 context,相似于 Array.prototype.map
中的其余两个参数,其中 iteratee 表示处理函数,context 表示指定的执行上下文,即 this 的值。git
而后在源码中,咱们看到,咱们将 iteratee 和 context 传入一个 cb 函数,而后覆盖掉 iteratee 函数,而后将这个函数用做最终的处理函数。github
实际上,须要这么麻烦吗?不就是使用 iteratee 函数处理每次迭代的值吗?不就是经过 context 指定 this 的值吗?咱们能够直接这样写呐:数组
_.map = function (obj, iteratee, context) { var length = obj.length, results = Array(length); for (var index = 0; index < length; index++) { results[index] = iteratee.call(context, obj[index], index, obj); } return results; }; // [2, 3, 4] console.log(_.map([1, 2, 3], function(item){ return item + 1; })) // [2, 3, 4] console.log(_.map([1, 2, 3], function(item){ return item + this.value; }, {value: 1}))
你看看也没有什么问题呐,但是,万一 iteratee 咱们不传入一个函数呢?好比咱们什么也不传,或者传入一个对象,又或者传入一个字符串、数字呢?架构
若是用咱们的方法天然是会报错的,那 underscore 呢?app
// 使用 underscore // 什么也不传 var result = _.map([1,2,3]); // [1, 2, 3] // 传入一个对象 var result = _.map([{name:'Kevin'}, {name: 'Daisy', age: 18}], {name: 'Daisy'}); // [false, true] var result = _.map([{name: 'Kevin'}, {name: 'Daisy'}], 'name'); // ['Kevin', 'daisy']
咱们会发现,underscore 居然还能根据传入的值的类型不一样,实现的效果不一样。咱们总结下:ide
由此,咱们能够推测在 underscore 的 cb 函数中,有对 iteratee 值类型的判断,而后根据不一样的类型,返回不一样的 iteratee 函数。函数
因此咱们来看看 cb 函数的源码:
var cb = function(value, context, argCount) { if (_.iteratee !== builtinIteratee) return _.iteratee(value, context); if (value == null) return _.identity; if (_.isFunction(value)) return optimizeCb(value, context, argCount); if (_.isObject(value) && !_.isArray(value)) return _.matcher(value); return _.property(value); };
这一看就牵扯到了 8 个函数!不要惧怕,咱们一个一个看。
if (_.iteratee !== builtinIteratee) return _.iteratee(value, context);
咱们看看 _.iteratee 的源码:
_.iteratee = builtinIteratee = function(value, context) { return cb(value, context, Infinity); };
由于 _.iteratee = builtinIteratee
的缘故,_.iteratee !== builtinIteratee
值为 false,因此正常状况下 _.iteratee(value, context)
并不会执行。
可是若是咱们在外部修改了 _.iteratee 函数,结果便会为 true,cb 函数直接返回 _.iteratee(value, context)
。
这个意思实际上是说用咱们自定义的 _.iteratee 函数来处理 value 和 context。
试想咱们并不须要如今 _.map 这么强大的功能,我只但愿当 value 是一个函数,就用该函数处理数组元素,若是不是函数,就直接返回当前元素,咱们能够这样修改:
<html> <head> <title>underscore map</title> </head> <body> <script src="../vender/underscore.js"></script> <script type="text/javascript"> _.iteratee = function(value, context) { if (typeof value === 'function') { return function(...rest) { return value.call(context, ...rest) }; } return function(value) { return value; }; }; // 若是 map 的第二个参数不是函数,就返回该元素 console.log(_.map([1, 2, 3], 'name')); // [1, 2, 3] // 若是 map 的第二个参数是函数,就使用该函数处理数组元素 var result = _.map([1, 2, 3], function(item) { return item + 1; }); console.log(result); // [2, 3, 4] </script> </body> </html>
固然更多的状况是自定义对不一样的 value 使用不一样的处理函数,值得注意的是,underscore 中的多个函数都是用了 cb 函数,而由于 cb 函数使用了 _.iteratee 函数,若是你修改这个函数,其实会影响多个函数,这些函数基本都属于集合函数,具体包括 map、find、filter、reject、every、some、max、min、sortBy、groupBy、indexBy、countBy、sortedIndex、partition、和 unique。
if (value == null) return _.identity;
让咱们看看 _.identity 的源码:
_.identity = function(value) { return value; };
这也就是为何当 map 的第二个参数什么都不传的时候,结果会是一个相同数组的缘由。
_.map([1,2,3]); // [1, 2, 3]
若是直接看这个函数,可能以为没有什么用,但用在这里,却又十分的合适。
if (_.isFunction(value)) return optimizeCb(value, context, argCount);
当 value 是一个函数的时候,就传入 optimizeCb 函数,咱们来看看 optimizeCb 函数:
var optimizeCb = function(func, context, argCount) { // 若是没有传入 context,就返回 func 函数 if (context === void 0) return func; switch (argCount) { case 1: return function(value) { return func.call(context, value); }; case null: 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); }; };
也许你会好奇,为何我要对 argCount 进行判断呢?就不能直接返回吗?好比这样:
var optimizeCb = function(func, context) { // 若是没有传入 context,就返回 func 函数 if (context === void 0) return func; return function() { return func.apply(context, arguments); }; };
固然没有问题,但为何 underscore 要这样作呢?其实就是为了不使用 arguments,提升一点性能而已,若是不是写一个库,其实还真是没有必要作到这点。
而为何当参数是 3 个时候,参数名称分别是 value, index, collection ,又为何没有参数为 2 的状况呢?其实这都是根据 underscore 函数用到的状况,没有函数用到两个参数,因而就省略了,像 map 函数就会用到 3 个参数,就根据这三个参数的名字起了这里的变量名啦。
if (_.isObject(value) && !_.isArray(value)) return _.matcher(value);
这段就是用来处理当 map 的第二个参数是对象的状况:
// 传入一个对象 var result = _.map([{name:'Kevin'}, {name: 'Daisy', age: 18}], {name: 'Daisy'}); // [false, true]
若是 value 是一个对象,而且不是数组,就使用 _.matcher 函数。看看各个函数的源码:
var nativeIsArray = Array.isArray; _.isArray = nativeIsArray || function(obj) { return Object.prototype.toString.call(obj) === '[object Array]'; }; _.isObject = function(obj) { var type = typeof obj; return type === 'function' || type === 'object' && !!obj; }; // extend 函数能够参考 《JavaScript 专题之手写一个 jQuery 的 extend》 _.matcher = function(attrs) { attrs = _.extend({}, attrs); return function(obj) { return _.isMatch(obj, attrs); }; }; // 该函数判断 attr 对象中的键值是否在 object 中有而且相等 // var stooge = {name: 'moe', age: 32}; // _.isMatch(stooge, {age: 32}); => true // 其中 _.keys 至关于 Object.keys _.isMatch = function(object, attrs) { var keys = _.keys(attrs), length = keys.length; if (object == null) return !length; var obj = Object(object); for (var i = 0; i < length; i++) { var key = keys[i]; if (attrs[key] !== obj[key] || !(key in obj)) return false; } return true; };
return _.property(value);
这个就是处理当 value 是基本类型的值的时候,返回元素对应的属性值的状况:
var result = _.map([{name: 'Kevin'}, {name: 'Daisy'}], 'name'); // ['Kevin', 'daisy']
咱们看下源码:
_.property = function(path) { // 若是不是数组 if (!_.isArray(path)) { return shallowProperty(path); } return function(obj) { return deepGet(obj, path); }; }; var shallowProperty = function(key) { return function(obj) { return obj == null ? void 0 : obj[key]; }; }; // 根据路径取出深层次的值 var deepGet = function(obj, path) { var length = path.length; for (var i = 0; i < length; i++) { if (obj == null) return void 0; obj = obj[path[i]]; } return length ? obj : void 0; };
咱们好像发现了新大陆,原来 value 还能够传一个数组,用来取深层次的值,举个例子:
var person1 = { child: { nickName: 'Kevin' } } var person2 = { child: { nickName: 'Daisy' } } var result = _.map([person1, person2], ['child', 'nickName']); console.log(result) // ['Kevin', 'daisy']
若是你想学习 underscore 的源码,在分析集合相关的函数时必定会接触 cb 和 optimizeCb 函数,先掌握这两个函数,会帮助你更好更快的解读源码。
underscore 系列目录地址:https://github.com/mqyqingfeng/Blog。
underscore 系列预计写八篇左右,重点介绍 underscore 中的代码架构、链式调用、内部函数、模板引擎等内容,旨在帮助你们阅读源码,以及写出本身的 undercore。
若是有错误或者不严谨的地方,请务必给予指正,十分感谢。若是喜欢或者有所启发,欢迎 star,对做者也是一种鼓励。