核心观点:数组是“开箱即用”的数据结构,但“怎么用”决定了程序的性能与健壮性。
掌握其底层机制,才能在工程实践中游刃有余。
一、为什么数组如此重要?
在所有数据结构中,数组是最基础、最常用、性能最优的线性结构之一。
JavaScript 中的 Array 虽然语法灵活,但其行为受引擎(如 V8)优化策略影响极大。理解其初始化、访问和遍历方式,是写出高性能代码的第一步。
二、数组的多种初始化方式(含易错点)
1. 字面量方式(推荐 ✅)
javascript
编辑
const arr = [1, 2, 3, 4, 5];
- 优点:简洁、可读性强、引擎高度优化。
- 内存:直接分配连续内存空间(若元素类型一致,V8 会使用“快速元素”模式)。
2. 构造函数:空数组
javascript
编辑
const arr1 = new Array(); // 等价于 []
const arr2 = []; // 更推荐
- 两者功能相同,但字面量更简洁,且避免构造函数被污染的风险。
3. 构造函数:指定长度(⚠️ 高危操作!)
javascript
编辑
const arr = new Array(5);
console.log(arr); // [empty × 5] —— “稀疏数组”
-
问题:创建的是稀疏数组(sparse array) ,内部没有实际元素,只有
length = 5。 -
后果:
arr[0]返回undefined,但0 in arr为falseforEach、map等方法会跳过这些“空槽”- 性能差(引擎无法使用连续内存优化)
✅ 正确做法:配合 .fill() 初始化
javascript
编辑
const arr = new Array(5).fill(0); // [0, 0, 0, 0, 0]
💡 注意:
.fill()对对象是浅拷贝!
javascript
编辑
const arr = new Array(3).fill({});
arr[0].name = '黄国文';
console.log(arr); // [{name: '黄国文'}, {name: '黄国文'}, {name: '黄国文'}] ❌ 全部引用同一个对象!
✅ 安全写法:
javascript
编辑
const arr = Array.from({ length: 3 }, () => ({}));
4. Array.from():灵活初始化(推荐 ✅)
javascript
编辑
// 创建长度为5的数组,每个元素为索引值
const arr = Array.from({ length: 5 }, (_, i) => i); // [0, 1, 2, 3, 4]
// 类数组转真数组
const nodeList = document.querySelectorAll('div');
const divs = Array.from(nodeList);
- 优势:避免稀疏数组,支持映射函数,语义清晰。
5. 扩展运算符 + Array()(ES6 技巧)
javascript
编辑
const arr = [...new Array(5)].map((_, i) => i); // [0,1,2,3,4]
- 利用扩展运算符将稀疏数组“打散”成
undefined填充,再通过map赋值。
三、数组访问与遍历:性能对比
| 方式 | 示例 | 时间复杂度 | 性能 | 可中断 | 可读性 |
|---|---|---|---|---|---|
| for 循环(缓存 length) | for(let i=0, len=arr.length; i<len; i++) | O(1) per access | ⭐⭐⭐⭐⭐ | ✅ | 中 |
| for...of | for (let item of arr) | O(n) | ⭐⭐⭐⭐ | ✅ | 高 |
| forEach | arr.forEach(item => ...) | O(n) | ⭐⭐ | ❌ | 高 |
| map/filter/reduce | arr.map(x => x*2) | O(n) | ⭐⭐ | ❌ | 高(函数式) |
| for...in | for (let key in arr) | O(n) | ⭐ | ✅ | 低(不推荐用于数组!) |
⚠️ 关键陷阱:
for...in不适合数组:它遍历的是所有可枚举属性(包括原型链上的),且顺序不保证。forEach无法 break/continue:想提前退出?只能用try/catch或改用for。- 动态查询
arr.length有开销:虽然现代引擎已优化,但缓存len仍是最佳实践。
✅ 性能建议:
- 追求极致性能 → 用
for+ 缓存length- 平衡可读性与功能 → 用
for...of- 函数式编程场景 → 用
map/filter/reduce
四、数组 vs 链表:何时选择谁?
| 特性 | 数组 | 链表 |
|---|---|---|
| 内存布局 | 连续 | 离散(节点+指针) |
| 随机访问 | O(1) | O(n) |
| 插入/删除(头部) | O(n) | O(1) |
| 插入/删除(尾部) | O(1) amortized(JS 动态扩容) | O(1) if tail pointer |
| 内存开销 | 低(仅数据) | 高(每个节点需存储指针) |
| CPU 缓存友好 | ✅(局部性原理) | ❌ |
📌 结论:
- 小规模、频繁随机访问 → 选数组(JS 默认就是)
- 超大规模、频繁头尾增删 → 考虑模拟链表(但 JS 中极少需要)
💡 JavaScript 引擎对数组做了大量优化,除非极端场景,否则无需手动实现链表。
五、大厂高频面试题(附答案)
1. new Array(3) 和 new Array(3).fill(undefined) 有什么区别?
答:
new Array(3)创建稀疏数组,无实际元素,0 in arr === false;.fill(undefined)创建密集数组,每个位置都是undefined值,0 in arr === true。- 前者
forEach不执行,后者会执行 3 次。
2. 如何高效创建一个长度为 n、元素为递增整数的数组?
答(多种写法):
js 编辑 // 方法1:Array.from const arr = Array.from({ length: n }, (_, i) => i); // 方法2:扩展运算符 + keys const arr = [...Array(n).keys()]; // 方法3:传统 for(性能最优) const arr = []; for (let i = 0; i < n; i++) arr[i] = i;
3. 为什么 for 循环比 forEach 快?
答:
for是底层循环,无函数调用开销;forEach每次迭代都要入栈/出栈一个回调函数,增加 GC 压力;- 引擎对
for的优化更彻底(如循环展开、寄存器分配)。
4. for...in 能用来遍历数组吗?为什么?
答:不推荐。
for...in遍历的是对象的可枚举属性名,对数组而言是字符串化的下标;- 会遍历原型链上新增的属性(如
Array.prototype.myMethod = ...);- 顺序不保证(尽管现代引擎通常按数字顺序);
- 性能差(需做属性查找)。
✅ 正确做法:用for...of或forEach。
5. JavaScript 数组是真正的“连续内存”吗?
答:取决于引擎和数据类型。
- V8 引擎中,若数组元素类型一致(如全是 number),会使用 PACKED_ELEMENTS 模式,近似连续内存;
- 若混入 string/object,则降级为 DICTIONARY_ELEMENTS(哈希表),失去连续性;
- 动态扩容时会“搬家”,但 JS 层面对用户透明。
六、总结:数组使用的最佳实践
- ✅ 优先使用字面量
[1,2,3]或Array.from() - ❌ 避免
new Array(n)不带.fill() - ✅ 遍历时缓存
length:for (let i = 0, len = arr.length; ...) - ✅ 需要中断 → 用
for或for...of - ✅ 函数式处理 → 用
map/filter/reduce - ❌ 不要用
for...in遍历数组 - ✅ 大量数值计算 → 保持数组类型一致,触发引擎优化
🌟 记住:
在 JavaScript 中,数组不仅是数据结构,更是性能的关键入口。
理解其初始化与遍历的本质,你就能写出既优雅又高效的代码。