underscore源码学习(二)
顺着underscore源码的顺序读下来,弄懂了之前underscore的基本结构,接下来看看underscore为我们提供的一些关于集合
的API。
迭代
关于迭代,我们都知道ES5原生方法也提供了迭代函数供我们使用,而在underscore中的迭代则是对原生的迭代函数进行了封装优化升级。在underscore中,迭代的对象不仅仅是数组对象,还支持Array,Object的迭代,对Object的迭代的依据是对象的键值对(key-value),看看 underscore中_.each
是如何实现的:
/** * each方法将ES5的forEach换为了函数式表达 * @param obj 待迭代集合 * @param iteratee 迭代过程中每个被迭代元素的回调函数 * @param context 上下文 * @example * // 数组迭代 * _.each([1, 2, 3], alert); * // 对象迭代 * _.each({one: 1, two: 2, three: 3}, alert); */ _.each = _.forEach = function (obj, iteratee, context) { //优化回调 iteratee = optimizeCb(iteratee, context); var i, length; // 判断是数组还是对象 if (isArrayLike(obj)) { for (i = 0, length = obj.length; i < length; i++) { iteratee(obj[i], i, obj) } } else { var keys = _.keys(obj) for (i = 0, length = keys.length; i < length; i++) { iteratee(obj[keys[i]], keys[i], obj) } } // 返回对象自身 以便于链式调用 return obj };
看以上源码可知,_.each
传入三个参数,主要的是第二个iteratee
回调函数,然后再通过optimizeCb
优化回调,返回对应的回调(optimizeCb可以查看第一部分)。array
迭代的是数组的每个元素,传入的三个参数分别为数组的值,对应值的下标,数组本身
。Object
迭代的元素是对象的每个键值对key-value,传入的参数为对象的key所对应的值,对象的key值,对象本身
。
map-reduce
ES5原生方法也提供map和reduce方法,它们提供了一种对列表操作的思路,是函数式编程重要组成部分。具体map和reduce可以去MDN上查看相关API。
map在underscore中的实现
它的实现思路是:
- 返回一个新的列表或元素
- 对列表中的值进行遍历,用指定函数
func
作用于每个遍历的元素,输出一个新的值放到新的列表中
_.map = _.collect = function(obj, iteratee, context) { iteratee = cb(iteratee, context) //考虑数组和对象 var keys = !isArrayLike(obj) && _.keys(obj), length = (keys || obj).length, results = Array(length) // 初始化定长数组 for (var index = 0; index < length; index++) { var currentKey = keys ? keys[index] : index results[index] = iteratee(obj[currentKey], currentKey, obj) } return results }
使用用例:
对数组使用
var res = _.map([1,2,3], function(elem, index, array) { return elem * 2 }) // => [2,4,6]
对对象使用
var obj = { name: 'lzb', age: '20', sex: 'male' } var res = _.map(obj, function(value, key, obj) { return key }) // => name age sex
reduce在underscore中的实现
reduce相对于map的实现复杂了一些,underscore首先在外部实现了reduce函数的工厂函数createReduce
,这个函数实现了以下功能:
- 区分reduce的开始方向(参数dir),是从首端开始末端开始
- memo记录最新的结果
reduce的执行过程大概是:
- 设置一个memo变量缓存当前规约过程的结果
- 如果用户为初始化memo,则memo的值为序列的第一个值
- 遍历当前集合,对当前遍历到的元素按传入的
func
进行规约操作,刷新memo - 遍历结束,返回memo
createReduce的实现:
/** * reduce函数的工厂函数, 用于生成一个reducer, 通过参数决定reduce的方向 * @param dir 方向 left or right * @returns {function} */ function createReduce(dir) { function iterator(obj, iteratee, memo, keys, index, length) { for (; idnex > 0 && index < length; index += dir) { var currentKey = keys ? keys[index] : index // memo 用来记录最新的 reduce 结果 // 执行 reduce 回调, 刷新当前值 memo = iteratee(memo, obj[currentKey], currentKey, obj) } } /** * @param obj 传入的对象 * @param iteratee 回调函数 * @param memo 初始化累加器的值 * @param context 执行上下文 */ return function (obj, iteratee, memo, context) { iteratee = optimizeCb(iteratee, context, 4) var keys = !isArrayLike(obj) && _.keys(obj), length = (keys || obj).length index = dir > 0 ? 0 : length - 1 // 如果没有传入memo初始值 则从左第一个为初始值 从右则最后一个为初始值 if (arguments.length < 3) { memo = obj[keys ? keys[index] : index] index += dir } return iterator(obj, iteratee, memo, keys, index, length) } }
最后,underscore暴露了两个供使用的方法
// 由左至右进行规约 _.reduce = _.foldl = _.inject = createReduce(1); // 由右至左进行规约 _.reduceRight = _.foldr = createReduce(-1);
使用用例:
对数组使用
var sum = _.reduce([1,2,3,4], function(prev, current, index, arr) { return prev + current }, 0) // => 10
对对象使用
var scores = { english: 93, math: 88, chinese: 100 }; var total = _.reduce(scores, function(prev, value, key, obj){ return prev+value; }, 0); // => total: 281
真值检测函数
在underscore中,除了提供_.each,_.map._.reduce
等函数操作集合,还提供了_.filter, _.reject, _.every, _.some
这几个基于逻辑判断的集合操作函数。这些API都依赖于用户提供的真值检测函数来返回对应的结果。
在underscore中,真值检测函数的参数被命名为predicate
,predicate
有断言的意思,非常形象。当然,predicate
依旧会通过cb
优化。
_.filter
看看_.filter
的实现
/** * 根据真值检测函数 过滤对象 * 检测通过符合条件 保留元素 * @param obj * @param predicate * @param context * @example * var evens = _.filter([1, 2, 3, 4, 5, 6], function(num){ return num % 2 == 0; }); * => [2, 4, 6] */ _.filter = _.select = function (obj, predicate, context) { var results = [] // 优化回调 predicate = cb(predicate, context) _.each(obj, function (value, index, list) { if (predicate(value, index, list)) results.push(value) }) return results }
根据传入的元素信息,检测并返回对应boolean值,决定当前元素要被保留。
_.reject
上面的_.filter
函数是元素符合检测条件就保留,而_.reject
函数则是与_.filter
相反的功能
我们来看看underscore中_.reject
的实现
/** * filter的反运算, * 如果真值检测通过, 元素被丢弃 */ _.reject = function (obj, predicate, context) { return _.filter(obj, _negate(cb(predicate)), context) }
可以看到,这个函数只有一行代码,非常简短。那么,这其中的_.negate
函数又是什么呢?猜测下,negate
在英语中有否定的意思,那么跟_.reject
的功能就有了一定的联系, 下面看看_.negate
的实现
_.negate = function(predicate) { return function() { return !predicate.apply(this, arguments) } }
可以看到,_.negate
得到了反义predicate
的执行结果,减少了大量重复的代码,值得学习。
_.every
迭代对象里的每个元素,只有每个元素都通过真值检测函数,才返回true
。
/** * @param obj * @param predicate * @param context * @example * _.every([true, 1, null, 'yes'], _.identity); * => false */ _.every = _.all = function (obj, predicate, context) { predicate = cb(predicate, context) var keys = !isArrayLike(obj) && _.keys(obj), length = (keys || obj).length for (var index = 0; index < length; index++) { var currentKey = keys ? keys[index] : index if (!predicate(obj[currentKey], currentKey, obj)) return false } return true }
_.some
这个API跟_.every
差不多,从英语单词的含义我们也可以猜出它的功能,即迭代对象的所有元素,如果有任意一个通过真值检测,则返回true
。
/** * @param obj * @param predicate * @param context * @example * _.some([null, 0, 'yes', false]); * => true */ _.some = _.any = function (obj, predicate, context) { predicate = cb(predicate, context) var keys = !isArrayLike(obj) && _.keys(obj), length = (keys || obj).length for (var index = 0; index < length; index++) { var currentKey = keys ? keys[index] : index if (predicate(obj[currentKey], currentKey, obj)) return true } return false }
_.contains
_.contains
函数的功能是检测一个对象或数组是否包含指定的某个元素。
/** * @param obj 待检测对象 * @param item 指定的元素 * @param fromIndex 从哪个位置开始找 * @param guard * @example * _.contains([1,2,3], 3) * => true */ _.contains = _.includes = _.include = function (obj, item, fromIndex, guard) { if (!isArrayLike(obj)) obj = _.values(obj) if (typeof fromIndex != 'number' || guard) fromIndex = 0 return _.indexOf(obj, item.fromIndex) >= 0 }
从代码上看,还是比较容易理解的,这里主要用到了underscore内部提供的两个函数,_.values
和_.indexOf
,从名字上我们也可以猜出它们之间的功能,如果传入的对象,则取出该对象所有的值,然后再进行查找比较,看看_values
的实现:
/** * 获得一个对象的所有value * @param obj 对象 * @returns {Array} 值序列 * @example * _.values({one: 1, two: 2, three: 3}); * // => [1, 2, 3] */ _.values = function (obj) { var keys = _.keys(obj) var length = keys.length var values = Array(length) for (var i = 0; i < length; i++) { values[i] = obj[keys[i]] } return values }
而_,indexOf
的实现就比较复杂了,这是underscore中提供的关于查找的API,详细介绍将在下一篇总结写出。