Mais um ano e mais uma versão do nosso amado ECMAScript. Para quem não sabe, o ECMA é a especificação principal sobre a qual o JavaScript é baseado. Ele sofre modificações todos os anos, com pequenas alterações sendo lançadas durante os meses e mudanças maiores ao longo dos anos.

Nesse vídeo eu explico um pouco mais sobre o processo de lançamento de novas funcionalidades do JavaScript, se você ainda não assistiu, eu recomendo fortemente para poder entender melhor como tudo funciona!

A maioria dessas modificações ficam em notas do TC39, o technical committee #39, que é o comitê técnico que avalia e discute sobre o futuro da linguagem especificamente. Essas notas são abertas e você pode ver todas elas no repositório oficial do comitê.

Assim como eu faço com outras tecnologias, o JS não é diferente! Em 2022 publiquei um artigo sobre as novidades daquele ano e ainda me atrevi a prever o que viria este ano, será que eu acertei?

Array.findLast

Uma das propostas que estavam abertas no TC39 era a capacidade de inverter a direção inicial da busca em um Array. Hoje, os métodos find e findIndex vão sempre começar buscando um array pelo seu primeiro elemento (que é o 0, como deve ser).

Porém, no caso de arrays ordenados, ter a capacidade de buscar ao contrário -  começando da cauda para a cabeça - é muito mais rápido e eficiente. Mas ai você pode argumentar: "Poxa, mas não é só inverter o array primeiro e depois usar o find?", basicamente sim, mas não.

Quando invertemos o array usando o método reverse, vamos executar uma operação sobre ele que, invariavelmente vai iterar por metade dos itens desse array. Hoje esse método é bastante rápido, a especificação diz que ele deve funcionar dessa forma:

function reverse(array) {
  let len = array.length
  let middle = Math.floor(len / 2)
  let lower = 0

  while (lower !== middle) {
    let lowerVal
    let upperVal

    let upper = len - lower - 1
    let upperP = upper.toString()
    let lowerP = lower.toString()

    let lowerExists = array.hasOwnProperty(lowerP)
    if (lowerExists) lowerVal = array[lowerP]

    let upperExists = array.hasOwnProperty(upperP)
    if (upperExists) upperVal = array[upperP]

    if (lowerExists && upperExists) {
      array[lowerP] = upperVal
      array[upperP] = lowerVal
    } else if (!lowerExists && upperExists) {
      array[lowerP] = upperVal
      delete array[upperP]
    } else if (lowerExists && !upperExists) {
      delete array[lowerP]
      array[upperP] = lowerVal
    } else {
      if (lowerExists || upperExists) throw new Error('This should never happen')
    }
    lower++
  }
  return array
}
Essa implementação foi tirada diretamente da especificação da ECMA262, seção 23.1.3.26 e transformada de pseudo-código para JavaScript

Como você pode ver, a implementação começa do início e do final ao mesmo tempo, invertendo os lugares de cada posição até, finalmente, os dois ponteiros se encontrarem no meio. Você pode imaginar que ele não é tão rápido, mas vamos fazer um teste com essa função:

for (let i = 1; i <= 1_000_0000; i = i * 10) {
  console.log(`i: ${i}`)
  let a = [...Array(i).keys()]
  console.time('reverse')
  reverse(a)
  console.timeEnd('reverse')
}

A nossa saída será algo assim (reduzido para caber):

i: 1 -> reverse: 0.006ms 
i: 10 -> reverse: 0.046ms 
i: 100 -> reverse: 0.392ms 
i: 1000 -> reverse: 2.335ms 
i: 10000 -> reverse: 4.026ms 
i: 100000 -> reverse: 68.4ms 
i: 1000000 -> reverse: 150.565ms 
i: 10000000 -> reverse: 1.542s 

Veja que ele é exponencial. Claro que, nas implementações modernas, o reverse é muito mais otimizado e funciona muito mais rápido, mas o ponto é: Vamos ter um overhead de processamento.

A nova proposta cria dois métodos novos, o findLast e findLastIndex que fazem exatamente o mesmo que os outros métodos originais, mas começando do final. Evitando que tenhamos que inverter o array. Veja um exemplo:

