JavaScript 的 foreach 用不了 break/continue?同样写法下 for 循环也不行
JavaScript 的 foreach 用不了 break/continue?同样写法下 for 循环也不行
今天在群里和群友一起探讨一个 JavaScript 异步问题的时候,就 foreach
/map
函数进行了一番学习和讨论。当然,群友的侧重点还是在异步的实现方面。
对于我而言,更感兴趣的反而是数组这边为什么不能够使用 break/continue/return。这边这里就结合一下自己的理解,以及 MDN 上实现的 polyfill 去分析一下其中详情。
for
循环 vs forEach
这里会简单的就 for
循环 和 forEach
进行一下对比,输入和条件都是一样的。
for
循环
正常情况下,大多数人是这样使用 for
循环 的:
const arr = [1, 2, 3, 4, 5];
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
以上面的数组为例,它是一个升序数组,假设需求是只打印出 3 以下(包括 3)的所有数字,那么可以使用 break
关键词去跳出循环,达到提升性能的效果:
const arr = [1, 2, 3, 4, 5];
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
if (arr[i] >= 3) break;
}
这样,控制台就只会输出 1,2,3,而不会输出 3 以上的数字。对于传统的 for
循环 来说,还要自己控制 下标(index),需要开发自己去实现增长,使用起来却是有一点的麻烦。
forEach
也因此,除了传统的 for
循环 之外,ES5 也提供了诸如 forEach()
和 map()
这样使用起来更加方便的迭代函数。
以 forEach()
为例,完成对数组所有数值的输出可以这样实现:
const arr = [1, 2, 3, 4, 5];
arr.forEach((el, index) => {
console.log(el, index);
});
比起传统的 for
循环 而言,显然 forEach()
的代码量更少,写起来更加的简洁干净。只是,如果有同样的需求,即只打印出 3 以下(包括 3)的所有数字,使用 forEach()
就没有办法像 for
循环 那样终止迭代了:
const arr = [1, 2, 3, 4, 5];
arr.forEach((el, index) => {
console.log(arr[i]);
// 会报错
if (el >= 3) break; // SyntaxError: Illegal break statement
});
在这个情况下,使用 return 关键字是不会产生任何效果的,使用 break 以及 continue 会出现 SyntaxError
的报错。
也因此,之前也看到过有一些文章会推荐用抛出异常的方式去强行终止程序,再在外侧使用 try/catch
去接住抛出的异常,使得程序不至于被终止——这其实不是一个好习惯,这个代码这样也存在逻辑上的问题。
而且,其实在实现同样写法的情况下,使用 for
循环 也会造成同样的后果。
回调函数是问题的根本
forEach
的完整语法其实是这样的:
arr.forEach(callback(currentValue [, index [, array]])[, thisArg])
用中文解释一下是这样的:
-
forEach
接受一个 回调函数(callback) 作为必要的参数而 回调函数 又会接受以下三个参数:
-
currentValue
当前被操作的值
-
index
当前被操作的值的索引,可选
-
array
forEach()
方法正在操作的数组,可选
-
-
forEach
接受一个thisArg
作为可选参数thisArg
可是做回调函数中的this
也就是说,当调用 forEach
的时候,其实是回调函数在进行真正的操作。所以 forEach
又可以被重写成这样:
function cb(currentValue, index, array) {
console.log(currentValue);
// break & continue 当然不会工作
// 因为 cb 都不在循环体内
// return 只会起到中止 cb 的作用
// 所以对于 forEach 来说,它会结束当前迭代,进入下一个循环
return;
}
arr.forEach(cb);
而 forEach
的 polyfill 也是这么实现的:
// Production steps of ECMA-262, Edition 5, 15.4.4.18
// Reference: https://es5.github.io/#x15.4.4.18
if (!Array.prototype['forEach']) {
Array.prototype.forEach = function (callback, thisArg) {
// 省略一些异常的处理,英文的备注,只留下了处理 callback 这一部分的代码
var T, k;
var O = Object(this);
var len = O.length >>> 0;
if (arguments.length > 1) {
T = thisArg;
}
k = 0;
while (k < len) {
var kValue;
if (k in O) {
kValue = O[k];
// T 就是 this,有参数的情况下就是 thisArg
// kValue 即 currentValue
// k 即 index
// O 即 array
callback.call(T, kValue, k, O);
}
k++;
}
};
}
使用 cb,for
循环 也会报错
在 for
循环 内写回调函数的效果也是一样的:
const arr = [1, 2, 3, 4, 5];
function cb(currentValue, index, array) {
console.log(currentValue);
// 这里用 break/continue 同样会报错
// return 提前终止函数,不会进行其他的操作
// 相当于直接在 for循环 内使用 continue
if (index >= 3) {
return;
}
console.log('永远<3', index);
}
for (let i = 0; i < arr.length; i++) {
cb(arr[i], i);
}
输出结果为:
notes>node test.js
1
永远<3 0
2
永远<3 1
3
永远<3 2
4
5
关于 map
map
和 forEach
同样都是 ES5 的产物,实现方式也颇为相似,所以出现异常的原理也是一样的。
唯一的不同就在于,map
在实现内加入了一个数组,最后结果也返回了一个数组,简化版约为:
// 这里就不重写 Array.prototype.map 了,直接接受一个数组作为参数
const map = (arr, cb) => {
const returnVal = [];
for (let i = 0; i < arr.length; i++) {
const el = arr[i];
returnVal.push(cb(el));
}
return returnVal;
};
// 测试代码
const arr = [1, 2, 3, 4, 5];
// 主要内容的实现依旧在回调函数之中,所以同样,使用 break/continue 没有任何作用
// 提前 reeturn 只是向 map 中的返回数组中推进一个 undefined
console.log(map(arr, (val) => val ** 2)); // [ 1, 4, 9, 16, 25 ]
forEach
的代替品
ES6 推出了一个 for...of
的语法,内部的实现原理是通过实现 可迭代(iterable) 去实现的,因此内部可以使用 continue, break,return,yield,效果如下:
for (const val of arr) {
if (val % 2 === 0) continue;
console.log(val ** 2);
}
console.log('-----------');
for (const val of arr) {
if (val > 2) break;
console.log(val);
}
console.log('-----------');
for (const val of arr) {
if (val === 1) return;
console.log(val); // 不会有任何输出
}
输出结果为:
1
9
25
-----------
1
2
-----------