Операции агрегирования
Операция агрегирования превращает всю коллекцию в одно значение. Примеры: количество элементов, их сумма, самый большой элемент или все элементы, объединённые в один результат.
Массивы дают часть этого напрямую. numbers.length считает элементы. Math.max() и Math.min() находят наибольшее и наименьшее число; элементы передаются им так: Math.max(...numbers). Для суммы или среднего метода нет: элементы объединяются с помощью reduce().
Будьте внимательны с пустыми массивами. Math.max() без значений возвращает -Infinity, а Math.min() возвращает Infinity, поэтому проверяйте numbers.length > 0, если массив может оказаться пустым.
Во многих языках есть отдельные методы fold и reduce для пошагового объединения элементов. Здесь это один и тот же метод, reduce(), и разница только в том, передаёте ли вы начальное значение. reduceRight() делает тот же проход, но от последнего элемента к первому.
Основные агрегаты
Самые частые агрегаты — количество, минимум, максимум, сумма и среднее.
numbers.lengthдаёт количество.Math.max(...numbers)иMath.min(...numbers)дают наибольшее и наименьшее число. Они работают только с числами.- Сумма — это
reduce(), который прибавляет каждый элемент к накопленному итогу. Среднее — это сумма, делённая на количество; деление сохраняет дробную часть, поэтому62 / 4равно15.5.
Иногда наименьший или наибольший элемент нужно выбирать по правилу, а не по самому значению. reduce() умеет и это: на каждом шаге вы оставляете лучший из двух элементов. Это работает и для слов, и для объектов, где Math.min() уже не поможет.
Иногда каждый элемент сначала нужно превратить в число, а результаты сложить. Это тоже один reduce(): прибавляйте вычисленное значение вместо самого элемента.
Количество, минимум, максимум, сумма и среднее
Для чисел 6, 42, 10 и 4 программа выводит количество через length, наибольшее и наименьшее через Math.max() и Math.min(), а сумму — через reduce(). Среднее равно 62 / 4 = 15.5.
Вывод: программа на TypeScript и захваченный вывод
const numbers = [6, 42, 10, 4]
// метода sum() не существует: reduce() складывает элементы
const sum = numbers.reduce((total, n) => total + n, 0)
console.log('Количество:', numbers.length)
console.log('Максимум:', Math.max(...numbers))
console.log('Минимум:', Math.min(...numbers))
console.log('Среднее:', sum / numbers.length)
console.log('Сумма:', sum)
Минимум и максимум по правилу
Первый reduce() оставляет элемент с меньшим остатком от деления на 3. Остатки чисел 5, 42, 10 и 4 равны 2, 0, 1 и 1, поэтому побеждает 42. Второй оставляет более длинное слово и находит "three".
Вывод: программа на TypeScript и захваченный вывод
const numbers = [5, 42, 10, 4]
const words = ['one', 'two', 'three', 'four']
// элемент, у которого остаток от деления на 3 самый маленький
const minRemainder = numbers.reduce((best, n) => (n % 3 < best % 3 ? n : best))
console.log(minRemainder)
// самое длинное слово, сравнение по длине
const longest = words.reduce((best, word) => (word.length > best.length ? word : best))
console.log(longest)
Сумма функции от каждого элемента
Колбэк прибавляет к итогу вычисленное значение вместо самого элемента. Удвоение чисел 5, 42, 10 и 4 даёт 122. Деление их пополам даёт 30.5, потому что деление сохраняет дробную часть.
Вывод: программа на TypeScript и захваченный вывод
const numbers = [5, 42, 10, 4]
// reduce() складывает значение, вычисленное из каждого элемента
console.log(numbers.reduce((total, n) => total + n * 2, 0))
console.log(numbers.reduce((total, n) => total + n / 2, 0))
Fold и reduce
Fold и reduce шаг за шагом объединяют все элементы в одно значение. Они хранят текущее значение, которое называется аккумулятором. Для каждого элемента они вызывают функцию с аккумулятором и элементом, а функция возвращает новый аккумулятор.
Здесь это один и тот же метод. Разница — во втором аргументе reduce():
reduce(fn, initial)— это fold. Он начинает с начального значения, которое вы передали, поэтому функция выполняется для каждого элемента.reduce(fn)— это reduce. Он начинает с первого элемента, поэтому функция работает начиная со второго элемента, а первый элемент через неё никогда не проходит.
Это важно, когда функция изменяет элемент. Если она удваивает каждый элемент, то reduce(fn) оставляет первый элемент как есть.
Это важно и для пустых массивов. reduce(fn, initial) просто возвращает начальное значение, а у reduce(fn) нет ничего, с чего начать, и он выбрасывает TypeError.
Чтобы объединять с другого конца, используйте reduceRight(): тот же метод, но проход идёт от последнего элемента к первому.
Reduce
reduce() без начального значения берёт 5 как старт и прибавляет к нему 2, 10 и 4, поэтому результат равен 21. Тот же вызов на пустом массиве выбрасывает ошибку, и программа выводит сообщение, которое она несёт: Reduce of empty array with no initial value.
Вывод: программа на TypeScript и захваченный вывод
const numbers = [5, 2, 10, 4]
// без начального значения reduce() начинает с первого элемента
const sum = numbers.reduce((acc, element) => acc + element)
console.log(sum)
// у пустого массива нет первого элемента, с которого можно начать
const empty: number[] = []
try {
empty.reduce((acc, element) => acc + element)
} catch (error) {
console.log((error as Error).message)
}
Fold
Здесь reduce() получает начальное значение 0 и прибавляет каждый элемент, умноженный на 2. Удваивается каждый элемент: 10 + 4 + 20 + 8 = 42.
Вывод: программа на TypeScript и захваченный вывод
const numbers = [5, 2, 10, 4]
// второй аргумент — начальное значение, поэтому через функцию проходит каждый элемент
const sumDoubled = numbers.reduce((sum, element) => sum + element * 2, 0)
console.log(sumDoubled)
Fold против reduce
Оба вызова используют одну и ту же функцию: сумма плюс элемент, умноженный на 2. Без начального значения результат равен 37, потому что первый элемент 5 является стартом и не удваивается. С начальным значением 0 результат равен 42.
Вывод: программа на TypeScript и захваченный вывод
const numbers = [5, 2, 10, 4]
// без начального значения: 5 — это старт, и он никогда не удваивается, то есть 5 + 4 + 20 + 8
const reduced = numbers.reduce((sum, element) => sum + element * 2)
console.log('Reduce:', reduced)
// начальное значение 0: удваивается каждый элемент, то есть 10 + 4 + 20 + 8
const folded = numbers.reduce((sum, element) => sum + element * 2, 0)
console.log('Fold:', folded)
Fold справа
reduceRight() проходит массив от последнего элемента к первому. Сложению порядок не важен, поэтому результат снова равен 42.
Вывод: программа на TypeScript и захваченный вывод
const numbers = [5, 2, 10, 4]
// reduceRight() проходит массив от последнего элемента к первому
const sumDoubledRight = numbers.reduceRight((sum, element) => sum + element * 2, 0)
console.log(sumDoubledRight)
Fold с индексом
Иногда важна позиция элемента, а не только его значение.
Колбэк reduce() получает индекс третьим аргументом, после аккумулятора и элемента. Условие на этот индекс затем решает, что делать с элементом. reduceRight() передаёт те же индексы; он лишь проходит их с конца.
Fold с индексом
Складываются только элементы с чётными индексами: по индексу 0 стоит 5, а по индексу 2 стоит 10, поэтому сумма равна 15.
Вывод: программа на TypeScript и захваченный вывод
const numbers = [5, 2, 10, 4]
// третий аргумент колбэка — это индекс
const sumEven = numbers.reduce((sum, element, idx) => (idx % 2 === 0 ? sum + element : sum), 0)
console.log(sumEven)
Fold с индексом справа
То же правило с reduceRight(). Колбэк видит индекс 3 первым, а индекс 0 последним, и результат по-прежнему равен 15.
Вывод: программа на TypeScript и захваченный вывод
const numbers = [5, 2, 10, 4]
// reduceRight() считает те же индексы, он лишь проходит их с конца
const sumEven = numbers.reduceRight((sum, element, idx) => (idx % 2 === 0 ? sum + element : sum), 0)
console.log(sumEven)
Безопасный reduce
reduce() без начального значения нуждается в первом элементе, с которого можно начать. На пустом массиве он выбрасывает TypeError и останавливает программу.
Один выход — начальное значение. Другой — честно сказать «результата нет»: сначала проверить длину и вернуть undefined. Тип возвращаемого значения тогда — number | undefined, что заставляет вызывающий код подумать о случае пустого массива, а ?? за один шаг подставляет запасное значение вместо undefined.
Reduce, который может вернуть undefined
sumOrUndefined() возвращает 21 для чисел 5, 2, 10 и 4 и undefined для пустого массива. sumOrUndefined(empty) ?? 0 превращает этот undefined в 0.
Вывод: программа на TypeScript и захваченный вывод
// sumOrUndefined для пустого массива отвечает undefined вместо выбрасывания ошибки.
function sumOrUndefined(items: number[]): number | undefined {
if (items.length === 0) return undefined
return items.reduce((sum, element) => sum + element)
}
const numbers = [5, 2, 10, 4]
const empty: number[] = []
console.log(sumOrUndefined(numbers))
console.log(sumOrUndefined(empty))
// ?? подставляет запасное значение вместо undefined
console.log(sumOrUndefined(empty) ?? 0)
Накопительные fold и reduce
Накопительный fold или reduce не выбрасывает промежуточные шаги. Он сохраняет аккумулятор после каждого элемента, поэтому результатом становится массив всех промежуточных значений.
Метода для этого нет. Вы храните переменную с суммой и внутри цикла добавляете её в массив результата через push(). Для накопительного reduce результат начинается с первого элемента. Для накопительного fold он начинается с начального значения, поэтому содержит на один элемент больше, чем исходный массив.
Накопительный reduce
Числа от 0 до 5 складываются одно за другим. Массив результата хранит каждую промежуточную сумму и выводится на одной строке с помощью join(' '): 0 1 3 6 10 15.
Вывод: программа на TypeScript и захваченный вывод
const numbers = [0, 1, 2, 3, 4, 5]
// первый элемент — это старт; каждая следующая сумма сохраняется в результате
let sum = numbers[0]
const runningReduceSum = [sum]
for (const item of numbers.slice(1)) {
sum += item
runningReduceSum.push(sum)
}
console.log(runningReduceSum.join(' '))
Накопительный fold
То же самое с начальным значением 10. Результат начинается с самого числа 10 и содержит на один элемент больше, чем исходный массив: 10 10 11 13 16 20 25.
Вывод: программа на TypeScript и захваченный вывод
const numbers = [0, 1, 2, 3, 4, 5]
// начальное значение 10 — это первый элемент результата
let sum = 10
const runningFoldSum = [sum]
for (const item of numbers) {
sum += item
runningFoldSum.push(sum)
}
console.log(runningFoldSum.join(' '))