[译]JavaScript中的数组
本文要解释一下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 < 232−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 ≤ 232−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)就讲了几个数组操作相关的建议.