Explorando o Método .reduce() no JavaScript
Entenda a anatomia, o fluxo de execução e a impressionante versatilidade do .reduce() para transformar coleções e recriar métodos nativos de array.
Nesse artigo vamos entender a fundo o motivo do .reduce() ser considerado um dos métodos mais poderosos e flexíveis quando o assunto é manipulação de arrays no JavaScript.
O método
.reduce()itera sobre cada item do array executando uma função de callback (reducer) acumuladora e, ao final, retorna um único valor consolidado.
Exemplo inicial: somando todos os elementos de um array numérico.
const values = [1, 2, 3, 4, 5]
const result = values.reduce((total, current, index, arr) => {
return total + current
}, 0)
console.log(result) // 15
Neste exemplo, a cada iteração o valor do elemento atual (current) é somado ao acumulador (total), e ao término o resultado final 15 é retornado.
Agora vamos entender em detalhes a estrutura e o ciclo de execução desse código.
Anatomia do .reduce()
A assinatura padrão do método recebe a função redutora e o valor inicial:
reduce((prev, current, index, arr) => {}, init)
- reducer: Callback executado a cada iteração da coleção.
- init: Valor usado para inicializar o acumulador. Na primeira iteração, esse valor é atribuído ao parâmetro
prev. - prev: Acumulador. Na primeira iteração assume o valor passado pelo
init; nas iterações seguintes, recebe o valor retornado pelo callback na iteração anterior. - current: O valor do elemento atual do array sendo processado.
- index: O índice numérico correspondente ao item atual.
- arr: O array original que está sendo iterado.
O processo de iteração do .reduce() ocorre da esquerda para a direita. No exemplo da soma, o valor inicial atribuído a total foi 0, e progressivamente somou-se 1, 2, 3 até o último elemento da lista.
Execução Passo a Passo do .reduce()
Para visualizar com clareza o ciclo interno de iterações, podemos inspecionar o acumulador a cada ciclo:
const values = [1, 2, 3, 4, 5]
const result = values.reduce((total, current, index, arr) => {
console.log(total, '+', current, '=', total + current)
return total + current
}, 0)
Executando esse código, observamos o fluxo passo a passo:
0 + 1 = 1 // 1ª iteração
1 + 2 = 3 // 2ª iteração
3 + 3 = 6 // 3ª iteração
6 + 4 = 10 // 4ª iteração
10 + 5 = 15 // 5ª iteração
Com esse rastreamento fica evidente como o valor retornado por um ciclo se torna o ponto de partida do ciclo seguinte.
Tipos de Retorno do .reduce()
No exemplo anterior, o resultado final foi um tipo primitivo (number), mas isso não é uma regra. O .reduce() pode retornar qualquer estrutura de dados: um novo array, um objeto estruturado, um boolean ou até uma nova função.
const values = [1, 2, 3]
// Exemplo 1: retornando um novo array
const result1 = values.reduce((prev, current) => {
return [...prev, current * 10]
}, [])
// Exemplo 2: retornando uma função
const result2 = values.reduce(() => {
return () => 'executado'
}, null)
// Exemplo 3: retornando um boolean
const result3 = values.reduce((prev, current) => {
return prev && current > 0
}, true)
console.log(result1) // [10, 20, 30]
console.log(result2()) // 'executado'
console.log(result3) // true
Compreender essa maleabilidade é o que transforma o reduce em uma ferramenta coringa.
A Versatilidade: Recriando Métodos Nativos de Array
Para comprovar o poder do .reduce(), vamos utilizá-lo para reproduzir com exatidão o comportamento de métodos populares como .map(), .filter(), .every() e .find().
1. Imitando o método .map()
O método
.map()executa um callback a cada iteração e retorna um novo array com o mesmo comprimento do original.
Exemplo: multiplicar cada item por 2.
Usando .map() nativo:
const values = [1, 2, 3, 4]
const result = values.map((value) => value * 2)
console.log(result) // [2, 4, 6, 8]
Usando .reduce():
const values = [1, 2, 3, 4]
const result = values.reduce((prev, current) => {
return [...prev, current * 2]
}, [])
console.log(result) // [2, 4, 6, 8]
O ponto crucial aqui é o valor inicial []: a cada iteração, preenchemos o acumulador com o valor transformado.
2. Imitando o método .filter()
O método
.filter()cria uma nova lista contendo apenas os elementos que satisfazem uma determinada condição booleana.
Exemplo: filtrar elementos menores ou iguais a 5.
Usando .filter() nativo:
const values = [1, 23, 4, 56, 3, 2, 100]
const result = values.filter((value) => value <= 5)
console.log(result) // [1, 4, 3, 2]
Usando .reduce():
const values = [1, 23, 4, 56, 3, 2, 100]
const result = values.reduce((prev, current) => {
if (current <= 5) {
return [...prev, current]
}
return prev
}, [])
console.log(result) // [1, 4, 3, 2]
3. Imitando o método .every()
O método
.every()testa se todos os elementos do array passam na condição implementada pela função fornecida.
Usando .every() nativo:
const values = [2, 4, 6, 8]
const result1 = values.every((item) => item % 2 === 0)
const result2 = values.every((item) => item > 3)
console.log(result1) // true
console.log(result2) // false
Usando .reduce():
const values = [2, 4, 6, 8]
const result1 = values.reduce((prev, current) => {
return prev && current % 2 === 0
}, true)
const result2 = values.reduce((prev, current) => {
return prev && current > 3
}, true)
console.log(result1) // true
console.log(result2) // false
4. Imitando o método .find()
O método
.find()itera sobre o array e retorna o primeiro elemento que satisfaz a condição de busca.
Exemplo: encontrar o primeiro número maior que 10.
Usando .find() nativo:
const values = [5, 12, 8, 130, 44]
const result = values.find((value) => value > 10)
console.log(result) // 12
Usando .reduce():
const values = [5, 12, 8, 130, 44]
const result = values.reduce((prev, current) => {
if (prev !== undefined) return prev
return current > 10 ? current : undefined
}, undefined)
console.log(result) // 12
Caso Prático: Removendo Itens Duplicados de um Array
Um caso de uso muito comum no dia a dia é deduplicar coleções de dados:
const values = [1, 1, 2, 2, 3, 4, 3, 5, 5, 4, 10]
const result = values.reduce((prev, current) => {
if (!prev.includes(current)) {
return [...prev, current]
}
return prev
}, [])
console.log(result) // [1, 2, 3, 4, 5, 10]
Podemos encapsular essa solução em uma função utilitária reutilizável:
function removeDuplicates(array) {
return array.reduce((prev, current) => {
return prev.includes(current) ? prev : [...prev, current]
}, [])
}
const unicos = removeDuplicates([1, 1, 2, 2, 3, 4, 3, 5, 5, 4, 10])
console.log(unicos) // [1, 2, 3, 4, 5, 10]
Construindo Nossa Própria Função Reduce do Zero
Para consolidar definitivamente o funcionamento interno, vamos implementar nossa própria versão do reduce e validar o comportamento com testes unitários usando o AVA.
Passo 1: Definir a assinatura da função
function arrayReduce(array, reducer, init) {
// lógica de redução
}
Passo 2: Iterar e propagar o acumulador
function arrayReduce(array, reducer, init) {
let prev = init
for (let index = 0; index < array.length; index++) {
prev = reducer(prev, array[index], index, array)
}
return prev
}
Passo 3: Escrever os testes unitários
Teste 1: Somatório de elementos
import test from 'ava'
test('sum all items: should return the same value', (t) => {
const values = [100, 213, 2, 329]
const resultCustom = arrayReduce(values, (prev, current) => prev + current, 0)
const resultNative = values.reduce((prev, current) => prev + current, 0)
t.is(resultCustom, resultNative)
})
Teste 2: Remoção de duplicados
test('remove duplicated: should return the same result', (t) => {
const values = [1, 1, 2, 2, 3, 4, 3, 5, 5, 4, 10]
const resultCustom = arrayReduce(
values,
(prev, current) => {
if (!prev.includes(current)) {
return [...prev, current]
}
return prev
},
[]
)
const resultNative = values.reduce((prev, current) => {
if (!prev.includes(current)) {
return [...prev, current]
}
return prev
}, [])
t.deepEqual(resultCustom, resultNative)
})
Conclusão e Referências
O .reduce() é a espinha dorsal de diversas bibliotecas e padrões funcionais no JavaScript moderno (como Redux e pipelines funcionais). Dominar seus parâmetros e seu fluxo de acumulação é um divisor de águas para escrever código declarativo e elegante.
Para aprofundar seus estudos: