Voltar
JavaScriptArtigo · 26 de mar. de 2024 · 16 min

jonathanjuliani

ECMAScript ES7 ao ES10: as features que você usa sem saber de onde vieram

Novidades do JavaScript ES7 ao ES10: includes, spread em objetos, flat, fromEntries e gotchas reais. Guia prático com exemplos.

Linha do tempo das features do ECMAScript ES7 ao ES10 no JavaScript

Você provavelmente já usou includes(), spread em objetos e flat() no dia a dia — mas sabia que essas três features vieram em releases diferentes do ECMAScript? Bora ver o que mudou do ES7 ao ES10, com os casos que a documentação oficial passa batido.

O que você vai aprender:

  • Array.includes() e String.includes() — e o gotcha com objetos que pega todo mundo
  • Object.values() e Object.entries() — e o irmão menor que virou maior no ES10
  • Spread operator em objetos — por que substituiu o Object.assign de vez
  • finally em Promises e for await...of — onde cada um encaixa de verdade
  • flat(), flatMap(), Object.fromEntries(), trimStart/End e o catch sem parâmetro
  • Por que o sort() virar stable no ES10 importa mais do que parece

Pré-requisitos: familiaridade com JavaScript básico (funções, arrays, objetos). Se você nunca viu uma Promise, vale dar uma passada nesse post antes de chegar no ES9.


ES7: includes e o operador exponencial

O ES2016 (ES7) foi um release enxuto — três adições. Mas uma delas você usa provavelmente toda semana.

Array.includes e String.includes: chega de indexOf

Antes do ES7, checar se um valor existia num array ou string significava usar indexOf() e torcer pra lembrar que ele retorna -1 quando não encontra — não false. O includes() resolve isso com um retorno direto em boolean.

Em strings:

código
const frase = 'Hello world';
 
frase.includes('w'); // true
frase.includes('L'); // false — é case-sensitive, se liga
frase.includes('x'); // false

Em arrays simples, funciona exatamente como você espera:

código
const animais = ['cachorro', 'gato', 'urso'];
 
animais.includes('cachorro'); // true
animais.includes('peixe'); // false
 
const numeros = [1, 2, 3, 4, 5];
 
numeros.includes(3); // true
numeros.includes(0); // false

Bora deixar interessante — o que acontece com arrays de objetos?

código
const arrayDeObj = [
  { id: 1, desc: 'Obj 1' },
  { id: 2, desc: 'Obj 2' },
];
 
arrayDeObj.includes(4); // false — faz sentido
arrayDeObj.includes({}); // false — ok
arrayDeObj.includes({ id: 1, desc: 'Obj 1' }); // false — peraí, WTF?

"Tá Jon, mas eu passei exatamente o mesmo objeto. Por que retorna false?"

Porque o includes() faz comparação por referência, não por valor. { id: 1, desc: 'Obj 1' } que você passou no includes é um objeto novo na memória — diferente do que está no array, mesmo tendo os mesmos valores. Pra comparar objetos dentro de array você precisa de um find() ou some() com comparação explícita das propriedades.

Operador exponencial **

Antes você precisava do Math.pow(5, 2) pra elevar à potência. Agora:

código
const elevarA2 = (num) => num ** 2;
elevarA2(5); // 25
 
const elevarA3 = (num) => num ** 3;
elevarA3(5); // 125

Sussa.


ES8: padStart, padEnd, Object.values e Object.entries

O ES2017 trouxe algumas features que parecem pequenas mas aparecem bastante em código de produção.

padStart e padEnd: formatação de string sem gambiarra

O padStart e padEnd adicionam caracteres (espaço por padrão, ou o que você passar) no início ou no final da string até ela atingir o tamanho especificado. Útil pra formatar datas, IDs, logs:

código
const nome = 'Jonathan';
 
nome.padStart(10); // '  Jonathan' — 2 espaços no começo
nome.padEnd(10); // 'Jonathan  ' — 2 espaços no final
 
// caso de uso real: formatar número de pedido com zeros à esquerda
const pedido = '42';
pedido.padStart(6, '0'); // '000042'

