本文要解釋一下Javascript中的數組是如何工作的,你將會知道,它們比你想的更像普通對象.
1.概述
在Javascript中,對象是一個從字符串到值的映射.數組也是對象,只是包含有一些特殊的屬性:
-
數組索引(下標):如果一個數組對象的屬性的數字值(實際上是字符串值)是一個小於232-1的非負整數,則該屬性就會被看成是一個數組索引.
- "length"屬性:該屬性的值是一個非負整數,表示了數組的長度.這個長度的值通常是數組的最大索引轉換成數字后,再加1.
下面要說的這個表現有時候會讓人感到震驚,尤其對於那些剛剛從其他語言轉來的人,就是:JavaScript中的數組索引實際上是字符串.(在引擎內部,為了獲得更快的訪問速度,數組索引通常是用數字來實現的.但是規范就是這么規定的,程序員們看到的表現也是這樣的).例如:
> var arr = ['a', 'b', 'c']; > arr['0'] 'a' > arr[0] 'a'
> 2 in [ 'a', 'b', 'c' ] true > 3 in [ 'a', 'b', 'c' ] false
2.稀疏數組
正如我們看到的,數組也是從字符串到值的映射.這就意味着數組可以有孔(hole), 一個有孔的數組稱之為稀疏數組(sparse array).稀疏數組中索引的個數小於length屬性的值.在使用數組字面量定義數組時,你可以通過在逗號前面不寫任何值來創建一個孔(最尾部的逗號會被忽略).數組的遍歷方法比如 forEach和 map會忽略掉數組中的孔 .下面,讓我們比較一下稀疏數組和密集數組(dense array):> var sparse = [ , , 'c' ]; > var dense = [ undefined, undefined, 'c' ]; > 0 in sparse false > 0 in dense true > for(var i=0; i<sparse.length; i++) console.log(sparse[i]); undefined undefined c > for(var i=0; i<dense.length; i++) console.log(dense[i]); undefined undefined c > sparse.forEach(function (x) { console.log(x) }); c > dense.forEach(function (x) { console.log(x) }); undefined undefined c > sparse.map(function (x,i) { return i }); [ , , 2 ] > dense.map(function (x,i) { return i }); [ 0, 1, 2 ] > sparse.filter(function () { return true }) [ 'c' ] > dense.filter(function () { return true }) [ undefined, undefined, 'c' ]
3.數組索引
關於什么樣的屬性才能稱之為數組索引,ECMAScript規范有這樣的定義.一個字符串s必須滿足下面兩個要求,才能成為數組的索引:
要求1: 字符串s 解析成為一個無符號32位整數之后,再轉換成字符串的值必須要和s相同.
要求2: 字符串s 解析成為整數之后的值必須小於232−1 (數組的最大長度).
要求2: 字符串s 解析成為整數之后的值必須小於232−1 (數組的最大長度).
因此,如果按照數值大小比較,一個數組的索引s必須滿足下面的范圍表達式:
上面的要求1表明了:雖然很多字符串都可以被轉換成一個無符號32位整數,但只有其中的少數可以作為合法的數組索引.比如:
上例中只有第一條語句中參數"0"滿足了要求1,是個有效的數組索引.
0 ≤ s < 2 32−1ToUint32是一個規范內部的方法,它可以將其他的值轉換成無符號32位整數.你也可以使用JavaScript代碼來實現這個內部方法 [1]:
function ToUint32(x) { return x >>> 0; }
> ToUint32('0') 0 > ToUint32('00') 0 > ToUint32('03') 3 > ToUint32('abc') 0 > ToUint32(Math.pow(2,32)+3) 3
所有不滿足數組索引要求的字符串都會被看成普通屬性:
> var arr = ['a', 'b', 'c']; > arr['0'] 'a' > arr['00'] undefined
4.length
譯者注:很巧,上周我剛剛寫過一篇文章: JavaScript:數組的length屬性
數組length屬性的值的范圍是:
0 ≤ l ≤ 2 32−1 (32位)
4.1 索引屬性的影響
在有新的元素添加時,數組的length屬性會自動增大:
上面所有帶星號的length賦值是有效的,其他的都是無效的:
你可以測試一下:
如果你設置一個超大的length屬性,也會拋出異常:
> var arr = []; > arr.length 0 > arr[0] = 'a'; > arr.length 1
4.2 減小length屬性
如果length屬性當前的值為 l,被賦一個新的值 l',且 l' 比原值l小,那么在下面范圍內的索引都會被刪除.l' ≤ i < l例如:
> var arr = [ 'a', 'b', 'c' ]; > arr.length 3 > 2 in arr true > arr.length = 2; 2 > 2 in arr false
4.3 增大length屬性
增大length屬性的值會創建一個稀疏數組:> var arr = ['a']; > arr.length = 3; > arr [ 'a', , ,]
4.4 length屬性的有效值
你可以給length屬性賦任何值,引擎內部會使用 ToUint32方法將所賦的值轉換成數字,轉換成的數字必須滿足length屬性值的合法范圍.例如:> ToUint32('0') //* 0 > ToUint32('000') //* 0 > ToUint32('-1') 4294967295 > ToUint32(Math.pow(2,32)-1) //* 4294967295 > ToUint32(Math.pow(2,32)) 0 > ToUint32('abc') 0
> Number('0') 0 > Number('000') 0 > Number('-1') -1 > Number(Math.pow(2,32)-1) 4294967295 > Number(Math.pow(2,32)) 4294967296 > Number('abc') NaN
> [].length = -1 RangeError: Invalid array length > [].length = Math.pow(2,32) RangeError: Invalid array length > [].length = 'abc' RangeError: Invalid array length
5.數組實例
數組的對象實例和普通對象非常類似,只是在定義下面兩種屬性時會有一些額外的操作:- 數組索引:可能會增大length屬性的值.
- "length"屬性:過大的話會拋出異常,如果新值小於舊值的話會刪除超出的元素.
6.超出限制
如果你使用了一個不在索引范圍內的索引的話,會發生什么?答案就是該索引會被看成一個普通屬性.比如我們設置一個過大的索引值.> var arr = ['a', 'b']; > arr[Math.pow(2,32)-1] = 'c'; > arr [ 'a', 'b' ] > arr.length 2 > arr[Math.pow(2,32)-1] 'c'
> var arr = new Array(Math.pow(2,32)-1) // max length > arr.push('x') RangeError: Invalid array length
譯者注:這個地方有個讓人吃驚的表現,我剛好剛講過:JavaScript:數組能越界?
7.建議
使用數組時的兩個建議:- 假裝數組索引就是數字.這正是引擎內部的實現方式,而且這也是ECMAScript未來的大方向.
- 在對待數組時不要太過聰明.只需要遵循標准的處理模式,引擎通常會知道你想干什么,從而進行對應的優化.不需要你特殊處理.文章“Performance Tips for JavaScript in V8” (作者是Chris Wilson)就講了幾個數組操作相關的建議.