Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

Array de objetos em JavaScript: filtrar e somar

Receitas de array de objetos: somar com reduce, filtrar por propriedade, agrupar com Object.groupBy e achar duplicado — tudo executado no Node 24.

Rodolfo Mori7 min de leitura

Um array de objetos é uma lista em que cada posição guarda um registro completo, com várias propriedades relacionadas. É o formato comum de uma resposta de API, das linhas de um pedido ou de um carrinho salvo no navegador.

Pense numa planilha de pedidos: o array é o conjunto de linhas; cada objeto é uma linha; e propriedades como sku, preco e quantidade são as colunas. Filtrar escolhe algumas linhas, ordenar muda a sequência, agrupar separa as linhas por uma coluna e reduzir transforma o conjunto em um resultado, como o total da compra. No JavaScript não existe uma planilha por trás: existem objetos guardados nos índices do array, e cada método percorre essas referências.

Cinco operações cobrem boa parte do trabalho: somar um campo com reduce, filtrar por propriedade com filter, ordenar por chave com toSorted, agrupar com Object.groupBy e indexar por identificador com Map. Vamos usar o mesmo pedido de uma loja de periféricos para enxergar o que entra e o que sai de cada etapa — inclusive quando um item procurado não existe.

A lista de itens de um pedido

js
const itens = [
  { sku: 'TEC-001', nome: 'Teclado mecânico', preco: 289.9, quantidade: 1, categoria: 'periferico' },
  { sku: 'MOU-014', nome: 'Mouse sem fio', preco: 149.9, quantidade: 2, categoria: 'periferico' },
  { sku: 'CAD-220', nome: 'Cadeira gamer', preco: 1199, quantidade: 1, categoria: 'movel' },
  { sku: 'MON-007', nome: 'Monitor 27 polegadas', preco: 1899.9, quantidade: 1, categoria: 'monitor' },
];

console.log(itens.length);
console.log(itens[1].nome);
console.log(itens.at(-1).nome);
4 Mouse sem fio Monitor 27 polegadas

Cada posição do array guarda um objeto inteiro, e o acesso é sempre em dois passos: primeiro a posição, depois a propriedade. at(-1) pega o último sem precisar escrever itens[itens.length - 1].

Somar um campo com reduce

reduce percorre a lista carregando um acumulador. O segundo argumento — o valor inicial — é o que decide se a conta funciona:

js
const itens = [
  { sku: 'TEC-001', preco: 289.9, quantidade: 1 },
  { sku: 'MOU-014', preco: 149.9, quantidade: 2 },
  { sku: 'CAD-220', preco: 1199, quantidade: 1 },
];

const total = itens.reduce((soma, item) => soma + item.preco * item.quantidade, 0);

console.log(total);
console.log(total.toFixed(2));
1788.7 1788.70

Sem o 0, o acumulador começa valendo o primeiro objeto da lista, e a soma vira concatenação de texto:

js
const itens = [
  { sku: 'TEC-001', preco: 289.9 },
  { sku: 'MOU-014', preco: 149.9 },
  { sku: 'CAD-220', preco: 1199 },
];

const total = itens.reduce((soma, item) => soma + item.preco);

console.log(total);
[object Object]149.91199

Nenhum erro, nenhum aviso: só um total que não é número. Esse é o tipo de bug que chega ao cliente. Em array de objetos, o valor inicial do reduce nunca é opcional.

Filtrar por propriedade e somar o recorte

filter devolve um array novo com os itens que passaram no teste, e ele encadeia direto com reduce e map:

js
const itens = [
  { nome: 'Teclado mecânico', preco: 289.9, categoria: 'periferico' },
  { nome: 'Mouse sem fio', preco: 149.9, categoria: 'periferico' },
  { nome: 'Cadeira gamer', preco: 1199, categoria: 'movel' },
  { nome: 'Monitor 27 polegadas', preco: 1899.9, categoria: 'monitor' },
];

const perifericos = itens.filter((i) => i.categoria === 'periferico');
console.log(perifericos.length, perifericos.map((i) => i.nome));

const totalPerifericos = perifericos.reduce((s, i) => s + i.preco, 0);
console.log(totalPerifericos.toFixed(2));

const acimaDe1000 = itens.filter((i) => i.preco > 1000).map((i) => i.nome);
console.log(acimaDe1000);
2 [ 'Teclado mecânico', 'Mouse sem fio' ] 439.80 [ 'Cadeira gamer', 'Monitor 27 polegadas' ]

Cada método da cadeia cria um array intermediário. Em lista de pedido isso é irrelevante; em lista com dezenas de milhares de linhas, vale medir antes de encadear cinco operações.

Ordenar por um campo calculado

