前言
很久很久以前,我在一本《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.prototype,classArr也就可以成为了真正的数组
数组就是对象,arr数组其实也可以添加其他非数字的属性,并且这并不会改变arr.length的值
let arr = [1, 2, 3]
arr['a'] = 5
console.log(arr.length); // 3
对象中数字键名属性称为数组索引属性,非数字键名属性称为命名属性
在JS内存中,两种属性存储在单独的数据结构中,分别由elements和properties指针指向这两种数据结构
之所以储存两个单独的区域,是为了高效的增删改查~~~
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)
再来看看对象数据在内存的表现吧
阐述这么多,主要是为了确认数组和对象之间关系,其实是可以直接划上等号的
数组元素空单元
var arr = ['0', , '2']
数组的索引是连续的,如果中间某个索引不存在对应的值,那这个索引的位置也就被称为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,在内存中值是散列表模式形式,在索引访问数组时,需要通过计算得到哈希值,通过哈希值去访问键值
其实不难看出HashTable比FixedArray访问更慢,而大多数稀疏数组都是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
在数组方法上有不同的表现
使用map、filter、some、forEach、every、reduce、for in等方式在遍历稀疏数组时,都会自动忽略空单元格
let arr = [1, , 3]
arr.forEach((i) => {
console.log(i); // 1 3
})
只有通过for、for of、find、findIndex遍历稀疏数组,才能访问到完整的索引
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 天,点击查看活动详情