JavaScript moderno

Lucas Bittencourt

Lucas Bittencourt / fevereiro 03, 2023

15min de leitura

JavaScript logo

Índices

Introdução

O JavaScript moderno possui muitas features (e features novas sendo entregues todo ano), das quais muita gente acaba não se atualizando.

Falsy, Truthy & Nullish values

O JavaScript converte, de forma implícita, valores quando estão em um contexto booleano (ou operações condicionais)

Falsy values

Falsy values são valores que quando convertidos de forma implícita, se equivalem ao false.

  • false
  • '', "", `` (string vazia)
  • 0, -0, 0n, -0n, 0.0, -0.0 (zeros absolutos)
  • null
  • undefined
  • NaN

Truthy values

Truthy values são valores que quando convertidos de forma implícita, se equivalem ao true.

  • true
  • Qualquer String com conteúdo
  • Qualquer número menor que zero ou maior que zero (incluindo pontos flutuantes)
  • Infinity (Infinito)
  • -Infinity (menos infinito)
  • [] (Array, vazio ou com conteúdo)
  • {} (Objeto, vazio ou com conteúdo)

Nullish values

Nullish values são 2 valores únicos e sempre são considerados valores falsy.

  • null
  • undefined

Numeric separators

Numeric separators melhora a legibilidade de números literais com uma separação visual entre os grupos de dígitos, utilizando underscore (_).

const oneMillion = 1_000_000; // 1000000 (1 milhão)
const oneBillion = 1_000_000_000; // 1000000000 (1 bilhão)
const oneMillionFiftyCents = 1_000_000.5; // 1000000.50 (1 milhão e 50 centavos)

Template literals

