jQuery中each方法原理浅析

在引用类型中,话说数组操作貌似比对象的操作,有更高的效率,所以,在jQuery这样的框架中,都是把目标集合,变成一个数组进行操作,有时候,也是类数组。

概况

在我们最常用的jQuery方法中,我觉得类似addClass,removeClass,css等,都是把对一个目标集合进行的操作,也就是对一个类数组进行的操作。

之所以说是一个类数组,那是因为,这个集合,它和数组的相似之处只有两点:有length属性,可以根据length判断集合中,待处理的元素的个数;可以通过obj[num]的方法,访问集合中的第num个元素。之所以说,这个集合是类数组,还因为,它并不是继承自Array对象,所以不包含slice,splice,sort,reverse等数组自带的方法,所以称之为“类数组”。

既然是对一个集合进行操作,那么在源码的实现中,就需要对这个集合中的每一个元素进行操作,所以jQuery中,也就定义了each方法,也就是遍历集合中的每一个元素,并对这个集合中的元素,进行操作。

ES5中,也添加了一个新的方法,来对数组中的每一个元素执行一个操作,就是forEach方法,后面说明。

jQuery中的each方法

jQuery中的each方法,在jQuery框架中,each方法分别处于jQuery的静态方法(其实也算是对象吧,所以后面就以jQuery对象来代表这个意思)中,和jQuery的实例中,如果对于jQuery对象和jQuery实例分不太清的,请参考:jQuery的链式操作原理简介,这里就简单的写一下,两种each的分别是怎么调用的:


jQuery.each();//这each就是处于jQuery对象的each方法
jQuery("p").each();//这里,就是实例中的each方法了。
//因为,jQuery("p")就是返回的一个实例,可以参考刚刚提到的那篇文章。


因为实例中的each是基于jQuery对象中的each方法实现的,所以,这里我们主要先看下jQuery对象中each方法的实现。

那么,在看jQuery源码中实现each方法之前,我们是不是也可以先自己想想,如果自己来实现这个方法,应该怎么样实现呢?其实这个方法说起来,也算是一个很基础的方法了,首先,它需要一个待操作的对象(数组或者类数组),其次,就是对其中的元素,进行怎么样的操作。

Ok,我们就先考虑着两种输入的情况,那么我们是不是就可以实现一个each方法,下面就是我写的一种最简单的each实现方法:


function each(obj,callback){
    //obj为待操作的对象,类型为数组或者类数组
    //callback为对每一个元素,所需要执行的操作
	
    var i,len=obj.length;
	
    if(!obj){
	return [];
    }
	
    if(typeof len != "number" || !obj[len-1]){
	//判断是否为数组或者类数组
	//这里的判断比较简单,有些不严谨,
	//后面我们看下jQuery中,是如何判断一个引用类似是否为类数组的
	//如果不为数组,也不会类数组,那么直接返回obj
	return obj;
    }
	
    //如果为类数组或者数组,则执行循环处理每一个元素
    for(i=0;i<len;i++){
	//对每一个元素,执行callback方法
	//callback传入的参数,可以使元素的位置和元素的值。
	//然后把返回值覆盖原来的值
	obj[i] = callback(obj[i],i);
    }

    return obj;
}

var a = [1,2,3,4];
var ca = function(item,index){
    if(index < 2){
	item = item*2;
    }
    return item;
}

each(a,ca);
console.log(a); //[2,4,3,4]


好吧,我这里写的这个方法,改变了原来的数组,这是为了能更明显的看出来,确实是遍历了整个数组啊,对吧。OK,上面的简单方法看完了,接下来,再看看jQuery中,是怎么实现each方法的呢,还是看源代码吧。


//因为之前刚说了,看看类数组是怎么判断的,并且,在jQuery的each方法中,
//也做了这个判断,所以,先看看数组和类数组的判断吧。

function isArraylike( obj ) {
    //这里,数组和类数组都会返回true,其他类型返回false
    var length = obj.length,
	type = jQuery.type( obj );
	//获取obj的type值,

    if ( jQuery.isWindow( obj ) ) {
	//不能为window对象
	return false;
    }

    if ( obj.nodeType === 1 && length ) {
	//如果obj是Element对象,并且存在length属性
	//则判定位数组,可以算作类数组。
	//话说,这个是在判断哪种类型呢?
	//Element类型,正常是没有length属性的,人为添加可以执行到这里
	//而一些有length属性的,比如getElementsByTagName却没有nodeType属性
	//所以,这里暂时想不到为什么有这个判断,是什么情况下,
	//才会执行这个判断呢?
		
	return true;
    }

    //下面的判断条件:
    //1,type=array,真正的数组,返回true
    //2,type!=function,并且,obj有length属性,并且可以使用length属性
    //在obj取出对应的属性值。
    //这里为了防止obj[length-1]的值为undefined,false等,所以才使用的
    //(length-1) in obj的判断方法。
    //其他的,都返回false
    return type === "array" || type !== "function" &&
	( length === 0 ||
	typeof length === "number" && length > 0 && ( length - 1 ) in obj );
}


这里顺便说下类数组吧,类数组在原生的JS中,也是能经常碰到的,比如我们最常使用的getElementsByTagNamequerySelectorAllchildrendom方法获取子元素)等,获取到的结果,都是一个类数组。

也可以这么说,当获取页面元素时,如果返回的不是一个单一的元素,而是一组符合一定要求的集合时,一般就是类数组(我目前还想不到例外的情况),比如比较常见的NodeList对象,HTMLCollection对象,就都是属于类数组的。

类数组的标准呢,就是:

1:有length属性,并且length是一个数值,并且这个属性代表了这个集合的大小.

