模拟jQuery实现类数组对象
jQuery最令人惊赞的东西就是那个类数组对象,亦即俗话中的jQuery对象。注意,在jQuery类库中,jQuery是作为命名空间而存在的函数。它拥有许多静态方法,由于函数也是对象,对象就有原型,而jQuery的原型方法是异常频繁地调用它的静态方法。它的第一个原型方法叫init,这是受Prototype类库影响的结果。init方法也可以作为构造函数使用,它new出来的实例就是jQuery对象。在jQuery命名空间的原型中,也有一个叫jquery的属性,起着其他类库版本号的作用,由于名字特殊,也用作识别是否为jQuery对象的作用。换言之,这里有三处可以集中添加方法与属性的地方,jQuery命名空间,用于添加静态方法,jQuery的原型,用于添加原型方法,只要依赖extend来添加,jQuery.prototype.init实例(jQuery对象),它的属性比较少,只有length,prevObject,selector与context。至于jQuery.prototype.init实例的原型,都是往jQuery.prototype里搬。接着下来,我们沿着jQuery的思路,实现一个类数组对象吧。
首先我们要定义一个类,我称之为eve,它其实是个工厂。
var eve = function(){ return new eve.prototype.init(arguments); }
如果是数组,传入一个数字,会返回与数字值相等长度的空数组,如果传入几个参数,这几个参数会成为返回数组的元素,如果什么都不会就返回空数组(这种方式一定要使用new操作符)。嘛,由于我们的类数组,用不着百分之一百模拟,就按其方式二生成类数组。
类数组要求拥有length属性与索引,length容易办,但索引我们要一个个赋值,如果在最前面添加新的元素时,就要重排索引,这很要命,效率很低。不过我们可以用数组的push方法实现。
setArray : function(els) { this.length = 0;//设置length以及重排索引 Array.prototype.push.apply(this, els); return this; },
为了确保用得了setArray方法,我们必须保证els为数组,因为里面用到 Array.prototype.push.apply(this, els),看到Array没有?至于push方法,简单,保证调用者拥有length属性就行了。 下面的makeArray,就是将非数组对象转换为数组对象,就算为空,也要用[]包裹起来。
makeArray : function( arr ) {//把传入参数变成数组 var ret = []; if( arr != null ){ var i = arr.length; //单个元素,但window, string、 function有 'length'的属性,加其它的判断 if( i == null || arr.split || arr.setInterval || arr.call ){ ret[0] = arr; }else{ try{ ret = Array.prototype.slice.call(arr) }catch(e){ while( i ) ret[--i] = arr[i];//Clone数组 } } } return ret; },
有了这两个方法,我们就可以写我们的构造方法了。
init : function(obj){ this.setArray(this.makeArray(obj)); return this; },
但仅仅是这样无法new实例的,因为这里的this出了些状况。前两个this为eve的实例,第三个返回的this为eve.prototype.init的实例。
看到没有?报错找不到makeArray方法,换言之eve.prototype.init实例无法调用eve的原型方法。如果是jQuery,就是jQuery对象无法调用其jQuery命名空间对象的原型方法。解决方法很简单,直接把它们两个的原型重叠起来。为其命名空间对象的原型添加方法就是为其真身的原型添加方法。
eve.prototype.init.prototype = eve.prototype;
我们再向它添加一些方法,看到底能仿真到什么程序。其实上面已经能用索引取值了,即e[1],会弹出2。但toString却是[object Object],而是不数字的序列。在jQuery中有个get()方法,如果不加参数,能取出里面的数组,我们把它的toString方法赋其eve.prototype就是。
toString : function(){//返回一个字符串 var array = Array.prototype.slice.call( this ); return array.toString(); }, get: function( num ) { return num === undefined ? Array.prototype.slice.call( this ) : this[ num ]; }
很好,现在如果不用instanceof与typeof判断,光凭感觉,是分辩不出它是数组与类数组了。我们再给添加一些数组方法。
shift :[].shift, push: [].push, sort: [].sort, pop: [].pop, splice: [].splice, concat: [].concat, slice: [].slice, constructor:eve, //******************** eve.toString = function(){ return "function Array(){\n [variant code]\n}" }
在jQuery中,它还实现eq方法与index方法来取得元素或元素的索引值,还实现javascript1.6的几个迭代器,each(forEach),map,filter。这样高度仿真的类数组对象让jQuery类库轻易实现了链式操作。但显然,它的filter与map方法比我们在网上找到的实现复杂多了,因为早在jQuery1.0.1中就搞了pushStack方法,用于保存执行push,pop等破坏性操作前的数组对象。
可能有人对init方法感觉非常不爽,因为它的两种this导致了需要用到两个原型。那是jQuery的init除了有生成jQuery对象的能力,还能充当domReady用。如果是单纯的类数组,我们完全可以去掉它,但生成类数组对象时,我们就一定要用new操作符了。
var eve = function(){ this.setArray(this.makeArray(arguments)); return this; } eve.prototype = { isArray : function( obj ) { return Object.prototype.toString.call(obj) === "[object Array]"; }, setArray : function(elems) { this.length = 0;//设置length以及重排索引 Array.prototype.push.apply(this, elems); return this; }, makeArray : function( arr ) {//把传入参数变成数组 var ret = []; if( arr != null ){ var i = arr.length; //单个元素,但window, string、 function有 'length'的属性,加其它的判断 if( i == null || arr.split || arr.setInterval || arr.call ){ ret[0] = arr; }else{ try{ ret = Array.prototype.slice.call(arr) }catch(e){ while( i ) ret[--i] = arr[i];//Clone数组 } } } return ret; }, inArray : function(elem, array) { for (var i = 0, length = array.length;i < length; i++) // Use === because on IE, window == document if (array[i] === elem) return i; return -1; }, index:function(el){return this.inArray(el,this)}, toString : function(){//返回一个字符串 var array = Array.prototype.slice.call( this ); return array.toString(); }, valueOf:function(){return Array.prototype.slice.call( this );}, shift :[].shift, push: [].push, sort: [].sort, pop: [].pop, splice: [].splice, concat: [].concat, slice: [].slice, constructor:eve, get: function( num ) { return num === undefined ? Array.prototype.slice.call( this ) : this[ num ]; } } eve.toString = function(){ return "function Array(){\n [variant code]\n}" }