Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

underscore数组遍历函数分析(一) #4

Open
yinguangyao opened this issue Mar 19, 2018 · 0 comments
Open

underscore数组遍历函数分析(一) #4

yinguangyao opened this issue Mar 19, 2018 · 0 comments

Comments

@yinguangyao
Copy link
Owner

underscore数组遍历方法分析(一)

这是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。

最后的三个判断分别是:

  1. 如果obj的类型是数组,那么返回true
  2. 如果obj的length是0,也返回true。即使是{length: 0}这种情况,因为在调用isArrayLike的each和map等方法中会在for循环里面判断length,所以也不会造成影响。
  3. 最后这个(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的源码和例子来看。

  1. 如果value不存在,那就对应上面的_.map(obj)的情况,map中的iteratee就是_.identity函数,他会将后面接收到的obj[currentKey]直接返回。
  2. 如果value是一个函数,就对应_.map(obj, func)这种情况,那么会再调用optimizeCb方法,这里就和each的实现是一样的
  3. 如果value是个对象,对应_.map(obj, arrts)的情况,就会比较obj中的属性是否在arr里面,这个时候会调用_.matcher函数
  4. 这种情况一般是用在_.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类似,这里就不多做解释了,有兴趣的可以自己去看一下。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant