Underscore.js源码学习
前言
Vue的源码看的头大…
先看看Underscore的吧…
这个还看得懂。
Underscore.js
Underscore.js is a utility-belt library for JavaScript that provides support for the usual functional suspects (each, map, reduce, filter…) without extending any core JavaScript objects.
一个提供常用函数,比如forEach,map,filter等,支持低版本的浏览器
github地址 underscore.js
这次来学学一些关于数组的函数
forEach
内置的forEach的语法
1 | [].forEach(function(v, i ,a) { |
在Underscore中为
1 | _.forEach([], function(v, i, a) { |
源代码如下
1 | export function each(obj, iteratee, context) { |
流程其实很清晰
optimizeCb先是对传进来的函数和上下文进行处理isArrayLike判断传进来的对象是数组,直接用索引取值来执行函数- 不是数组,视作为一个对象,获取键名,通过键名取值来执行函数
接下来看下optimizeCb这个函数
1 | function optimizeCb(func, context, argCount) { |
这里函数根据参数argCount来决定返回函数的参数个数。
switch语句中argCount默认为3,也就是数组的函数方法中最常见的回调函数的参数,即item,index,array这种格式。
接下来是isArrayLike(obj)这个函数
1 | var isArrayLike = createSizePropertyCheck(getLength); |
这里使用了getLength这个变量和createSizePropertyCheck这个函数。
1 | var getLength = shallowProperty('length'); |
例子
1 | getLength([1, 2, 3]); // 输出3(数组的长度) |
又使用了shallowProperty
1 | function shallowProperty(key) { |
shallowProperty这个函数也不复杂,就是对对象取属性值的操作,接收一个属性名,返回一个函数,这个函数接收一个对象,返回对应属性名的属性值。
createSizePropertyCheck 使用了我们创建的getLength出来的函数。这个函数又返回一个函数,用来检查传进来的参数的length属性值,这里返回的判断为typeof sizeProperty == 'number' && sizeProperty >= 0 && sizeProperty <= MAX_ARRAY_INDEX,这里前两个判断还是挺好理解的,一个是判断类型是不是为number,一个判断长度是不是大于等于0,至于最后一个判断,MAX_ARRAY_INDEX这个变量对于的值为Math.pow(2, 53) - 1,这个值为JavaScript最大的整型数字,可以通过Number.MAX_SAFE_INTEGER来查看。但是我自己试了下,new不出来这么大的数组,可能是一种折中的解决方案吧。
回到each函数,只差最后一个keys函数了
1 | function keys(obj) { |
这里使用了isObject(obj)函数,nativeKeys(obj)函数 _has函数,hasEnumBug变量和collectNonEnumProps(obj, keys)函数
isObject
1 | export function isObject(obj) { |
判断入参是否为一个对象,判断了对象typeof后的值。
除了type === 'object'这个标准的判断之外,!!obj条件把null值给排除,type === 'function'把函数也给归到对象里面。
nativeKeys
1 | var nativeKeys = Object.keys; |
这里为使用原生的方法,如果存在的话。
_has
1 | var ObjProto = Object.prototype; |
这里是使用了原生的hasOwnProperty方法
hasEnumBug
1 | // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed. |
这里在源代码中有注释,翻译过来大概是在IE9以下的版本某些属性名不能被for-in遍历(这里的某些属性名在nonEnumerableProps定义了),会忽略这个操作。
我在IE11上用IE5的打开一个脚本测试发现可以遍历出属性,不知道是不是用的IE11的问题。
这句话可能说的比较含糊,根据他这个语句,我觉得意思应该是重写了某些属性,但是该属性依然不可枚举这样的bug。
collectNonEnumProps
1 | function collectNonEnumProps(obj, _keys) { |
这个方法就是对尝试对丢失的属性进行查找并添加到属性名的数组中。
ok,把函数搞清楚之后,步骤就清晰了,对于keys方法
- 判断不是对象,不是返回一个空的数组
- 判断原生的
keys方法可用,使用原生的方法 - 原生的
keys方法不可以,使用for-in遍历并存到数组中。 - 存在IE遗失属性的bug,就尝试寻找丢失的属性名。
对于each,基本上就是这样,最后返回了数组本身,方便链式调用。
map
内置的map的语法
1 | [].map(function(v, i ,a) { |
在Underscore中为
1 | _.map([], function(v, i, a) { |
它的源码如下
1 | export function map(obj, iteratee, context) { |
经过each中大部分函数的学习之后,看之后的函数就会简单很多了,
首先通过cb函数来处理回调函数和上下文,这个cb和之前的optimizeCb不同
cb
1 | // The function we actually call internally. It invokes _.iteratee if |
这里的注释说明了一般情况下会使用内部的baseIteratee来处理回调函数和上下文,如果用户自己指定了iteratee,就使用用户自己定义的。
baseIteratee
1 | // Keep the identity function around for default iteratees. |
这里的baseIteratee主要对回调进行处理,默认的identity回调,这样子在主函数,也就是map函数的内部就不用去判断回调函数是否为空了。
这里前两个返回还是挺好理解的,重要的是后面两个返回,一个是在是对象但不是数组的情况下的matcher函数和最后返回的property函数
matcher
1 | function matcher(attrs) { |
这里又用到了extendOwn和isMatch函数
extendOwn
1 | export var extendOwn = createAssigner(keys); |
例子
1 | extendOwn({}, {a: 1}, {b: 2}); // 输出{a: 1, b: 2} |
createAssigner接收一个获取keys的函数,返回了一个函数,这个函数的作用就是把第二个之后的参数都合并到第一个参数中,并返回第一个参数。
isMatch
1 | export function isMatch(object, attrs) { |
isMatch就是判断传进的对象是否和attr中全部的属性名相等。
例子
1 | isMatch({a: 1}, {a: 1}); // 输出true |
最后一个返回就是property函数了
property
1 | export function property(path) { |
shallowProperty之前说过了就是取对象的属性值的,所以只要看deepGet函数就行
deepGet
1 | function deepGet(obj, path) { |
这里传进来的path就是一个数组,通过迭代来取得层级属性的值。
例子
1 | deepGet({a: {b: 1}}, ['a', 'b']) // 输出1 |
回到map函数中
接着var _keys = !isArrayLike(obj) && keys(obj)
当传入的是对象的是否,!isArrayLike(obj)会是true,就会执行keys函数来返回obj的属性名数组,根据&&这个操作符,会返回后面的值,也就是obj的属性名数组。
然后length = (_keys || obj).length,如果前一步确定是对象了,就会获取_keys数组的长度,否则就是正常的获取obj数组的长度
接着便是很简单的遍历调用并存储结果,最后返回这个结果。
filter
内置的filter的语法
1 | [].filter(function(v, i ,a) { |
在Underscore中为
1 | _.filter([], function(v, i ,a) { |
它的源代码为
1 | export function filter(obj, predicate, context) { |
还是先通过cb来处理回调和上下文。
然后可以看到,内部使用了each函数来遍历,在函数体内执行传进来的判定函数predicate来验证是否加入结果集中。
find
内置的find的语法
1 | [].find(function(v, i ,a) { |
在Underscore中
1 | _.find([], function(v, i ,a) { |
它的源代码为
1 | export function find(obj, predicate, context) { |
主要的实现在findIndex和findKey中,一个是寻找数组索引,一个是寻找对象的键。
findIndex
1 | export var findIndex = createPredicateIndexFinder(1); |
通过createPredicateIndexFinder来创建回调函数
首先是cb处理上下文,然后getLength获取数组长度,然后通过入参dir来确定遍历的方向,然后就是遍历来找到第一个确定的索引,找不到就返回-1.
这里要注意的一点是循环体中的index += dir,这里传入1就是正向查找,传入-1就是反向查找,也可以看到源码中也有一个findLastIndex
1 | export var findLastIndex = createPredicateIndexFinder(-1); |
findKey
1 | export function findKey(obj, predicate, context) { |
老样子,还是cb处理上下文,之后用keys来获取对象的所有的键,然后就是简单的遍历了。
函数默认就是返回undefined,findKey没找到返回undefined,void 0返回的就是undefined,findIndex没找到就返回-1,所以最后判断是否找到了,找到了就通过obj[property]这种形式返回。如果没找到,没有返回,函数默认的返回就是undefined。
reduce && reduceRight
内置的reduce语法
1 | [].reduce(function(pre, cur, i, a) { |
在Underscore中为
1 | _.reduce([], function(pre, cur, i ,a) { |
这里可能有人没怎么使用过这个函数,对这个函数的作用不是特别清楚。MDN上对reduce的解释为
reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。
可以举个例子
1 | [1, 2, 3, 4].reduce(function(pre, cur, i ,a) { |
这个函数的四个参数分别为:
pre累加器cur当前值i当前值在数组内的索引a原数组
当传入第二个参数作为起始值时,会从第一个元素开始遍历,也就是第一次遍历时,pre = 0,cur = 1
而如果不传入第二个参数,则函数第一项会作为起始值,跳过第一项,从第二项开始遍历,也就是第一次遍历时,pre = 1,cur = 2
它的源码为
1 | export var reduce = createReduce(1); |
这里主要的实现是通过createReduce这个函数,所以找到这个函数
1 | function createReduce(dir) { |
这个函数内部又定义了一个reducer函数,返回了一个函数,返回的函数中调用了这个reducer(并且通过optimizeCb做了上下文绑定)。
reducer做的事情其实很简单,就是根据dir和initial变量来确定整个运行过程。
其中dir控制了遍历的方向。
1 | index = dir > 0 ? 0 : length - 1; |
而initial控制了是否设置第一个索引值作为默认值。
1 | // 无初始值 |
createReduce返回的函数中,使用了函数内部的变量arguments的length属性来确定用户是否传入了memo变量。
1 | var initial = arguments.length >= 3; // 小于3,没有传入memo初始值,initial为false,反之为true |
通过createReduce,也可以生成一个从右往左的reduce,也就是reduceRight
1 | export var reduceRight = createReduce(-1); |
后记
感觉Underscore中大量使用了返回函数的形式来组织代码,看起来跳来跳去的,得耐心下来读。
暂时就写这么多,学一学一些基本的Polyfill也是相当不错的。后续应该会接着更新~