问题描述
不知道大家在平时写代码的过程中是否遇到以下的报错信息
SyntaxError: Illegal break statement
SyntaxError: Illegal continue statement: no surrounding iteration statement
以上的报错原因就是非法使用break/continue关键字,具体点说就是这两个关键字应该在循环体里面使用。乍一看,感觉没毛病很正常,哪个憨憨会把这两个关键字放到循环体外面使用而产生报错呢?
那么,show you the issue code:
function main() {
const nums = [1, 2, 3, 4];
nums.forEach(num => {
if (num === 2) {
break;
}
console.log(num);
})
}
是不是以为运行结果只有1?没这么简单,结果如下:
对的,没看错,看似是在终止循环,结果却是把break放到循环体外面了。
为什么呢?下面我们一起来看看这个forEach里面到底干了些什么。
再现forEach
我们知道,forEach是js数组原生提供的一个API函数,函数的传参是一个回调函数,forEach函数内部就是将遍历数组并将数组的每个元素都执行一次此回调函数。
我们先将上面的代码换一个形式:
function main() {
const nums = [1, 2, 3, 4];
nums.forEach(f)
}
function f(num) {
if (num === 2) {
break;
}
console.log(num);
}
此代码和原始代码是一模一样的效果,只不过这里将匿名的回调函数抽出来变成一个有名字的函数。
下一步,我们尝试自己手动实现一个forEach函数:
function main() {
const nums = [1, 2, 3, 4];
// nums.forEach(f);
forEach(nums, f);
}
function f(num) {
if (num === 2) {
break;
}
console.log(num);
}
function forEach(nums, callback) {
for (let num of nums) {
callback(num);
}
}
main();
到这里,想必大家都明白了为什么在forEach里面使用break/continue关键字会报错了。
我们知道在js里,每一组{}都是一个块级作用域,在这里for循环体内是一个块级作用域,callback函数内部是自己的一个函数作用域,并且我们知道js原生只有全局作用域和函数作用域,块级作用域也是基于函数作用域和词法环境而实现的。
正因为如此,在callback函数的函数作用域里没有循环体,所以会报错。
但是如果将代码改成如下的常见形式即不会出现错误:
function main() {
const nums = [1, 2, 3, 4];
// nums.forEach(f);
forEach(nums, f);
}
function forEach(nums, callback) {
for (let num of nums) {
if (num === 2) {
break;
}
console.log(num);
}
}
main();
原因很简单,因为此时break和for循环体是在同一个函数作用域内,并且break所处在的块级作用域也是在for循环体的块级作用域内。
扩展(return和异常)
既然知道了forEach的实现原理,那么我们再看看在forEach中使用return会是什么效果:
function main() {
const nums = [1, 2, 3, 4];
nums.forEach(num => {
if (num === 2) {
return;
}
console.log(num);
});
}
结果是1,3,4。
咦,为什么3,4还在呢?请看下面这段代码:
function main() {
const nums = [1, 2, 3, 4];
forEach(nums, f);
}
function f(num) {
if (num === 2) {
return;
}
console.log(num);
}
function forEach(nums, callback) {
for (let num of nums) {
callback(num);
}
}
main();
此时结果就很显然了,因为这个return只是把当前回调函数给终止了,但是它并不会影响后续元素继续执行回调函数。
如MDN文档所介绍,如果想中止forEach循环,只能通过抛异常并将其捕获来处理:
function main() {
const nums = [1, 2, 3, 4];
try {
forEach(nums, f);
} catch (e) {
console.log(e)
}
}
function f(num) {
if (num === 2) {
throw "终止循环";
}
console.log(num);
}
function forEach(nums, callback) {
for (let num of nums) {
callback(num);
}
}
main();
结语
这篇文章通过自己手动模拟一个forEach函数来展示forEach的底层实现原理,并结合break/continue、return关键字以及异常来演示其运行效果,其中还涉及到js的作用域和词法环境的相关概念,如果有理解不正确的地方,欢迎大家进行指正。