JavaScript稀疏数组与孔hole
csRyan 人气:1稀疏数组是什么
在绝大多数JavaScript的实现中,数组是稀疏的,我们可以认为js的数组都是稀疏的(虽然ES标准并没有这样规定)。
稀疏数组与密集数组最大的不同,就是稀疏数组中可以有“孔”(hole)。孔是逻辑上存在于数组中,但物理上不存在与内存中的那些数组项。在那些仅有少部分项被使用的数组中,孔可以大大减少内存空间的浪费。比如,我们要表示一个长度为10000的数组,它的最后一个项是字符串'a'。如果按照密集数组的做法,我们需要开辟10000个项的空间,有9999个项的空间都被浪费了。而如果按照稀疏数组的做法,稀疏数组只需要记录:“数组第10000个项的值为'a'”,这节省了很多内存空间。
JavaScript数组天生就是稀疏数组
js数组就是若干个下标(数字)与值之间的映射。从下标x到值y的映射表示:“数组第x个项的值为y”。这实际上就是上例中稀疏数组的记录方法。
在Chrome控制台的执行结果
如上图,如果你调用new Array(3),你得到的数组中只有一个属性length,记录了它的长度,但是没有任何下标(数字)与值之间的映射。这是一个只有3个孔的数组。
如上图,如果你继续执行a[1] = 'aaa',那么实际上是在这个稀疏数组中增加了一条从1到"aaa"之间的映射。
如上图,如果你继续执行a[10000]='bbb',也只不过是又增加了一条从10000到"bbb"之间的映射而已。length自动变为了10001,这符合我们的直觉。不存在映射关系,但又处在数组长度范围内的数组项,就是孔。此时,这个数组与长度为2的普通数组['aaa', 'bbb'],占用相同大小的内存空间。
JavaScript数组稀疏特性带来的“怪异现象”
slice会复制孔
var arr = [ 'a', , 'b' ] // ["a", undefined × 1, "b"] arr.slice(1,2) // [undefined × 1] arr.slice() // ["a", undefined × 1, "b"]
forEach、every会跳过孔(不对孔调用回调函数)
var arr = [ 'a', , 'b' ] // ["a", undefined × 1, "b"] arr.forEach(function (x, i) { console.log(i+'.'+x) }) // 0.a // 2.b arr.every(function (x) { return x.length === 1 }) // true
map不对孔调用回调函数,但是孔会保留
arr.map(function (x,i) { return i+'.'+x }) // [ '0.a', undefined × 1, '2.b' ]
filter不对孔调用回调函数,但是孔会被过滤掉
arr.filter(function (x) { return true }) // [ 'a', 'b' ]
join会将孔转化为一个空字符串进行拼接,与undefined一样
arr.join('-') // 'a--b' [ 'a', undefined, 'b' ].join('-') // 'a--b'
而其他所有的数组方法会正常对待孔,就像数组中真的存在这个“空位”一样:
var arr2 = arr.slice() arr2.sort() // [ 'a', 'b', undefined × 1 ]
初始化无孔数组的方法
因为数组中的孔会造成上述的那些“怪异现象”,所以我们有时希望初始化一个没有孔的数组。
比如我们希望初始化[0,1,2]这样的数组,但是我们无法通过new Array(3)与map方法得到:
var a1 = new Array(3) // [undefined × 3] a1.map(function (x, i) { return i }) // [undefined × 3] // 因为map会跳过孔,所以实际上回调函数没有被调用过
a1有孔
正确的方法:
var a2 = Array.apply(null, Array(3)) // [undefined, undefined, undefined] a2.map(function (x, i) { return i }) // [0, 1, 2] // map的回调函数执行了3次
a2无孔
[undefined × 3]和[undefined, undefined, undefined],chrome控制台用这两种表示方式来区分孔和真正的undefined值!
从上面两幅图的对比可以看出,第一种方法没有构造出映射,只创造出了3个孔。而第二种方法创建出了真正的“从下标到值之间的映射”,映射的值为undefined。因此map不会跳过这些数组项。
Array.apply(null, Array(n))的原理
为什么var a2 = Array.apply(null, Array(3))能创造出无孔的数组呢?
我们将一个含有3个孔的数组作为第二个参数传递给apply,apply将利用这个数组来决定调用Array()的参数。
因为apply将数组中的孔视为undefined,所以Array调用的参数实际上为Array(undefined, undefined, undefined)。
又因为通过Array(a,b,c)这种方法调用Array会返回[a,b,c],所以Array(undefined, undefined, undefined)返回的是[undefined, undefined, undefined]。
参考资料
https://2ality.com/2013/11/initializing-arrays.html
https://2ality.com/2013/07/array-iteration-holes.html
加载全部内容