JavascriptCarlos Costa

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: