成因、坑点与解决方案:从本质到实战一次讲透
稀疏数组是指索引不连续、存在未赋值"空洞(empty slot)"的 JavaScript 数组,其核心特征:
// 1. 稀疏数组(天然空洞)
const sparseArr = [];
sparseArr[10] = 'JavaScript'; // 仅给索引10赋值,0-9均为空洞
console.log('稀疏数组长度:', sparseArr.length); // 11(最大索引+1)
console.log('索引5的值:', sparseArr[5]); // undefined(空洞)
console.log('实际存在的键:', Object.keys(sparseArr)); // ['10'](仅赋值索引)
console.log('是否包含索引5:', sparseArr.hasOwnProperty(5)); // false(空洞不占内存)
// 2. 密集数组(显式赋值undefined)
const denseArr = new Array(11).fill(undefined);
denseArr[10] = 'JavaScript';
console.log('密集数组长度:', denseArr.length); // 11
console.log('索引5的值:', denseArr[5]); // undefined(显式赋值)
console.log('实际存在的键:', Object.keys(denseArr)); // ['0','1',...,'10'](全索引存在)
console.log('是否包含索引5:', denseArr.hasOwnProperty(5)); // true(占用内存)
稀疏数组几乎都是"无意产生"的,以下是开发中最易踩坑的场景:
// 业务场景:根据ID索引存储用户数据,ID从100开始
const userList = [];
userList[100] = { id: 100, name: '张三' };
userList[105] = { id: 105, name: '李四' };
console.log(userList.length); // 106(而非2)
console.log(userList[99]); // undefined(空洞)
// 此时 userList 是典型的稀疏数组:0-99、101-104均为空洞
// 错误认知:new Array(5) 会创建 [undefined, undefined, ...]
const emptyArr = new Array(5);
console.log(emptyArr); // [empty × 5](纯空洞数组)
console.log(emptyArr.map(item => item || '默认值')); // [empty × 5](map跳过空洞)
// 对比:真正的密集空数组
const realEmptyArr = Array.from({ length: 5 });
console.log(realEmptyArr); // [undefined, undefined, undefined, undefined, undefined]
console.log(realEmptyArr.map(item => item || '默认值')); // ['默认值',...](5个)
const scoreList = [90, 85, 78, 92];
delete scoreList[1]; // 删除索引1的元素,留下空洞
console.log(scoreList); // [90, empty, 78, 92]
console.log(scoreList.length); // 4(长度不变)
// 遍历陷阱:forEach跳过空洞
scoreList.forEach((score, index) => {
console.log(`索引${index}:${score}`); // 仅输出索引0、2、3
});
const arr1 = [1, 2];
const arr2 = arr1.slice(0, 0); // 截取空范围,返回稀疏数组
arr2[5] = 6;
console.log(arr2); // [empty × 5, 6]
console.log(arr2.concat([7])); // [empty × 5, 6, 7](拼接后仍保留空洞)
稀疏数组的危害集中在"遍历 / 方法调用"环节:
// 业务场景:检查购物车是否有选中商品(选中商品存于数组指定索引)
const cartSelected = [];
cartSelected[3] = ['goods1', 'goods2']; // 稀疏数组:0-2为空洞
// 期望:检查是否有选中商品,实际报错
try {
const hasSelected = cartSelected.some(ids => ids.length > 0);
} catch (e) {
console.error(e); // TypeError: Cannot read properties of undefined (reading 'length')
}
// 原因:some()遍历索引0时,ids = undefined,访问length报错
// 业务场景:将商品ID数组转为商品名称数组
const goodsIds = [];
goodsIds[2] = 'g001';
goodsIds[5] = 'g002'; // 稀疏数组:长度6,仅2、5有值
// 期望:返回长度6的名称数组,实际返回稀疏数组
const goodsNames = goodsIds.map(id => {
const nameMap = { g001: '手机', g002: '电脑' };
return nameMap[id] || '未知商品';
});
console.log(goodsNames); // [empty × 2, '手机', empty × 2, '电脑']
console.log(goodsNames.length); // 6,但索引0-1、3-4仍为空洞
// 后续逻辑陷阱:如果用goodsNames.length做循环,会拿到undefined
// 业务场景:统计数组中有效数据的数量
const dataList = [];
dataList[1] = '有效数据1';
dataList[4] = '有效数据2';
// 错误1:for...in仅遍历有值索引,统计结果偏小
let count1 = 0;
for (const index in dataList) {
count1++;
}
console.log('for...in统计:', count1); // 2(正确,但易被误认为"遍历全索引")
// 错误2:for循环遍历全索引,统计结果偏大
let count2 = 0;
for (let i = 0; i < dataList.length; i++) {
if (dataList[i]) count2++;
}
console.log('for循环统计:', count2); // 2(看似正确,但如果有值为0/null会误判)
// 正确统计:结合hasOwnProperty
let count3 = 0;
for (let i = 0; i < dataList.length; i++) {
if (dataList.hasOwnProperty(i)) count3++;
}
console.log('正确统计:', count3); // 2
核心思路:用有效值填充所有空洞,确保数组每个索引都有明确值(无空洞)。
/**
* 将稀疏数组转为密集数组
* @param {Array} sparseArr - 稀疏数组
* @param {any} defaultValue - 空洞填充值
* @returns {Array} 密集数组
*/
const toDenseArray = (sparseArr, defaultValue = undefined) => {
return Array.from({ length: sparseArr.length }, (_, index) => {
// 有值则保留,无值则用默认值填充
return sparseArr[index] ?? defaultValue;
});
};
// 实战示例:修复购物车选中检查问题
const cartSelected = [];
cartSelected[3] = ['goods1', 'goods2'];
// 转为密集数组,空洞填充为空数组
const denseCart = toDenseArray(cartSelected, []);
console.log(denseCart); // [[], [], [], ['goods1', 'goods2']](长度4,无空洞)
const hasSelected = denseCart.some(ids => ids.length > 0);
console.log(hasSelected); // true(正常执行,无报错)
// 场景:快速创建指定长度的密集空数组
const createDenseEmptyArray = (length) => {
// 先创建长度为length的数组,填充空数组(注意:fill的引用类型会共享,需额外处理)
return Array(length).fill().map(() => []);
};
// 示例:创建长度5的密集数组,每个元素都是独立空数组
const denseArr = createDenseEmptyArray(5);
denseArr[2].push('test');
console.log(denseArr); // [[], [], ['test'], [], []](无共享问题)
// 原理:Object.assign会遍历所有可枚举属性,自动填充空洞为undefined
const sparseArr = [];
sparseArr[4] = 'test';
const denseArr = Object.assign([], sparseArr);
console.log(denseArr); // [undefined, undefined, undefined, undefined, 'test']
// 再替换undefined为自定义默认值
const finalArr = denseArr.map(item => item ?? '默认值');
console.log(finalArr); // ['默认值', '默认值', '默认值', '默认值', 'test']
Array.from({ length }) 创建密集数组;