«Простой» цикл считается плохой практикой при программировании в функциональном стиле.

Синтаксис распространения не является функцией функции-редьюсера, поэтому его производительность не поддерживает ваш аргумент в пользу избегания reduce. С таким же успехом мы можем определить функцию-редуктор, которая не создает новый объект при каждой итерации:

const reduced = arr.reduce((acc, val) => {
  obj[val] = (obj[val] ? obj[val] + 1 : 1);
  return obj;
}, {});

Настоящая причина снижения производительности связана с дополнительными накладными расходами на вызовы дополнительных функций.

Однако, если мы используем функциональный подход к композиции, эта более медленная производительность предпочтительнее цикла, который поощряет побочные эффекты и мутации с дополнительным бонусом, заключающимся в том, что о нем труднее рассуждать, потому что мы должны учитывать внешнюю область в дополнение к ее внутренняя логика — это означает, что нашей команде приходится иметь дело с повышенной сложностью нескольких областей…

Таким образом, использование циклов в функциональной композиции дает производительность разработчика и надежность кода в обмен на более быстрое выполнение.

Но… тогда вы должны учитывать, что for of и for in должны быть заполнены для устаревших браузеров, а не Array функций более высокого порядка. Конечно, в большинстве современных проектов используется транспилятор, который может внедрять полифиллы. Однако функции родного языка, поддерживаемые всеми браузерами, следует предпочесть полифиллу, который увеличивает начальную нагрузку нашего приложения.

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

Если мы рассмотрим этот пример, который вы называете неэффективным, мы увидим, что его не стоит оптимизировать; это просто увеличило бы наше время доставки без заметной разницы в исполнении.

Просто создайте массив из тысяч чисел и сравните время выполнения, и мы увидим это:

const arr = [];
for (let i = 0; i < 100000; i++) {
  arr.push(Math.ceil(Math.random() * 10));
}

Давайте посмотрим, сколько времени потребуется, чтобы применить эту функцию reduce:

let then = performance.now();
const reduced = arr.reduce((acc, val) => ({
  ...acc, 
  [val]: (acc[val] ? acc[val] + 1 : 1)
}, {}));
console.log(performance.now() - then);
// ~21 chrome | ~139 firefox

Теперь давайте сделаем то же самое с циклом for… of:

const looped = { total: 0 };
for (let val of arr) {
  looped[val] = (looped[val] ? looped[val] + 1 : 1)
}
console.log(performance.now() - then);
// ~8 chrome | ~35 firefox

Вау! Очевидно, что for… of петля является победителем, верно? За исключением… эти результаты в мс. Между 8 мс и 139 мс нет заметных изменений. Время, сэкономленное на переборе 100 000 элементов массива, буквально не имеет значения — наблюдатель воспринимает оба результата как мгновенные.

Так что reduce здесь лучший выбор, потому что нам нужно рассмотреть только одну функцию с двумя случаями… Нам просто нужно проверить передачу объекта без записи для val и передачу объекта с запись для val:

it(‘should add new key having value of 1', () => {
  expect(reduceCount({}, 2))
    .toEqual({ '2' : 1 });
});
it(‘should increment existing key by 1', () => {
  expect(reduceCount({ ‘2’: 10 }, 2))
    .toEqual({ ‘2' : 11 });
});

Готово! Если мы пройдем эти тесты, мы можем быть уверены, что наш процесс работает так, как ожидалось. Более того, после прочтения кода и обращения к этим тестам ожидаемое поведение, которое мы определяем, должно быть абсолютно ясным:

Наша функция reduceCount должна добавить новый ключ со значением 1 или увеличить существующий ключ на 1.

И если мы определили функцию, которая применяет этот редюсер, все, что нам нужно сделать, чтобы протестировать ее, — это убедиться, что мы вызываем arr.reduce с правильными аргументами:

const countVals = arr => arr.reduce(reduceCount, {});
it(‘should reduce vals using reduceCount', () => {
  const arr = [1,2,3];
  countVals(arr);
  expect(arr.reduce)
    .toHaveBeenCalledWith(reduceCount, {});
});

С другой стороны, чтобы проверить наш цикл for, нам нужно сначала определить массив чисел, затем нам нужно определить ожидаемый объект, что еще больше увеличивает сложность и снижает надежность наших тестов:

let arr;
beforeEach(() => {
  arr = [1, 2, 2, 3, 3, 3];
});
it('should yield object counting unique values', () => {
  expect(loopCount(arr))
    .toEqual({ '1': 1, '2': 2, '3': 3 });
});

Теперь, я думаю, мы закончили. Но хороший ли это тест? Я бы сказал нет.

Утверждение, что должен быть получен объект, подсчитывающий уникальные значения, не дает ясного представления ничего полезного о нашей логике — поэтому новые разработчики тратят больше времени на распаковку кода или спрашивая у коллег — и запас для ошибка слишком высока, потому что для написания этого теста разработчик должен:

  • Создайте массив значений.
  • Сопоставьте количество для каждого отдельного значения в массиве.
  • Определите объект, содержащий эти значения и их количество.
  • Верить и молиться о лучшем…

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

Что еще хуже, этот тест займет больше времени, и мы должны проверить каждый случай сразу! Мы тестируем это:

  • Наш промежуточный массив определен.
  • Создается новый ключ.
  • Существующий ключ обновляется.

Я бы не стал рассматривать такое широкое утверждение как тестирование отдельной части нашего кода в конкретном случае, а вы? И что еще хуже, мы в основном тестируем функции родного языка! Наша настоящая логика где-то там зарыта.

Мы могли бы проверить это, определив функцию, содержащую нашу логику:

looped[val] = addCount(looped, val);

Затем мы можем проверить, что он применяется:

it('should apply addCount to each item', () => {
  const arr = [1,2,3];
  loopCount(arr);
  expect(addCount)
    .toHaveBeenCalledTimes(arr.length);
});

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

И хотя это простой пример, в большинстве случаев BAU результат один и тот же: использование цикла снижает производительность разработчика, снижает надежность кода, увеличивает сложность и в то же время дает прирост производительности, который никто, вероятно, не заметит…

И заинтересованные стороны будут меньше беспокоиться о сэкономленных вами 100 мс и больше беспокоиться об увеличении времени доставки в результате ненужной оптимизации…

И еще масса причин не зацикливаться на этом обсуждении здесь и здесь