JavaScript 稀疏数组与孔(hole)

作者: csRyan | 来源:发表于2017-08-22 00:55 被阅读305次

    在绝大多数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]。


    参考资料

    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)

        本文链接:https://www.haomeiwen.com/subject/hslkdxtx.html