Introdução
Os métodos de iteração de Array no JS, apesar de bem úteis e cobrirem grande parte dos problemas enfrentados por nós no dia-a-dia, nem sempre são tão intuitivos assim a primeira vista, uma prova disso é a quantidade incrível de resultados no Google ao pesquisar sobre "map vs forEach", ou esta pergunta no stackoverflow com mais de 261 mil views.
Por isso, ao invés de ser apenas outro texto como os vários artigos disponíveis no Google explicando as diferenças entre cada método, hoje vamos abordar esse tema da perspectiva de que, como dito na introdução, cada método de array existe com o propósito de representar a solução de um problema comum no dia-a-dia, ou seja, eles são implementações de algoritmos comuns que acabamos tendo que escrever ao utilizar arrays no nosso código.
Assim, vamos ver as diferenças entre eles a partir dos algoritmos que eles representam, logo, se você é daqueles que usa um for ou um forEach para tudo, se prepara que talvez seja hora de largar eles, e facilitar a sua vida sem ter que reinventar a roda todas as vezes.
ForEach
Acho que um bom começo, seria começar a falar dos métodos de iteração pelo método que ahn..., itera sobre os itens de um array (trocadilho intencional).
O mais básico de todos os métodos de array, serve para representar a operação de iterar sobre os itens do array, assim sendo equivalente a um simples for em cima do array, também podemos ver o forEach como a combinação entre um for e uma função, ex:
// Manualmente
const numbers = [1, 2, 3, 4, 5];
for (const number of numbers) {
console.log(number);
}
// Função utilitária
function forEach(array, consumer) {
for (const item of array) {
consumer(item);
}
}
// Métodos de array
numbers.forEach((value) => console.log(value));
Por representar uma operação tão fundamental para que qualquer outra operação que envolva os itens de um array possa ser realizada, que muitas pessoas acabam por não entender a utilidade dos outros métodos de array, afinal você realmente pode fazer o mesmo com o forEach ou até mesmo com o for clássico, entretanto estaria tendo trabalho a toa nesses casos.
Map
O map representa a ideia matemática de mapear, sabe igual aqueles jogos infantis de ligar os pontos, ex:
Nesta imagem a função ilustra o caminho de como ligar os números do conjunto x para os do conjunto y, na matemática é para isso que uma função serve, mapear (mostrar o caminho) um conjunto de entradas (que são os valores válidos para o parâmetro recebido por ela) em um conjunto de saídas.
Assim, se enxergarmos os conjuntos como se fossem arrays no JS, o nome começa a fazer bem mais sentido.
De qualquer forma, agora que passamos por essa curiosidade por trás do nome do método map, um outro nome que ele poderia ter seria transform, afinal o algoritmo que ele representa é exatamente esse, pegar cada valor do array, aplicar uma função nele, e formar um novo array a partir desse original.
Outra forma de ver isso, seria dizer que o map é o resultado da combinação do forEach com uma função que modifica os valores do array (apesar da ideia do map ser a realização de uma computação pura, então os valores não são modificados, e sim novos são produzidos a partir dos antigos):
// Manualmente
const x = [1, 4, 2, 3, 5];
const y = [];
for (const n of x) {
y.push(n + 1);
}
// Função utilitária
function map(array, fn) {
const mapped = [];
for (const item of array) {
mapped.push(fn(item));
}
return mapped;
}
// Métodos de array
const z = x.map(n => n + 1);
Essa é uma operação realmente comum quando precisamos traduzir um tipo de dado para outro, por exemplo converter todos os valores em string, ou converter todos os objetos nos valores de uma propriedade em específico deles, traduzir um DTO vindo de uma API/Banco de dados num objeto de domínio/view model, e assim por diante.
Filter
Este talvez seja o mais intuitívo de todos, ele é a operação de escolher um sub-conjunto de valores de um array baseado em um critério, que nós chamamos de função predicado (uma função que recebe um valor e devolve um boolean), basicamente é o que você ganha quando combina a lógica do map com um if, ex:
// Manualmente
const numbers = [1, 2, 3, 4, 5];
const evens = [];
for (const number of numbers) {
if (number % 2 === 0) {
evens.push(number);
}
}
// Função utilitária
function filter(array, predicate) {
const slice = [];
for (const item of array) {
if (predicate(item)) {
slice.push(item);
}
}
return slice;
}
// Métodos de array
const isOdd = n => n % 2 !== 0;
const odds = numbers.filter(isOdd);
Find
O filter é muito útil, mas as vezes estamos interessados em apenas um item do array, só que o filter sempre vai retornar todos os itens que baterem com o critério passado, e por isso que nós temos o find, nesse caso, a busca para quando ele achar o primeiro item a bater com o predicado da esquerda para a direita, sendo a combinação do filter com um break, no caso de um loop, ou um early return no caso de uma função:
// Manualmente
const numbers = [1, 2, 3, 4, 5];
let target;
for (const number of numbers) {
if (number === 3) {
target = number;
break;
}
}
// Função utilitária
function find(array, predicate) {
for (const item of array) {
if (predicate(item)) {
return item;
}
}
}
// Métodos de array
const oneOrNone = numbers.find(number => number === 1);
Também existe o findLast (que procura da direita para a esquerda), o findIndex (que é o mesmo que o find, entretanto retorna a posição do item no array), e o findLastIndex, mas esses eu deixo para você leitor pensar em como seria a implementação.
Some
Tanto ele quanto o seu irmão every, não são tão famosos quanto os seus primos: map, filter, e reduce, mas são igualmente úteis, sendo a representação da ideia de saber se alguém no array bate com um critério em específico, praticamente uma versão do método includes só que você pode escolher qual critério ele vai usar para saber se existe algum item que bate com o critério ou não por meio de uma função predicado.
Tanto ele quanto o every seriam a combinação do forEach com um if só que devolvendo um boolean:
// Manualmente
const numbers = [1, 2, 3, 4, 5];
let hasSomeEven = false;
for (const number of numbers) {
if (number % 2 === 0) {
hasSomeEven = true;
break;
}
}
// Função utilitária
function some(array, predicate) {
for (const item of array) {
if (predicate(item)) {
return true;
}
}
return false;
}
// Métodos de array
const isOdd = n => n % 2 !== 0;
const hasSomeOdd = numbers.some(isOdd);
Outra situação onde você pode bater o olho e trocar por um some, seria quando você tem várias condições or, ex:
let numero = 10;
if (numero === 5 || numero === 10 || numero === 15) {
console.log("O número é 5, 10, ou 15.");
}
// vs
if ([5, 10, 15].some(x => x === numero)) {
console.log("O número é 5, 10, ou 15.");
}
Every
Se o some seria a ideia de saber se alguém no array bate com determinado critério, o every seria a ideia de saber se todo mundo no array bate com esse critério. Assim, ele seria o some, só que usando um operador not:
// Manualmente
const numbers = [1, 2, 3, 4, 5];
let allAreEven = true;
for (const number of numbers) {
if (!(number % 2 === 0)) {
areAllEven = false;
break;
}
}
// Função utilitária
function every(array, predicate) {
for (const item of array) {
if (!predicate(item)) {
return false;
}
}
return true;
}
// Métodos de array
const isOdd = n => n % 2 !== 0;
const allAreOdd = numbers.every(isOdd);
Da mesma forma, se o some seria como expressamos condições or em arrays, o every seria como expressamos condições and neles, ex:
const isAuthenticated = true;
const hasEnoughMoney = true;
const isProductInStock = true;
if (isAuthenticated && hasEnoughMoney && isProductInStock) {
purchase(product);
}
// vs
const canPurchase = [
isAuthenticated,
hasEnoughMoney,
isProductInStock
].every(condition => condition === true);
if (canPurchase) {
purchase(product);
}
reduce
Se o filter seria o mais intuitivo, eu diria que o reduce seria o mais complicado de se entender, embora provalemente seja o mais útil de todos. Ele representa uma operação onde cada passo depende do resultado do passo anterior, mais ou menos uma combinação do forEach com o operador de atribuição composto, ex:
// Manualmente
const numbers = [1, 2, 3, 4, 5];
let sum = 0;
for (const number of numbers) {
sum += number;
}
// Função utilitária
function reduce(array, concat, initial) {
let result = initial;
for (const item of array) {
result = concat(result, item);
}
return result;
}
// Métodos de array
const total = numbers.reduce((x, y) => x + y, 0);
Aqui podemos notar que na forma manual o sum += number
pode ser traduzido em:
sum = sum + number;
Só que essa é uma operação específica de concatenação de strings ou de soma de números, ela não vai funcionar em arrays por exemplo, então se quisermos tornar ela mais genérica podemos ver que a parte especial do reduce seria:
result = result operação item
Onde essa operação seria uma operação binária (que precisa de dois valores para acontecer), sendo assim ela é equivalente a função binária (que aceita dois parâmetros), e por isso que ela é representada como:
result = concat(result, item);
Isso é particularmente útil quando você tem uma função que consegue combinar dois valores quaisquer do array, por isso que ela funciona tão bem com soma, multiplicação, concatenação de arrays ou strings, or ou and lógicos, e etc, afinal todas essas são operações binárias.
Conclusão
Como vimos ao longo deste artigo, cada método de array representa um algoritmo diferente, mesmo que cada método seja só um anterior só que com um detalhe extra, se tornando um uma versão mais específica do outro, então cada vez que você se encontrar digitando um algoritmo semelhante aos apresentados aqui, seria uma ideia legal considerar adaptar o seu codigo para usar um método nativo de array no lugar para evitar o esforço extra.
Até a próxima.
ASS: Suporte Cansado...
Top comments (0)