ES11 - Novas features do JavaScript

Neste post vamos ficar por dentro das novidades do ES11 (ECMAScript 2020) no JavaScript.

Se quiser ver o que rolou nas versões anteriores recomendo a leitura dos posts: ES07, ES08, ES09 e ES10.

Tópicos

String.matchAll()

Foi incluído na String o método matchAll(), que retorna um iterator em cada valor que fez match, ou seja, cada valor que foi encontrado através da regex.

Sintaxe: String.matchAll(regex)

Link para testar a Regex.

Exemplo:

const frase = "O aprendizado é contínuo e sempre haverá um próximo nível! 🚀";
const regex = /[^\\s]+/g; 
// pega todas as palavras separadas por espaço em branco

// resultado da regex aplicado na frase será equivalente a um 
// frase.split(" "); - só para deixar mais claro a regex

console.log(frase.matchAll(regex));
// Resultado: Object [RegExp String Iterator] {}

for (const match of frase.matchAll(regex)) {
  console.log(match[0]);
}

/** 
Usando desestruturação
for (const [value] of frase.matchAll(regex)) {
  console.log(value);
}
*/

// Resultado:

/**
O
aprendizado
é
contínuo
e
sempre
haverá
um
próximo
nível!
🚀
*/

// pedaço por pedaço
const chunkByChunk = frase.matchAll(regex);

// Acessando cada valor usando o método next() do Objeto Iterator
console.log(chunkByChunk.next().value[0]);
console.log(chunkByChunk.next().value[0]);
console.log(chunkByChunk.next().value[0]);

/**
O
aprendizado
é
*/

Dynamic Import

Importação dinâmica. É uma nova maneira de fazer importação no JavaScript, a palavra reservada import pode ser chamada como uma função que irá importar dinamicamente um módulo — ela irá retornar uma promise.

Como retorna uma promise, podemos usar async/await para deixar o código mais agradável:

import('/pages/product.js')
  .then((module) => {
    // Utilizando o módulo importado
  });

// Com async/await
const module = await import('/pages/products.js');

Com a importação dinâmica as páginas da web podem ficar mais rápidas e utilizar os recursos quando necessários.

Importação estática é a maneira convencional e amplamente conhecida, o recurso já fica disponível em tempo de execução, com importação dinâmica o recurso é carregado quando a promise é resolvida. Essa é uma técnica avançada que pode ser utilizada para fazer code splitting no React.

Um bom caso de uso é utilizar em Menus da aplicação, ao invés de carregar todo o conteúdo que cada item do menu tem, carrega apenas quando clicar em algum link.

O Gatsby faz o pre-fetch quando faz um hover (passar o mouse em cima do link) no item do menu, assim, quando o usuário clica, redireciona para página e o conteúdo que foi importado dinamicamente já estará disponível.

Esse cenário explicado acima, também é conhecido como lazy-loading modules em navegações na Single Page Application (SPA).

Importação dinâmica não veio para substituir a importação estática, cada um tem seu caso de uso. Para alguns exemplos mais avançados leia aqui.

BigInt

É o sétimo tipo de dados primitivo numérico na linguagem, representa números inteiros. Útil para armazenar, de maneira segura, números muito grandes, além do valor limite seguro dos inteiros – Number.MAX_SAFE_INTEGER.

Criando um BitIng:

const bigOne = 1n; // log: 1n
Const bigAnotherOne = BigInt(1) // log: 1n

console.log(12372163778217893721893721812n)
// 12372163778217893721893721812n
console.log(Number.MAX_SAFE_INTEGER);
// 9007199254740991

const nrMuitoGrande = BigInt(Number.MAX_SAFE_INTEGER);

console.log(nrMuitoGrande);
// 9007199254740991n

Podemos utilizar os operadores matemáticos: + - * ** / .

const bigFour = 4n;
const bigFive = 5n;

let bigCalc = bigFour * bigFive;

console.log(bigCalc); // 20n

Veja outros tipos de dados tipos de dados no JavaScript.

Promise.allSettled()

Método que recebe um array de promises como parâmetro e após a sua execução retorna um array de objeto com o seguinte formato: [ { status: 'fulfilled' | 'rejected', value: Object }, {...}, {...} ]  para cada promise resolvida ou rejeitada. Sendo o status uma String com status resolvida ou rejeitada e value é o valor que a promise resolveu.

Retorna um objeto com status e o value:

// objeto: { status: status: 'fulfilled' | 'rejected', value: Object , value: Object }.

A diferença entre Promise.allSettled() e Promise.all(): o segundo é resolvido apenas se todas as promessas forem resolvidas com êxito e sem erros. Se alguma promessa for rejeitada, entrará no catch e as outras não serão resolvidas. Enquanto a primeira função resolve todas as promises e mostra o status de cada uma independente de ter sido resolvida ou rejeitada.

