mapdigit 2014-12-01
很疑惑的一个概念,或者大家很少听说过,网上 关于伪数组的文章页比较少,但是我们每天都在不知不觉的使用的这伪数组,这里收集一些网上关于伪数组的文章和自己对伪数组的一些概念的理解 jQuery 和argument 是我们最常见的两个伪数组,下边是一个例子,看一下jQuery 中伪数组的用法
var $div=$("#tag"); console.log($div); console.log($div.length); console.log($div.selector); console.log($div[0]); console.log($div["selector"]); console.log($div["length"]); console.log($div["length"].slice(0,1))
你可以注意到最后一个结果就是使用数组的属性slice 的时候是报错误的,好像伪数组又无法正常的使用数组的方法,这里可以对伪数组进行一个简单的概述就是对象冒充数组,有了数组的形态其实就是有了length的概念,但是并不能真正使用数组的方法。下边介绍一下js中数组的概念
JavaScript没有真正的数组,因此typeof运算符不能辨别数组和对象。伪数组在JavaScript中有很高的易用性,程序员不用给它设置维度,而且永远不用担心产生越界错误,但JavaScript数组的性能相比真正的数组可能更糟糕。要判断一个值是否为数组,必须使用constructor属性,例如:
if(value && typeof value === 'object' && value.constructor === Array) {
}
arguments不是一个数组,它是一个带有length成员属性的对象,很多时候会把它理解为一个伪数组。
使用上面的检测方法会将arguments识别为一个数组,有时候这是希望得到的结果,尽管arguments不包含任何数组的方法。
还有一种关于伪数组的解释能通过Array.prototype.slice转换为真正的数组的带有length属性的对象。
这种对象有很多,比较特别的是arguments对象,还有像调用getElementsByTagName,document.childNodes之类的,它们都返回NodeList对象都属于伪数组。
我们可以通过Array.prototype.slice.call(fakeArray)将伪数组转变为真正的Array对象。
在我看来两者的说法大致相同都是一个对象,但是有个数组length属性,那么他在js的编辑器下就能看起来像一个数组就像你用console 打出一个伪数组,结果是一个
[ div:"", content:"", length:1, proto: ]
var fakeArray01 = {0:'a',1:'b',length:2};//这是一个标准的有伪数组对象 var arr01 = Array.prototype.slice.call(fakeArray01); alert(arr01[0]);//a var arr02 = [].slice.call(fakeArray01); alert(arr02[0]);//a
slice 可以用来获取数组片段,它返回新数组,不会修改原数组。
示例中可以看到fakeArray被成功的转换成了Array对象。也许大家对Array.prototype.slice.call这种写法比较陌生,其实我们也可以通过[].slice.call这种形式实现同样的效果,那为什么我们要通过prototype的形式实现呢,答案是以prototype的形式执行程序效率更高,同样代码也更加优美。
伪数组的实现
让我们再深入的看一下伪数组的实现。
我们来看一些特殊的用例:
var fakeArray01 = {a:'a',b:'b',length:2};//没有length下标对应的值 var arr01 = Array.prototype.slice.call(fakeArray01); alert(arr01[0]);//undefined var fakeArray02 = {0:'a',1:'b',length:'num'};//length不是数值 var arr02 = Array.prototype.slice.call(fakeArray02); alert(arr02[1]);//undefined
同样fakeArray01和fakeArray02被转换成了真正的数组,但是数组中的值都为undefined
查看 V8 引擎 array.js 的源码,可以将 slice 的内部实现简化为:
function slice(start, end) { var len = ToUint32(this.length), result = []; for(var i = start; i < end; i++) { result.push(this[i]); } return result; }
可以看出,slice 并不需要 this 为 array 类型,只需要有 length 属性即可。并且 length 属性可以不为 number 类型,当不能转换为数值时,ToUnit32(this.length) 返回 0.
根据以上结论可以得出:fakeArray01被转换成了lenth为2的数组,其值都被初始化为undefined,fakeArray02被转换成了length为0的数组,自然访问下标为1的元素返回undefined
针对于标准浏览器slice实现已经可以解释所有的问题,但是IE在处理NodeList时出现了问题。IE中无法将NodeList转换为真正的数组,会出错。这又是为什么呢?严格说,在IE内部定义了一个抽象类Arraioid,Array和Arguments都继承与此,所以可以用slice。但DOM对象是通过COM接入到JScript的,slice检测的时候失效。
Jquery内部大量运用了伪数组。可以说整个Jquery对象,都是构建在伪数组的基础之上的,好让我们来看一些Jquery的实际运用:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>fakeArray</title> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <script src="jquery-1.4.2.js" type="text/javascript"></script> <script> $(document).ready(function(){ var body = $("body"); alert(body.get(0).tagName); }); </script> </head> <body> <div id="test"></div> </body> </html>
再简单不过的程序了,好,让我们来看一下其内部的实现原理:
jQuery.fn = jQuery.prototype = { init: function( selector, context ) { var match, elem, ret, doc; // Handle $(""), $(null), or $(undefined) if ( !selector ) { return this; } // Handle $(DOMElement) if ( selector.nodeType ) { this.context = this[0] = selector; this.length = 1; return this; } // The body element only exists once, optimize finding it if ( selector === "body" && !context ) { this.context = document; this[0] = document.body; this.selector = "body"; this.length = 1; return this; } //... ... }, get: function( num ) { return num == null ? // Return a 'clean' array this.toArray() : // Return just the object ( num < 0 ? this.slice(num)[ 0 ] : this[ num ] ); } }
最后,我们来解释一下,程序的执行细节.但是在这之前,还得说一下关于Jquery的内部的一些东西。
用过Jquery的用户应该都知道$()函数,它是Jquery的选择器代表。我们可能通过$()函数去选取页面中的元素(具体语法可参数Jquery帮助文档)。实际上当我们执行$()函数时,程序去执行上面列出的init方法,我们来看一下在调用$(document)时所发生的事件:
//$(document) init: function( selector, context ) { var match, elem, ret, doc; // Handle $(DOMElement) : 处理DOM元素, if ( selector.nodeType ) { this.context = this[0] = selector; //给属性0赋予selector值,此时就是document对象 this.length = 1; //创建伪数组,更新下标 return this; //返回Jquery对象 } //... ... }
$("body")是同样的道理,不再多说了。
我们知道Jquery里所有的操作返回的都是Jquery对象,那我们如何得到其所对应的dom对象呢,Jquery为我们提供了一个get方法,这是专门用来从jquery对象中取得DOM对象用的,由此,便有了body.get(0),那为什么又是get(0)而不是get()呢,因为Jquery的所有操作都是针对于数组进行的。所以,在get方法里,我们要传一个下标值,来得到具体的元素。现在该看get方法的具体实现了:
get: function( num ) { return num == null ? //如果没有num,则直接返回DOM数组 this.toArray() : //如果指定的num,则返回指定下标的元素 //this.slice是jquery的另一个方法,它内部其实还是调用Array.prototype.slice来实现将伪数组转换为真实的数组 ( num < 0 ? this.slice(num)[ 0 ] : this[ num ] ); }
关于伪数组就到这吧,我想应该已经差不多了。
补充:
最近在一个项目中用到伪数组
场景 var resourceIDMapping={
1:"机票",
2:"酒店",
3:"签证",
4:"保险",
5:"交通",
6:"景点",
7:"餐饮",
8:"会议",
9:"设备",
10:"其他",
11:"导游",
12:"领队",
13:"火车",
14:"门票",
"length":14
};
这样一个伪数组,因为后台定义类型的时候下标并不是从零开始,
var resourceIDArray=[].slice.call(resourceIDMapping); 转换成一个数组,
调用的时候是这样<%=resourceIDArray[parseInt(d.ResourceID)]%>,调用的时候大部分都是正常,只有ResourceID=14 的时候 无法正常显示内容,无论怎么修改都不能正常显示,百思不能其解,最后发现把length 修改成15就正常,但是实际上内容只有14项,原因是resourceIDArray 是首先作为一个数组处理,视为已经越界,不会去真正拿对象中14:"" 对应的内容