JavaScript 中的稀疏数组

前言

最近有空在看一本关于 JS 数据结构和算法的书,里面有提到数组,却对数组的基本概念轻轻带过,虽然用了 JS 很久但是一直忙于需求业务的实现从未停下好好回视一下这个 既熟悉又陌生的朋友,于是查阅了一些资料,尤其是密集数组和稀疏数组的区别,意犹未尽之下,写了这篇文章,以便更好地帮助理解书中的要点,稍显浅显,也有不足望各位提点。

什么是稀疏数组?

通常编程语言中(C、JAVA等)数组都是预先设定好长度的,他们的内存占用是固定的,内存地址是连续不间断、紧密相连的,我们称之为密集数组,好比 JS 中类型化数组(TypedArray)的 ArrayBuffer,但我们这里着重要说的是通过 Array 创建的数组,它其实是个对象,当我们通过 new Array() 创建一个数组时,它只是一个带有 length 属性的数组对象,我们可以像对象一样去操作它的属性:

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

虽然可以像操作 Object 那样为数组对象添加属性,但是真正计入数组元素的只能是以整数类型的字符作为下标去映射值的元素,同时会影响其长度属性 length,我们接上段代码继续:

// 虽然之前赋上了一个新的属性 name,但是其长度仍然输出10,而非11
console.log(array.length) // 10

// 用整数类型下标定义一个数组元素才会计入数组长度
array['0'] = 'first' // 下标其实都是字符串
array[11] = 'eleventh' // 看起来这个下标是一个数字,但是 JS 会自动把它转为字符
console.log(array.length) // 12

长度属性是可以手动变更的:

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

数组对象本身可能不占用太多内存,它只是包含了映射关系和长度,真正占用内存的是元素所映射的目标,也就是“键值”中的“值”。在V8引擎里,JS 的数组得到进一步的优化,其中有个概念叫 Holey(有孔洞的),也就是那些没有被指明映射关系的空间,他们不占用内存。如上看到,作为一门动态语言,我们随时可以向这个数组对象里添加、修改元素映射,也能随时改变其长度 length。自然地,其元素所占的内存地址也就无所谓固定连续了,我们称这种数组为稀疏数组(Sparse Array)。

创建带有孔洞的稀疏数组

  1. 使用 Array 构造函数:
let sparse = new Array(100)
  1. 通过字面量:
let sparse = []
// 用一个超过原始长度的下标为元素赋值,也就为该元素创建了一个映射关系,同时改变了该数组的长度
sparse[99] = 'last'
console.log(sparse.length) // 100

// 用对象字面量书写数组时,允许元素留空
sparse = [, , ,] // 这就创建了长度为3的空洞的数组
console.log(sparse.length) // 3
  1. 手动修改一个大于原数组长度的 length 值:
let sparse = ['red', 'green', 'blue'] // 三个元素的数组
sparse.length = 100 // 此时长度已是100,但有效元素仍然只有3个

删除元素的映射

从数组中删除某个元素可以使用 popshiftsplice 方法,那如何解除元素与某个对象的映射关系呢?我们可以像操作对象一样用 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 的数组本质上是对象——由整数字符作为下标与目标值构成映射关系的自带长度属性的对象,长度可以大于有效(已创建映射关系的)元素的个数。从映射状态来看,完全映射的数组有着和传统密集数组类似的表现,而不完全映射的数组在生产中会有些特别,但又在情理之中。

  1. 访问没有映射关系的数组元素时,相当于一个申明了却没有定义值的变量,所以会输出 undefined
let sparse = new Array(10)
console.log(sparse[0]) // undefined
  1. 不完全映射的数组,在用 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
  1. 使用 for 语句也只是用数组的 length 值作为循环依据,它不会主动判断当前位置是否有映射值,所以当循环体试图通过下标访问没有映射关系的位置时,会输出 undefined
for( let i = 0; i < sparse.length; i += 1){
  console.log(i, sparse[i])
}
  1. 映射完全的数组,可以被所有常用数组方法遍历:
let array = ['1', 'day', 'white', 'Jake', undefined, null, 0] // 一个完全映射的数组

// 输出每个元素,包括null、undefined、0
array.forEach((value, i) => {
  console.log(i, value)
})
  1. 在做 some()every() 等操作时,不完全映射的数组的表现是特殊的,但也在情理之中,这取决于这些方法的设计,例如 some(),即便长度大于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
  1. 当把长度设成小于实际元素个数的值时,会把超出长度的元素从当前数组中剔除:
let array = ['one', 'two', 'three']

array.length = 1
console.log(array) // ['one']

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

只是让数组中的映射元素个数与长度属性相同,并不能改变其稀疏的特性:

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 所创建的数组无所谓疏密,因为它们本质上还是对象。在实际生产时,对于数据集合的操作应当尽量保持映射完全,避免不合预期的意外。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容