Promise.allSettled() verifica se todas as promessas que informamos foram cumpridas, não é à toa esse nome: AllSettled = tudo resolvido!

const promises = [
  fetch("https://api.github.com/users/tgmarinho"), // fulfilled
  fetch("https://api.github.com/users/NO_EXIST"), // fulfilled
  fetch("opsErrado://api.github.com/users/tgmarinho") // rejected
];

Promise.allSettled(promises).then(async (data) => {
  console.log("Promise allSettled");

  for await (let obj of data) {
    if (obj.status === "fulfilled") {
      let user = await obj.value.json();
      console.log("login: ", user?.login);
    }
  }
});

// Diferença com Promise.all()
Promise.all(promisesAll)
  .then(async (data) => {
    for await (let obj of data) {
      let user = await obj.json();
      console.log("login: ", user?.login);
    }
  })
  .catch((error) => {
    console.log("PromiseALL");
    console.log(error);
  });

Caso queria testar o código, clique aqui.

globalThis

Assim como o Java, JavaScript está em bilhões de dispositivos e é executado em vários ambientes — Navegador (Browser), Desktop (Electron), Node.js, Mobile (React Native). Pensando nisso foi introduzido o objeto global: globalThis que funciona em todos esses ambientes de maneira genérica.

console.log(globalThis); // window se navegador ou global no Node.js

// posso definir uma variável global username dentro do globalThis.
globalThis.username = "rocketseat";

console.log(globalThis.username);
// rocketseat

Sem o globalThis tínhamos essa solução:

const getGlobal = function () {
  if (typeof self !== "undefined") {
    return self;
  }
  if (typeof window !== "undefined") {
    return window;
  }
  if (typeof global !== "undefined") {
    return global;
  }
  throw new Error("unable to locate global object");
};

const globals = getGlobal();

console.log({ globals }); 
// irá obter a variável dependendo do seu ambiente

Como podemos ver, havia uma maneira para acessar a variável global em diferentes ambientes, mas não era uma boa solução. Com globalThis disponível essa solução anterior não é mais necessária.

Acesse a variável global do seu ambiente:

console.log(globalThis);

for-in mechanics

As especificações anteriores deixaram para trás alguns detalhes como, por exemplo, como deveria funcionar a ordem da iteração em propriedades de objetos e arrays no for-in, então cada browser implementou do seu jeito. Mudar a implementação em cada browser seria complicado, mas enfim chegaram em um consenso e agora está padronizado. Não há o que demostrar, só tem teoria mesmo.

O laço for...in  itera sobre propriedades de um objeto, na ordem original de inserção, se usado em um array ele trará o índice do array.

Optional Chaining

Trouxe uma melhoria e segurança no código com o novo operador ?.  nativo na linguagem que, ao invés de lançar erro Cannot read property of undefined em tempo de execução, atribui um undefined quando tentamos acessar alguma propriedade do objeto de maneira encadeada e que não existe.

Evita muitos ifs aninhados e ternários complexos de entender.

Imagine um cenário com um objeto aninhado e complexo que para acessar informações do pet de uma pessoa é preciso encadear (chain) cada objeto.

const entity = {
  person: {
    name: "thiago",
    pet: {
      name: "mel",
      breed: "lhasa",
      // disease: "alergia",
    },
    address: {
      street: "Rua da Galáxia, nr: Infintiy",
    },
  },
};

Se eu quiser saber se o pet tem doença, devo fazer algo assim:

// if tradicional
if (entity.person && entity.person.pet && entity.person.pet.disease) {
  console.log(entity.person.pet.disease); // não entra aqui
}

// Short Circuit
const petDisease1 = entity.person.pet && entity.person.pet.disease; // undefined

// Optional Chaining
const petDisease2 = entity.person?.pet?.disease; // undefined

Disease (doença) não existe no objeto pet.

Pode utilizar optional chaining para acessar valores da DOM:

const age = myForm.querySelector('input[name=age]')?.value

Podemos usar o operador em arrays e funções também:

  • Arrays:

Usando o operador de encadeamento opcional ?. quando tentarmos acessar algum índice do array para obter alguma propriedade ou invocar algum método receberemos: undefined.

const people = ["joe", "duo", "peter"];

console.log(people[0]?.toString()); // joe
console.log(people[5]?.toString()); // undefined
console.log(people[5].toString()); // TypeError: Cannot read property 'toString' of undefined

const people = [{ name: "thiago" }, { name: "robson" }];

console.log(people[1].name); // robson
console.log(people[2]?.name); // undefined
console.log(people[2].name); // TypeError: Cannot read property 'name' of undefined
  • Função:

