JavaScript moderno

Lucas Bittencourt / fevereiro 03, 2023
• 15min de leitura

Índices
- Introdução
- Falsy, Truthy & Nullish values
- Numeric separators
- Template literals
- Shorthand property names
- Operador lógico AND
- Operador lógico OR
- Nullish Coalescing operator
- Logical Assignment operator
- Optional Chaining
- Destructuring assignment
- Rest & Spread operators
- Default function parameters
- Arrow functions expressions
- async/await
- try/catch/finally
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!');
}
}