【学习】underscore源码学习(二)——集合函数
这一篇到了源码170行以后,主要是
underscore(下文用下划线代替- -)
向外暴露的一些核心API,_
扩展了很多原生JS拥有的方法,有很多优秀地设计思想值得我们学习和探究。我们在用的过程中,了解到底层的实现更有利于我们将来的选择及性能优化
集合函数 170 - 486行
each
_.each(obj, iteratee, [context])
_
的each方法遍历所传obj(这里可看作一个列表,数组或类数组)中的所有元素,按顺序用遍历输出每个元素。如果传递了context参数,则把iteratee绑定到context对象上。每次调用iteratee都会传递三个参数:(element, index, list)。如果list是个JavaScript对象,iteratee的参数是 (value, key, list))。返回list以方便链式调用。
1 | _.each = _.forEach = function(obj, iteratee, context) { |
1 | _.each([1, 2, 3], i => console.log(i)); |
iteratee = optimizeCb(iteratee, context)形成一个递归调用
,if分支用于控制是迭代数组还是对象,最后返回的是迭代的自身。
map
_.map(obj, iteratee, [context])
通过转换函数(iteratee迭代器)映射列表中的每个值产生价值的新数组。该方法接收三个参数,循环对象,迭代器和环境参数。后两个参数作为活动对象传到了cb方法中。
1 | _.map = _.collect = function(obj, iteratee, context) { |
1 | _.map([1,2,3], num => num * 2); |
- keys, length, results这三个变量的声明很精髓,(后面也有很多地方用到) !有隐式转换变为布尔型,所以keys =
布尔
&& _.keys(obj) 因为逻辑与,表达式成立的条件是,前面部分会为真,即这句直接拿到了 _.key(obj)。 - 数组中才有length这一属性,普通对象中是没有定义的
- Array(length)会创建一个长度为length的数组这么再来看循环体中的results[index]就清楚多了
reduce reduceRight
_.reduce(list, iteratee, [memo], [context])
_.reduceRight(list, iteratee, memo, [context])
reduce方法把list中元素归结为一个单独的数值。
Memo是reduce函数的初始值,reduce的每一步都需要由iteratee返回。这个迭代传递4个参数:memo,value 和 迭代的index(或者 key)和最后一个引用的整个 list。
1 | var createReduce = function(dir) { |
1 | _.reduce([1,2,3], (memo, num) => memo + num) |
如果没有memo传递给reduce的初始调用,iteratee不会被列表中的第一个元素调用。第一个元素将取代 传递给列表中下一个元素调用iteratee的memo参数。
最外层的匿名函数中的dir传到了 index = dir > 0 ? 0 : length - 1
由dir来判断是reduce还是reduceRight,这里高度抽象的思想值得我们学习
createReduce是 _
作用域中的私有变量,外部访问不到,这里 _.reduce 是向外暴露的接口,我们调用的api 其实是调用craeteReduce()所返回的函数
find
_.find(obj, predicate, [context])
keyFinder的结果由传入obj是数组(下标)还是对象(key)决定,这里的key是一个递归调用。在obj中逐项查找,返回第一个通过predicate迭代函数真值检测的元素值,如果没有值传递给测试迭代器将返回undefined。 如果找到匹配的元素,函数将立即返回,不会遍历整个list。
1 | _.find = _.detect = function(obj, predicate, context) { |
1 | _.find([2,4,5,6,7,8], num => num % 2 != 0) |
filter
_.filter(obj, predicate, [context])
遍历所传obj中的每个值,返回包含所有通过predicate真值检测的元素值。(如果存在原生filter方法,则用原生的filter方法。)
1 | _.filter = _.select = function(obj, predicate, context) { |
1 | _.filter([2,3,4,6,8,10], num => num % 2 === 0) |
这里额外说两句,其实暴露的API写法大同小异,主要是理解核心方法中的 cb
optimizeCb
知道 _
是怎么通过高阶函数传参,拿到传入对象(函数)的作用域 func.apply(context, arguments);
reject
_.reject(list, predicate, [context]) 返回list中没有通过predicate真值检测的元素集合,与filter相反
1 | _.reject = function(obj, predicate, context) { |
对_.filter的一个封装, _.negate(916行)
可见到,返回的是结果为false的集合
every
_.every(obj, [predicate], [context]) 如果list中的所有元素都通过predicate的真值检测就返回true。
1 | _.every = _.all = function(obj, predicate, context) { |
1 | _.every([false, 0, null, undefined, NaN, 1], item => item != true) |
some
_.some(obj, [predicate], [context]) 如果obj中有任何一个元素通过 predicate 的真值检测就返回true。一旦找到了符合条件的元素, 就直接中断对obj的遍历.
1 | _.some = _.any = function(obj, predicate, context) { |
1 | _.some([false, 0, null, undefined, NaN, 1], item => item == true) |
contains
_.contains(obj, value, [fromIndex]) 如果obj包含指定的value则返回true。如果obj 是数组,内部使用indexOf判断。使用fromIndex来给定开始检索的索引位置。这个方法ES6已有实现 参考Array.prototype.includes
1 | _.contains = _.includes = _.include = function(obj, item, fromIndex, guard) { |
1 | _.contains([a,b,c], 3) |
invoke
_.invoke(list, methodName, *arguments) 在list的每个元素上执行methodName方法。 任何传递给invoke的额外参数,invoke都会在调用methodName方法的时候传递给它
1 | _.invoke = restArgs(function(obj, path, args) { |
1 | _.invoke([[5, 1, 7], [3, 2, 1]], 'sort'); |
pluck
_.pluck(list, propertyName) 萃取数组对象中某属性值,返回一个数组
1 | _.pluck = function(obj, key) { |
1 | var stooges = [{name: 'moe', age: 40}, {name: 'larry', age: 50}, {name: 'curly', age: 60}]; |
这是对 _.map用法的一个封装
where、findWhere
_.where(obj, attrs) 遍历obj中的每一个值,返回一个数组,这个数组包含attrs所列出的属性的所有的 键 - 值对。
1 | _.where = function(obj, attrs) { |
_.findWhere(obj, attrs) 遍历整个obj,返回匹配 properties参数所列出的所有 键 - 值 对的第一个值。
1 | _.findWhere = function(obj, attrs) { |
max 和 min
_.max(obj, [iteratee], [context]) 返回list中的最大值
1 | _.max = function(obj, iteratee, context) { |
这里是实现看起来挺复杂,不要畏惧,一行一行来。 第一段if做了安全检测,防止内存溢出,虽然一般人用不会乱传数字,但xx就不一定了是吧。 让我们继续来断句
1 | if ( |
通过条件为 第2行 的 逻辑或 || 成立
1 iteratee == null
2 typeof iteratee == ‘number’ && typeof obj[0] != ‘object’
两句任其一为真即可,1.8.3与1.8.2中唯一的变化就是对obj类型检测提炼了,让其能对对象或数组都能安全迭代,最后返回的 result 即为安全的value值
那么当安全检测未通过时,_
是这么来处理的,iteratee = cb(iteratee, context)
让其提供安全的作用域,然后对传入的obj进行迭代(_.each) computed拿到了迭代对象的作用域,进行自处理,把拿到的值进行安全处理(赋值)
继续断句:
1 | if ( |
这样看就容易多了,-Infinity限制了所传或最终得到的结果会在js允许的安全范围内
_.min(obj, [iteratee], [context]) 返回list中的最小值。如果传递iteratee参数,iteratee将作为list中每个值的排序依据,如果list为空,将返回-Infinity,所以你可能需要事先用isEmpty检查 list 。
这里和 _.max 差不多就不做过多解释了
1 | _.min = function(obj, iteratee, context) { |
sample 和 shuffle
_.sample(obj, [n], [guard]) 从 obj中产生一个随机样本。传递一个数字表示从obj中返回n个随机元素。否则将返回一个单一的随机项
1 | _.sample = function(obj, n, guard) { |
我们先看if段,当n或guard未传时返回所传时,返回obj中的一个随机项。
若传了n和guard时,用私有变量sample
存储了对obj的处理——数组浅拷贝,对象就创建一个副本。然后遍历sample将其内容打乱并返回。
_.shuffle(obj) 返回一个随机乱序的 list 副本
1 | _.shuffle = function(obj) { |
上懂了sample方法这里就很简单了,shuffle方法即是对sample的一种应用,传入obj,n取无限大(即obj.length)这样返回的是打乱后的obj
sortBy
_.sortBy(obj, iteratee, [context]) 返回一个排序后的list拷贝副本。如果传递iteratee参数,iteratee将作为list中每个值的排序依据。迭代器也可以是字符串的属性的名称进行排序的(比如 length)。
1 | _.sortBy = function(obj, iteratee, context) { |
我们直接看return就好, _.pluck()返回由obj的某些值所组成的’数组’(当然,1.8.3扩展后也可以是对象),然后再对这个’数组’进行原生的sort()排序,并将结果返回
group
这是用于聚合group by操作的内部函数
1 | var group = function(behavior, partition) { |
所传的behavior其实是一个回调函数,只是这里表现得很语义化,我们读源码要根据上下文来看,不然很容易迷糊。这里的group是一个高阶函数,对内层进行了封装,group执行后返回一个闭包,该闭包执行后返回处理结果。
在闭包用变量result
对partition进行兼容处理,若数组则[[], []]分隔开,对象的话默认处理为空对象,iteratee我们见得很多了,即返回一个安全的执行环境。然后迭代所传对闭包所传的参数obj,内部有声明一个变量key用于返回新的执行环境
这里有点绕,允许我啰嗦一下(以后水平上来了会精简,这里我尽量用自己的话说清楚)iteratee()后返回的执行环境在 group执行返回的闭包中,这个闭包里进行的迭代由于绑定了obj,就由group的调用 _ 这个环境跳到了 所要迭代的对象 obj 中了~ 然后此时的回调 behavior再来对result进行处理
有点把自己绕进去了,我们先来看看实际运用,就知道为什么要进行这个封装了
groupBy _.groupBy(result, value, key) 把一个集合分组为多个集合,通过 value 返回的结果进行分组. 如果 value 是一个字符串而不是函数, 那么将使用 value 作为各元素的属性名来对比进行分组.
1 | _.groupBy = group(function(result, value, key) { |
indexBy
_.indexBy(result, value, key) 给定一个集合(对象),和 一个用来返回一个在列表中的每个元素键 的iterator 函数(或属性名), 返回一个每一项索引的对象。和groupBy非常像,但是当你知道你的键是唯一的时候可以使用indexBy 。
1 | _.indexBy = group(function(result, value, key) { |
countBy countBy_.countBy(list, iteratee, [context]) 排序一个列表组成一个组,并且返回各组中的对象的数量的计数。类似groupBy,但是不是返回列表的值,而是返回在该组中值的数目。
1 | _.countBy = group(function(result, value, key) { |
toArray
_.toArray(obj) 把obj(任何可以迭代的对象)转换成一个数组,在转换 arguments 对象时非常有用。
1 | _.toArray = function(obj) { |
size
.size(obj) 返回所传合法对象的长度。
1 | _.size = function(obj) { |
这其实是一个扩展处理,我们知道对象是没有length属性的。这里进行了处理,返回 由对象的key值所组成数组的长度~
partition
_.partition(array, predicate) 拆分一个数组(array)为两个数组: 第一个数组其元素都满足predicate迭代函数, 而第二个的所有元素均不能满足predicate迭代函数。
1 | _.partition = group(function(result, value, pass) { |
group的第一个参数是个回调没异议,第二个传了一个true是什么呢? 我们可翻到上面看看,partition
的值决定了如何处理result 这里变成了 [[], []] 的形式。那么很简单了,根据回调的pass结果,通过放到 [[], []][0]中, 未通过则放到[[], []][1]中
小结
随着 underscore.js 源码的阅读与学习深入,我们可以了解到很多底层封装的原理与细节。在阅读源码时,不要害怕自己看不懂,理解错,不清楚的地方可以单独拎出来,自己写写或者代码跑起来看看效果以便加深理解。
Collections Function中,其实很多都是对第一章核心函数的一些封装和重复使用,如cb optimizeCb这类用得多的,向外暴露的API又可以让封装的其他API来使用,如 where findWhere就是对filter的运用, pluck是对map的二次封装。