JavaScript 稀疏数组与孔(hole)

在绝大多数JavaScript的实现中,数组是稀疏的,我们可以认为js的数组都是稀疏的(虽然ES标准并没有这样规定)。

稀疏数组是什么

稀疏数组与密集数组最大的不同,就是稀疏数组中可以有“孔”(hole)。孔是逻辑上存在于数组中,但物理上不存在与内存中的那些数组项。在那些仅有少部分项被使用的数组中,孔可以大大减少内存空间的浪费。比如,我们要表示一个长度为10000的数组,它的最后一个项是字符串'a'。如果按照密集数组的做法,我们需要开辟10000个项的空间,有9999个项的空间都被浪费了。而如果按照稀疏数组的做法,稀疏数组只需要记录:“数组第10000个项的值为'a'”,这节省了很多内存空间。

JavaScript数组天生就是稀疏数组

js数组就是若干个下标(数字)与值之间的映射。从下标x到值y的映射表示:“数组第x个项的值为y”。这实际上就是上例中稀疏数组的记录方法。

JavaScript 稀疏数组与孔(hole)_第1张图片
在Chrome控制台的执行结果

如上图,如果你调用 new Array(3),你得到的数组中只有一个属性length,记录了它的长度,但是 没有任何下标(数字)与值之间的映射。这是一个只有3个孔的数组。

JavaScript 稀疏数组与孔(hole)_第2张图片

如上图,如果你继续执行 a[1] = 'aaa',那么实际上是在这个稀疏数组中增加了一条 从1到"aaa"之间的映射

JavaScript 稀疏数组与孔(hole)_第3张图片

如上图,如果你继续执行 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会跳过孔,所以实际上回调函数没有被调用过
JavaScript 稀疏数组与孔(hole)_第4张图片
a1有孔

正确的方法:

var a2 = Array.apply(null, Array(3))
// [undefined, undefined, undefined]
a2.map(function (x, i) { return i })
// [0, 1, 2]
// map的回调函数执行了3次
JavaScript 稀疏数组与孔(hole)_第5张图片
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]。


参考资料

http://2ality.com/2012/06/dense-arrays.html
http://2ality.com/2013/07/array-iteration-holes.html
http://2ality.com/2013/11/initializing-arrays.html
http://2ality.com/2015/09/holes-arrays-es6.html

你可能感兴趣的:(JavaScript 稀疏数组与孔(hole))