Ordenar por uma propriedade que já existe é assunto de ordenar array com sort. O caso próprio de array de objetos é outro: ordenar por um número que não está no objeto e precisa ser calculado antes — o subtotal da linha, por exemplo.

js
const itens = [
  { sku: 'TEC-001', nome: 'Teclado mecânico', preco: 289.9, quantidade: 1 },
  { sku: 'MOU-014', nome: 'Mouse sem fio', preco: 149.9, quantidade: 2 },
  { sku: 'CAD-220', nome: 'Cadeira gamer', preco: 1199, quantidade: 1 },
  { sku: 'MON-007', nome: 'Monitor 27 polegadas', preco: 1899.9, quantidade: 1 },
];

const porSubtotal = itens
  .map((i) => ({ ...i, subtotal: i.preco * i.quantidade }))
  .toSorted((a, b) => b.subtotal - a.subtotal);

console.log(porSubtotal.map((i) => `${i.nome}: ${i.subtotal.toFixed(2)}`));
console.log(porSubtotal.slice(0, 2).map((i) => i.sku));
console.log(itens[0].subtotal);
[ 'Monitor 27 polegadas: 1899.90', 'Cadeira gamer: 1199.00', 'Mouse sem fio: 299.80', 'Teclado mecânico: 289.90' ] [ 'MON-007', 'CAD-220' ] undefined

O map com spread cria objetos novos com o campo a mais, em vez de escrever subtotal dentro dos itens originais — por isso a última linha imprime undefined. É o detalhe que separa este encadeamento de um laço que suja os dados de entrada: a lista que chegou continua exatamente como chegou, e o slice(0, 2) entrega o pódio sem que ninguém precise confiar na ordem da API.

Repare também que o mouse subiu na frente do teclado por causa da quantidade: 149,90 sozinho perde para 289,90, mas duas unidades somam 299,80. Ordenar pelo preco cru daria outro ranking — e o errado, se a pergunta era “o que mais pesou nesse pedido?”.

Agrupar por categoria

Object.groupBy recebe a lista e uma função que devolve a chave de cada item:

js
const itens = [
  { nome: 'Teclado mecânico', preco: 289.9, categoria: 'periferico' },
  { nome: 'Mouse sem fio', preco: 149.9, categoria: 'periferico' },
  { nome: 'Cadeira gamer', preco: 1199, categoria: 'movel' },
  { nome: 'Monitor 27 polegadas', preco: 1899.9, categoria: 'monitor' },
];

const porCategoria = Object.groupBy(itens, (i) => i.categoria);

console.log(Object.keys(porCategoria));
console.log(porCategoria.periferico.map((i) => i.nome));
console.log(porCategoria.eletrodomestico);
[ 'periferico', 'movel', 'monitor' ] [ 'Teclado mecânico', 'Mouse sem fio' ] undefined

Categoria que não aparece na lista não vira chave — daí o undefined da última linha, que precisa de guarda antes do .map. Uma particularidade do groupBy: o objeto devolvido não herda de Object.prototype.

js
const itens = [
  { nome: 'Teclado mecânico', categoria: 'periferico' },
  { nome: 'Cadeira gamer', categoria: 'movel' },
];

const porCategoria = Object.groupBy(itens, (i) => i.categoria);

console.log(porCategoria);
console.log(porCategoria.hasOwnProperty);
[Object: null prototype] { periferico: [ { nome: 'Teclado mecânico', categoria: 'periferico' } ], movel: [ { nome: 'Cadeira gamer', categoria: 'movel' } ] } undefined

O [Object: null prototype] na saída não é erro: é proteção. Sem protótipo, uma categoria chamada constructor ou toString não colide com método herdado. O preço é que porCategoria.hasOwnProperty(...) não existe — use Object.hasOwn(porCategoria, 'movel').

Total por categoria com reduce

Quando o que você quer não são as listas, e sim o número somado, o reduce com acumulador em objeto resolve:

js
const itens = [
  { nome: 'Teclado mecânico', preco: 289.9, categoria: 'periferico' },
  { nome: 'Mouse sem fio', preco: 149.9, categoria: 'periferico' },
  { nome: 'Cadeira gamer', preco: 1199, categoria: 'movel' },
  { nome: 'Monitor 27 polegadas', preco: 1899.9, categoria: 'monitor' },
];

const totalPorCategoria = itens.reduce((acc, item) => {
  acc[item.categoria] = (acc[item.categoria] ?? 0) + item.preco;
  return acc;
}, {});

console.log(totalPorCategoria);
{ periferico: 439.79999999999995, movel: 1199, monitor: 1899.9 }

O ?? 0 cobre a primeira vez que cada categoria aparece, quando ainda não existe chave. E o return acc é obrigatório: esquecer dele faz o acumulador virar undefined na segunda volta. Para percorrer o resultado depois, veja percorrer objeto em JavaScript.