2:可以通过类似数组的中括号方式访问这个集合中的属性或者值,arr[num]

3:没有真正数组中的方法,比如slice,push,pop等原生Array对象的方法。

OK,说完了上面的这些简单介绍,下面看jQuery中的each方法的实现吧


each: function( obj, callback, args ) {
    var value,
	i = 0,
	length = obj.length,
	isArray = isArraylike( obj );

    //根据args,也就是第三个参数,分为两种逻辑
    if ( args ) {
	if ( isArray ) {
	    //如果是类数组,则使用数组的循环每一个元素
	    for ( ; i < length; i++ ) {
		//如果有args这个属性,那么callback方法的参数
		//调用的是args,callback方法的内部this指向obj[i];
		value = callback.apply( obj[ i ], args );

		//这里的在循环时,如果callback返回了一个false,
		//那么就会终止循环,所以这里要注意,在回调函数中
		//除非是要接受遍历,否则千万不要随意的返回false哦
		if ( value === false ) {
		    break;
		}
	    }
	} else {
	    for ( i in obj ) {
			
	        //当obj不为类数组时,则以对象的取值方法,进行处理。
	        value = callback.apply( obj[ i ], args );

		if ( value === false ) {
		    break;
		}
            }
	}

    // A special, fast, case for the most common use of each
    } else {
	//如果没有传入第三个参数,则执行该逻辑
	if ( isArray ) {
	    for ( ; i < length; i++ ) {
		//如果为数组,则按照数组的方式进行遍历
		//callback传入的参数分别为i和obj[i]
		//callback中的this指向obj[i]
		value = callback.call( obj[ i ], i, obj[ i ] );

		if ( value === false ) {
		    //同样,如果返回值value=false,则结束整个遍历
		    break;
		}
	    }
	} else {
	    for ( i in obj ) {
		value = callback.call( obj[ i ], i, obj[ i ] );

		if ( value === false ) {
		    break;
		}
	    }
	}
    }

    //返回obj
    return obj;
}


看完each的源码,感觉其实是很简单的逻辑,这里主要说一下以后使用时,需要注意的地方:

1:上面刚说的这个each方法是定义在jQuery对象上的,即,如果调用该each方法,需要使用jQuery.each(obj,callback,args);方法调用。被遍历的类数组是objcallback是对每一个数组的回调函数,被遍历的数组中的每一个元素,都会以参数的形式,传入callback函数,并且callback函数中的this,就是指向当前正在遍历的数组元素。

但是这里有一点的差别就是,如果jQuery.each方法时,第三个参数是否为空,将影响callback的形参的格式。看下面的一个简单的示例:


var obj = [1,2,3,4,5],
    args = [11,12,13];
	
function callback(index,item){
    console.log(arguments);
}

jQuery.each(obj,callback);
jQuery.each(obj,callback,args);


从这个例子的结果中可以看出一些,当传入了第三个参数时,回调函数的形参就是args,而当没有传入第三参数时,回调函数的形参就只有两个形参,index(数组中的第几个元素,从0开始),item(当前元素的值)。

2:之前说的这些,都是定义在jQuery对象上的静态方法,当然,也是jQuery实例中,可以对选择器获取的类数组进行遍历的each方法,是怎么基于上述的代码实现的,从上述的代码中,我们也看到了,each方法是对第一个传入的obj对象进行遍历的,那么在实例中的each方法,这就很明显了,所以有如下代码:


each: function( callback, args ) {
    return jQuery.each( this, callback, args );
},


jQuery的实例each方法,this是指代的需要遍历的数组的,所以。jQuery.fn.each方法中,只需要传入两个参数,回调函数callbackargs即可。只要需要的第一个参数,则传入this

当然,在调用该方法时,也要注意是否有第二个参数args的传入。如果有args方法时,则callback中的形参,和上面说的有相同的情形出现。所以要注意也要利用该方法,实现一些特殊的功能。

3:关于回调函数callback对于整个遍历的影响,还有一个需要特别注意的地方,那就是,如果callback显示的返回false,那么整个遍历就会结束,这个可以用来判断,在一组数据中,第一个符合要求或者不符合的元素,看下面这个简单的例子。


var obj = [1,2,3,4,5];
	
function callback(index,item){
    console.log(item);
    if(this > 3){
	return false;
    }
}


这样,只会在控制台打印出1,2,3,4而后面的5却不会再继续遍历下去了,所以,这里的return false不仅是结束了这个回调,也是结束了整个遍历。

扩展

jQuery中的each方法,说起来大家都是很熟悉的,这里只是对源码有一点点的说明,并且,因为在ES5中,数组操作新添加了很多的功能,包括someeverymapfilterforEach。它们的功能各有不同,只是这里说到了each方法,而each方法的功能和数组中的forEach方法,有着相近的功能,那就是遍历整个数组,只是其中有些不同而已,看下面的一个例子:


var obj = [1,2,3,4,5];
	
obj.forEach(function(item,index,arr){
    console.log("item="+item); //数组中的当前元素
    console.log("index="+index); //当前的排序
    console.log(arr);    //整个数组本身
    console.log(this == window);  //this指向window
});


不知道你是否注意到了,回调函数中的itemindex是和jQuery.fn.each(function(index,item){})相反的,并且,回调函数内部的this指向也是不同的,jQuery中的each中,this指向是当前正则遍历的数组元素,而forEach中的this指向是window的,所以,这些是我们平时需要稍微注意的一点地方。

至于ES5中,数组的新方法的确切的用法,这里不做说明。

感谢查看,如发现文中的问题,请指教,谢谢!

本文地址:http://www.zhangyunling.com/?p=218

发表评论

电子邮件地址不会被公开。 必填项已用*标注

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>