Bem-vindo JavaScript Moderno

JavaScript moderno é o nome que usamos para a linguagem a partir do ECMAScript 2015, também chamado de ES6, que trouxe a maior mudança da história do JS. Durante o ASP.NET Cast de 16/10/2019, falamos muito sobre essa história, e ficou claro que muita coisa passou batido para muita gente.

Como boa parte dos alunos do balta.io busca se atualizar, este artigo reúne os recursos que você precisa dominar para sair do JavaScript antigo. Assim, você vai conhecer let e const, arrow functions, módulos, classes, spread, rest, destructuring e map, sempre com exemplos práticos.

O que mudou no JavaScript moderno

Por muitos anos, o JavaScript foi visto como uma linguagem arcaica, usada apenas para pequenos efeitos em páginas web. No entanto, a especificação ECMAScript passou a evoluir com frequência, e a linguagem ganhou recursos que antes só existiam em outras plataformas.

O marco dessa virada foi o ES6, publicado em 2015. Desde então, a especificação recebe uma nova versão por ano, sempre com melhorias incrementais.

Na prática, isso significa que o código escrito hoje é bem diferente daquele que você talvez tenha aprendido. Se você ainda está começando, vale ler antes o artigo sobre o que é JavaScript para entender o papel da linguagem.

A referência mais completa sobre cada recurso é a documentação do JavaScript na MDN. Ao longo do texto, você verá os pontos principais de cada recurso com exemplos curtos.

Let e const no JavaScript moderno

Antigamente, no JS, usávamos sempre o var, tanto para valores fixos quanto para variáveis. Afinal, a linguagem nem tinha uma forma própria de declarar constantes.

Uma das mudanças do JavaScript moderno é a chegada do let para variáveis e do const para constantes. Depois de criar uma constante, se você tentar atribuir um novo valor a ela, receberá um erro.

let myVariable = "Some value";
const myConstant = "Some value";

// TypeError: Assignment to constant variable.
myConstant = "Another value";

Além disso, let e const têm escopo de bloco. Ou seja, uma variável declarada dentro de um if ou de um for só existe ali dentro, o que evita vários bugs clássicos do var.

Vale um detalhe importante sobre o const. Ele impede uma nova atribuição, mas não torna o objeto imutável, então você ainda pode alterar as propriedades de um objeto declarado com const.

Se quiser se aprofundar nesse assunto, o artigo entendendo as variáveis no JavaScript detalha escopo, hoisting e boas práticas.

Arrow functions

As arrow functions são a forma do JavaScript moderno de escrever funções com menos código. Com elas, você deixa de usar a palavra-chave function.

// Modelo antigo
function myFunction() {
    return "Value";
}

// Modelo novo
const myFunction = () => {
    return "Value";
};

Além da estética, as arrow functions mudam o comportamento do this. Elas não criam um this próprio e herdam o valor do contexto onde foram definidas.

Por isso, em callbacks dentro de classes e componentes, as arrow functions eliminam truques antigos como var self = this ou o uso de bind. Em muitas situações, elas se tornaram praticamente obrigatórias.

Contudo, existem exceções. Métodos de objeto que dependem do próprio this e funções construtoras continuam exigindo a sintaxe tradicional.

Parâmetros em arrow functions

Você também pode receber parâmetros, bastando informar os nomes na assinatura da função. Funciona da mesma forma para um ou para vários parâmetros.

// Recebendo um parâmetro
const echo = (value) => {
    return value;
};

// Recebendo N parâmetros
const sum = (first, second) => {
    return first + second;
};

Arrow functions sem corpo

Se a sua arrow function tem apenas uma expressão de retorno, você pode usar a forma sem corpo, também chamada de bodyless. Nesse caso, você remove as chaves e a palavra return, porque o retorno fica implícito.

Além disso, se existir apenas um parâmetro, os parênteses também se tornam opcionais.

// Sem corpo com apenas um parâmetro
const echo = value => value;

// Sem corpo com N parâmetros
const sum = (first, second) => first + second;