Achar duplicado

O SKU repetido no carrinho é o caso clássico:

js
const itens = [
  { sku: 'TEC-001', nome: 'Teclado mecânico' },
  { sku: 'MOU-014', nome: 'Mouse sem fio' },
  { sku: 'TEC-001', nome: 'Teclado mecânico' },
  { sku: 'CAD-220', nome: 'Cadeira gamer' },
];

const skus = itens.map((i) => i.sku);
const unicos = new Set(skus);

console.log(skus.length, unicos.size);
console.log(skus.length !== unicos.size);

const duplicados = skus.filter((sku, i) => skus.indexOf(sku) !== i);
console.log(duplicados);
4 3 true [ 'TEC-001' ]

O truque do filter com indexOf funciona porque indexOf sempre devolve a primeira posição do valor: quando ela é diferente do índice atual, aquele item é uma repetição.

Objeto igual não entra no includes

Nunca tente localizar um objeto pelo conteúdo com includes:

js
const itens = [
  { sku: 'TEC-001', nome: 'Teclado mecânico' },
  { sku: 'MOU-014', nome: 'Mouse sem fio' },
];

const procurado = { sku: 'TEC-001', nome: 'Teclado mecânico' };

console.log(itens.includes(procurado));
console.log(itens[0] === procurado);
console.log(JSON.stringify(itens[0]) === JSON.stringify(procurado));
console.log(itens.some((i) => i.sku === procurado.sku));
false false true true

Conteúdo idêntico, referências diferentes. O JSON.stringify prova que os dados batem, mas não serve como comparação de verdade — ele depende da ordem das chaves e engasga com undefined, Date e valores circulares. A comparação honesta é por campo identificador, com some, como detalha buscar elemento em array.

Índice por SKU com Map

Quando você vai procurar muitas vezes na mesma lista, transformar o array num Map uma vez sai mais barato do que rodar find a cada consulta:

js
const catalogo = [
  { sku: 'TEC-001', nome: 'Teclado mecânico', preco: 289.9 },
  { sku: 'MOU-014', nome: 'Mouse sem fio', preco: 149.9 },
  { sku: 'CAD-220', nome: 'Cadeira gamer', preco: 1199 },
];

const porSku = new Map(catalogo.map((p) => [p.sku, p]));

console.log(porSku.get('CAD-220'));
console.log(porSku.has('MON-007'));
console.log(porSku.size);
{ sku: 'CAD-220', nome: 'Cadeira gamer', preco: 1199 } false 3

new Map recebe uma lista de pares [chave, valor] — exatamente o formato que o map produziu. O Map também é a forma correta de eliminar duplicados mantendo o objeto: [...new Map(itens.map((i) => [i.sku, i])).values()].

Maior, menor e média

js
const itens = [
  { nome: 'Teclado mecânico', preco: 289.9 },
  { nome: 'Mouse sem fio', preco: 149.9 },
  { nome: 'Cadeira gamer', preco: 1199 },
];

const maisCaro = itens.reduce((maior, i) => (i.preco > maior.preco ? i : maior));
const media = itens.reduce((s, i) => s + i.preco, 0) / itens.length;

console.log(maisCaro.nome);
console.log(media.toFixed(2));
console.log(Math.min(...itens.map((i) => i.preco)));
Cadeira gamer 546.27 149.9

Aqui o reduce aparece sem valor inicial de propósito: o acumulador começa sendo o primeiro item, que é o candidato natural a “maior”. O preço disso é que lista vazia quebra:

js
const carrinhoVazio = [];

console.log(carrinhoVazio.reduce((s, i) => s + i.preco, 0));

const maisCaro = carrinhoVazio.reduce((maior, i) => (i.preco > maior.preco ? i : maior));
console.log(maisCaro);
0 file:///private/tmp/loja/arrays/pedidos-vazio.mjs:5 const maisCaro = carrinhoVazio.reduce((maior, i) => (i.preco > maior.preco ? i : maior)); ^ TypeError: Reduce of empty array with no initial value at Array.reduce (<anonymous>) at file:///private/tmp/loja/arrays/pedidos-vazio.mjs:5:32 Node.js v24.16.0

A soma com 0 passou tranquila; a busca do maior, sem inicial, não teve de onde começar. Carrinho vazio é estado normal de e-commerce, não caso raro.

Erros comuns: undefined dentro do reduce

O erro mais frequente com array de objetos não nasce no reduce — nasce no map que veio antes dele:

js
const catalogo = [
  { sku: 'TEC-001', nome: 'Teclado mecânico', preco: 289.9 },
  { sku: 'MOU-014', nome: 'Mouse sem fio', preco: 149.9 },
];