Template strings (ou template literals) é uma syntactic sugar para construirmos strings. No JavaScript, é utilizado a crase (`) para iniciar a interpolação.

É boa prática sempre usarmos o template literals para concatenação de strings e o + para operações matemáticas.

const username = 'Lucas';
const lastname = 'Bittencourt';

// Antes
const fullname = username + ' ' + lastname;

// Depois
const fullname = `${username} ${lastname}`; // Lucas Bittencourt

Conseguimos utilizar um número indefinido de variáveis dentro da interpolação.

const information = `Olá, ${fullname}. Tudo bem? Posso te chamar de ${username}?`;

Shorthand property names

Quando vamos inicializar um objeto literal, conseguimos usar uma shorter syntax para definir as propriedades do objeto, caso os nomes das propriedades sejam iguais.

const username = 'Lucas';
const lastname = 'Bittencourt';

const getFullname = () => `${username} ${lastname}`;

// Forma completa
const person = {
  username: username,
  lastname: lastname,
  getFullname: getFullname,
  age: 23,
};

// Shorthand property names
const person = {
  username,
  lastname,
  getFullname,
  age: 23,
};

Não é possível utilizar o shorthand de forma aninhada, ou seja, acessando referências de objetos ao mesmo tempo:

const otherPerson = {
  name: person.name, // OK
  person.name, // Uncaught SyntaxError: Unexpected token '.'
};

É possível utilizar uma shorter syntax para definir funções como propriedades ao inicializar objetos:

const fullname = 'Lucas Bittencourt';
const person = {
  fullname,
  getFullname() {
    return this.fullname;
  },
};

console.log(person.getFullname()); // 'Lucas Bittencourt'

Há diferenças semânticas em criar funções em objetos dessa forma. Leia aqui

Operador lógico AND

O operador lógico AND (&&) retorna o valor da direita caso o valor da esquerda seja truthy.

console.log(false && 'Lucas Bittencourt'); // false
console.log('' && 'Lucas Bittencourt'); // ''
console.log(0 && 'Lucas Bittencourt'); // 0
console.log(undefined && 'Lucas Bittencourt'); // undefined
console.log(null && 'Lucas Bittencourt'); // null
console.log(NaN && 'Lucas Bittencourt'); // NaN

console.log(true && 'Lucas Bittencourt'); // Lucas Bittencourt
console.log('Bittencourt' && 'Lucas Bittencourt'); // Lucas Bittencourt
console.log(1 && 'Lucas Bittencourt'); // Lucas Bittencourt

Podemos usar inúmeras vezes esse operador lógico e ele irá sempre o primeiro valor falsy, caso o valor da esquerda seja truthy:

console.log(0 && false && 'Lucas Bittencourt'); // 0
console.log(1 && true && 'Lucas Bittencourt'); // 'Lucas Bittencourt'
console.log(1 && false && 'Lucas Bittencourt'); // false

Operador lógico OR

O operador lógico OR (||) retorna o valor da direita caso o valor da esquerda seja falsy.

console.log(false || 'Lucas Bittencourt'); // Lucas Bittencourt
console.log('' || 'Lucas Bittencourt'); // Lucas Bittencourt
console.log(0 || 'Lucas Bittencourt'); // Lucas Bittencourt
console.log(undefined || 'Lucas Bittencourt'); // Lucas Bittencourt
console.log(null || 'Lucas Bittencourt'); // Lucas Bittencourt
console.log(NaN || 'Lucas Bittencourt'); // Lucas Bittencourt

console.log(true || 'Lucas Bittencourt'); // true
console.log('Bittencourt' || 'Lucas Bittencourt'); // Bittencourt
console.log(1 || 'Lucas Bittencourt'); // 1

Podemos usar inúmeras vezes esse operador lógico e ele irá sempre o primeiro valor truthy, caso o valor da esquerda seja falsy:

console.log(0 || false || 'Lucas Bittencourt'); // Lucas Bittencourt
console.log(0 || true || 'Lucas Bittencourt'); // true
console.log(1 || true || 'Lucas Bittencourt'); // 1

Nullish Coalescing operator

O operador ?? retorna o valor da direita caso o valor da esquerda seja nullish (undefined ou null). Diferente do ||, que retorna o valor da direita caso o valor da esquerda seja falsy.

console.log('' || 'Lucas Bittencourt'); // 'Lucas Bittencourt'
console.log('' ?? 'Lucas Bittencourt'); // ''

console.log(0 || 'Lucas Bittencourt'); // 'Lucas Bittencourt'
console.log(0 ?? 'Lucas Bittencourt'); // 0

console.log(true || 'Lucas Bittencourt'); // true
console.log(true ?? 'Lucas Bittencourt'); // true

Podemos usar inúmeras vezes esse operador lógico e ele irá sempre o primeiro valor truthy, caso o valor da esquerda seja nullish:

console.log(undefined ?? null ?? 'Lucas Bittencourt'); // 'Lucas Bittencourt'
console.log(undefined ?? false ?? 'Lucas Bittencourt'); // false

Logical Assignment operator

Essa funcionalidade junta operadores lógicos para atribuir um valor condicionalmente.

AND equals (&&=)

&&=: A atribuição de valor é feita caso a variável que for receber o valor seja truthy.

let username = 'Lucas';

// Antes
if (username) {
  username = 'lucasgdb';
}

// Depois
username &&= 'lucasgdb';
console.log(username); // 'lucasgdb'
let username = '';
username &&= 'lucasgdb';
console.log(username); // ''
let username = null;
username &&= 'lucasgdb';
console.log(username); // null

Podemos utilizar async/await de forma mais elegante, sem utilizar condicionais:

let username = null;
username &&= await getUsername();

OR equals (||=)

||=: A atribuição é feita caso a variável que for receber o valor seja falsy.

let username;

// Antes
if (!username) {
  username = 'lucasgdb';
}

// Depois
username ||= 'lucasgdb';
console.log(username); // 'lucasgdb'
let username = 'Lucas Bittencourt';
username ||= 'lucasgdb';
console.log(username); // 'Lucas Bittencourt'

Podemos utilizar async/await de forma mais elegante, sem utilizar condicionais:

let username = null;
username ||= await getUsername();

Nullish Coalescing equals (??=)

??=: A atribuição é feita caso a variável que for receber o valor seja nullish (null ou undefined).

let username = null;

// Antes
if (username === null || username === undefined) {
  username = 'lucasgdb';
}

// Depois
username ??= 'lucasgdb';
console.log(username); // 'lucasgdb'
let username = '';
username ??= 'lucasgdb';
console.log(username); // ''
let username = 'Lucas Bittencourt';
username ??= 'lucasgdb';
console.log(username); // 'Lucas Bittencourt'
let username;
username ??= 'lucasgdb';
console.log(username); // 'lucasgdb'

Podemos utilizar async/await de forma mais elegante, sem utilizar condicionais:

let username = null;
username ??= await getUsername();

Optional Chaining

Optional Chaining é uma maneira segura de acessar propriedades de objetos que podem não existir no meio do caminho. ?. É como o . para acessar os objetos, mas não causa um erro se a referência acessada for nullish.

console.log(nullishReference.name); // Uncaught TypeError: Cannot read properties of undefined (reading 'name')

Para corrigir isso, utilizaremos o Optional Chaining: (?.)

console.log(nullishReference?.name); // undefined
console.log(nullishReference?.['name']); // undefined

Isso é equivalente a:

console.log(nullishReference ? undefined : nullishReference.name); // undefined

Podemos usar em condicionais, já que retornando undefined, o JavaScript irá converter implicitamente pra falsy e negar a condição:

if (nullishReference?.name) {
  console.log('Condição inacessível.');
}

Conseguimos acessar valores de arrays de forma segura:

const myArray = [];
console.log(myArray[0]); // undefined
console.log(myArray[0][0]); // Uncaught TypeError: Cannot read properties of undefined (reading 'name')
console.log(myArray[0]?.[0]); // undefined

Conseguimos invocar métodos de forma segura:

console.log(nullishReference()); // Uncaught TypeError: nullish is not a function
console.log(nullishReference?.()); // undefined

Destructuring assignment

Podemos utilizar essa funcionalidade para copiar valores de Arrays e Objetos literais.

// Exemplo 1
const myArray = [1, 2, 3, 4];
const [a, b, c, d] = myArray;
console.log(a, b, c, d); // 1 2 3 4

// Exemplo 2
const [a, b, c, d] = [1, 2, 3, 4];
console.log(a, b, c, d); // 1 2 3 4

// Exemplo 3
const getUser = () => [1, 2, 3, 4];
const [a, b, c, d] = getUser();
console.log(a, b, c, d); // 1 2 3 4

Conseguimos pular índices de Arrays que não queremos:

const [a, b, , , c, d] = [1, 2, 3, 4, 5, 6];
console.log(a, b, c, d); // 1 2 4

Conseguimos desestruturar Arrays de forma aninhada:

const myArray = [[1, 2, 3], [4, 5, 6], [1, 2, 3]];
const [[a, , c], , [, h, i]] = myArray;
console.log(a, c, h, i); // 1 3 2 3

Tudo no JavaScript é Objeto, então podemos desestruturar da seguinte forma:

const { 0: a, 1: b, 3: d } = [1, 2, 3, 4];
console.log(a, b, d); // 1 2 5 6

Em Objetos, podemos desestruturar quantas propriedades quisermos:

// Exemplo 1
const person = { username: 'Lucas', lastname: 'Bittencourt' };
const { username, lastname } = person;
console.log(username, lastname); // 'Lucas' 'Bittencourt'

// Exemplo 2
const person = { username: 'Lucas', lastname: 'Bittencourt' };
const { username } = person;
console.log(username); // 'Lucas'

// Exemplo 3
const { username, lastname } = { username: 'Lucas', lastname: 'Bittencourt' };
console.log(username, lastname); // 'Lucas' 'Bittencourt'

// Exemplo 4
const getUser = () => ({ username: 'Lucas', lastname: 'Bittencourt' });
const { username, lastname } = getUser();
console.log(username, lastname); // 'Lucas' 'Bittencourt'

Conseguimos receber um valor padrão, caso a propriedade seja undefined:

const person = { username: 'lucasgdb' };
const { name = 'Lucas Bittencourt', username } = person;
console.log(name, username); // 'Lucas Bittencourt' 'lucasgdb'

Conseguimos desestruturar Objetos de forma aninhada:

const person = { username: 'Lucas', lastname: 'Bittencourt', information: { age: 23 } };
const { username, lastname, information: { age } } = person;
console.log(username, lastname, age); // 'Lucas' 'Bittencourt' 23

Conseguimos renomear ao desestruturar objetos:

const person = { name: 'Lucas Bittencourt' };
const { name: fullname } = person;
console.log(fullname); // 'Lucas Bittencourt'

É possível desestruturar parâmetros de função quando recebemos Objetos ou Arrays literais:

function getUsername({ name, lastname }) {
  return `${name} ${lastname}`;
}

console.log(getUsername({ user: 'Lucas', lastname: 'Bittencourt' })); // 'Lucas Bittencourt'

function sumNumbers([a, b, c]) {
  return a + b + c;
}

console.log(sumNumbers([1, 2, 3])); // 6

Spread & Rest operators

A sintaxe dos operadores Rest e Spread são idências: ambos utilizam ... -- A diferença é onde são usadas.

Spread operator

É possível criar variáveis copiando Objetos dentro de outros Objetos literais:

const person = {
  name: 'Lucas',
  lastname: 'Bittencourt',
  age: 23,
};

const newPerson = { ...person, age: 24 };

console.log(person); // { name: 'Lucas', lastname: 'Bittencourt', age: 23 }
console.log(newPerson); // { name: 'Lucas', lastname: 'Bittencourt', age: 24 }

É possível criar variáveis copiando Arrays dentro de outros Arrays literais:

const numbers = [1, 2, 3, 4];
console.log(numbers); // [1, 2, 3, 4]

const newNumbers = [...numbers, 5, 6];
console.log(newNumbers); // [1, 2, 3, 4, 5, 6]

Lembrando que não estamos fazendo uma cópia profunda. Copiar usando spread operator apenas realiza um shallow copy, ou seja, copia apenas o primeiro level, os leveis mais baixos ainda são mantidos por referência.

Rest operator

Quando desestruturamos um objeto, podemos criar uma variável que recebe o resto dos parâmetros que não foram desestruturados manualmente.

Utilizando Rest operator em Arrays literais:

const numbers = [1, 2, 3, 4, 5, 6, 7];

// Exemplo 1
const [n1, n2, n3, ...otherNumbers] = numbers;
console.log(n1, n2, n3); // 1 2 3
console.log(otherNumbers); // [4, 5, 6, 7]

// Exemlo 2
const [n1, , n3, ...otherNumbers] = [0, ...numbers];
console.log(n1, n3); // 1 3
console.log(otherNumbers); // [4, 5, 6, 7]

// Exemplo 1
const [n1, n2, n3, ...otherNumbers] = numbers;
console.log(n1, n2, n3); // 0 1 2
console.log(otherNumbers); // [3, 4, 5, 6, 7]

Utilizando Rest operator em Objetos literais:

const person = {
  username: 'Lucas',
  lastname: 'Bittencourt',
  age: 23,
  languages: ['JavaScript', 'SQL'],
};

const { username, lastname, ...otherInformation } = person;
console.log(otherInformation); // Lucas Bittencourt
console.log(username, lastname); // { age: 23, languages: ['JavaScript', 'SQL'] }

Utilizando Rest operator em parâmetros de função:

function removeObjectFields({ name, lastname, ...otherInformation }) {
  return otherInformatio;
}

const myLiteralObject = { name: 'Lucas', lastname: 'Bittencourt', age: 23 };
console.log(removeObjectFields(myLiteralOject)); // { age: 23 }

function removeNumbers([, , , ...otherNumbers]) {
  return otherNumbers;
}

console.log(removeNumbers([1, 2, 3, 4, 5, 6])); // [4, 5, 6]

Rest parameters

A sintaxe do Rest parameters permite uma função aceitar um número indeterminado de parâmetros que é transformado em um array dentro da função, sendo uma maneira de representar variadic functions dentro do JavaScript.

function sum(...numbers) {
  let total = 0;

  for (let i = 0; i < numbers.length; i++) {
    total += numbers[i];
  }

  return total;
}

console.log(sum(1, 2, 3, 4, 5)); // 15

Não podemos criar mais parâmetros depois do rest parameter. Este deve ser o último, ou único parâmetro:

function sum(...numbers, ...moreNumbers) { } // Uncaught SyntaxError: Rest parameter must be last formal parameter

function sum(number1, number2, ...numbers) { } // OK

Pela flexibilidade do JavaScript, conseguimos mesclar essas 3 features: Rest parameters, destructuring e rest operator:

function sum(...[a, b, c, ...otherNumbers]) {
  let total = a + b + c;

  for (let i = 0; i < otherNumbers.length; i++) {
    total += otherNumbers[i];
  }

  return total;
}

console.log(sum(1, 2, 3, 4, 5)); // 15

Default function parameters

Default function parameters permite parâmetros nomeados serem inicializados com um valor padrão caso um undefined ou nenhum valor seja passado.

function getUsername(name = 'Lucas Bittencourt') {
  return name;
}

console.log(getUsername('lucasgdb')); // lucasgdb

console.log(getUsername()); // 'Lucas Bittencourt'
console.log(getUsername(undefined)); // 'Lucas Bittencourt'

const nullishReference = undefined;
console.log(getUsername(nullishReference)); // 'Lucas Bittencourt'

console.log(getUsername(null)); // null

Utilizando objeto literal como parâmetro padrão:

function getUsername(user = { username: 'Lucas Bittencourt' }) {
  return user.username;
}

console.log(getUsername({ username: 'lucasgdb' })); // 'lucasgdb'
console.log(getUsername()); // 'Lucas Bittencourt'
console.log(getUsername({})); // undefined

Conseguimos utilizar destructuring + rest parameters:

function getUsername({ username } = { username: 'Lucas Bittencourt' }) {
  return username;
}

console.log(getUsername({ username: 'lucasgdb' })); // 'lucasgdb'
console.log(getUsername()); // 'Lucas Bittencourt'
console.log(getUsername({})); // undefined

Conseguimos utilizar destructuring + default destructuring values + rest parameters:

function getUsername({ username = 'lucasgdb' } = { username: 'Lucas Bittencourt' }) {
  return username;
}

console.log(getUsername({ username: 'Lucas' })); // 'Lucas'

// Caso o parâmetro seja undefined
console.log(getUsername()); // 'Lucas Bittencourt'

// Caso o username seja undefined
console.log(getUsername({ })); // 'lucasgdb'

Não é possível usar o default parameters junto com rest parameters:

function sum(...numbers = [1, 2, 3]) { } // Uncaught SyntaxError: Rest parameter may not have a default initializer

Arrow functions expressions

Arrow functions são funções compactas, comparadas às funções comuns.

const getUsername = (name) => {
  return name.trim();
};

const getUsername = (name, lastname) => {
  return `${name.trim()} ${lastname.trim()}`;
};

Caso a arrow function tenha apenas 1 parâmetro, torna-se opcional os parênteses:

const getUsername = name => {
  return name.trim();
};

Caso a arrow function tenha apenas uma linha de retorno, torna-se opcional as chaves + return:

const getUsername = name => name.trim();

Ao retornar apenas um objeto literal, colocamos em volta parênteses:

const getUserInfo = name => ({
  name: name.trim(),
});

Ao retornar apenas um array literal, não precisa colocar parênteses em volta:

const getNumbers = () => [1, 2, 3];

Para criar uma arrow function assíncrona, a keyword async deve estar antes dos parâmetros:

const getUsername = async (name) => name.trim();
const getUsername = async name => name.trim();
const getUsername = async () => 'Lucas Bittencourt';

Arrow functions possui diferenças semânticas e limitadas, comparadas com funções comuns. Veja aqui

async/await

No JavaScript moderno, temos uma syntactic sugar para Promises, que substitui o .then:

// Antes
getUser().then((user) => console.log(user));

// Depois
async function handleGetUser() {
  const user = await getUser();
  console.log(user);
}

Agora, vamos imaginar um cenário maior, com mais chamadas de funçõs assíncronas:

// Antes
getUser().then((user) => {
  changeUserPassword(user.id, 'new-password').then(() => {
    sendEmail(user.email, 'success').then((result) => {
      if (result.success) {
        console.log('OK');
        return;
      }

      console.error(result.error);
    });
  });
});

// Depois
async function handleUser() {
  const user = await getUser();

  await changeUserPassword(user.id, 'new-password');

  const { success, error } = await sendEmail(user.email, 'success');

  if (success) {
    console.log('OK');
    return;
  }

  console.error(error);
}

top-level await

Podemos usar o await sem depender de uma função assíncrona:

const username = await Promise.resolve('Lucas Bittencourt');
console.log(username); // 'Lucas Bittencourt'

try/catch/finally

Usando os conhecimentos aprendidos na seção async/await, vamos aprofundar e colocar tratamento de erros. try/catch/finally é uma versão mais moderna do .catch().finally()

try: Vai executar o código normalmente

catch: Vai executar caso algum erro seja lançado dentro do try. (Opcional)

finally: Vai executar independente se deu erro ou não, no final. (Opcional)

// Antes
getUser()
  .then((user) => console.log(user))
  .catch((error) => console.error(error))
  .finally(() => console.info('Código executado!'));

// Depois
async function handleGetUser() {
  try {
    const user = await getUser();
    console.log(user);
  } catch (error) {
    console.error(error);
  } finally {
    console.info('Código executado!');
  }
}