這篇文章來回答javascript通用循環遍歷方法forEach中最後提到的關於偽數組的問題。
什麼是偽數組
能通過Array.prototype.slice轉換為真正的數組的帶有length屬性的對象。
這種對象有很多,比較特別的是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的形式實現呢,答案是以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
IE的問題
針對於標准浏覽器slice實現已經可以解釋所有的問題,但是IE在處理NodeList時出現了問題。IE中無法將NodeList轉換為真正的數組,會出錯。這又是為什麼呢?嚴格說,在IE內部定義了一個抽象類Arraioid,Array和Arguments都繼承與此,所以可以用slice。但DOM對象是通過COM接入到JScript的,slice檢測的時候失效。
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="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 ] );
}
關於偽數組就到這吧,我想應該已經差不多了。
注:有機會的話,將來可能會出一個"超越Jquery"系列,專門分析Jquery內部執行細節。但是由於Jquery內部的有各種歪門邪道的手法還不是很理解,所以這是將來的問題了。
參考:
http://lifesinger.org/blog/2010/05/array-prototype-slice/