为什么JavaScript允许我们创建稀疏数组?

6
我想知道像 var foo = new Array(20)var foo = [1,2,3]; foo.length = 10 或者 var foo = [,,,] 这样的代码用途是什么(另外,为什么要使用 delete 操作符而不是从数组中直接删除元素)。正如您可能已经知道的那样,所有这些都会导致稀疏数组。
但是为什么我们被允许做上述的事情呢?为什么有人想要创建一个默认长度为20的数组(就像第一个例子中那样)?为什么有人想要修改和破坏数组的length属性(就像第二个例子中那样)?为什么有人会做像[ , , , ]这样的事情?为什么你会使用delete而不是仅仅从数组中删除元素?能否提供一些使用这些语句的用例? 我已经搜索了大约3个小时,但没有找到答案。大多数来源(2ality博客,JavaScript:权威指南第6版以及在Google搜索结果中出现的其他文章,例如“JavaScript稀疏数组”)都说稀疏数组是奇怪的行为,应该避免使用它们。我读过的所有来源都没有解释,或者至少试图解释,为什么我们首先被允许创建稀疏数组。除了《你不知道的JavaScript:类型与语法》,这本书讲解了JavaScript为什么允许创建稀疏数组:
一个数组有长度属性但没有明确值的插槽,这是JS中一种奇怪的外来数据结构,具有一些非常奇怪和令人困惑的行为。创建这样一个值的能力纯粹来自于旧的、已弃用的历史功能("类似数组的对象",如arguments对象)。
因此,该书暗示arguments对象以某种方式使用我上面列出的示例之一来创建稀疏数组。那么,arguments在何处和如何使用稀疏数组?
还有一个让我感到困惑的地方就是书中的这部分内容:“JavaScript:The Definitive Guide 6th Edition”的这句话:“足够稀疏的数组通常以比密集数组更慢、更节省内存的方式实现”。在稀疏数组的上下文中,“更节省内存”看起来像是“更慢”的矛盾之处,那么两者之间的区别是什么?这里是该书特定部分的链接

1
我认为“纯粹来自旧的、弃用的、历史功能”已经涵盖了它。 ;) - Alan Larimer
1
答案很简单,就是向后兼容或内存控制。 - Shardj
1
@Shard 这就是其他文章所说的。那么,向后兼容性是为了什么?它如何帮助内存控制?《JavaScript权威指南》中提到,“稀疏数组很慢,但更节省内存”,这是什么意思? - doubleOrt
1
我唯一使用过它是用来填充模板。考虑一个类似表格的结构,如果存在缺失值,则需要渲染空单元格。通过使用固定的数组长度(例如20),您可以保证使用简单循环'for each value in array'创建20个单元格。否则,您还需要包括一个值检查。'如果有值,则呈现值,否则呈现空单元格'。这只是一个例子,也可以以不同方式进行修复。 - Shilly
1
@Taurus 为旧网站提供向后兼容性,以便仍然使用该功能。至于内存控制,如果您知道您的数组中只需要20个值,则应指定该值。否则,后端的动态数组将猜测您的数组大小,并将其设置为比实际需要大数百或数千倍。 - Shardj
显示剩余6条评论
2个回答

0

因为JS数组是一种非常奇特的数据类型,当使用正确的工具时,它们不遵守时间复杂度规则,这可能会让人感到惊讶。我的意思是 for in循环或 Object.keys()方法。尽管我是一个非常注重功能的人,但在这里我更倾向于使用 for in 循环,因为它是breakable的。

在JS中,稀疏数组有一些非常有益的用例,例如在O(1)的时间复杂度下将项插入和删除已排序的数组,而不会破坏已排序的结构,如果您的值类似于限价订单簿中的数字。换句话说,如果您可以在键和值之间建立直接的数值关联。


