За последние несколько лет такие методы в ES6, как filter (), forEach () и map (), стали очень популярными. Но действительно ли они лучше?

Мы все привыкли к чистому коду, который предоставляют нам Array.map и Array.filter, но для ваших реальных приложений циклы for лучше, чем эти удобные методы?

Есть несколько показателей, которые мы рассмотрим при сравнении методов в классе Array со «старым» способом работы с циклом for: производительность, удобочитаемость и масштабируемость. Вот один из примеров типов циклов, которые мы будем сравнивать сегодня:

const items = [’abc’, ’def’, ’ghi’, ’jk’];

Это идеальный вариант использования метода filter:

const threes = items.filter(item => item.length === 3);

Или способ решения проблемы с помощью цикла:

const threes = [];
for(let i = 0; i < items.length; i++) {
  if (items[i].length === 3) {
    items.push(items[i]);
  }
}

В поисках лучшего

Из всех методов, которые мы рассматриваем в этой статье, ясно, что их временная сложность равна O (n). Это означает, что в худшем случае для решения проблемы нам потребуется хотя бы каждый элемент массива. Подумайте об этом: мы не можем отфильтровать элементы длиной 3, фактически не глядя на каждый элемент. Если мы остановимся посередине, что будет O (n / 2), мы не решили проблему.

Учитывая, что у нас одинаковая временная сложность, нам необходимо рассмотреть три вопроса:

  • Какой способ быстрее?
  • Какой метод более масштабируемый?
  • Какой метод более читабелен и удобен в обслуживании?

Фактор первый: производительность

Производительность можно рассчитать в JavaScript с метрикой операций в секунду. Хотя мы не можем напрямую получить доступ к процессору, чтобы проверить производительность нашего кода так близко к «металлу», мы можем проверить, насколько быстро выполняется наш код.

Мы воспользуемся библиотекой под названием Benchmark.js, чтобы помочь нам в этом, что даст нам статистически значимые результаты. Benchmark выполнит наш код в течение нескольких итераций и усреднит для нас результаты. Для теста я выбрал массив из 1000 объектов Человек, которые имеют такие свойства, как имя, возраст и пол. В нашем тесте производительности мы отфильтруем всех лиц старше 10 лет.

const people = [
  {name: ’Jane Doe’, age: 32, gender: ”Female”},
  ... 999 other persons
}

К нашему удивлению, циклы for намного быстрее, чем метод Array.filter. Если быть точным, метод Filter на 77% медленнее, чем цикл for. Почему это? Одна из причин может заключаться в том, что циклы for выполняются синхронно, а метод фильтрации создает по одной новой функции для каждого элемента в массиве. Затем эта новая функция помещается в стек вызовов и запускается по очереди в цикле обработки событий. Эти накладные расходы требуют времени, которое полностью игнорируется циклом for, и запускают ваши операции прямо в той же функции.

Ясно, что цикл for является явным победителем в чистой производительности, если мы посмотрим на метрику операций в секунду или время. Нам нужно будет посмотреть, как он будет выглядеть по остальным двум показателям, и будет ли он явным победителем или нет.

Фактор второй: читаемость

Конечно, одним из основных факторов написания кода является удобочитаемость. Более читаемый код легче поддерживать, даже если это простой цикл for. Когда мы смотрим на слова, а не на числа в цикле for, мы это понимаем. Массив имеет имена методов, которые более информативны, чем цикл for.

  • indexOf = находит индекс первого элемента, который соответствует
  • forEach = запускает функцию для каждого элемента
  • filter = фильтры для любых элементов, которые соответствуют
const threes = [];
for(let i = 0; i < items.length; i++) {
  if (items[i].length === 3) {
    items.push(items[i]);
  }
}

Давайте рассмотрим наш предыдущий пример троек и проанализируем его еще немного.

  • Первая строка не имеет смысла, мы создаем новый массив для хранения наших отфильтрованных элементов, но он не добавляет никакой бизнес-логики.
  • Вторая строка также бессмысленна - она ​​предназначена для перебора всего массива, никакой бизнес-логики здесь нет.
  • В третьей строке находится логика, нам нужны элементы длиной 3.
  • В четвертой строке тоже нет логики - она ​​просто завершает нашу функцию.
const threes = items.filter(item => item.length === 3);

Когда мы сравниваем это с нашим примером фильтра, он ограничивается одной строкой, в которой есть вся необходимая логика.

Фактор третий: масштабируемость

Чтобы понять масштабируемость в JavaScript, нам нужно сначала понять, как JavaScript работает в основе: цикл событий и очередь событий. Поскольку JavaScript является однопоточным, у нас одновременно выполняется только 1 операция. Современные веб-сайты и веб-приложения должны выполнять несколько операций одновременно, так как же это работает? Цикл событий делает это возможным за счет совместного использования единственного потока, доступного окну браузера.

На диаграмме выше показано, как работает цикл событий JavaScript. У нас есть очередь событий, которая помещает события в очередь «первым пришел - первым обслужен». Когда функция запускается в цикле событий, она создает свой собственный «стек вызовов», в который помещаются все ссылки на функции в стеке «последний вошел - первым ушел». Но вопрос в том, действительно ли это помогает нам в циклах?

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

В качестве примера можно рассмотреть следующий код. При загрузке тела мы запускаем функцию, которая печатает первые 10 000 чисел. И у нас есть кнопка, которая запускает функцию, печатающую «ПРИВЕТ».

function start() {
  for(let i = 0; i < 10000; i++) {
    console.log(i);
  }
}
function print() {
  console.log('HELLO');
}

index.html:

<body onload="start()">
  <button onclick="print()">print</button>
</body>

Когда страница загружается, начинается печать чисел. Если мы нажмем кнопку перед числом 9999, оно не будет напечатано сразу. Это связано с тем, что событие кнопки onclick было помещено в очередь событий. Он будет запущен, как только цикл событий сможет вызвать следующее событие. Не имеет значения, какой тип цикла мы используем для итерации.

1
2
————-> click button
3
...
9994
9995
9998
————-> click button
9999
HELLO
HELLO

Как нам сделать это лучше?

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

  • Каждая итерация - это новое событие
  • Каждый «кусок» массива - это новое событие

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