永远不要使用稀疏数组

122 阅读4分钟

前言

很久很久以前,我在一本《JS高级程序设计》看到了稀疏数组和密集数组的对比,也没太当一回事,因为在开发中从未用到稀疏数组~~~

最近收到一个需求:生成一个长度为10的数组,并且里面的值是从1~10的数字

需求很简单,我第一反应是通过Array和map来实现

Array(10).map((_, i) => i)			// (10) [empty × 10]

结果却差强人意 ,造成这个问题的原因就是Array(10)生成的数组是稀疏数组!!!

数组

数组和对象的关系

但是在JS中,数组更像一种特殊的对象,对象的属性是数字,并且还必须有一个length属性

let arr = [1, 2, 3]
let classArr = {
    1: 1,
    2: 2,
    3: 3,
    length: 3
}

console.log(arr[1]);            // 1
console.log(classArr[1]);       // 1

classArr被称为类数组,其实他和arr是差不多,唯一的区别在于arr可以使用数组的方法,这是因为arr的原型对象是Array.prototype,如果将classArr的原型也改为Array.prototypeclassArr也就可以成为了真正的数组

数组就是对象,arr数组其实也可以添加其他非数字的属性,并且这并不会改变arr.length的值

let arr = [1, 2, 3]
arr['a'] = 5
console.log(arr.length);		// 3

对象中数字键名属性称为数组索引属性,非数字键名属性称为命名属性

在JS内存中,两种属性存储在单独的数据结构中,分别由elementsproperties指针指向这两种数据结构

js-object.f06193ed

之所以储存两个单独的区域,是为了高效的增删改查~~~

function Foo(properties, elements) {
  //添加可索引属性
  for (let i = 0; i < elements; i++) {
    this[i] = `element${i}`
  }

  //添加常规属性
  for (let i = 0; i < properties; i++) {
    const prop = `property${i}`
    this[prop] = prop
  }
}
const foo = new Foo(12, 12)

再来看看对象数据在内存的表现吧

image-20230211173630784

阐述这么多,主要是为了确认数组和对象之间关系,其实是可以直接划上等号的

数组元素空单元
var arr = ['0', , '2']

image-20230211181413691

数组的索引是连续的,如果中间某个索引不存在对应的值,那这个索引的位置也就被称为Holey(有孔洞的),或者称为空单元

空单元只有一个作用:在一个数组中不存在空单元时,数组根据索引查找键值时,不存在找不到,已经无需查找原型

是不是感觉是理所当然的,但是在JS数组中确实是这样的,通过是否存在空单元来决定是否查找原型

密集数组

而当数组没有出现空单元时,那就称这个数组为密集数组

[1, 2, 3]								// [1, 2, 3]
Array.apply(null, Array(3))			    // [undefined, undefined, undefined]
Array.from({ length: 3 }, () => { })	// [undefined, undefined, undefined]
Array(3).fill()						    // [undefined, undefined, undefined]
new Array(3).fill()					    // [undefined, undefined, undefined]

JS中普遍都是密集数组

稀疏数组

当数组中至少出现一个空单元时,那就称这个数组为稀疏数组

Array(3)				// [empty × 3]
new Array(3)			// [empty × 3]
[1, , 2]				// [1,empty,2]

需要注意的是,通过in关键字判断空单元格索引时,返回的是false

let arr = [1, , 3]
console.log('1' in arr);	// false

再猜猜访问稀疏数组的空单元时会返回什么喃?

是不是以为会报错~~~

let arr = Array(3)
console.log(arr[0])		// undefined

是不是有点意外!!!输出的竟然是undefined,那这Array(3)Array(undefined,undefined,undefined)输出的值是一样,但是Array(3)Array(undefined,undefined,undefined)的数组类型又不一样,这不是自相矛盾了吗?

其实在内存中,undefined也算值,至于为什么空单元格会输出undefined

我个人感觉这也是JS的无奈之举,毕竟不返回undefined,那又能返回什么喃?

密集数组和稀疏数组区别

本质区别

稀疏数组:

  • 有映射的目标的索引不连续
  • 数组的length长度等于映射的目标的个数

密集数组:

  • 有映射的目标的索引连续
  • 数组的length长度等于映射的目标的个数
密集数组访问元素的速度快于稀疏数组

在JS中数组的数据结构可以分为两种模式,想了解的可以看深入V8 - js数组的内存是如何分配的

  • 存储结构是 FixedArray,在内存中是一段连续、不间断的储存空间,可以通过索引轻松获取对应的键值
  • 储存结构是HashTable,在内存中值是散列表模式形式,在索引访问数组时,需要通过计算得到哈希值,通过哈希值去访问键值

其实不难看出HashTableFixedArray访问更慢,而大多数稀疏数组都是HashTable储存结构

const arr = new Array(200000)
arr[19999] = 88
console.time("time")
arr[19999]
console.timeEnd("time")             // time: 0.004150390625 ms


const ddd =new Array(200000).fill()
ddd[19999] = 88
console.time("time")
ddd[19999]
console.timeEnd("time")            // time: 0.0029296875 ms
在数组方法上有不同的表现

使用mapfiltersomeforEacheveryreducefor in等方式在遍历稀疏数组时,都会自动忽略空单元格

let arr = [1, , 3]
arr.forEach((i) => {
    console.log(i);			// 1  3
})						

只有通过forfor offindfindIndex遍历稀疏数组,才能访问到完整的索引

let arr = [1, , 3]
for (const i of arr) {
    console.log(i);		// 1 undefined 3
}

所以明白了为什么一开始通过Array(10).map((_, i) => i)方式不能生成我们所需要的数组了吧~~~

那如果想获取所想的数组,可以使用for循环

也可以先把稀疏数组转为密集数组,再调用map方法

Array.from(Array(10)).map((_, i) => i)
Array.apply(null, Array(10)).map((_, i) => i)

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 10 天,点击查看活动详情