这是underscore源码理会系列第三篇文章,重要引见underscore中each、map、filter、every、reduce等我们经常运用的一些遍历数组的要领。
each
在underscore中我们最经常运用的就是each和map两个要领了,这两个要领平常吸收三个参数,分别是数组/对象、函数、上下文。
// iteratee函数有三个参数,分别是item、index、array或许value、key、obj
_.each = _.forEach = function(obj, iteratee, context) {
// 假如不传context,那末each要领内里的this就会指向window
iteratee = optimizeCb(iteratee, context);
var i, length;
// 假如是类数组,平常来讲包含数组、arguments、DOM鸠合等等
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函数的源码很简单,函数内部会运用isArrayLike要领来推断当前传入的第一个参数是类数组或许对象,假如是类数组,直接运用接见下标的体式格局来遍历,并将数组的项和index传给iteratee函数,假如是对象,则先获取到对象的keys,再举行遍历后将对象的value和key传给iteratee函数
不过在这里,我们重要剖析optimizeCb和isArrayLike两个函数。
optimizeCb
// 这个函数重如果给传进来的func函数绑定context作用域。
var optimizeCb = function (func, context, argCount) {
// 假如没有传context,那就直接返回func函数
if (context === void 0) return func;
// 假如没有传入argCount,那就默许是3。这里是依据第二次传入的参数个数来给call函数传入差别数目的参数
switch (argCount == null ? 3 : argCount) {
case 1: return function (value) {
return func.call(context, value);
};
case 2: return function (value, other) {
return func.call(context, value, other);
};
// 平常是each、map等
case 3: return function (value, index, collection) {
return func.call(context, value, index, collection);
};
// 平常是reduce等
case 4: return function (accumulator, value, index, collection) {
return func.call(context, accumulator, value, index, collection);
};
}
// 假如参数数目大于4
return function () {
return func.apply(context, arguments);
};
};
实在我们很轻易就看出来optimizeCb函数只是帮func函数绑定context的,假如不存在context,那末直接返回func,不然则会依据第二次传给func函数的参数数目来推断给call函数传几个值。
这里有个重点,为何要用这么贫苦的体式格局,而不直接用apply来将arguments悉数传进去?
原因是call要领的速率要比apply要领更快,由于apply会对数组参数举行磨练和拷贝,所以这里就对经常运用的几种情势运用了call,其他状况下运用了apply,概况能够看这里:call和apply
isArrayLike
关于isArrayLike要领,我们来看underscore的完成。(这个延长比较多,假如没兴致,能够跳过)
// 一个高阶函数,返回对象上某个详细属性的值
var property = function (key) {
return function (obj) {
return obj == null ? void 0 : obj[key];
};
};
// 这里有个ios8上面的bug,会致使相似var pbj = {1: "a", 2: "b", 3: "c"}这类对象的obj.length = 4; jQuery中也有这个bug。
// https://github.com/jashkenas/underscore/issues/2081
// https://github.com/jquery/jquery/issues/2145
// MAX_SAFE_INTEGER is 9007199254740991 (Math.pow(2, 53) - 1).
// http://ecma-international.org/ecma-262/6.0/#sec-number.max_safe_integer
var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
// 听说用obj["length"]就能够处理?我没有ios8的环境,有兴致的能够尝尝
var getLength = property('length');
// 推断是不是是类数组,假如有length属性而且值为number范例即可视作类数组
var isArrayLike = function (collection) {
var length = getLength(collection);
return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};
在underscore中,只需带有length属性,都能够被认为是类数组,所以即使是{length: 10}这类状况也会被归为类数组。
我个人感觉如许写实在太甚单方面,我照样更喜好jQuery内里isArrayLike要领的完成。
function isArrayLike(obj) {
// Support: real iOS 8.2 only (not reproducible in simulator)
// `in` check used to prevent JIT error (gh-2145)
// hasOwn isn't used here due to false negatives
// regarding Nodelist length in IE
var length = !!obj && "length" in obj && obj.length,
type = toType(obj);
// 消除了obj为function和全局中有length变量的状况
if (isFunction(obj) || isWindow(obj)) {
return false;
}
return type === "array" || length === 0 ||
typeof length === "number" && length > 0 && (length - 1) in obj;
}
jQuery中运用in来处理ios8下面谁人JIT的毛病,同时还会消除obj是函数和window的状况,由于假如obj是函数,那末obj.length则是这个函数参数的个数,而假如obj是window,那末我在全局中定义一个var length = 10,这个一样也能获取到length。
末了的三个推断分别是:
- 假如obj的范例是数组,那末返回true
- 假如obj的length是0,也返回true。即使是{length: 0}这类状况,由于在挪用isArrayLike的each和map等要领中会在for轮回内里推断length,所以也不会形成影响。
- 末了这个(length – 1) in obj我个人明白就是为了消除{length: 10}这类状况,由于这个能够满足length>0和length===”number”的状况,然则平常状况下是没法满足末了(length – 1) in obj的,然则NodeList和arguments这些却能够满足这个前提。
map
说完了each,我们再来讲说map,map函数实在和each的完成很相似,不过不一样的一个处所在于,map函数的第二个参数不一定是函数,我们能够什么都不传,以至还能够传个对象。
var arr = [{name:'Kevin'}, {name: 'Daisy', age: 18}]
var result1 = _.map(arr); // [{name:'Kevin'}, {name: 'Daisy', age: 18}]
var result2 = _.map(arr, {name: 'Daisy'}) // [false, true]
所以这里就会对传入map的第二个参数举行推断,团体来讲map函数的完成比each越发简约。
_.map = _.collect = function (obj, iteratee, context) {
// 由于在map中,第二个参数能够不是函数,所以用cb,这点和each的完成不一样。
iteratee = cb(iteratee, context);
// 假如不是类数组(是对象),则获取到keys
var keys = !isArrayLike(obj) && _.keys(obj),
length = (keys || obj).length,
results = Array(length);
// 这里依据keys是不是存在来推断传给iteratee是key照样index
for (var index = 0; index < length; index++) {
var currentKey = keys ? keys[index] : index;
results[index] = iteratee(obj[currentKey], currentKey, obj);
}
return results;
};
cb
我们来看看map函数中这个cb函数究竟是什么来源?
_.identity = function (value) {
return value;
};
var cb = function (value, context, argCount) {
// 假如value不存在
if (value == null) return _.identity;
// 假如传入的是个函数
if (_.isFunction(value)) return optimizeCb(value, context, argCount);
// 假如传入的是个对象
if (_.isObject(value)) return _.matcher(value);
return _.property(value);
};
cb函数在underscore中平常是用在遍历要领中,大多数状况下value都是一个函数,我们连系上面map的源码和例子来看。
- 假如value不存在,那就对应上面的_.map(obj)的状况,map中的iteratee就是_.identity函数,他会将背面吸收到的obj[currentKey]直接返回。
- 假如value是一个函数,就对应_.map(obj, func)这类状况,那末会再挪用optimizeCb要领,这里就和each的完成是一样的
- 假如value是个对象,对应_.map(obj, arrts)的状况,就会比较obj中的属性是不是在arr内里,这个时刻会挪用_.matcher函数
- 这类状况平常是用在_.iteratee函数中,用来接见对象的某个属性,详细看这里:iteratee函数
matcher
那末我们再来看matcher函数,matcher函数内部对两个对象做了浅比较。
_.matcher = _.matches = function (attrs) {
// 将attrs和{}合并为一个对象(防止attrs为undefined)
attrs = _.extendOwn({}, attrs);
return function (obj) {
return _.isMatch(obj, attrs);
};
};
// isMatch要领会对吸收到的attrs对象举行遍历,同时比较obj中是不是有这一项
_.isMatch = function (object, attrs) {
var keys = _.keys(attrs), length = keys.length;
// 假如object和attr都是空,那末返回true,不然object为空时返回false
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;
};
matcher是个高阶要领,他会将两次吸收到的对象传给isMatch函数来举行推断。起首是以attrs为被遍历的对象,经由过程对照obj[key]和attrs[key]的值,只需obj中的值和attrs中的不想等,就会返回false。
这里还会消除一种状况,假如attrs中对应key的value恰好是undefined,而且obj中并没有key这个属性,如许obj[key]和attrs[key]实在都是undefined,这里运用!==来比较必然会返回false,实际上二者应该是不想等的。
所以运用in来推断obj上究竟有无key这个属性,假如没有,也会返回false。假如attrs上面一切属性在obj中都能找到,而且二者的值恰好相称,那末就会返回true。
这也就是为何_.map([{name:’Kevin’}, {name: ‘Daisy’, age: 18}], {name: ‘Daisy’}); 会返回 [false, true]。
重写each
each和map完成道理基本上一样,不过map越发简约,这里能够用map的情势重写一下each
_.each = _.forEach = function (obj, iteratee, context) {
iteratee = optimizeCb(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;
iteratee(obj[currentKey], currentKey, obj);
}
return obj;
};
filter、every、some、reject
这几种要领的完成和上面的each、map相似,这里就不多做诠释了,有兴致的能够本身去看一下。