在 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.iterator
和map
函数。
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);
});
- ✅ 简洁
- ❌ 无法使用
break
或continue
- ❌ 无法中断循环(除非抛出异常)
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...in
与 for...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 对小数组、大数组分别优化 |
遍历方式多样 | 支持传统循环、函数式编程、迭代器等 |
方法丰富 | 提供 map 、filter 、reduce 等实用方法 |
可迭代对象 | 实现 Symbol.iterator ,兼容现代语法 |
稀疏数组机制 | 支持动态索引,但部分方法会跳过"空"元素 |
💡 结语:
JavaScript 数组远不止是"一个装数据的容器",它融合了函数式编程思想、现代迭代器机制和底层性能优化。掌握数组的底层原理和高级技巧,是成为高级前端工程师的必经之路。
如果你喜欢这篇文章,欢迎点赞、收藏、转发!如果你有其他关于数组的高级技巧,也欢迎留言交流 👇