Repare que não usamos return na versão sem corpo. Se você escrever return sem as chaves, o código gera um erro de sintaxe.

Para entender todas as formas de declarar funções, confira o artigo entendendo funções no JavaScript.

Exports e imports no JavaScript moderno

Outro recurso importante do JavaScript moderno é o sistema de módulos, com importação e exportação de objetos, funções e classes. Com ele, a modularização das aplicações ficou muito mais simples.

Você pode usar o export default para exportar um único valor principal de um arquivo.

// course.js
const course = {
    title: "Modern JS",
    tag: "7199"
};

export default course;

Outra opção é usar o export const diretamente na declaração. Esse é o meio mais comum e, particularmente, o que eu prefiro.

// course.js
export const Course = {
    title: "Modern JS",
    tag: "7199"
};

Importando módulos

Com tudo exportado, você importa os valores usando o import. Para valores exportados com default, basta escolher um nome e fazer a importação.

Já os valores exportados com const são exportações nomeadas. Nesse caso, você precisa usar o mesmo nome, entre chaves.

// Importação quando o módulo usa export default
import course from './path/to/course.js';

// Importação quando o módulo usa export const
import { Course } from './path/to/course.js';

Você também pode usar alias, ou seja, um nome personalizado, nos dois casos.

// Export default com nome personalizado
import myCourse from './path/to/course.js';

// Export const com nome personalizado
import { Course as MyCourse } from './path/to/course.js';

Se um arquivo tiver muitas exportações nomeadas e você quiser importar todas de uma vez, use o curinga * com um alias. O resultado é um objeto que reúne todas as exportações do módulo.

// Importa todas as exportações nomeadas e cria o alias courses
import * as courses from './path/to/course.js';

console.log(courses.Course.title);

Classes e herança

Antes do ES6, o JavaScript já suportava orientação a objetos, mas por meio de prototypes. Essa abordagem era bem diferente do que temos em linguagens como C# e Java.

Hoje, temos a palavra-chave class e herança com extends. Por baixo dos panos, tudo continua usando prototypes, mas a sintaxe ficou muito mais familiar.

class Payment {
    customer = "";

    pay = () => {
        // pagar...
    };
}

const payment = new Payment();

class CreditCardPayment extends Payment {
    creditCardNumber = "";

    installment = () => {
        // parcelamento
    };
}

No JavaScript moderno, os campos declarados direto no corpo da classe, como customer, chegaram oficialmente na especificação ES2022. Antes disso, você inicializava essas propriedades dentro do constructor.

Lembre-se também de que objetos e instâncias de classes são passados por referência. Dessa forma, ao atribuir um objeto a outra variável, você não cria uma cópia, ao contrário do que acontece com valores primitivos como string, number e boolean.

Spread operator no JavaScript moderno

Se arrays e objetos são passados por referência, como criar uma cópia com elementos ou propriedades extras? É justamente aí que entra o spread operator do JavaScript moderno.

Para usá-lo, basta escrever ... antes do objeto ou array anterior. Assim, você espalha o conteúdo dele dentro do novo valor.

Isso vale tanto para arrays quanto para objetos.

// Spread operator em arrays
const numbers = [1, 2, 3];
const newNumbers = [...numbers, 4];

// Spread operator em objetos
const course = {
    title: "balta.io course",
    tag: "7198"
};

const newCourse = {
    ...course,
    category: "Frontend"
};

Repare que numbers e course continuam intactos. Por isso, o spread se tornou a forma padrão de trabalhar com dados imutáveis em bibliotecas como React.

No entanto, o spread faz apenas uma cópia rasa. Se o objeto tiver outros objetos dentro, essas partes internas continuam compartilhadas.

Rest operator

No JavaScript moderno, o rest operator permite enviar uma quantidade variável de argumentos para uma função. Ele usa os mesmos ... do spread, mas faz o caminho inverso.

Enquanto o spread espalha os elementos, o rest junta os argumentos em um array. Com isso, você pode usar métodos como filter e map diretamente sobre eles.

