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 arr1false,说明索引 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。
  • forEachmapfilter 等迭代方法会跳过空槽。

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) 稀疏数组的性能问题

  • 稀疏数组的访问可能更慢,因为引擎需要检查槽位是否存在。
  • 某些数组方法(如 mapfilter)在稀疏数组上会跳过空槽,导致结果数组也是稀疏的,可能不符合预期。

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