在 JavaScript 的编程世界里,循环是一种基础且重要的控制结构,不同类型的循环有着各自的特点和适用场景。本文将深入探讨 for 循环、for...in 循环以及 for...of 循环,并对它们进行性能分析。同时,我们还会研究 forEach 方法,因为它与 for 循环有着紧密的联系。
for 循环与 forEach 底层原理
for 循环
for 循环由程序员自己完全掌控循环的过程,它包含三个部分:初始化、条件判断和迭代语句。在性能方面,存在以下几种情况:
- 基于
var声明:在不确定循环次数的情况下,for循环和while循环性能相近。这是因为var声明的变量存在函数作用域和变量提升的特性,在循环中使用var声明的变量可能会导致一些意外行为,但在性能上二者没有明显差异。 - 基于
let声明:for循环使用let声明变量时性能更好。原因在于let声明的变量具有块级作用域,每次迭代都会创建一个新的变量实例,不会像var那样在全局作用域中留下不释放的变量,从而减少了内存开销。
forEach 方法
forEach 是数组原型上的方法,用于对数组的每个元素执行一次给定的函数。它的底层实现其实就是一个 for 循环。我们可以通过以下代码模拟其实现:
Array.prototype.forEach = function forEach(callback, context) {
let self = this,
i = 0,
len = self.length;
context = context == null? window : context;
for (; i < len; i++) {
typeof callback === "function"? callback.call(context, self[i], i) : null;
}
};
这里 forEach 接收一个回调函数 callback 和一个可选的 context(用于指定回调函数执行时的 this 指向)。在内部,通过 for 循环遍历数组,对每个元素执行回调函数。
for...in 循环的 BUG 及解决方案
for...in 循环用于迭代对象的所有可枚举属性(包括私有属性和公有属性)。然而,它存在一些问题:
-
性能问题:它会按照原型链一级级查找属性,这在性能上是比较消耗的,尤其是当对象的原型链较长时。
-
遍历问题:
- 不能迭代
Symbol属性:Symbol类型的属性是独一无二的,for...in无法遍历到这类属性。 - 迭代顺序问题:它的迭代顺序会以数字属性优先,这可能与我们预期的顺序不一致。
- 公有可枚举属性问题:公有可枚举的属性(一般是自定义属性)也会被迭代,这可能会导致一些意外结果。
- 不能迭代
解决方案可以通过以下方式:
Object.prototype.fn = function fn() {};
let obj = {
name: 'rx',
age: 1,
[Symbol('AA')]: 100,
0: 200,
1: 300
};
let keys = Object.keys(obj);
if (typeof Symbol!== "undefined") keys = keys.concat(Object.getOwnPropertySymbols(obj));
keys.forEach(key => {
console.log('属性名:', key);
console.log('属性值:', obj[key]);
});
这里先使用 Object.keys() 获取对象的可枚举字符串属性,然后检查 Symbol 是否存在(因为在旧版本 JavaScript 中可能不存在 Symbol),如果存在则使用 Object.getOwnPropertySymbols() 获取对象的 Symbol 属性,最后通过 forEach 遍历所有属性。
for...of 循环的底层机制
for...of 循环基于迭代器(iterator)规范进行遍历。迭代器是一个对象,它必须具备 next 方法,每次执行 next 方法会返回一个对象,该对象包含 value(当前迭代的值)和 done(表示迭代是否结束)两个属性。
部分数据结构的迭代器
JavaScript 中的数组、部分类数组、Set、Map 等数据结构默认实现了迭代器规范,即它们都有一个 Symbol.iterator 方法。例如数组:
arr = [10, 20, 30];
arr[Symbol.iterator] = function () {
let self = this,
index = 0;
return {
next() {
if (index > self.length - 1) {
return {
done: true,
value: undefined
};
}
return {
done: false,
value: self[index++]
};
}
};
};
这里手动为数组定义了一个符合迭代器规范的 Symbol.iterator 方法,实际上数组默认就有这样的实现。
让对象具备可迭代性
对于普通对象,默认是不具备迭代器规范的。但我们可以通过给对象添加 Symbol.iterator 方法使其可迭代。例如类数组对象:
let obj = {
0: 200,
1: 300,
2: 400,
length: 3
};
obj[Symbol.iterator] = Array.prototype[Symbol.iterator];
for (let val of obj) {
console.log(val);
}
这里通过将数组的 Symbol.iterator 方法赋值给类数组对象,使得该对象可以使用 for...of 循环进行遍历。
性能测试
通过实际的性能测试可以更直观地了解不同循环的性能差异。我们使用 console.time() 和 console.timeEnd() 来测量不同循环的执行时间。
let arr = new Array(9999999).fill(0);
// FOR 循环测试
console.time('FOR~~');
for (let i = 0; i < arr.length; i++) {}
console.timeEnd('FOR~~');
// WHILE 循环测试
console.time('WHILE~~');
let i = 0;
while (i < arr.length) {
i++;
}
console.timeEnd('WHILE~~');
// FOREACH 测试
console.time('FOREACH~~');
arr.forEach(function (item) {});
console.timeEnd('FOREACH~~');
// FOR IN 测试
console.time('FOR IN~~');
for (let key in arr) {}
console.timeEnd('FOR IN~~');
// FOR OF 测试
console.time('FOR OF~~');
for (const val of arr) {
console.log(val);
}
console.timeEnd('FOR OF~~');
一般来说,在简单遍历数组时,for 循环(尤其是基于 let 声明变量的情况)和 while 循环性能较好,因为它们的控制逻辑简单直接。forEach 方法由于其内部是 for 循环实现,性能也不错,但它的回调函数会带来一定的额外开销。for...in 循环由于其遍历机制涉及原型链查找,性能相对较差,特别是在遍历大型数组时。for...of 循环在遍历可迭代对象时性能也较为可观,且代码简洁,更适用于需要获取迭代值的场景。
综上所述,在实际编程中,我们应根据具体的需求和场景选择合适的循环类型,以提高代码的性能和可读性。希望本文对您理解 JavaScript 中的循环机制有所帮助。