A especificação ECMAScript é como um retrato da linguagem JavaScript que é repintada a cada ano. Como é típico do JavaScript moderno, as especificações e a prática do mundo real movem-se em conjunto. A versão mais recente da especificação, ECMAScript 2024, inclui sete novos recursos JavaScript e deverá ser finalizada em junho. Este artigo apresenta quatro dos novos recursos que já estão disponíveis em navegadores e ambientes de servidor e prontos para uso hoje:

  • Promessa.comResolvers é um mecanismo poderoso para gerenciar operações assíncronas quando é necessário controle externo sobre resolução e rejeição.
  • Object.groupBy e Map.groupBy permitem organizar coleções com base em propriedades principais.
  • Atômicos.waitAsync facilita a comunicação segura e a sincronização entre threads de trabalho.
  • String.isWellFormed e String.toWellFormed adicione ferramentas valiosas para lidar com entradas do usuário e dados de rede.

Promessa.comResolvers

Vamos começar com o novo método estático em Promisechamado withResolvers(). As promessas do JavaScript nos dão várias maneiras de lidar com operações assíncronas. O withResolvers() método é usado para criar as três partes de um Promise: o Promise ele mesmo e o resolve() e reject() funções.

O benefício de withResolvers() é que ele cria todos os três como referências expostas externamente. Nos casos em que se deseja criar uma promessa, e também ter acesso à resolução e rejeição da promessa a partir de código externo, este é o método a utilizar.

A especificação em si é caracteristicamente espartana em sua descrição. A documentação do Mozilla fornece mais detalhes. A principal lição é que withResolvers() é equivalente a:


let resolve, reject;
const promise = new Promise((res, rej) => {
  resolve = res;
  reject = rej;
});
// use resolve and reject to control the promise

No trecho acima, declaramos o resolve e reject referências no escopo anexo e, em seguida, use-as dentro do corpo do Promise retorno de chamada para consultar o resolve e reject argumentos. Dessa forma, estamos controlando o retorno de chamada da promessa fora do próprio retorno de chamada.

O Promise.withResolvers() a sintaxe é mais compacta e não precisamos declarar o resolve e reject separadamente. Com este método, poderíamos escrever exatamente a mesma funcionalidade acima, assim:


let { promise, resolve, reject } = Promise.withResolvers();

A essência desse recurso é que você o utiliza quando precisa de acesso externo a resolve() e reject(). Este não é um cenário muito comum, mas acontece. Vamos considerar um exemplo simples:


function race(operation1, operation2) {
  const { racePromise, resolve, reject } = Promise.withResolvers();

  operation1().then(resolve).catch(reject);
  operation2().then(resolve).catch(reject);

  return racePromise;
}

function fetchData1() {
  return new Promise((resolve) => {
    setTimeout(() => resolve("Data from source 1"), 1000);
  });
}

function fetchData2() {
  return new Promise((resolve) => {
    setTimeout(() => resolve("Data from source 2"), 500);
  });
}

race(fetchData1, fetchData2)
  .then((data) => console.log("Winner:", data))
  .catch((error) => console.error("Error:", error));

Aqui, temos duas operações, fetchData1() e fetchData2(), esse retorno promete. Eles executam tempos limite e fetchData2() é sempre mais rápido em 500 milissegundos. Nós usamos withResolvers() dentro de race() função para expor o resolve e reject funções. Essas funções são então usadas por uma nova promessa, chamada racePromise.

Usamos então o resolve e reject funções para responder aos dois fetchData operações. Isso é interessante porque você pode ver que nem sequer fornecemos um retorno de chamada para racePromise. Em vez disso, controlamos a promessa externamente. Usamos esse controle para vincular o resultado das outras duas promessas ao racePromise.

Este é um exemplo inventado e um tanto irrealista porque as duas promessas das corridas não começam ao mesmo tempo. O objetivo é mostrar a essência de como e quando usar withResolvers().

Object.groupBy e Map.groupBy

O prático método groupBy é uma maneira rápida de organizar coleções com base em um valor de string. É um método estático em Object e Map, que funciona em uma coleção semelhante a um array. O groupBy() O método recebe dois argumentos: a coleção e um retorno de chamada que opera nela. Você recebe de volta uma nova instância de objeto que possui valores de rótulo de string como chaves e os elementos de matriz correspondentes como valor.

Portanto, quando você tem um array e precisa dividir os elementos em grupos rotulados por strings de acordo com alguns critérios internos, este é o método a ser usado.

Isso é algo bastante comum que surge na codificação do dia-a-dia. Olhar para um exemplo deve deixar isso claro. Digamos que temos uma coleção de raças de cães e seus tamanhos:


const dogBreeds = (
  { breed: 'Shih Tzu', size: 'Toy' },
  { breed: 'Leonberger', size: 'Giant' },
  { breed: 'King Charles Spaniel', size: 'Toy' },
  { breed: 'Great Pyrenees', size: 'Giant' },
  { breed: 'Corgi', size: 'Small' },
  { breed: 'German Shepherd', size: 'Large' },
);

