Explorando o método .reduce() no javascript

Carlos Costa

Nesse post vamos entender o motivo do .reduce() ser o método mais poderoso quando o assunto é array no javascript.

Uma breve introdução.

O método .reduce() itera sobre cada item do array executando um callback(reducer) e por final retorna um valor.

Exemplo: somando todos os elementos de um array.

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(current) é somado ao total e por final o resultado 15 é retornando.

Agora vamos entender a estrutura e execução desse código. Primeiro vamos desmenbar os elementos do reduce.

Anatomia do .reduce()


reduce((prev, current, index, arr) => {}, init)

reducer ― callback que será executado a cada iteração.

init ― valor usado para inicializar o reduce, na primeira iteração esse valor será atríbuído ao parâmetro prev.

prev ― na primeira iteração esse parâmetro assume o valor passado pelo init, nas iterações seguintes esse parâmetro receber o valor retornado pelo callback(reducer).

current ― assumirá o valor atual do array em cada iteração.

index ― index correspondente a cada item.

arr ― array que está sendo iterado.

O processo de iteração do .reduce() é da esqueda para a direta, considerando o nosso primeiro exemplo, o primeiro item assumido pelo total será 0, em seguida o 1, 2, 3… até o último item do array.

Execução do .reduce()


Agora vamos visualizar o processo de iteração de um .reduce() para o nosso primeiro exemplo.

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 obtemos a seguinte saída. 👇

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

Observando essa saída fica muito mais simples de entender o processo de iteração de cada items do array, e como cada item é somado até obter o valor final.

Retorno do .reduce()


No primeiro exemplo, o resultado final do .reduce() é do tipo primitivo(number), mas isso não é uma regra, também podemos retornar um array, objeto, função, etc…

Vamos para mais alguns exemplos.

{
  const values = [1, 2, 3]

  // ex1: retornando um array
  const result1 = values.reduce(() => {
    return []
  }, 0)

  // ex2: retornando uma função
  const result2 = values.reduce(() => {
    return () => {}
  }, 0)

  // ex3: retornando um boolean
  const result3 = values.reduce(() => {
    return false
  }, 0)

  console.log(result1) // []
  console.log(result2) // [Function (anonymous)]
  console.log(result3) // false
}

O entendimento desse recurso é de extrema importância para explorarmos o poder do reduce.

A versatilidade do .reduce()


Como já entedemos melhor o funcionamento do .reduce(), vamos explorar um pouco da sua versatilidade e usá-lo para imitar o comportamento de outros métodos do objeto array como .map(), .every(), .filter() e .find().

Imitando o método .map()


O método .map() executa um callback a cada iteração e por final retorna um array de mesmo tamanho.

O .map() é geralmente usado para alterar os items do array sem comprometer o seu tamanho final do array.

Exemplo: multiplicar por 2 cada item do array.

Usando .map()

const values = [1, 2, 3, 4]
const result = values.map((value) => value * 2)

console.log(result) //[2, 4, 6, 8]

Usando .reduce()

const result = values.reduce((prev, current) => {
  return [...prev, current * 2]
}, [])

console.log(result) //[2, 4, 6, 8]

Nesse exemplo, o ponto crucial é o parâmetro de inicialização do reduce([]), a cada iteração esse array será preenchido com um novo item que foi multiplicado por 2. O retorno final será todos os items do array multiplicados por 2.

Imitando o método .filter()


O método filter serve para filtrar elementos de um array, um callback é executado a cada iteração retornando uma condição para o elemento permanecer ou não no array.

Exemplo: filtrando todos os element menores ou iguais a 5.

usando .filter()

const values = [1, 23, 4, 56, 3, 2, 100]

const result = values.filter((value) => values <= 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]

Imitando o método .every()


O método every serve para verificar se todos os elementos do array seguem uma mesma condição, o retorno é sempre um valor booleano.

Exemplo 1: verificando se todos os items do array são pares.

Exemplo 2: verificando se todos os items do array são maiores que 3.

usando .every()

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

Imitando o método .find()


O método .find() itera sobre um array com o objetivo de encontar o primeiro item que corresponda a uma determinada condição

Exemplo: encontrar o primeiro elemento maior que 10.

usando .find()

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, index, arr) => {
  if (current > 10) {
    return [...prev, current]
  }
  return prev
}, [])[0]

console.log(result) // 12

Removendo items duplicados com reduce


const values = [1, 1, 2, 2, 3, 4, 3, 5, 5, 4, 10]

// verifica se determinado item pertence ao array

function arrayIncludes(arr, item) {
  for (let element of arr) {
    if (element === item) {
      return true
    }
  }

  return false
}

const result = values.reduce((prev, current, index, arr) => {
  if (!arrayIncludes(prev, current)) {
    return [...prev, current]
  }

  return prev
}, [])

console.log(result) // [ 1, 2, 3, 4, 5, 10 ]

Nesse exemplo, a cada iteração é verificado ser o item(current) já foi ou não adicionado ao array(prev), caso não tenha sido, o elemento é adicionado.

Parte da lógica de verificação foi isolada no método arrayIncludes para tornar o código mais legível.

Para simplificar mais e diminuir um pouco o nosso código vamos utilizar o método .includes() do próprio objeto array do javascript.

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 ]

E por final vamos transformar a nossa solução em uma função.

function removeDuplicates(array) {
  return array.reduce((prev, current) => {
    if (!prev.includes(current)) {
      return [...prev, current]
    }

    return prev
  }, [])
}

const result1 = removeDuplicates([1, 1, 2, 2, 3, 4, 3, 5, 5, 4, 10])

console.log(result1) // [ 1, 2, 3, 4, 5, 10 ]

Criando nosso próprio reduce


Para finalizar esse post vamos construir a nossa função reduce.

1° passo - definir a estrutura da nossa função.

function arrayReduce(array, reducer, init) {
  //...
}

2° passo - estruturar o loop e passar ao callback(reducer) os devidos parâmetros.

function arrayReduce(array, reducer, init) {
  for (let index = 0; index < array.length; index++) {
    reducer(prev, array[index], index, array)
  }
}

3° passo - adicionar uma variável axiliar(prev) que vai servir para recuperar o valor anterior retornado de cada iteração.

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
}

Agora vamos fazer alguns pequenos testes unitários para a nossa solução(arrayReducer).

No primeiro teste, vamos usar o exemplo de somar todos os items de um array. Os testes serão feitos com test runner AVA.

import test from 'ava'

test('sum all items: should return the same value', (t) => {
  const values = [100, 213, 2, 329]

  const result_a = arrayReduce(
    values,
    (prev, current) => {
      return prev + current
    },
    0
  )

  const result_b = values.reduce((prev, current) => {
    return prev + current
  }, 0)

  t.deepEqual(result_a, result_b)
})

Neste segundo caso de teste, vamos usar o exemplo de remover items duplicados de um array.

test('remove duplicated: should return the same result', (t) => {
  const values = [1, 1, 2, 2, 3, 4, 3, 5, 5, 4, 10]

  const result_a = arrayReduce(
    values,
    (prev, current) => {
      if (!prev.includes(current)) {
        return [...prev, current]
      }
      return prev
    },
    []
  )

  const result_b = values.reduce((prev, current) => {
    if (!prev.includes(current)) {
      return [...prev, current]
    }

    return prev
  }, [])

  t.deepEqual(result_a, result_b)
})

Referências