Object.values: só os valores, sem as chaves

código
const obj = {
  id: 1,
  nome: 'teste',
  desc: 'teste description',
};
 
Object.values(obj);
// [1, 'teste', 'teste description']

Tem um objeto e precisa só dos valores pra iterar? Object.values e acabou.

Object.entries: o par chave/valor em array

E o irmão maior — o Object.entries — retorna um array de arrays, cada um com [chave, valor]:

código
const obj = {
  id: 1,
  nome: 'teste',
  desc: 'teste description',
};
 
Object.entries(obj);
// [
//   ['id', 1],
//   ['nome', 'teste'],
//   ['desc', 'teste description']
// ]

Com entries() você consegue iterar sobre objetos com map, reduce, for...of — coisa que não rola diretamente num objeto simples. Um caso que aparece bastante: transformar as chaves ou valores de um objeto sem perder a estrutura. Vai ficar ainda mais útil quando chegar no Object.fromEntries lá no ES10.

[IMAGEM: diagrama comparando a saída de Object.values vs Object.entries no mesmo objeto | alt: "Diagrama mostrando Object.values retornando array de valores e Object.entries retornando array de pares chave-valor"]


ES9: spread em objetos, finally e for await

O ES2018 foi um dos releases mais úteis pra quem trabalha com async e com objetos no dia a dia.

Spread em objetos: chega de Object.assign

O spread (...) já funcionava com arrays desde o ES6. Com objetos só chegou no ES9 — e substitui o Object.assign na maioria dos casos com uma sintaxe muito mais limpa.

Extrair parte de um objeto e guardar o resto:

código
const obj = {
  id: 1,
  nome: 'Jonathan',
  bio: 'Fullstack developer',
};
 
const { id, ...resto } = obj;
 
console.log(id); // 1
console.log(resto); // { nome: 'Jonathan', bio: 'Fullstack developer' }

Criar uma cópia sem compartilhar a referência de memória (o problema clássico do Object.assign):

código
const copia = { ...obj };
// objeto novo, referência nova — modificar copia não afeta obj

A posição da chave que você extrai não importa — você pode pegar qualquer propriedade do meio do objeto e o ...resto vai ter tudo que sobrou:

código
const { nome, ...restoDoObj } = obj;
 
console.log(nome); // 'Jonathan'
console.log(restoDoObj); // { id: 1, bio: 'Fullstack developer' }

"Tá Jon, mas o Object.assign já fazia cópia. Quando uso um em vez do outro?"

O spread cria uma cópia rasa (shallow) e é mais legível. O Object.assign ainda tem uma vantagem: você pode passar múltiplos objetos fonte de uma vez e já vai mergindo tudo no destino — Object.assign(destino, obj1, obj2, obj3). Com spread você faz { ...obj1, ...obj2, ...obj3 }, que é igualmente válido. Na prática, spread virou o padrão — Object.assign aparece mais em código antigo ou quando você precisa mutar o objeto de destino intencionalmente.

Se quiser ver spread com arrays também — que tem seus próprios gotchas — tem um post completo sobre spread e rest operators aqui.

finally em Promises: o bloco que sempre executa

Se você veio de Java, PHP ou Python já conhece o finally do try/catch. No JS demorou, mas chegou no ES9 — e agora funciona tanto no try/catch quanto encadeado em Promises:

código
const url = 'https://jsonplaceholder.typicode.com/users';
 
const fetchUsers = async () => {
  const data = await fetch(url)
    .then((res) => res.json())
    .catch((err) => console.log('Deu ruim:', err))
    .finally(() => console.log('Requisição finalizada — com ou sem erro'));
};
 
fetchUsers();

O finally executa independente de o then ou o catch ter rodado. Útil pra fechar loaders, limpar estado de loading, fechar conexões — qualquer coisa que tem que acontecer sempre.

for await...of: iterando Promises em sequência

Esse é o mais tretinha do ES9. Quando você tem um array de Promises e precisa iterar o resultado de cada uma em sequência — não em paralelo — o for await...of é a solução limpa.

