JavaScript 中的 Array 构造函数与稀疏数组的实现原理与差异
字数 1444 2025-12-12 01:53:51
JavaScript 中的 Array 构造函数与稀疏数组的实现原理与差异
题目描述
本次要讲解的是 JavaScript 中的 Array 构造函数与稀疏数组(Sparse Array)的原理、差异以及底层实现。很多开发者会混淆数组的创建方式(如 new Array(5) 与 [1, 2, 3]),以及稀疏数组与密集数组在内存、性能、遍历方法上的区别。理解这些细节对优化性能和避免意外 bug 至关重要。
解题步骤
1. Array 构造函数的两种用法
JavaScript 的 Array 构造函数有两种使用方式,行为截然不同。
a) 单个数字参数
const arr1 = new Array(5);
console.log(arr1); // [ <5 empty items> ]
console.log(arr1.length); // 5
console.log(arr1[0]); // undefined
console.log(0 in arr1); // false
- 创建一个长度为 5 的数组,但数组元素是“空的”(empty slots),称为稀疏数组。
- 这些空槽位并不存储实际值,
arr1[0]返回undefined是因为数组的原型链上Array.prototype[0]不存在,而访问不存在的属性返回undefined。 - 但
0 in arr1是false,说明索引0并非数组自身的属性。
b) 多个参数或单个非数字参数
const arr2 = new Array(1, 2, 3);
console.log(arr2); // [1, 2, 3]
console.log(0 in arr2); // true
- 参数被当作数组的初始元素,创建的是密集数组(所有索引都有值)。
注意陷阱:
new Array(5.5); // 报错:Invalid array length
new Array('5'); // 创建一个元素为 '5' 的数组:['5']
2. 稀疏数组与密集数组的内存结构
理解底层实现有助于优化性能。
密集数组:
- 内存中连续存储每个元素(在 V8 引擎中,对于同类型元素会使用“快速元素”存储,如
PACKED_SMI_ELEMENTS表示小整数)。 - 示例:
const dense = [1, 2, 3]; // 内存布局(简化): // 索引 0: 1 // 索引 1: 2 // 索引 2: 3
稀疏数组:
- 只有长度(length)属性,索引对应的槽位不存在,不占用元素存储空间。
- 示例:
const sparse = new Array(5); // 内存布局:只有一个 length=5 的记录,没有元素存储。 sparse[2] = 'x'; // 现在索引 2 有值,但 0、1、3、4 仍然是空槽。
3. 稀疏数组的行为差异
由于底层存储不同,稀疏数组在一些操作上表现特殊。
a) 遍历方法跳过空槽
const sparse = new Array(3);
sparse[1] = 'b';
sparse.forEach((item, i) => console.log(i, item));
// 只输出:1 'b',跳过索引 0 和 2。
forEach、map、filter等迭代方法会跳过空槽。
b) 与 undefined 的区别
const sparse = new Array(3);
const dense = [undefined, undefined, undefined];
console.log(sparse[0] === dense[0]); // true,都是 undefined
console.log(0 in sparse); // false
console.log(0 in dense); // true
- 稀疏数组的空槽是“缺失的属性”,而
[undefined, undefined]的每个索引都是已初始化的属性,值为undefined。
c) 数组扩展运算符与 Array.from
const sparse = new Array(3);
console.log([...sparse]); // [undefined, undefined, undefined]
console.log(Array.from(sparse)); // [undefined, undefined, undefined]
- 扩展运算符和
Array.from会将空槽转换为值为undefined的元素,生成密集数组。
4. 性能影响与最佳实践
a) 创建数组的推荐方式
- 使用字面量
[]创建密集数组。 - 需要预分配长度时,可使用
new Array(length),但要意识到它是稀疏的。 - 如果希望预分配并填充默认值,用
Array.from({ length: 5 }, () => 0)或new Array(5).fill(0)。
b) 稀疏数组的性能问题
- 稀疏数组的访问可能更慢,因为引擎需要检查槽位是否存在。
- 某些数组方法(如
map、filter)在稀疏数组上会跳过空槽,导致结果数组也是稀疏的,可能不符合预期。
c) 何时会遇到稀疏数组
- 直接设置大于当前长度的索引:
const arr = []; arr[10] = 'x'; // 索引 0-9 为空槽 - 删除元素:
const arr = [1, 2, 3]; delete arr[1]; // 索引 1 变为空槽
5. 检测稀疏数组
可以写一个辅助函数来检测是否有空槽:
function isSparse(arr) {
for (let i = 0; i < arr.length; i++) {
if (!(i in arr)) return true;
}
return false;
}
console.log(isSparse(new Array(5))); // true
console.log(isSparse([1, 2, 3])); // false
总结
new Array(number)创建稀疏数组,只有长度,没有元素。- 稀疏数组 的空槽是“缺失的属性”,遍历时被跳过,与显式的
undefined值不同。 - 性能上,密集数组通常更快,尤其在迭代和内存访问上。
- 实践中,尽量使用数组字面量或
Array.from创建密集数组,避免意外的稀疏行为。