一行代码引发的血案:new Array(5) 到底发生了什么?

在 JavaScript 中,数组(Array)是一种极其重要的数据结构,它不仅支持基本的元素存储与访问,还提供了丰富的操作方法。更重要的是,JavaScript 数组在底层实现上有着不同于传统语言(如 C/C++)的独特机制,这使得它既灵活又强大。

本文将带你从数组的底层原理 出发,深入探讨其创建方式、稀疏数组、静态方法、实例方法、遍历方式、reduce 的作用机制等内容,帮助你真正掌握 JavaScript 数组的精髓。


📌 一、JavaScript 数组的本质

1. 数组是对象的子类

JavaScript 中的数组本质上是对象,它继承自 Object,并具有特殊的内部属性 [[Class]] 标记为 "Array"。它通过数字索引访问元素,底层使用哈希表和连续内存块结合的方式实现高性能访问。

javascript 复制代码
typeof []; // "object"
Array.isArray([]); // true

2. 数组是可迭代对象(Iterable)

数组实现了 Symbol.iterator 接口,因此可以被 for...of 遍历,也可以与 ... 扩展运算符、Array.from() 等方法无缝配合。

javascript 复制代码
for (const item of [1, 2, 3]) {
  console.log(item);
}

🧱 二、数组的创建方式与底层实现

1. 使用字面量语法 []

这是最推荐的创建数组的方式,简洁、直观、性能好。

javascript 复制代码
const arr = [1, 2, 3];

2. 使用构造函数 new Array()

javascript 复制代码
const arr = new Array(5); // 创建长度为 5 的空数组(稀疏数组)

⚠️ 特别注意:

  • new Array(5) 并不会真正创建 5 个 undefined 元素,而是一个长度为 5 的稀疏数组。
  • 使用 for...in 遍历会失败,因为这些位置并没有实际的键值对。
  • 稀疏数组的 length 是 5,但 Object.keys(arr) 会返回空数组。
javascript 复制代码
const arr = new Array(5);
console.log(arr); // [empty × 5]
console.log(Object.keys(arr)); // []

✅ 如何真正初始化一个固定长度的数组?

javascript 复制代码
const arr = new Array(5).fill(undefined);
console.log(arr); // [undefined, undefined, undefined, undefined, undefined]

🧩 三、稀疏数组(Sparse Array)

稀疏数组是指数组中某些索引位置没有实际值,这些位置是"空"的。

javascript 复制代码
const arr = new Array(5);
arr[8] = 'hello';
console.log(arr); // [empty × 5, empty × 3, 'hello']
  • 稀疏数组的 length 为 9,但前 9 个位置中只有索引 8 有值。
  • 使用 map()forEach() 等方法不会遍历空位。
  • fill() 可以将稀疏数组"填充"为密集数组。

🧮 四、Array 的静态方法详解

1. Array.of()

创建一个新数组,参数直接作为数组元素。

javascript 复制代码
Array.of(1, 2, 3); // [1, 2, 3]
Array.of(3); // [3](注意:不是长度为 3 的空数组)

2. Array.from()

将类数组对象(如 arguments、DOM NodeList)或可迭代对象(如 Set、Map)转换为数组。

javascript 复制代码
Array.from('hello'); // ['h', 'e', 'l', 'l', 'o']
Array.from({ length: 5 }, (_, i) => i); // [0, 1, 2, 3, 4]
  • 支持第二个参数作为映射函数。
  • 底层实现依赖 Symbol.iteratormap 函数。
javascript 复制代码
// 将 ASCII 字符串转换为字符数组
console.log(Array.from(new Array(26), (val, index) => String.fromCharCode(65 + index)));
// 输出: ['A', 'B', ..., 'Z']

🛠️ 五、Array 的实例方法详解

1. 修改原数组的方法

方法 作用
push() 在数组末尾添加元素
pop() 删除最后一个元素
shift() 删除第一个元素
unshift() 在数组开头添加元素
splice() 删除或添加任意位置的元素
fill() 用固定值填充数组
reverse() 反转数组
sort() 对数组排序(默认按字符串排序)
javascript 复制代码
let arr = [1, 2, 3];
arr.fill(0); // [0, 0, 0]

2. 不修改原数组的方法

方法 作用
map() 映射新数组
filter() 过滤符合条件的元素
find() 查找第一个符合条件的元素
findIndex() 查找第一个符合条件的元素索引
slice() 截取子数组
concat() 合并数组
javascript 复制代码
[1, 2, 3].map(x => x * 2); // [2, 4, 6]

🧭 六、遍历数组的 5 种方式详解

在 JavaScript 中,有多种方式可以用来遍历数组,每种方式都有其适用场景和行为特点。下面我们详细介绍:

1. for (let i = 0; i < arr.length; i++):传统计数循环

这是最基础的数组遍历方式,适用于需要访问索引和元素的场景。