0
我在想像 var foo = new Array(20), var foo = [1,2,3]; foo.length = 10 或者 var foo = [,,,] 这样的代码有什么用途。
理论上,人们通常使用稀疏数据结构的原因是相同的(不一定按重要性排序):内存使用(var x = []; x[0]=123;x[100000]=456; 不会占用 100000 个“插槽”),性能(例如,通过 for-in 或 reduce() 对前述 x 取平均值),以及方便(没有“硬”越界错误,无需显式增长/缩小);
话虽如此,从语义上讲,js 数组只是一个具有索引键和特殊属性“length”的特殊关联集合,满足其所有索引属性都大于它。虽然这是一个相当优雅的定义,但它的缺点是使得稀疏定义数组有些混乱和容易出错,正如你所注意到的那样。
但是为什么我们被允许做上述事情呢?

即使我们不允许定义稀疏数组,我们仍然可以将未定义的元素放入数组中,导致与稀疏数组相同的可用性问题。因此,假设有[0,undefined,...,undefined,1,undefined][0,...,1,]是一样的,这只会使数组更消耗内存并且迭代更慢。

足够稀疏的数组通常以比密集数组更慢、更节省内存的方式实现。更节省内存和更慢似乎是矛盾的

用于一般数据的“密集数组”通常被实现为一个连续的内存块,其中填充了相同大小的元素;如果添加更多元素,则继续填充内存块,如果用尽则分配新的内存块。由于重新分配意味着将所有元素移动到新的内存块中,因此该内存通常会过度分配以最小化重新分配的机会(类似于黄金比例乘以上次容量)。因此,这些数据结构通常是有序/本地遍历最快的(更加CPU/缓存友好),对于不可预测的插入/删除(对于足够大的N)最慢,并且具有高内存开销~ sizeof(elem)* N +未来元素的额外空间。

相比之下,“稀疏数组/矩阵/…”通过在内存中连接分散的较小内存块或使用某种“逻辑压缩”的密集数据结构的形式来实现;在任何一种情况下,由于明显的原因,内存消耗都会减少,但相对地,遍历它们需要更多的工作和更少的局部内存访问模式。
因此,如果相对于相同有效遍历的元素进行比较,稀疏数组消耗的内存要少得多,但比密集数组慢得多。然而,考虑到使用稀疏数据和在“零”上平凡地运算的算法,稀疏数组在某些情况下可能会更快(例如,将非常大的矩阵乘以少数非零元素...)。

内存使用(var x = []; x [0] = 123; x [100000] = 456;不会消耗100000个“插槽”)。但是为什么要这样做?为什么不只是说'var x = []; x.push(456);'?为什么要在特定索引处插入元素(远大于数组当前大小的索引)? - doubleOrt
假设我有一个数组 [1,2,3,4,5,6,7],由于某种原因,我想标记奇数,那么我应该使用 delete 在每个奇数索引(1、3、5、7)上进行标记,而不是像 "odd_number_was_here"false 这样的东西,因为这既可以帮助我标记我的索引,又可以帮助节省内存消耗。 - doubleOrt
还有一件让我感到困惑的事情是书中“JavaScript权威指南第六版”中的这部分内容:“足够稀疏的数组通常以比密集数组更慢、更节省内存的方式实现”。“更节省内存”和“更慢”似乎是矛盾的。这里是该书特定部分的链接。 - doubleOrt
@Taurus,“为什么要这样做?”因为某些接口可能需要这样做(考虑一个函数要求将一些1D数据呈现在图表中),或者因为这样做更方便(因为你真的想要一个长度为100000的向量,或者一个几乎为零的大矩阵)。我同意,在js代码中这是相当罕见的...正如其他人所说,我们都认为这是一种相当奇特且容易出错的功能;我的理由是禁止它不会给你带来任何好处,所以... - Massimiliano Janes
@Taurus,假设我有一个数组[...],在这种情况下,我看不到使用delete的理由;即使您需要删除所有奇数索引数字,我也会更有效率/优雅地将它们移动到数组的末尾,并通过相应地设置长度来全部删除它们。 - Massimiliano Janes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接