【问题标题】:Why are we allowed to create sparse arrays in JavaScript?为什么我们可以在 JavaScript 中创建稀疏数组?
【发布时间】:2017-10-02 13:32:49
【问题描述】:

我想知道像 var foo = new Array(20)var foo = [1,2,3]; foo.length = 10var foo = [,,,] 这样的代码的用例是什么(另外,为什么要使用 delete 运算符而不是仅仅从数组中删除项目) .您可能已经知道,所有这些都将导致稀疏数组。

但是为什么我们被允许做上述事情呢?为什么有人要创建一个默认情况下length20 的数组(如第一个示例所示)?为什么有人要修改和破坏数组的length 属性(如第二个示例所示)?为什么有人想做[, , ,] 之类的事情?为什么要使用 delete 而不是仅仅从数组中删除元素? 任何人都可以为这些陈述提供一些用例吗?



我一直在寻找一些答案约 3 小时。没有什么。大多数来源(2ality 博客、JavaScript: The Definitive Guide 6th edition,以及当您搜索“JavaScript 稀疏数组”之类的内容时在 Google 搜索结果中弹出的一大堆其他文章)说稀疏数组是奇怪的行为,你应该远离他们。我读到的任何资料都没有解释,或者至少试图解释,为什么我们一开始就被允许创建稀疏数组。除了 You Don't Know JS: Types & Grammar 之外,这本书讲述了为什么 JavaScript 允许创建稀疏数组:

在其槽中没有显式值但具有暗示槽存在的长度属性的数组是 JS 中一种奇怪的奇异数据结构类型,具有一些非常奇怪和令人困惑的行为。创建此类值的能力纯粹来自旧的、已弃用的历史功能(“类似数组的对象”,如参数对象)。

因此,本书暗示arguments 对象以某种方式在某处使用我上面列出的示例之一来创建稀疏数组。那么,arguments 在哪里以及如何使用稀疏数组?



让我感到困惑的是“JavaScript:权威指南第 6 版”一书中的这一部分:

足够稀疏的数组通常以比密集数组更慢、内存效率更高的方式实现。

“更节省内存”对我来说似乎与“更慢”相矛盾,那么两者之间有什么区别,尤其是在稀疏数组的上下文中? Here 是指向本书特定部分的链接。

【问题讨论】:

  • 我认为“纯粹来自旧的、已弃用的历史功能”涵盖了它。 ;)
  • 答案很简单,向后兼容或者内存控制
  • @Shard 这就是其他文章所说的。那么,向后兼容是为了什么?它如何帮助记忆控制? Javascript:权威指南说它“稀疏数组很慢,但内存效率更高”,这是什么意思?
  • 我唯一一次使用它是填充模板。考虑一个类似表格的结构,如果缺少值,您确实希望呈现空单元格。通过使用固定的数组长度(例如 20),您可以保证使用简单的循环“针对数组中的每个值”创建 20 个单元格。否则,您还必须包括值检查。 '如果值,则呈现值,否则呈现空单元格'。只是一个例子,也可以用不同的方式修复它。
  • @Taurus 向后兼容仍然使用该功能的旧网站。对于内存控制,如果您知道数组中只需要 20 个值,那么您应该指定它。否则后端的动态数组会猜测您的数组的大小并将其设置为比应有的大数百或数千。

标签: javascript sparse-matrix


【解决方案1】:

我想知道像 var foo = new Array(20), var foo = [1,2,3]; 这样的代码的用例是什么foo.length = 10 或 var foo = [,,,] 是

理论上,出于同样的原因,人们通常使用稀疏数据结构(不一定按重要性顺序):内存使用量(var x = []; x[0]=123;x[100000]=456; 不会消耗 100000 个“插槽”),性能(比如说,取平均前面提到的 x,通过 for-in 或 reduce() )和便利性(没有“硬”越界错误,无需显式增长/收缩);

也就是说,从语义上讲,js 数组只是一个特殊的关联集合,它具有索引键和一个特殊的属性“长度”,它满足大于其所有索引属性的不变性。虽然是一个非常优雅的定义,但它的缺点是渲染稀疏定义的数组有点令人困惑且容易出错,正如您所注意到的。

但是为什么我们可以做上述事情呢?

即使我们不允许定义稀疏数组,我们仍然可以将未定义的元素放入数组中,从而导致与稀疏数组基本相同的可用性问题。 所以说,让[0,undefined,...,undefined,1,undefined][0,...,1,] 相同只会给你带来更多的内存消耗数组和更慢的迭代。

足够稀疏的数组通常以比密集数组更慢、内存效率更高的方式实现。对我来说,更节省内存和更慢似乎是矛盾的

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

相反,“稀疏数组/矩阵/...”是通过将分布在内存中的较小内存块“链接”在一起或通过使用密集数据结构的某种“逻辑压缩”形式或两者兼而有之来实现的;在任何一种情况下,内存消耗都会因明显的原因而减少,但相对而言,遍历它们需要更多的工作和更少的本地内存访问模式。

因此,如果相对于相同的有效遍历元素,稀疏数组消耗的内存要少得多,但比密集数组要慢得多。但是,考虑到您使用稀疏数组和稀疏数据以及对“零”进行微不足道的算法,在某些情况下,稀疏数组的结果会更快(例如,将非常大的矩阵与很少的非零元素相乘......)。

【讨论】:

  • 内存使用情况(var x = []; x[0]=123;x[100000]=456; 不会消耗 100000 'slots') 但是为什么要是这样吗?为什么不直接说var x = []; x.push(456);?为什么要在特定索引处插入元素(远大于数组的当前大小)?
  • 假设我有一个数组[1,2,3,4,5,6,7],出于某种原因,我想标记奇数,是否应该在每个奇数索引(1,3, 5,7) 而不是 "odd_number_was_here"false 之类的东西,因为这既可以帮助我标记我的索引,也可以帮助减少内存消耗?
  • 其他让我感到困惑的是“JavaScript:权威指南第 6 版”一书中的这一部分:Arrays that are sufficiently sparse are typically implemented in a slower, more memory-efficient way than dense arrays aremore memory-efficientslower 对我来说似乎很矛盾。 Here 是本书特定部分的链接。
  • @Taurus,为什么要那样做?因为某些接口可能需要这样做(考虑一个要求在图形中呈现一些一维数据的函数)或者因为它更方便这样做(因为你真的想要一个 100000 长度的向量,或者一个几乎为零的大矩阵)。我同意这在 js 代码中非常罕见……正如其他人所说,我们都同意这是一个相当奇特且容易出错的功能;我的理由是不允许它不会给你带来任何好处,所以......
  • @Taurus,假设我有一个数组[...],在这种情况下,我认为没有理由使用删除;即使您需要删除所有奇数索引号,我会发现将它们移动到数组的末尾并通过相应地设置长度来删除它们会更有效/更优雅
【解决方案2】:

因为 JS 数组是一种非常奇怪的数据类型,它不遵守时间复杂度规则,正如您在使用正确工具时可能期望的那样。我的意思是for in 循环或Object.keys() 方法。尽管我是一个非常实用的人,但我会在这里转向for in 循环,因为它是breakable。

在 JS 中有一些非常有益的稀疏数组用例,例如在 O(1) 中将项目插入和删除到排序数组中,而不会干扰排序结构,如果您的值是像限价订单簿这样的数字。或者换句话说,如果你可以在你的键和值之间建立直接的数字相关性。

【讨论】:

    猜你喜欢
    • 2019-02-10
    • 2017-08-24
    • 2010-09-05
    • 2012-06-02
    • 2021-10-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多