Недавно я проходил собеседование, которое включало сравнение двух разных схем. Опущу детали, но прямо в середине собеседования всплыла одна очень важная вещь — нельзя прерывать цикл forEach(). Я забыл об этом, и, видимо, похоронил свои шансы получить работу. Я надеюсь, после прочтения этой статьи вы не повторите мою ошибку.

MDN знает всё

Как отмечает MDN:

Не существует другого способа остановить или прервать цикл forEach() loop кроме выброса исключения. Если вам нужно подобное поведение, метод forEach() неподходящий инструмент

Звучит весьма категорично. Однако это правда: важно знать, какой инструмент выбрать.

Перед тем, как разобраться, почему нельзя прерывать forEach(), давайте рассмотрим, что такое цикл и откуда появился forEach().

Что такое цикл

Циклы в программировании решают очень распространённую задачу: запускать тот же самый код для всех данных. Проще говоря:

Повторение того же кода снова и снова (в цикле), пока мы не достигнем конечного состояния.

Задача

Для сравнения решим задачу, используя различные виды циклов. Вот она: сравнить два массива и посмотреть, совпадают ли их элементы.

Вот данные, которые мы будем сравнивать:

const jedis = ["Anakin","Luke"]
const sith = ["Palpatine", "Anakin"]

У нас есть два массива, оба с парой имён. В обоих списках есть Энакен. Это простейшая задача, однако она не так далека от того, которую я решал на собеседовании.

Путь

Я не собираюсь рассказывать, что один цикл лучше других. Все они предлагают уникальные программные решения и имеют своё применение. Хитрость в том, чтобы знать, когда и какой цикл использовать.

Традиционный цикл for

Если вы когда-либо проходили курс по программированию, вы знакомы со старым добрым циклом for. Долгое время он был полезным инструментом для программистов, да и сейчас полезен. Давайте решим нашу задачу с его помощью:

// снова наши данные для справки
const jedis = ["Anakin", "Luke"];
const sith = ["Palpatine", "Anakin"];
// запускаем цикл, определяем переменную итератора
for (let i = 0; i < jedis.length; i++) {
  // создаём переменную, на которую можем ссылаться
  const thisJedi = jedis[i];
  // проверяем, существует ли элемент в проверяемом массиве
  if (sith.includes(thisJedi)) {
    // если существует, значит, этот джедай ещё и ситх
    console.log(`${thisJedi} is also a Sith`);
    // можем выходить
    break;
  }
  console.log(`${thisJedi} is not a Sith`);
}

Цикл for предлагает весьма удобный способ выхода, если он удовлетворяет выбранному условию, что чрезвычайно полезно при циклической обработке большого количества данных. Он также был очень полезен в решении некоторых задач Проекта Эйлера, особенно этой.

 Альтернативный путь

forEach() был представлен в спецификации в 2009 году наряду с прочими достоинствами ES5. Он служит удобным методом написания чистого кода, который легко перебирает элементы массива.

Что он делает?

Цикл forEach() — это функция, запускающая другую функцию (обратный вызов) для каждого элемента массива. Мы определяем, что происходит в этой функции обратного вызова. JS принимает три параметра для этой функции:

  1. элемент массива;
  2. индекс элемента;
  3. массив полностью.

Давайте используем цикл forEach() для решения нашей задачи. Я включил все три параметра в функцию, но используем мы только первый — элемент, который я назвал jedi.

// Создаём глобальную переменную состояния для отслеживания
    let matching
    // запускаем цикл в массиве 
    jedis.forEach((jedi,index,array) => {
      // проверяем, есть ли элемент jedi в массиве sith
      if(!sith.includes(jedi)) {
        // если нет, задаём значение false глобальной переменной
        matching = false
      }
      // код продолжает работу...
    })
    console.log(matching) // false

Наше решение по сути делает то же самое. Единственное отличие в том, что код продолжает работать, пока не достигнет конца массива jedis. Навряд ли для такого маленького массива будет видна разница в производительности.

Но почему?

И наконец мы добрались до ответа на наш вопрос: почему нельзя прерывать цикл forEach()? Потому что цикл запускает функцию обратного вызова для каждого элемента, поэтому, даже если вы пишете return, он возвращается только в этом инстансе функции. Он продолжит работать. В случае функции forEach() ничего не произойдёт с возвращённым кодом. Имейте в виду, что это не так в других методах массива.

Из-за этого выражения break или continue также являются не допустимыми.

Другие пути

Существует довольно много различных циклов. Все они имеют различное применение, и я рекомендую ознакомиться с каждым из них. Вам не всегда нужен именно цикл forEach().

forEach() или map()

Вероятно, наиболее распространёнными методами в руководствах являются forEach() и map(). Самое существенное различие между ними в том, что map возвращает новый массив, а forEach() — нет.

Традиционные циклы

Цикл while

Методы массива

Array.forEach(), Array.map(), Array.filter(), Array.reduce(), Array.reduceRight(), Array.every(), Array.some(), Array.indexOf(), Array.lastIndexOf(), Array.find(), Array.findIndex()

Повторяемые циклы объектов (включая массивы)

for in, for of

Вот путь

Как упоминалось ранее, в документации MDN, выбор правильного инструмента крайне важен для успеха. На первый взгляд количество вариантов ошеломляет, но мне нравится подход: “если инструмент работает, значит, он подходит”.

Вы можете рефакторить свой код до посинения, но будете просто тратить время вместо того, чтобы создавать. На собеседовании я использовал правильный инструмент, но неправильным способом. Если бы я помнил, что цикл forEach() нельзя прервать, всё могло бы сложиться иначе ??‍♂️.

Хорошего кодинга!

Читайте также:


Перевод статьи Jared Nutt: Why you can’t break a forEach loop in JavaScript

Предыдущая статьяScrum мёртв. Восславим нового короля - Kanban!
Следующая статьяУчим ИИ отвечать на сообщения