const skusDoPedido = ['TEC-001', 'MOU-014', 'MON-007'];
const itens = skusDoPedido.map((sku) => catalogo.find((p) => p.sku === sku));

const total = itens.reduce((soma, item) => soma + item.preco, 0);

console.log(total);
file:///private/tmp/loja/arrays/pedidos-erro.mjs:9 const total = itens.reduce((soma, item) => soma + item.preco, 0); ^ TypeError: Cannot read properties of undefined (reading 'preco') at file:///private/tmp/loja/arrays/pedidos-erro.mjs:9:56 at Array.reduce (<anonymous>) at file:///private/tmp/loja/arrays/pedidos-erro.mjs:9:21 Node.js v24.16.0

Repare no rastro: a linha que aparece duas vezes com colunas diferentes é o callback por dentro e a chamada do reduce por fora. O MON-007 não está no catálogo, o find devolveu undefined, e o map guardou esse undefined como se fosse um item. O contexto completo desse erro está em TypeError: Cannot read properties of undefined.

A correção não é só filtrar o buraco: é saber qual SKU sumiu, porque isso costuma ser um problema de dados, não de código.

js
const catalogo = [
  { sku: 'TEC-001', nome: 'Teclado mecânico', preco: 289.9 },
  { sku: 'MOU-014', nome: 'Mouse sem fio', preco: 149.9 },
];

const skusDoPedido = ['TEC-001', 'MOU-014', 'MON-007'];

const encontrados = skusDoPedido
  .map((sku) => catalogo.find((p) => p.sku === sku))
  .filter((p) => p !== undefined);

const ausentes = skusDoPedido.filter((sku) => !catalogo.some((p) => p.sku === sku));

console.log(encontrados.reduce((soma, item) => soma + item.preco, 0).toFixed(2));
console.log('SKUs fora do catálogo:', ausentes);
439.80 SKUs fora do catálogo: [ 'MON-007' ]

Como montar o encadeamento

A ordem das operações muda o custo e a legibilidade. Filtre primeiro, para as etapas seguintes trabalharem com menos itens; transforme depois; e deixe a redução por último, porque é ela que fecha o resultado. Ordenar, quando entra, vem antes do corte — não faz sentido pegar os cinco primeiros de uma lista desordenada, como mostra ordenar array com sort.

Uma última regra prática: quando o encadeamento passa de três métodos, dê nome às etapas intermediárias com const. Ninguém consegue depurar uma expressão de seis linhas sem quebrá-la em pedaços — e o pedaço com nome é o que aparece legível no console.log.

Como microprática, adicione ao pedido um item de categoria monitor com quantidade 2. Calcule, em variáveis separadas, a lista filtrada dessa categoria e o total dela. O teste passa se a lista tiver dois registros de monitor e se o total considerar preco * quantidade, não apenas a quantidade de linhas.

Se os objetos em si ainda são novidade — como criar, acessar por colchetes e alterar propriedade —, o ponto de partida é objetos em JavaScript. O guia completo de JavaScript mostra a ordem de estudo inteira.

Prefere aprender em vídeo?

Tem uma aula sobre este assunto no nosso canal.

Ver todos os vídeos do canal
  • arrays
  • objetos
  • reduce
  • groupby
  • map

Perguntas frequentes

Por que o reduce precisa do zero no final?
Porque sem valor inicial o primeiro acumulador é o primeiro elemento do array — que, num array de objetos, é um objeto. A soma vira concatenação de texto e o resultado sai como "[object Object]149.9". Com o zero, o acumulador começa numérico e a conta funciona. O zero também é o que permite somar um array vazio sem erro.
Object.groupBy ou reduce para agrupar?
Use Object.groupBy quando você quer as listas de itens separadas por chave: é uma linha e diz o que faz. Use reduce quando o agrupamento envolve conta — total por categoria, contagem, média — porque aí você não quer as listas, quer o número já calculado.
Como remover objetos duplicados de um array?
Não dá para usar new Set direto no array de objetos, porque cada objeto é uma referência distinta e o Set mantém todos. O caminho é montar um Map com a chave de negócio (SKU, id, e-mail) e pegar os valores dele, ou filtrar comparando indexOf do campo identificador.
reduce é mais rápido que um for comum?
Não, e essa não é a razão para usá-lo. Em listas do tamanho que um pedido tem, a diferença é irrelevante. reduce ganha por outro motivo: ele diz na primeira linha que o resultado é um valor único acumulado, enquanto um for com contador só revela isso depois que você lê o corpo inteiro.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. MDN — Array.prototype.reduce() — developer.mozilla.org
  2. MDN — Object.groupBy() — developer.mozilla.org
  3. MDN — Map — developer.mozilla.org

Continue por aqui