Agora, digamos que queremos organizar esta coleção por tamanho. Queremos terminar com uma coleção de objetos onde as chaves são o tamanho da raça e os valores são a raça do cachorro. Normalmente, escreveríamos um loop para fazer isso, mas é um pouco complicado; parece que deveria haver uma maneira melhor. Agora com groupBy()há:


groupBy() is that better way:

Object.groupBy(dogBreeds, (x) => {
    return x.size;
})

This gives us output like so:

{ "Toy": (
    { "breed": "Shih Tzu", "size": "Toy" },
    { "breed": "King Charles Spaniel", "size": "Toy" }
  ),
  "Giant": (
    { "breed": "Leonberger", "size": "Giant" },
    { "breed": "Great Pyrenees", "size": "Giant" }
  ),
  "Small": (
    { "breed": "Corgi", "size": "Small" }
  ),
  "Large": (
    { "breed": "German Shepherd", "size": "Large" }
  )
}

Isso oferece uma maneira simples e funcional de agrupar coleções de objetos com base em alguma propriedade dos próprios objetos.

O groupBy() O método pega tudo o que é retornado pelo retorno de chamada e coleta automaticamente todos os elementos que são iguais de acordo com String equality. Se o valor de retorno não for uma string, ele será convertido em uma string. Se não puder ser coagido, ocorrerá um erro.

Atômicos.waitAsync

O novo Atomics.waitAsync() O método foi projetado para compartilhar dados entre threads de trabalho com segurança. Não bloqueia o thread principal como Atomics.wait() faz. Por ser usado entre threads, ele depende do SharedArrayBuffer aula.

Esta classe está desabilitada por padrão em navegadores modernos, a menos que os requisitos de segurança sejam atendidos. No Node.js, entretanto, a classe está habilitada por padrão.

Aqui está um exemplo simples de uso no Node. Observe que as importações são integradas ao Node, portanto, nenhuma instalação do NPM é necessária (mas observe que o import afirmação é):


// asyncWait.js
const { Worker, isMainThread, parentPort } = require('worker_threads');

const sharedBuffer = new SharedArrayBuffer(4); 
const int32Array = new Int32Array(sharedBuffer);

if (isMainThread) {
  const worker = new Worker(__filename);

  async function waitForMessage() {
    const initialValue = 0;
    const result = await Atomics.waitAsync(int32Array, 0, initialValue);
    if (result.async) {
      console.log("Message received from worker:", int32Array(0));
    } else {
      console.error("Timeout waiting for worker message");
    }
  }

  waitForMessage(); 
} else {
  setTimeout(() => {
    Atomics.store(int32Array, 0, 1);
  }, 2000); 
}

Para executar este programa, basta digitar: $ node asyncWait.js

O programa declara um SharedArrayBuffer (enrolado em um int32Array) e então verifica se estamos no thread principal. Se for o thread principal, geramos um thread de trabalho. (Se você é novo em threads de trabalho, aqui está uma boa introdução.)

O thread principal espera por uma atualização do trabalhador através do Atomics.waitAsync() chamar. Os três argumentos para waitAsync(1, 2, 3) são:

  1. Matriz compartilhada (int32Array): O espaço de memória compartilhada.
  2. Elemento a ser observado (0): O índice da matriz a ser aguardada.
  3. Valor inicial (initialValue = 0): notificar o thread principal apenas se esse valor for diferente (ou seja, se o trabalhador definir o valor para o valor inicial de 0, uma notificação não ocorrerá).

String.isWellFormed e String.toWellFormed

Entrada do usuário, dados incorretos e falhas de rede são fontes de strings malformadas. String.isWellFormed é uma verificação de sanidade. Ele determina se uma string é válida em UTF-16. UTF-16 é a codificação que o próprio JavaScript usa, então String.isWellFormed() garante que uma determinada string seja algo que o JavaScript possa manipular:


const string1 = "Hello, InfoWorld!";

const string2 = "Hello, uD800world!";

console.log(string1.isWellFormed()); // Output: true (well-formed)
console.log(string2.isWellFormed()); // Output: false (malformed)

Você pode aprender mais sobre o que constitui strings válidas em JavaScript nesta seção da referência do Mozilla. Os bandidos na codificação UTF-16 são conhecidos como “substitutos solitários”.

O parceiro ativo para String.isWellFormed, String.toWellFormed transforma uma determinada string em algo válido. Quaisquer substitutos isolados encontrados serão substituídos por U+FFFD, o caractere de ponto de interrogação de losango preto: �.


"Hello, uD800world!".toWellFormed()
// outputs: 'Hello, �world!'

Conclusão

Temos uma bela coleção de novos recursos no ECMAScript 2024. Promise.withResolvers() e Atomics.waitAsync() são casos de uso mais avançados, enquanto groupBy é uma adição conveniente que muitas vezes é útil, e os novos métodos de string são perfeitos para determinadas situações. Todos esses recursos são compatíveis com JavaScript em navegadores e ambientes de servidor, portanto você pode começar a usá-los hoje mesmo.