5 amazing new JavaScript features in ES15 (2024)

Last updated on June 01, 2024
5 amazing new JavaScript features in ES15 (2024)

2024: Another incredible year of brand new JS feature upgrades with ES15.

From sophisticated async features to syntactic array sugar and modern regex, JavaScript coding is now easier and faster than ever.

1. Native array group-by is here

Object.groupBy():

const fruits = [
  { name: 'pineapple🍍', color: '🟡' },
  { name: 'apple🍎', color: '🔴' },
  { name: 'banana🍌', color: '🟡' },
  { name: 'strawberry🍓', color: '🔴' },
];

const groupedByColor = Object.groupBy(
  fruits,
  (fruit, index) => fruit.color
);

console.log(groupedByColor);

Literally the only thing keeping dinosaur Lodash alive -- no more!

I was expecting a new instance method like Array.prototype.groupBy but they made it static for whatever reason.

Then we have Map.groupBy to group with object keys:

const array = [1, 2, 3, 4, 5];

const odd  = { odd: true };
const even = { even: true };

Map.groupBy(array, (num, index) => {
  return num % 2 === 0 ? even: odd;
});

// =>  Map { {odd: true}: [1, 3, 5], {even: true}: [2, 4] }

Almost no one ever groups arrays this way though, so will probably be far less popular.

2. Resolve promise from outside -- modern way

With Promise.withResolvers().

It's very common to externally resolve promises and before we had to do it with a Deferred class:

class Deferred {
  constructor() {
    this.promise = new Promise((resolve, reject) => {
      this.resolve = resolve;
      this.reject = reject;
    });
  }
}

const deferred = new Deferred();

deferred.resolve();

Or install from NPM -- one more dependency.

But now with Promise.withResolvers():

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

See how I use it to rapidly promisify an event stream -- awaiting an observable:

// data-fetcher.js
// ...
const { promise, resolve, reject } = Promise.withResolvers();

export function startListening() {
  eventStream.on('data', (data) => {
    resolve(data);
  });
}

export async function getData() {
  return await promise;
}

// client.js
import { startListening, getData } from './data-fetcher.js';

startListening();

// ✅ listen for single stream event
const data = await getData();

3. Buffer performance upgrades

Buffers are tiny data stores to store temporary data your app generates.

They make it incredible easy to transfer and process data across various stages in a pipeline.

Pipelines like:

  • File processing: Input file buffer process new buffer output file
  • Video streaming: Network response buffer display video frame
  • Restaurant queues: Receive customer queue/buffer serve customer
const fs = require('fs');
const { Transform } = require('stream');

const inputFile = 'input.txt';
const outputFile = 'output.txt';

const inputStream = fs.createReadStream(inputFile, 'utf-8');

const transformStream = new Transform({
  transform(chunk) {
    // ✅ tranform chunks from buffer
  },
});

const outputStream = fs.createWriteStream(outputFile);

// ✅ start pipeline
inputStream.pipe(transformStream).pipe(outputStream);

With buffers, each stage process data at different speeds independent of each other.

But what happens when the data moving through the pipeline exceeds the buffers capacity?

Before we'd have to copy all the current data's buffer to a bigger buffer.

Terrible for performance, especially when there's gonna be a LOT of data in the pipeline.

ES15 gives us a solution to this problem: Resizable array buffers.

const resizableBuffer = new ArrayBuffer(1024, {
  maxByteLength: 1024 ** 2,
});

// ✅ resize to 2048 bytes
resizableBuffer.resize(1024 * 2);

4. Asynchronous upgrades

Atomics.waitAsync(): Another powerful async coding feature in ES2024:

It's when 2 agents share a buffer...

And agent 1 "sleeps" and waits for agent 2 to complete a task.

When agent 2 is done, it notifies using the shared buffer as a channel.

const sharedBuffer = new SharedArrayBuffer(4096);

const bufferLocation = new Int32Array(sharedBuffer);

// ✅ initial value at buffer location
bufferLocation[37] = 0x1330;

async function doStuff() {
  // ✅ agent 1: wait on shared buffer location until notify
  Atomics.waitAsync(bufferLocation, 37, 0x1330).then((r) => {} /* handle arrival */);
}

function asyncTask() {
  // ✅ agent 2: notify on shared buffer location
  const bufferLocation = new Int32Array(sharedBuffer);
  Atomics.notify(bufferLocation, 37);
}

You'd be absolutely right if you thought this similar to normal async/await.

But the biggest difference: The 2 agents can exist in completely different code contexts -- they only need access to the same buffer.

And: multiple agents can access or wait on the shared buffer at different times -- and any one of them can notify to "wake up" all the others.

It's like a P2P network. async/await is like client-server request-response.

const sharedBuffer = new SharedArrayBuffer(4096);

const bufferLocation = new Int32Array(sharedBuffer);

bufferLocation[37] = 0x1330;

// ✅ received shared buffer from postMessage()
const code = `
var ia = null;
onmessage = function (ev) {
  if (!ia) {
    postMessage("Aux worker is running");
    ia = new Int32Array(ev.data);
  }
  postMessage("Aux worker is sleeping for a little bit");
  setTimeout(function () { postMessage("Aux worker is waking"); Atomics.notify(ia, 37); }, 1000);
}`;

async function doStuff() {
  // ✅ agent 1: exists in a Worker context
  const worker = new Worker(
    'data:application/javascript,' + encodeURIComponent(code)
  );
  worker.onmessage = (event) => {
    /* log event */
  };
  worker.postMessage(sharedBuffer);
  Atomics.waitAsync(bufferLocation, 37, 0x1330).then(
    (r) => {} /* handle arrival */
  );
}

function asyncTask() {
  // ✅ agent 2: notify on shared buffer location
  const bufferLocation = new Int32Array(sharedBuffer);
  Atomics.notify(bufferLocation, 37);
}

5. Regex v flag & set operations

A new feature to make regex patters much more intuitive.

Finding and manipulating complex strings using expressive patterns -- with the help of set operations:

// A and B are character class, like [a-z]

// difference: matches A but not B
[A--B]

// intersection: matches both A & b
[A&&B]

// nested character class
[A--[0-9]]

To match ever-increasing sets of Unicode characters, like:

  • Emojis: 😀, ❤️, 👍, 🎉, etc.
  • Accented letters: é, à, ö, ñ, etc.
  • Symbols and non-Latin characters: ©, ®, €, £, µ, ¥, etc

So here we use Unicode regex and the v flag to match all Greek letters:

const regex = /[\p{Script_Extensions=Greek}&&\p{Letter}]/v;

Final thoughts

Overall ES15 is significant leap for JavaScript with several features essential for modern development.

Empowering you to write cleaner code with greater conciseness, expressiveness, and clarity.

Coding Beauty Assistant logo

Try Coding Beauty AI Assistant for VS Code

Meet the new intelligent assistant: tailored to optimize your work efficiency with lightning-fast code completions, intuitive AI chat + web search, reliable human expert help, and more.

See also