深入理解JavaScript中的for循环:ES5与ES6的四种遍历方式详解
本文详细解析JavaScript中四种for循环:简单for、for-in、forEach及ES6新增的for-of。重点剖析for-in遍历对象属性而非索引的机制及其原型链遍历风险,对比forEach与for-of在控制流(break/continue)及性能上的差异,并提供稀疏数组处理与类数组遍历的最佳实践。
本文对循环机制进行了详尽梳理,阅读后您将掌握核心要点。以下展开具体介绍。
ES5 中的三种 for 循环
在ECMAScript5(简称 ES5)规范中,主要存在三种 for 循环形式:
- 基础 for 循环
- for-in 语句
- forEach 方法
随着2015年6月发布的ECMAScript6(简称 ES6)标准,新增了一种循环语法:
- for-of 语句
接下来,我们将逐一解析这 4 种 for 循环的具体用法与特性。
基础 for 循环
基础 for 循环
首先展示最常见的写法示例:
const arr = [1, 2, 3];
for(let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
若数组长度在循环过程中保持固定,建议将数组长度缓存至变量中,以提升执行效率。改进后的写法如下:
const arr = [1, 2, 3];
for(let i = 0, len = arr.length; i < len; i++) {
console.log(arr[i]);
}
for-in 语句的陷阱
for-in 语句
通常,开发者会使用 for-in 来遍历数组内容,参考代码如下:
const arr = [1, 2, 3];
let index;
for(index in arr) {
console.log("arr[" + index + "] = " + arr[index]);
}
一般情况下,上述代码的运行结果如下:
arr[0] = 1
arr[1] = 2
arr[2] = 3
然而,这种用法往往存在潜在问题。
for-in 的本质
for-in 循环遍历的是对象的属性名,而非数组的索引值。因此,for-in 遍历的对象不限于数组,同样适用于普通对象。
示例如下:
const person = {
fname: "san",
lname: "zhang",
age: 99
};
let info;
for(info in person) {
console.log("person[" + info + "] = " + person[info]);
}
运行结果如下:
person[fname] = san
person[lname] = zhang
person[age] = 99
需特别注意,for-in 遍历属性的顺序是不确定的。输出顺序既不与属性在对象中的定义顺序相关,也不受字母顺序或其他任何固定顺序影响。
Array 的内部机制
在 JavaScript 中,Array 本质上是一个对象,其索引即为属性名。事实上,JavaScript 中的 “array” 概念具有一定误导性,它与其他主流语言的数组差异显著:首先,JavaScript 中的 Array 在内存中并非连续存储;其次,Array 的索引并非偏移量,且索引类型并非 Number,而是 String。我们之所以能使用 arr[0] 这种写法,是因为语言底层会自动将 Number 类型的 0 转换为 String 类型的 “0”。因此,JavaScript 中不存在传统意义上的数组索引,只有类似 “0”、“1” 这样的属性名。有趣的是,每个 Array 对象都包含一个 length 属性,使其行为更贴近其他语言的数组。但为何 for-in 遍历时不输出 length 属性?这是因为 for-in 仅遍历“可枚举的属性”,而 length 属于不可枚举属性。实际上,Array 对象还包含其他不可枚举的属性。
回顾前文使用 for-in 遍历数组的例子,我们对代码进行如下修改:
const arr = [1, 2, 3];
arr.name = "Hello world";
let index;
for(index in arr) {
console.log("arr[" + index + "] = " + arr[index]);
}
运行结果是:
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[name] = Hello world
由此可见,for-in 循环访问了我们新增的 “name” 属性,因为 for-in 会遍历对象的所有属性,而不仅仅是索引。同时需注意,此处输出的索引值(如 “0”、“1”、“2”)并非 Number 类型,而是 String 类型,因为它们作为属性被输出。那么,是否只要不在 Array 对象中添加新属性,就能仅输出数组内容呢?答案是否定的。因为 for-in 不仅遍历 array 自身的属性,还会遍历 array 原型链上的所有可枚举属性。
下面看一个具体例子:
Array.prototype.fatherName = "Father";
const arr = [1, 2, 3];
arr.name = "Hello world";
let index;
for(index in arr) {
console.log("arr[" + index + "] = " + arr[index]);
}
运行结果是:
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[name] = Hello world
arr[fatherName] = Father
至此可以发现,for-in 并不适合用于遍历 Array 中的元素,它更适合遍历对象中的属性,这也是其设计初衷。但有一种例外情况,即稀疏数组。
考虑以下示例:
let key;
const arr = [];
arr[0] = "a";
arr[100] = "b";
arr[10000] = "c";
for(key in arr) {
if(arr.hasOwnProperty(key) &&
/^0$|^[1-9]\d*$/.test(key) &&
key <= 4294967294
) {
console.log(arr[key]);
}
}
for-in 仅遍历存在的实体。在上述例子中,for-in 遍历了3次(遍历属性分别为 “0”、“100”、“10000” 的元素),而普通 for 循环则会执行 10001 次。因此,只要处理得当,for-in 在遍历 Array 元素时也能发挥重要作用。
为避免重复编码,我们可以封装上述逻辑:
function arrayHasOwnIndex(array, prop) {
return array.hasOwnProperty(prop) &&
/^0$|^[1-9]\d*$/.test(prop) &&
prop <= 4294967294; // 2^32 - 2
}
使用示例如下:
for (let key in arr) {
if (arrayHasOwnIndex(arr, key)) {
console.log(arr[key]);
}
}
for-in 性能分析
如前所述,每次迭代操作需同时搜索实例属性和原型属性,导致 for-in 循环每次迭代产生更多开销,因此速度比其他循环类型慢,通常仅为其他类型循环速度的 1/7。除非明确需要迭代属性数量未知的对象,否则应避免使用 for-in 循环。若需遍历数量有限的已知属性列表,使用其他循环方式会更高效,例如:
const obj = {
"prop1": "value1",
"prop2": "value2"
};
const props = ["prop1", "prop2"];
for(let i = 0; i < props.length; i++) {
console.log(obj[props[i]]);
}
上述代码将对象属性存入数组后,直接遍历该数组。相较于 for-in 逐个查找属性,该方法仅关注给定属性,节省了循环开销与时间。
forEach 方法详解
forEach 方法
在 ES5 标准中,引入了新的循环机制,即 forEach 方法。
const arr = [1, 2, 3];
arr.forEach((data) => {
console.log(data);
});
运行结果:
1
2
3
forEach 方法会为数组中包含有效值的每一项执行一次 callback 函数。那些已被删除(如使用 delete 方法)或从未赋值的项将被跳过(但不包括值为 undefined 或 null 的项)。callback 函数依次接收三个参数:
- 数组当前项的值;
- 数组当前项的索引;
- 数组对象本身;
需注意,forEach 的遍历范围在第一次调用 callback 前即已确定。调用 forEach 后添加到数组中的新项不会被 callback 访问。若已存在的值被修改,传递给 callback 的值是 forEach 遍历到该时刻的值。已删除的项不会被遍历。
const arr = [];
arr[0] = "a";
arr[3] = "b";
arr[10] = "c";
arr.name = "Hello world";
arr.forEach((data, index, array) => {
console.log(data, index, array);
});
运行结果:
a 0 ["a", 3: "b", 10: "c", name: "Hello world"]
b 3 ["a", 3: "b", 10: "c", name: "Hello world"]
c 10 ["a", 3: "b", 10: "c", name: "Hello world"]
此处的 index 为 Number 类型,且不会像 for-in 那样遍历原型链上的属性。
因此,使用 forEach 时,无需专门声明 index 和遍历元素,因为这些均作为回调函数的参数提供。
此外,forEach 会遍历数组中的所有元素。ES5 还定义了一些其他有用的方法,部分列举如下:
- every: 循环在第一次 return false 后返回
- some: 循环在第一次 return true 后返回
- filter: 返回一个新数组,该数组内的元素满足回调函数
- map: 将原数组中的元素处理后再返回
- reduce: 对数组中的元素依次处理,将上次处理结果作为下次处理的输入,最终得到结果。
forEach 性能分析
首先感谢@papa pa的提醒,此前我的理解存在偏差。
大家可以参考 jsPerf,在不同浏览器下测试的结果均显示 forEach 的速度不如 for 循环。若将测试代码置于控制台执行,可能会得到不同结果,这主要源于控制台的执行环境与真实代码执行环境的差异。
ES6 for-of 语句
for-of 语句
先来看一个示例:
const arr = ['a', 'b', 'c'];
for(let data of arr) {
console.log(data);
}
运行结果是:
a
b
c
为何要引入 for-of?
回答此问题,我们先回顾 ES6 之前的 3 种 for 循环存在的缺陷:
- forEach 无法使用 break 和 return;
- for-in 缺点更为明显,它不仅遍历数组元素,还会遍历自定义属性,甚至访问原型链上的属性。此外,遍历数组元素的顺序可能是随机的。
鉴于上述缺陷,我们需要改进原有的 for 循环。但 ES6 不会破坏已有的 JS 代码。目前,成千上万的 Web 网站依赖 for-in 循环,其中一些甚至用于数组遍历。若通过修正 for-in 循环来增加数组遍历支持,将使情况更加混乱。因此,标准委员会在 ES6 中新增了一种循环语法以解决当前问题,即 for-of。
那么 for-of 具体能做什么呢?
- 与 forEach 相比,for-of 可以正确响应 break、continue 和 return。
- for-of 循环不仅支持数组,还支持大多数类数组对象,例如 DOM nodelist 对象。
- for-of 循环也支持字符串遍历,它将字符串视为一系列 Unicode 字符进行遍历。
- for-of 也支持 Map 和 Set(两者均为 ES6 中新增的类型)对象遍历。
总结而言,for-of 循环具备以下特征:
- 这是最简洁、最直接的遍历数组元素的语法。
- 该方法避开了 for-in 循环的所有缺陷。
- 与 forEach 不同,它可以正确响应 break、continue 和 return 语句。
- 其不仅可以遍历数组,还可以遍历类数组对象和其他可迭代对象。
但需注意,for-of 循环不支持普通对象。若想迭代对象的属性,可使用
for-in 循环(这也是其本职工作)。
最后,ES6 引入的另一种遍历数组值的方式是 Iterator。参考上个例子:
const arr = ['a', 'b', 'c'];
const iter = arr[Symbol.iterator]();
iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }
不过,该内容超出本文范围,且 Iterator 涉及内容较多,日后将专门撰文介绍,敬请关注本站。
总结
以上就是本文的全部内容,希望本文的内容对大家的学习或工作能带来帮助。如有任何疑问,欢迎留言交流。
JS处理含Emoji或中文的字符串转数组乱码问题
2026-11-05 16:12:28
JS怎么使用展开运算符合并对象
2026-11-05 14:42:18
ES6+环境下判断JS对象为空的简洁写法
2026-11-05 12:12:23
怎么通过replacer函数自定义JS中JSON的格式化输出
2026-11-05 11:19:44
JS正则表达式基础:字面量与RegExp构造函数的区别及常用元字符
2026-11-05 11:16:58
