本文深入解析 JavaScript 中伪数组的概念与实现机制。通过 Array.prototype.slice.call 方法可将具备 length 属性的对象转换为真数组,并探讨其底层逻辑、IE 浏览器兼容性问题以及在 jQuery 源码中的核心应用。
JavaScript 伪数组概述
本文旨在解答 JavaScript 通用循环遍历方法 forEach 中提及的伪数组相关问题。
什么是伪数组
伪数组是指那些拥有 length 属性,并能通过 Array.prototype.slice 转换为真正数组的对象。
这类对象在开发中十分常见,其中较为典型的是 arguments 对象。此外,调用 getElementsByTagName、document.childNodes 等方法返回的 NodeList 对象也属于伪数组范畴。
我们可以利用 Array.prototype.slice.call(fakeArray) 这一技巧,将伪数组成功转换为真正的 Array 对象。
以下是一个具体的转换示例:
代码如下:
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 形式,因为这样执行效率更高,且代码结构更为优雅。
伪数组实现与 slice 原理
伪数组的实现
让我们再深入的看一下伪数组的实现。
接下来我们分析一些特殊的用例情况:
代码如下:
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 类型,当无法转换为数值时,ToUint32(this.length) 将返回 0。
基于上述结论可以推断:fakeArray01 被转换为了 length 为 2 的数组,其值均初始化为 undefined;而 fakeArray02 被转换为了 length 为 0 的数组,因此访问下标为 1 的元素时返回 undefined。
IE 兼容性问题
IE的问题
针对标准浏览器,slice 的实现已能解释所有问题,但在 IE 中处理 NodeList 时却出现了异常。IE 无法将 NodeList 转换为真正的数组,导致报错。原因在于:严格来说,IE 内部定义了一个抽象类 Arraioid,Array 和 Arguments 均继承自此,因此可以使用 slice。然而,DOM 对象是通过 COM 接入到 JScript 的,slice 在检测时失效。
jQuery 中的伪数组应用
Jquery与伪数组
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="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 ] );
} 关于伪数组的讨论就到这里,相信内容已经足够详实。
注:有机会的话,将来可能会出一个“超越 jQuery”系列,专门分析 jQuery 内部执行细节。但是由于 jQuery 内部有各种复杂手法尚不完全理解,所以这是将来的问题。
参考:
http://lifesinger.org/blog/2010/05/array-prototype-slice/