const filterOnes = (...args) => {
    return args.filter(item => item === 1);
};

// Deve exibir [1]
console.log(filterOnes(1, 2, 3));

O rest precisa ser sempre o último parâmetro da função. Afinal, ele recolhe tudo o que sobrou.

Destructuring no JavaScript moderno

O destructuring do JavaScript moderno serve para extrair elementos de um array ou propriedades de um objeto diretamente para variáveis. Em outras palavras, você desmonta a estrutura em partes menores.

Destructuring de arrays

No caso dos arrays, basta declarar as variáveis entre colchetes e igualar à sua lista. Então, cada item da lista vai para a variável na mesma posição.

Caso queira pular algum item, deixe apenas a vírgula no lugar da variável.

const numbers = [1, 2, 3, 4];

const [first, second] = numbers;

// Pulando o segundo item com uma vírgula
const [one, , three] = numbers;

// Vai exibir 1 2
console.log(first, second);

// Vai exibir 1 3
console.log(one, three);

Destructuring de objetos

No caso de objetos, declare variáveis entre chaves com o mesmo nome das propriedades. Se não houver uma propriedade com aquele nome, a variável fica com o valor undefined.

const course = {
    title: "balta.io course",
    tag: "7199"
};

const { title, tag, price } = course;

// Vai exibir: balta.io course
console.log(title);

// Vai exibir: 7199
console.log(tag);

// Vai exibir: undefined
// Não existe a propriedade price no curso
console.log(price);

Também é possível definir valores padrão, como const { price = 0 } = course. Dessa forma, a variável recebe zero quando a propriedade não existe.

Map e outros métodos de array

Além do filter, que vimos antes, temos também o map. Ele percorre uma lista e gera um novo array com o resultado de uma função aplicada a cada elemento.

Por exemplo, você pode dobrar o valor de cada número de uma lista e guardar o resultado em uma nova variável, com poucas linhas de código.

const numbers = [1, 2, 3, 4];
const doubled = numbers.map((n) => {
    return n * 2;
});

// Vai exibir [1, 2, 3, 4]
console.log(numbers);

// Vai exibir [2, 4, 6, 8]
console.log(doubled);

Se lembrarmos que podemos remover os parênteses quando há apenas um parâmetro e usar funções sem corpo, o código fica ainda menor.

const doubled = numbers.map(n => n * 2);

Na prática, no JavaScript moderno, map, filter e reduce substituem boa parte dos laços for do JavaScript antigo. O guia básico sobre arrays no JavaScript mostra outros métodos úteis para o dia a dia.

Template literals

Outro recurso do JavaScript moderno que você vai usar o tempo todo são as template literals. Em vez de concatenar textos com o sinal de mais, você escreve a string entre crases e insere valores com ${}.

const name = "Balta";
const total = 3;

// Concatenação antiga
const oldMessage = "Hello, " + name + "! You have " + total + " courses.";

// Template literal
const message = `Hello, ${name}! You have ${total} courses.`;

Além disso, as template literals aceitam quebras de linha sem nenhum caractere especial. Por isso, elas facilitam muito a montagem de trechos de HTML e de mensagens longas.

Promises e async await no JavaScript moderno

Antes do ES6, o código assíncrono dependia de callbacks aninhados, o famoso callback hell. O JavaScript moderno resolveu esse problema em duas etapas.

Primeiro, o ES6 trouxe as Promises, objetos que representam um valor que vai chegar no futuro. Depois, o ES2017 trouxe as palavras-chave async e await, que deixam o código assíncrono com cara de código síncrono.

const getCourses = async () => {
    // Aguarda a resposta da API sem bloquear a página
    const response = await fetch("https://api.example.com/courses");
    const courses = await response.json();

    return courses.map(course => course.title);
};

Repare como esse exemplo combina vários recursos que vimos até aqui. Temos const, arrow function, await e map trabalhando juntos em poucas linhas.

Para tratar erros, basta envolver as chamadas em um bloco try/catch. Dessa forma, o fluxo de erro também fica parecido com o de linguagens como C#.