Pensando em um caso hipotético em que tentamos acessar uma função chamada sendEmail através do objeto person, não será lançada uma exceção se sendEmail não estiver presente no objeto person. Por outro lado, se sendEmail for uma propriedade dentro de person e não uma função, aí sim será lançada uma exceção.

entity.person.sendEmail?.(); // undefined

Várias linguagens de programação já tem esse operador, e o TypeScript também.

Nullish Coalescing Operator

Quando tentamos acessar uma propriedade e ela é nula (null) ou indefinida (undefined), podemos precisar de um valor padrão (default).

Hoje podemos fazer isso usando Logical OR ||

const name = user.name || "Joe Due"

O operador  || funciona bem com valores null e undefined, mas ele não lida bem com outros valores falsy.

E nessa versão do JavaScript foi introduzido o operador ?? — uma maneira mais eficiente de comparar valores falsy.

A diferença fica bem clara nesse exemplo:

const response = {
  settings: {
    headerText: "",
	  animationDuration: 0,
    showSplashScreen: false,
  },
};

// Operador ||
console.log(response.settings.headerText || "Hello, world!"); // Hello, word!
console.log(response.settings.animationDuration || 300); // 300
console.log(response.settings.showSplashScreen || true); // true

// Operador ??
console.log(response.settings.headerText ?? "Hello, world!"); // "" 
console.log(response.settings.animationDuration ?? 300); // 0 
console.log(response.settings.showSplashScreen ?? true); // false

Se a expressão do lado esquerdo do operador ?? é avaliado como undefined ou null , seu lado direito é retornado.

Então sempre que for lidar com valores falsy é melhor usar ??.

Várias linguagens de programação já tem esse operador, e o TypeScript também.

import.meta

Expõe metadados específicos do contexto de um módulo JavaScript, contendo informações sobre o módulo, por exemplo: a URL de onde está o arquivo no host.

Sintaxe:

import.meta

Para testar precisamos criar um módulo ou vamos obter um erro:

// Executando em um arquivo JS com Node.js sem estar em um módulo
console.log(import.meta);
                   ^^^^

SyntaxError: Cannot use 'import.meta' outside a module

Para criar um módulo, crie um projeto node com npm init -y e adicione a propriedade "type: "module" no package.json:

package.json:

{
  "name": "javascript",
  "version": "1.0.0",
  "description": "",
  "type": "module",
  "main": "index.js",
  "scripts": {
    "start": "node ."
  },
  "license": "MIT"
}

Depois crie o arquivo  calc.js  no mesmo local que foi criado o package.json, esse arquivo será importado no arquivo index.js que será criado logo em seguida.

calc.js:

console.log(import.meta.url);
import.meta.meuMetaDado = "teste";
console.log(import.meta.meuMetaDado);

export default {
  sum: (a, b) => a + b,
};

Por fim, crie o arquivo index.js:

import calc from "./calc.js";

console.log(calc.sum(1, 2));

console.log(import.meta.url);

Pronto, executando o projeto obtemos o seguinte resultado: npm start

❯ npm start  

> javascript@1.0.0 start /Users/tgmarinho/Developer/javascript
> node .

file:///Users/tgmarinho/Developer/javascript/calc.js
teste
3
file:///Users/tgmarinho/Developer/javascript/index.js

Observe que obtemos a URL de onde cada módulo se encontra no host, também podemos criar novas variáveis dentro do import.meta e atribuir valor.

Para outros casos de uso, confira acessando esse site.

Conclusão

Com essa versão o JavaScript evolui bem. Destacamos o globalThis que, embora não seja muito usado, é uma solução bacana. BigInt é muito importante, ainda mais para aplicações que envolvem cálculos. Os operadores Optional Chaining e Nullish Coalescing agregam valor para a linguagem, embora muitos estejam adotando o TypeScript, que já inclui essas duas técnicas. Promise.allSettled pode ser interessante para lidar com promises independente se elas são resolvidas ou rejeitadas, uma boa alternativa ao Promise.all, que como vimos cancela a execução se alguma promise for rejeitada no meio do caminho.

Private Methods não está finalizada, então não abordamos essa feature embora já esteja implementada no Node.js e Chrome.

O TC39, comitê técnico que mantém o JavaScript, propôs lançar novas atualizações da linguagem a cada ano. A próxima versão do JavaScript vai ser o ES12 (EcmaScript 2021), que será publicada em 2021, se você estiver na curiosidade para saber o que será lançado, pode ver aqui:

Atualizações da linguagem JavaScript para 2021

Propostas do Comitê Técnico 39 - ECMAScript | TC39:

Fontes:

E aí, o que achou do post?

Espero que tenha curtido! 💜

O aprendizado é contínuo e sempre haverá um próximo nível! 🚀