código
const urls = [
  'https://jsonplaceholder.typicode.com/users',
  'https://jsonplaceholder.typicode.com/posts',
  'https://jsonplaceholder.typicode.com/albums',
];
 
const fetchData = async (urls) => {
  const fetchArray = urls.map((url) => fetch(url)); // dispara todos de uma vez
  for await (const result of fetchArray) {
    // aguarda cada um em ordem
    const data = await result.json();
    console.log(data);
  }
};
 
fetchData(urls);

"Tá Jon, mas quando uso isso em vez de Promise.all?"

O Promise.all executa em paralelo e aguarda todos terminarem — se um falhar, todos falham juntos. O for await processa cada Promise em sequência, um por vez, e você pode tratar o erro individualmente dentro do loop. Usa Promise.all quando a ordem não importa e você quer velocidade máxima. Usa for await quando precisa de controle granular — como processar um upload por vez pra não sobrecarregar o servidor.


ES10: flat, fromEntries, trim melhorado e sort estável

O ES2019 trouxe um conjunto de features que parecem pequenas isoladas, mas que mudam bastante o jeito de trabalhar com arrays e objetos.

flat e flatMap: adeus array aninhado

O flat() pega sub-arrays e "joga" os valores pro array pai. Por padrão, ele desaninha só um nível:

código
const array = [1, 2, [10, 20]];
array.flat();
// [1, 2, 10, 20]

Mas você pode passar o número de níveis — ou Infinity pra desaninhar tudo, não importa quão fundo for:

código
const aninhado = [1, 2, [10, 20, [100, 200, [1000, 2000]]], [50, 38, 80]];
 
aninhado.flat(Infinity);
// [1, 2, 10, 20, 100, 200, 1000, 2000, 50, 38, 80]

O flatMap() é um map() seguido de um flat(1) — você itera cada item, retorna um array, e o resultado já sai desaninhado um nível:

código
const numeros = [1, 2, 3, 4];
 
numeros.flatMap((valor) => [valor, valor * 2]);
// [1, 2, 2, 4, 3, 6, 4, 8]

Sem o flatMap, você faria numeros.map(...).flat() — funciona igual, mas é um passo a mais.

Object.fromEntries: o par do entries

Lembra do Object.entries() lá no ES8 que transforma objeto em array de pares [chave, valor]? O Object.fromEntries() faz exatamente o inverso — transforma um array de pares em objeto:

código
const entries = [
  ['id', 1],
  ['nome', 'Jonathan'],
  ['bio', 'Fullstack developer'],
];
 
const obj = Object.fromEntries(entries);
// { id: 1, nome: 'Jonathan', bio: 'Fullstack developer' }

O caso de uso que aparece bastante na prática: você tem um objeto, quer transformar só os valores (sem perder as chaves), e depois voltar pra objeto. Com entries + map + fromEntries isso vira uma linha:

código
const precos = { banana: 1.5, manga: 3.0, abacaxi: 4.5 };
 
// aplicar 10% de desconto em tudo
const comDesconto = Object.fromEntries(Object.entries(precos).map(([fruta, preco]) => [fruta, preco * 0.9]));
// { banana: 1.35, manga: 2.7, abacaxi: 4.05 }

Antes do ES10 você faria isso com reduce — funciona, mas o fromEntries deixa a intenção muito mais explícita.

trimStart e trimEnd: nomes que fazem sentido

Já existiam trimLeft e trimRight — mas os nomes eram inconsistentes com o resto da API do JS. O ES10 trouxe os aliases trimStart e trimEnd, que fazem a mesma coisa com nomes mais coerentes:

código
const comEspacos = '   Hello world   ';
 
comEspacos.trimStart(); // 'Hello world   '
comEspacos.trimEnd(); // '   Hello world'
comEspacos.trim(); // 'Hello world' — remove dos dois lados, esse é o ES5

catch sem parâmetro: pequeno, mas útil

Antes do ES10, o catch exigia que você declarasse o parâmetro do erro — mesmo quando não ia usar:

