JavaScript数组的遍历陷阱

116 阅读2分钟

在JavaScript开发中,数组是最基础却最容易被误解的数据结构之一。本文将通过实际代码示例,深入剖析数组的创建、遍历方法以及开发中常见的陷阱。

一、数组创建的两种方式

1. 数组字面量 vs new Array()

// 数组字面量
const arr = [1, 2, 3]; 

// new Array创建稀疏数组
const sparseArr = new Array(5);
console.log(sparseArr); // [empty × 5]

使用new Array(length)创建数组时:

  • 创建的是稀疏数组(sparse array)
  • 数组元素未被初始化(值为empty而非undefined)
  • 无法通过for...in遍历(因为数组没有实际键值)

image.png

初始化处理

// 正确初始化固定长度数组
const arr = new Array(5).fill(undefined);
console.log(arr); // [undefined, undefined, ...]

二、数组遍历的六大方法

1. 传统计数循环

const names = ['张三','李四','王五'];
for(let i = 0; i < names.length; i++) {
  console.log(`索引 ${i}: ${names[i]}`);
}

优点:性能最优(对计算机好),可随时break终止
缺点:代码冗余,需手动处理索引边界,可读性差

2. forEach方法

names.forEach(name => {
  if (name === '张三') return; // 注意return不能终止循环!
  console.log(`处理 ${name}`);
});

循环陷阱

  • 无法使用break/return终止循环
  • 异步回调中可能产生意外执行顺序

image.png

3. for of循环

遍历对象的可迭代值(value)

for(const name of names) {
  if (name === '李四') break; // 支持break终止
  console.log(name);
}

只遍历值怎么获取索引呢?那就需要另一个api配合——entries()

for(const [index, name] of names.entries()) {
  console.log(`第${index}位: ${name}`);
}

image.png

4. for in循环

遍历对象的可枚举属性名(key)

const arr = new Array(5);
arr[2] = '测试';

// 问题1:无法遍历稀疏数组的空位
for(let key in arr) {
  console.log(key); // 只输出"2"
}

// 问题2:会遍历原型链属性
Array.prototype.customMethod = () => {};
for(let key in arr) {
  console.log(key); // 可能输出"customMethod"!
}

会遍历原型链属性!要慎用

可以配合hasOwnProperty,有才打印:

for(let key in arr) {
  if(arr.hasOwnProperty(key)) {
    console.log(arr[key]);
  }
}

5. map/filter

// 创建新数组时使用
const upperNames = names.map(name => name.toUpperCase());

console.log(names); // 原数组保持不变
new Array(3).map(() => 'x'); // [ <3 empty items> ]
  • map 不会处理空槽,结果仍然是一个空数组。

6. reduce

// 多维数组扁平化
const flatArr = [[1,2], [3,4]].reduce((acc, curr) => 
  acc.concat(curr), []
);

// 替代filter+map组合
const result = data.reduce((arr, item) => {
  if(item.valid) arr.push(item.value * 2);
  return arr;
}, []);

新的状态基于上一个状态,负责在复杂情况下只产生唯一的值,是个纯函数。

三、静态方法解析

1. Array.of():解决构造函数歧义

Array.of(3);    // [3] 
new Array(3);   // [empty × 3]

2. Array.from():类数组转换

// 生成字母表
const alphabet = Array.from({length: 26}, (_, i) => 
  String.fromCharCode(65 + i)
);

// DOM节点转换
const divs = Array.from(document.querySelectorAll('div'));

四、稀疏数组的真相

通过原型链分析揭示稀疏数组本质:

const sparse = new Array(3);
console.log(sparse.hasOwnProperty(0)); // false

// 对比真实数组
const dense = [undefined, undefined];
console.log(dense.hasOwnProperty(0)); // true

核心结论:稀疏数组的"空位"实际上是未被分配的索引,V8引擎会优化存储方式。

总结

  1. 初始化:优先使用字面量[]Array.of()
  2. 固定长度:必须使用new Array(length).fill()
  3. 遍历选择:
    • 需要索引:for...of + entries()
    • 仅需值:for...of
    • 只读操作:forEach
    • 复杂转换:reduce
  4. 性能关键:避免在循环中创建新数组,谨慎使用链式操作

数组操作的核心在于理解其"类对象"的本质和V8引擎的优化机制。