JavaScript 稀疏数组深度解析:原理、创建与遍历行为

作者:纸飞机

时间:2026-10-25

浏览:487

本文深入解析 JavaScript 中稀疏数组的底层机制。通过对比密集数组,阐明稀疏数组本质为对象映射,解释“孔洞”概念及其对内存占用的影响。详细演示使用构造函数、字面量及修改 length 创建稀疏数组的方法,并重点分析 delete 操作、forEach/map 等遍历方法在稀疏数组中的特殊表现及快速映射技巧,帮助开发者避免生产环境中的潜在陷阱。

文章目录

前言

近期研读 JS 数据结构与算法相关书籍时,发现书中对数组基础概念着墨不多。尽管日常开发中频繁使用数组,但多聚焦于业务逻辑实现,缺乏对这一基础数据结构的深入回顾。为此,查阅资料梳理密集数组与稀疏数组的区别,并整理此文以辅助理解书中要点,内容虽浅显,恳请指正。

什么是稀疏数组?

在 C、Java 等语言中,数组通常预先分配固定长度,内存地址连续且紧密,称为密集数组,类似 JS 中 TypedArray 的 ArrayBuffer。本文关注通过 Array 构造函数创建的数组,其本质是对象,仅包含 length 属性,可像操作对象一样访问其属性:

let array = new Array(10)
// 下标可以是任何的字符串,就像操作 Object 一样
array.name = 'This is an Array.'
array['name'] = 'This is an Array.' // 等效 array.name

尽管可为数组对象添加任意属性,但仅以整数类型字符为下标映射的值才计入数组元素,并影响 length 属性。结合前文代码继续分析:

// 虽然之前赋上了一个新的属性 name,但是其长度仍然输出10,而非11
console.log(array.length) // 10
// 用整数类型下标定义一个数组元素才会计入数组长度
array['0'] = 'first' // 下标其实都是字符串
array[11] = 'eleventh' // 看起来这个下标是一个数字,但是 JS 会自动把它转为字符
console.log(array.length) // 12

length 属性支持手动修改:

array.length = 100
console.log(array.length) // 100

数组对象本身内存占用较小,主要包含映射关系和长度信息,真正消耗内存的是映射目标(即“值”)。V8 引擎对 JS 数组进行了优化,引入“Holey”(孔洞)概念,指未建立映射关系的空间,不占用内存。作为动态语言,JS 允许随时增删改元素映射及长度,因此元素内存地址无需固定连续,此类数组称为稀疏数组(Sparse Array)。

创建带有孔洞的稀疏数组

  • 使用 Array 构造函数:
let sparse = new Array(100)
  • 通过字面量:
let sparse = []
// 用一个超过原始长度的下标为元素赋值,也就为该元素创建了一个映射关系,同时改变了该数组的长度
sparse[99] = 'last'
console.log(sparse.length) // 100
// 用对象字面量书写数组时,允许元素留空
sparse = [, , ,] // 这就创建了长度为3的空洞的数组
console.log(sparse.length) // 3
  • 手动修改大于原数组长度的 length 值:
let sparse = ['red', 'green', 'blue'] // 三个元素的数组
sparse.length = 100 // 此时长度已是100,但有效元素仍然只有3个

删除元素的映射

pop、shift、splice 可删除数组元素,但如何解除元素与对象的映射?可使用 delete 操作符,它仅断开元素与目标对象的关联形成孔洞,不删除映射目标本身,也不改变数组 length。

let one = '壹'
let array = []
array[0] = one // 将数组的第一个元素指到对象 one,长度变为 1
console.log(array, array.length) // ['壹'], 1
delete array[0] // 删除数组第一个元素的映射关系
console.log(array) // [空]
console.log(array.length) // 因为只是删除了元素的映射,长度并没有改变,仍然输出 1
console.log(one) // one 的值并不会删除,仍然保留,输出 '壹'

现象

JS 数组本质是对象,由整数字符下标与目标值构成映射,且自带 length 属性,长度可大于有效元素个数。完全映射数组表现类似传统密集数组;不完全映射数组在生产中表现特殊但符合逻辑。

  • 访问无映射关系的元素时,相当于访问未定义值的变量,输出 undefined:
let sparse = new Array(10)
console.log(sparse[0]) // undefined
  • 不完全映射数组在使用 map()、forEach() 遍历时,仅遍历已建立映射关系的元素:
// 此时数组元素的映射关系一个都没有创建,所以 forEach 不会有任何输出
sparse.forEach((value, i) => {
  console.log(i, value)
}) // nothing
// 根据下标为最后一个元素赋值
sparse[sparse.length - 1] = 'tenth'
// 只会输出已建立映射的元素 sparse[sparse.length-1] 的值 'tenth'
sparse.forEach((value, i) => {
  console.log(i, value)
}) // 9, tenth
  • 使用 for 语句时,循环依据仅为 length 值,不主动判断当前位置是否有映射。若通过下标访问无映射位置,将输出 undefined:
for( let i = 0; i < sparse.length; i += 1){
  console.log(i, sparse[i])
}
  • 完全映射数组可被所有常用数组方法正常遍历:
let array = ['1', 'day', 'white', 'Jake', undefined, null, 0] // 一个完全映射的数组
// 输出每个元素,包括null、undefined、0
array.forEach((value, i) => {
  console.log(i, value)
})
  • 执行 some()、every() 等操作时,不完全映射数组表现特殊,这取决于方法设计。例如 some(),即使 length 大于 0,若无建立映射的元素,其行为等同于对空数组 [] 操作:
let sparse = new Array(10)
// 由于还没有建立映射关系,所以 some 的回调也没有触发,于是得到的结果依然是 false
console.log(sparse.some(item => !item)) // false
// 在所有元素都被赋值后,用同样的回调,some 的结果发生了变化
sparse.fill(false) // 赋值
console.log(sparse.some(item => !item)) // true
  • 当将 length 设置为小于实际元素个数时,超出长度的元素将从当前数组中剔除:
let array = ['one', 'two', 'three']
array.length = 1
console.log(array) // ['one']

稀疏数组的快速映射(强制创建映射关系)

仅使映射元素个数等于 length 属性,并不能消除稀疏特性:

let sparse = new Array(5)
// Array.apply
let array1 = Array.apply(null, sparse)
// Array.from方法
let array2 = Array.from(sparse)
// 解构
let array3 = new Array(...sparse)
let array4 = [...sparse]
// 把所有元素映射为 undefined 了
array1.forEach((item, i) => {
  console.log(i, item) // 输出 undefined × 5
})
……

总结

以上是对 JS Array 的初步认知。在 JS 动态语言中,通过 Array 创建的数组无疏密之分,本质均为对象。实际生产中,建议尽量保持数据集合映射完全,以避免意外行为。更多关于 JavaScript 稀疏数组教程的资料请关注本站其它相关文章!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章