javascript 复制代码
for (let i = 0; i < arr.length; i++) {
  console.log(i, arr[i]);
}
  • ✅ 可中断(使用 break
  • ✅ 可访问索引和元素
  • ⚠️ 需手动处理索引逻辑

2. while:条件控制循环

适用于不确定遍历次数或需要复杂控制逻辑的场景。

javascript 复制代码
let i = 0;
while (i < arr.length) {
  console.log(arr[i]);
  i++;
}
  • ✅ 可中断
  • ✅ 灵活控制逻辑
  • ⚠️ 代码略显冗长

3. forEach():遍历数组中的每个元素

对数组中的每个元素执行一次回调函数。

javascript 复制代码
arr.forEach((item, index) => {
  console.log(index, item);
});
  • ✅ 简洁
  • ❌ 无法使用 breakcontinue
  • ❌ 无法中断循环(除非抛出异常)

4. for...of:现代简洁的遍历方式

用于遍历可迭代对象,如数组、字符串、Map、Set 等。

javascript 复制代码
for (const item of arr) {
  console.log(item);
}
  • ✅ 简洁易读
  • ✅ 可中断(使用 break
  • ✅ 支持异步
  • ✅ 遍历的是数组的"值",而不是索引

5. for...in:遍历对象键名(慎用于数组)

for...in 用于遍历对象的可枚举属性键名(包括数组的索引),但不推荐用于数组遍历。

javascript 复制代码
for (const key in arr) {
  console.log(key);
}
  • ❌ 不保证顺序
  • ❌ 会遍历原型链上的属性
  • ❌ 不推荐用于数组(尤其稀疏数组)

🧪 七、对比 for...infor...of 的行为差异(重点)

我们来看一个例子:

javascript 复制代码
const arr = new Array(5).fill(undefined);
arr[8] = undefined;
console.log(arr);
// 输出: [undefined, undefined, undefined, undefined, undefined, <3 empty items>, undefined]

1. 使用 for...of 遍历:

javascript 复制代码
for (const item of arr) {
  console.log(item);
}
  • ✅ 输出:undefined 一共 9 次(包括索引 0 到 8)
  • 因为 for...of 是基于数组的迭代器(Symbol.iterator),它会遍历所有索引位置的值 ,包括"空位"和值为 undefined 的位置。

2. 使用 for...in 遍历:

javascript 复制代码
for (const key in arr) {
  console.log(key);
}
  • ✅ 输出:"0", "1", "2", "3", "4", "8"(字符串类型)
  • ❌ 不会输出索引 5、6、7,因为它们是"空位",没有被赋值过。
  • ✅ 索引 8 被赋值了(即使值是 undefined),所以会被 for...in 遍历到。

📌 结论(关键点)

遍历方式 是否遍历未赋值的索引 是否遍历赋值为 undefined 的索引 遍历顺序是否稳定 是否推荐用于数组
for...of ✅ 是 ✅ 是 ✅ 稳定 ✅ 推荐
for...in ❌ 否(跳过空位) ✅ 是(只要赋值了,即使为 undefined ❌ 不稳定 ❌ 不推荐

✅ 总结:

  • for...in 遍历的是对象的可枚举属性
  • 对于数组来说,这些"属性"就是数组的索引(以字符串形式返回)。
  • for...in 不会遍历稀疏数组中"空位"(即未赋值的索引)。
  • 即使你在某个索引(如 arr[8] = undefined)上赋值为 undefined,只要赋值了,它就不再是"空位",而是"真实存在的属性",所以会被 for...in 遍历到。

📊 八、可迭代对象与 entries()

数组是可迭代对象,因此可以使用 .entries() 获取索引与值的配对。

javascript 复制代码
for (const [index, value] of arr.entries()) {
  console.log(index, value);
}

.entries() 返回的是一个迭代器对象,每次调用 .next() 返回 { value: [index, value], done: boolean }

javascript 复制代码
console.log(arr.entries()); // Array Iterator {}

🧠 九、数组在 V8 引擎中的实现原理(简要)

V8 引擎对数组做了大量优化,主要包括:

1. 小数组优化(Fast Elements)

  • 小数组会被存储为连续的内存块,访问速度非常快。
  • 使用 PACKED_SMI_ELEMENTS 表示纯整数数组,效率最高。

2. 大数组优化(Dictionary Mode)

  • 当数组变得稀疏时,V8 会将其转为哈希表结构,牺牲性能换取内存。
  • 这时数组的访问效率下降,但节省空间。

3. 动态扩容机制

  • 数组在添加元素超过当前容量时会自动扩容。
  • 扩容策略为指数增长,避免频繁分配内存。

🧪 十、总结:JavaScript 数组的"灵魂"

特性 描述
类型灵活性 支持混合类型,动态扩展
内存优化 V8 对小数组、大数组分别优化
遍历方式多样 支持传统循环、函数式编程、迭代器等
方法丰富 提供 mapfilterreduce 等实用方法
可迭代对象 实现 Symbol.iterator,兼容现代语法
稀疏数组机制 支持动态索引,但部分方法会跳过"空"元素

💡 结语:

JavaScript 数组远不止是"一个装数据的容器",它融合了函数式编程思想、现代迭代器机制和底层性能优化。掌握数组的底层原理和高级技巧,是成为高级前端工程师的必经之路。


如果你喜欢这篇文章,欢迎点赞、收藏、转发!如果你有其他关于数组的高级技巧,也欢迎留言交流 👇

相关推荐
fishcanf1y3 分钟前
记一次与Fibonacci斗智斗勇
算法
共享ui设计和前端开发人才3 分钟前
UI前端与数字孪生融合案例:智慧城市的智慧停车引导系统
前端·ui·智慧城市
福娃B5 分钟前
【React】React初体验--手把手教你写一个自己的React初始项目
前端·javascript·react.js
逆向APP5 分钟前
SwiftUI Bug记录:.sheet首次点击弹出空白视图,第二次才正常显示
前端
Yodame7 分钟前
webpack+vite前端构建工具全掌握(中篇)
前端
迪迪SAMA7 分钟前
不再死记硬背!帮你理解原型相关八股概念!
前端
用户1512905452208 分钟前
jQuery-ui源代码重点难点分析
前端
逆向APP11 分钟前
siwftui代码,.sheet不能跳转
前端
24kHT13 分钟前
2.3 前端-ts的接口以及自定义类型
java·开发语言·前端
追光的栗子13 分钟前
vue3+vite 项目中怎么引入 elementplus 组件库
前端·vue.js·element