const isEven = (number) => number % 2 === 0;
const numbers = [1, 2, 3, 4];

// Método existente
console.log(numbers.find(isEven)); // 2
console.log(numbers.findIndex(isEven)); // 1

// novo método
console.log(numbers.findLast(isEven)); // 4
console.log(numbers.findLastIndex(isEven)); // 3

Uso de Hashbang

A gramática conhecida como hashbang (ou shebang) é a famosa sequência de caracteres que começa com #! no início de um script. O que ela faz é definir qual vai ser o interpretador que aquele script vai usar para ser executado.

Hoje isso já é possível com o Node através de scripts como:

#!/usr/bin/env/node

console.log('Hello')

Por baixo dos panos, o seu shell vai remover a primeira linha e passar o resultado da leitura do arquivo para o interpretador que você selecionou, no caso é o resultado do comando env node, que é o local do executável do Node.js.

A proposta não altera o comportamento, simplesmente cria um padrão sobre como isso deve ser feito em cada local.

Array por cópia

Essa é uma das principais features – inclusive uma das que eu previ que iriam aparecer no JS esse ano – e uma das mais pedidas. Como vimos antes, o método reverse vai fazer uma substituição chamada in place, ou seja, ele vai substituir os elementos do mesmo array que foi passado, retornando a mesma referência. Essencialmente alterando o objeto original, e isso não é legal.

Para contornar esse tipo de comportamento, o que geralmente fazemos é algo assim:

const original = [1,2,3]
const novo = [...original]
console.log(novo.reverse()) // [3, 2, 1]
console.log(original) // [1, 2, 3]

A proposta adiciona quatro novos métodos ao Array.prototype, todos sendo uma variação dos métodos originais reverse, sort e splice, mas também criando um novo método chamado with, que retorna um novo array com um novo elemento em uma posição específica sendo alterada por outro valor, o que evita que façamos modificações também in place usando a famosa notação a[0] = 1.

Os novos métodos são chamados toReversed, toSorted e toSpliced (e, é claro, with).

Array.prototype.toReversed()

const original = [1, 2, 3, 4];
const reversed = original.toReversed();

console.log(original);
// [ 1, 2, 3, 4 ]

console.log(reversed);
// [ 4, 3, 2, 1 ]

Array.prototype.toSorted()

const original = [1, 3, 2, 4];
const sorted = original.toSorted();

console.log(original);
// [ 1, 3, 2, 4 ]

console.log(sorted);
// [ 1, 2, 3, 4 ]

Array.prototype.toSpliced()

const original = [1, 4];
const spliced = original.toSpliced(1, 0, 2, 3);

console.log(original);
// [ 1, 4 ]

console.log(spliced);
// [ 1, 2, 3, 4 ]

Array.prototype.with()

const original = [1, 2, 2, 4];
const withThree = original.with(2, 3);

console.log(original);
// [ 1, 2, 2, 4 ]

console.log(withThree);
// [ 1, 2, 3, 4 ]

WeakMaps com Symbols

Essa é uma proposta bastante complicada de explicar, até porque ela é um caso muito específico de outros casos bastante específicos... Mas, em suma, o que acontece é que agora a especificação permite o uso de Symbols como sendo chaves para WeakMaps.

Não vou entrar em detalhes sobre WeakMaps aqui, mas você pode ver mais sobre os usos reais nessa resposta do StackOverflow

Anteriormente, apenas objetos eram permitidos como chaves, porém, outro caso onde temos uma variável única que não pode ser recriada com o mesmo valor são os Symbols. Essa proposta faz com que WeakMaps aceitem esses valores como chaves.

const weak = new WeakMap();
const key = Symbol("ref");
weak.set(key, "ECMAScript 2023");

console.log(weak.get(key));
// ECMAScript 2023

Conclusão

Muitas das novidades que eu previ no último artigo acabaram sendo promovidas para um estágio superior, descartadas, algumas, inclusive, ganharam artigos próprios enquanto outras estão exatamente do jeito que estavam há um ano.

Essa atualização da especificação não toca muito no que a gente usa no dia-a-dia, mas promete ser uma grande melhora de qualidade de vida para quem está usando funcionalidades mais específicas que podem certamente ter um impacto grande em eficiência e performance.