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);

forEachpolyfill 也是这么实现的:

// 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

mapforEach 同样都是 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
-----------