código
// antes
try {
  // algo
} catch (err) {
  // err nunca usado, mas tinha que estar aqui
  console.log('deu ruim');
}
 
// ES10 em diante
try {
  // algo
} catch {
  console.log('deu ruim');
}

sort() stable: o que mudou e por que importa

Esse é o mais sutil, mas tem consequência real.

Um algoritmo de ordenação é stable quando elementos com o mesmo valor de comparação mantêm a ordem original entre si. Unstable significa que eles podem trocar de posição mesmo tendo o mesmo peso na ordenação.

[IMAGEM: diagrama comparando sort stable vs unstable com um array de objetos com mesmo valor de prioridade | alt: "Diagrama mostrando sort stable mantendo a ordem original de elementos com mesmo valor, e sort unstable trocando suas posições"]

Na prática: imagine que você tem uma lista de tarefas com prioridade, ordenada por data. Ao aplicar sort() por prioridade, um algoritmo stable mantém a ordem por data dentro das tarefas de mesma prioridade. Um unstable pode embaralhar.

Antes do ES10, o comportamento do sort() dependia do browser e do tamanho do array — Chrome usava um algoritmo diferente do Firefox, e arrays acima de certo tamanho podiam ter comportamento imprevisível. A partir do ES10, todo ambiente JavaScript é obrigado a implementar um sort stable. Se você já teve bug esquisito de ordenação que só aparecia em produção ou em certos browsers, esse pode ter sido o culpado.


TL;DR — Referência rápida ES7 ao ES10

FeatureVersãoO que fazGotcha
Array.includes()ES7Verifica se valor existe — retorna booleanComparação por referência: não funciona com objetos
String.includes()ES7Verifica se substring existeCase-sensitive
** (exponencial)ES7Eleva à potência
padStart / padEndES8Preenche string até tamanho alvo
Object.values()ES8Array com os valores do objeto
Object.entries()ES8Array de pares [chave, valor]
Spread em objetos ...ES9Copia / desestrutura objetosCópia rasa — objetos aninhados ainda compartilham referência
Promise.finally()ES9Executa sempre, com ou sem erro
for await...ofES9Itera Promises em sequênciaDiferente de Promise.all — sequencial, não paralelo
flat()ES10Desaninha sub-arraysPor padrão só 1 nível — use flat(Infinity) pra tudo
flatMap()ES10map() + flat(1) combinadosSó desaninha 1 nível, diferente de flat(Infinity)
Object.fromEntries()ES10Array de pares → objetoInverso do Object.entries()
trimStart / trimEndES10Remove espaços no início / fimAliases de trimLeft / trimRight
catch {} sem parâmetroES10Omite o parâmetro err quando não vai usar
sort() stableES10Mantém ordem relativa de itens iguaisComportamento era inconsistente entre browsers antes disso

Próximos passos

Se esse artigo fez você querer entender melhor o fundamento de algumas dessas features:

  • CommonJS no Node: require e module.exports ainda convivem com import — base útil antes de misturar módulos: Node require e exports explicados
  • Spread e rest operators em profundidade — arrays, objetos, parâmetros de função e os gotchas de cópia rasa: lê aqui
  • Promises e async/await do zero, pra chegar no for await e no finally sem travar: começa por aqui
  • Documentação oficial do TC39 com o histórico completo de cada proposta: tc39.es/ecma262

Antes de fechar

Qual dessas features você mais usa sem saber que era "nova"? Tenho uma suspeita que o spread em objetos é o campeão.

Se viu alguma cagada, se discorda de alguma coisa ou tem um caso de uso melhor pra alguma dessas features — me fala nos comentários. Vamos compartilhar conhecimento e código, é o melhor jeito de fixar qualquer coisa.

Inscreva-se no Ledger da Engenharia

Receba notas de arquitetura e performance na sua caixa de entrada. Mesma lista do convite—inscreva-se aqui quando quiser.

Sem spam. Sem APIs de terceiros. Apenas eu enviando updates.

O Ledger da Engenharia

Transmissoes quinzenais sobre arquitetura, performance e engenharia aplicada. Inscreva-se a partir de qualquer artigo—sem spam.