Optional chaining e nullish coalescing

O ES2020 trouxe dois operadores que reduzem bastante as verificações de nulo. O optional chaining, escrito como ?., interrompe a leitura quando encontra null ou undefined.

Já o nullish coalescing, escrito como ??, devolve um valor padrão apenas quando o valor da esquerda é null ou undefined.

const course = { title: "Modern JS", author: null };

// Retorna undefined em vez de lançar um erro
const authorName = course.author?.name;

// Usa o valor padrão apenas quando o valor é null ou undefined
const displayName = authorName ?? "Unknown author";

Na prática, esses operadores substituem cadeias longas de && e evitam o clássico erro de ler uma propriedade de undefined. Eles também mostram que o JavaScript moderno continua evoluindo a cada nova versão da especificação.

Erros comuns ao migrar para o JavaScript moderno

Quem aprende os recursos do JavaScript moderno costuma tropeçar em alguns detalhes. Conhecer esses erros antes economiza horas de depuração.

O primeiro erro é achar que const congela o objeto. Como vimos, ele só impede uma nova atribuição, então use Object.freeze quando precisar de imutabilidade de verdade.

O segundo erro é escrever return em uma arrow function sem chaves. Nesse caso, o interpretador acusa um erro de sintaxe, porque o retorno já é implícito.

Outro deslize frequente é usar o destructuring sem declarar as variáveis com const ou let. Em modo estrito, isso gera erro, e fora dele cria variáveis globais sem que você perceba.

Também é comum confundir a importação de exportações nomeadas. Lembre-se de que import * as courses não leva chaves, enquanto import { Course } exige o nome exato da exportação.

Por fim, muita gente esquece que o spread faz apenas uma cópia rasa. Portanto, ao copiar objetos com outros objetos dentro, alterações internas continuam afetando o original.

Para evitar esses problemas, ative um linter como o ESLint no seu editor. Assim, a ferramenta aponta os deslizes enquanto você escreve o código.

Como adotar o JavaScript moderno no seu código

Todos os navegadores atuais suportam os recursos apresentados aqui. Portanto, você já pode usar o JavaScript moderno em projetos novos sem medo.

Para projetos legados, a migração para o JavaScript moderno pode ser gradual. Primeiro, troque var por let e const, e depois converta callbacks para arrow functions.

Em seguida, adote módulos com import e export para organizar o código em arquivos menores. Ferramentas como Babel e bundlers continuam úteis quando você precisa atender navegadores muito antigos.

Se preferir aprender de forma estruturada, o balta.io tem um curso gratuito de JavaScript que cobre esses fundamentos.

Conclusão

O JavaScript evoluiu muito nos últimos anos e deixou de ser aquela linguagem arcaica e abandonada. Hoje, o JavaScript moderno tem recursos próprios que facilitam muito a nossa vida.

Let e const, arrow functions, módulos, classes, spread, rest, destructuring e map formam a base de qualquer código atual. Assim, dominar esses recursos é o primeiro passo para trabalhar com frameworks como React, Angular e Vue.

Além disso, template literals, async await e os operadores ?. e ?? completam o kit do dia a dia. Então, pratique cada exemplo deste artigo no console do navegador e transforme esses recursos em hábito.

FAQ

O que é JavaScript moderno?

É o JavaScript escrito a partir do ES6, de 2015. Ele inclui recursos como let, const, arrow functions, classes e módulos.

Ainda devo usar var?

Na maioria dos casos, não. Prefira const por padrão e let quando o valor precisar mudar, porque ambos têm escopo de bloco.

Arrow functions substituem todas as funções?

Não. Métodos de objeto que dependem do próprio this e funções construtoras ainda pedem a sintaxe tradicional com function.

Qual a diferença entre spread e rest?

Os dois usam ..., mas o spread espalha elementos de um array ou objeto. Já o rest junta vários argumentos em um único array.

Classes no JavaScript são iguais às do C#?

Não exatamente. A sintaxe é parecida, mas por baixo dos panos o JavaScript continua usando prototypes.