Почему море соленое? Простой вопрос, но оставайтесь со мной, потому что я думаю, что круговорот воды — это хорошая аналогия для того, как на самом деле работает метод reduce
.
Вкратце, вода (H2O) испаряется с поверхности океанов, образуя облака. Облака проходят над сушей и конденсируются, пока не начнут выпадать осадки в виде дождя/снега. В конце концов, выпавшая вода попадает в реки и начинает свой путь к морю. По пути вода собирает минералы, включая соли, и несет их в море. Когда круговорот начинается снова, минералы остаются позади, и со временем уровень концентрации увеличивается.
Чтобы увидеть, как круговорот воды может помочь нам понять принцип работы reduce
, мы должны разбить его на три элемента:
- Минералы соответствуют элементам в массиве, над которыми мы выполняем
reduce
. - Вода — это параметр аккумулятора или функция обратного вызова редуктора.
- Океан — это накопитель в форме аргумента, как в начальном, так и в конечном значении.
Итак, давайте приведем это в соответствие с кодом
Пожалуйста, извините за измерения концентрации, они, вероятно, ошибочны, я не химик.
Сначала мы подготовим некоторые тестовые данные. Следующий код генерирует симулированные образцы, по два в месяц в течение нескольких лет.
const mineralsPerCycle = concentrationSamplesOverYear(24);
console.table(mineralsPerCycle);
function concentrationSamplesOverYear(samples) {
const interval = (2 * Math.PI) / samples;
const captureSample = i =>
((Math.random() + 7) / 8) * ((Math.cos(i * interval) + 2) / 3);
return [...new Array(samples)].map((_, i) => captureSample(i));
}
В console.table
будут выведены значения, прежде чем мы их используем. Ниже приведен пример, но у вас будут другие значения.
┌─────────┬─────────────────────┐
│ (index) │ Values │
├─────────┼─────────────────────┤
│ 0 │ 0.89801916280756 │
│ 1 │ 0.9567662790947499 │
│ 2 │ 0.9325939089002321 │
│ 3 │ 0.8992754278881672 │
│ 4 │ 0.7532231143389726 │
│ 5 │ 0.6765845269058688 │
│ 6 │ 0.6187743088061717 │
│ 7 │ 0.5157538308846997 │
│ 8 │ 0.46555646525988514 │
│ 9 │ 0.38054565223528175 │
│ 10 │ 0.33107496732400704 │
│ 11 │ 0.3348125096349211 │
│ 12 │ 0.30271050596599436 │
│ 13 │ 0.30352471441053985 │
│ 14 │ 0.3696661578004031 │
│ 15 │ 0.4156042590776569 │
│ 16 │ 0.4608111994637522 │
│ 17 │ 0.53172225574472 │
│ 18 │ 0.6594949154650602 │
│ 19 │ 0.6714790771824638 │
│ 20 │ 0.7728233018044018 │
│ 21 │ 0.8208884212567936 │
│ 22 │ 0.924437922104001 │
│ 23 │ 0.9497900622814304 │
└─────────┴─────────────────────┘
Далее мы смоделируем накопление минералов, как подразумевается в каждой двухнедельной пробе.
let oceanConcentration = 0;
console.log(`
Initial concentration = ${oceanConcentration} mgs/ltr
`);
oceanConcentration = mineralsPerCycle.reduce(
waterCycle,
oceanConcentration);
console.log(`
Final concentration = ${oceanConcentration} mgs/ltr
`);
function waterCycle(currentConcentration, cycleConcentration) {
return currentConcentration + cycleConcentration;
}
/* Output
Initial concentration = 0 mgs/ltr
Final concentration = 14.945932946637733 mgs/ltr
*/
Обратите внимание в приведенном выше коде, как мы упростили вызов метода reduce, во-первых, обратившись к функции обратного вызова, а во-вторых, обратившись к переменной для начального значения аккумулятора.
function waterCycle(currentConcentration, cycleConcentration) {
const newConcentration = currentConcentration +
cycleConcentration;
console.log(`${cycleConcentration} + ${
currentConcentration} = ${
newConcentration}`);
return newConcentration;
}
Если мы заменим редуктор WaterCycle на вышеприведенную версию, то увидим, как концентрация «накапливается» с каждым образцом.
0.89801916280756 + 0 = 0.89801916280756
0.9567662790947499 + 0.89801916280756 = 1.85478544190231
0.9325939089002321 + 1.85478544190231 = 2.787379350802542
0.8992754278881672 + 2.787379350802542 = 3.686654778690709
0.7532231143389726 + 3.686654778690709 = 4.439877893029681
0.6765845269058688 + 4.439877893029681 = 5.11646241993555
0.6187743088061717 + 5.11646241993555 = 5.735236728741722
0.5157538308846997 + 5.735236728741722 = 6.2509905596264215
0.46555646525988514 + 6.2509905596264215 = 6.716547024886307
0.38054565223528175 + 6.716547024886307 = 7.097092677121588
0.33107496732400704 + 7.097092677121588 = 7.428167644445595
0.3348125096349211 + 7.428167644445595 = 7.762980154080516
0.30271050596599436 + 7.762980154080516 = 8.06569066004651
0.30352471441053985 + 8.06569066004651 = 8.369215374457049
0.3696661578004031 + 8.369215374457049 = 8.738881532257452
0.4156042590776569 + 8.738881532257452 = 9.154485791335109
0.4608111994637522 + 9.154485791335109 = 9.61529699079886
0.53172225574472 + 9.61529699079886 = 10.14701924654358
0.6594949154650602 + 10.14701924654358 = 10.806514162008641
0.6714790771824638 + 10.806514162008641 = 11.477993239191106
0.7728233018044018 + 11.477993239191106 = 12.250816540995508
0.8208884212567936 + 12.250816540995508 = 13.071704962252301
0.924437922104001 + 13.071704962252301 = 13.996142884356303
0.9497900622814304 + 13.996142884356303 = 14.945932946637733
Неудивительно, что функция обратного вызова метода reduce (параметр один) называется reducer. Однако смущает одно обстоятельство: обратный вызов называется reducer не потому, что он «сводит» массив из (потенциально) многих элементов к одному значению (это может быть и не так). Он называется reducer, потому что (для каждого элемента массива) он принимает два аргумента (в основном, мы расширим этот момент позже) — аккумулятор и элемент. Затем он уменьшает их до одного значения, чтобы сформировать новый аккумулятор.
Что касается того, сколько параметров ожидает метод Array.reduce
, то на самом деле он ожидает до четырех:
- Аккумулятор — входящее уменьшенное значение
- Элемент — элемент массива, который будет уменьшен
- Индекс элемента массива (часто не используется)
- Обрабатываемый массив (не уменьшаемый), используется очень редко.
В следующем разделе мы рассмотрим тот факт, что выход может быть не единственным значением.
Reduce, корень многих методов
Метод reduce
способен выполнять множество операций (мы рассмотрим это позже), и, освоив его, легко найти возможности для его использования, но обычно есть лучшие варианты.
Метод map
Как и reduce
, метод map
принимает обратный вызов, но в данном случае это функция отображения, которая принимает значение из массива и создает новое значение, одно за другим. Создаваемый новый массив будет того же размера, что и входной массив.
Если мы используем map
следующим образом,
function celsiusToFahrenheit(degCelsius) {
return (degCelsius * 9) / 5 + 32;
}
console.table([-40, 0, 16, 100].map(celsiusToFahrenheit));
на консоли будет представлена таблица температур по Фаренгейту для каждой из температур по Цельсию во входном массиве.
Это также можно записать с помощью метода reduce
следующим образом, используя ту же функцию отображения.
console.table([-40, 0, 16, 100].reduce((acc, celsius) =>
[...acc, celsiusToFahrenheit(celsius)], []));
Метод filter
Мы можем сделать нечто подобное для воспроизведения метода filter
, используя функцию предиката, например:
const greaterThanFifty = (value) => value > 50;
console.table([20, 40, 60, 80, 100].filter(greaterThanFifty));
// 60, 80, 100
Теперь с методом reduce
.
console.table([20, 40, 60, 80, 100].reduce((acc, val) =>
greaterThanFifty(val) ? [...acc, val] : acc, []));
В обоих примерах использование reduce
приводит к более длинному и немного более сложному решению. Однако reduce
может объединить обе операции за один проход.
console.table(
[-40, 0, 16, 100].reduce((acc, celsius) => {
const fahrenheit = celsiusToFahrenheit(celsius);
return greaterThanFifty(fahrenheit) ?
[...acc, fahrenheit] : acc;
}, [])
); // [60.8, 212]
На самом деле результатом reduce
даже не обязательно должен быть массив.
console.table(
[-40, 0, 16, 100].reduce(
(acc, celsius) => ({ ...acc, [celsius]:
celsiusToFahrenheit(celsius) }),
{}
)
); // {'16': 60.8, '100': 212}
В приведенном выше примере будет создан объект, содержащий отображение температуры по Цельсию на температуру по Фаренгейту, но только для тех температур по Фаренгейту, которые больше 50 градусов.
До и после
В этом заключительном разделе я хотел бы дать несколько советов по использованию метода reduce
.
Что делать
- Используйте
reduce
при преобразовании массива в другую структуру данных. - Рассмотрите возможность использования метода
reduce
, когда операция представляет собой комбинациюmap
ping иfilter
ing.
Не рекомендуется
- Не используйте метод
reduce
, если существуют лучшие альтернативные методы. Они обычно работают лучше, поскольку реализованы в движке JavaScript. - Не бойтесь хотя бы изучить возможность использования метода
reduce
, когда это уместно.
Злой близнец Reduce
Метод reduce
вряд ли будет использоваться вами каждый день, но знание о его существовании и возможностях добавит еще один инструмент в ваш инструментарий.
Еще менее используемый метод работы с массивами в (не очень) злом близнеце reduce reduceRight
, который, я думаю, достаточно очевиден, что он делает. Reduce
обрабатывает элементы в массиве слева направо (в порядке индексов), reduceRight
обрабатывает массив справа налево (в обратном порядке индексов). Но reduceRight
не эквивалентен Array.reverse().reduce()
, потому что третий параметр функции reducer будет уменьшаться, а не увеличиваться по мере прохождения метода по массиву.