Back

Discovering the Latest in JavaScript: New Features for 2024

Discovering the Latest in JavaScript: New Features for 2024

In 2024, we’ll see several new features that will make JavaScript more powerful and versatile. This article will explore some of JavaScript’s most exciting new features this year.

The new features in JavaScript for 2024 are Object.groupBy(), Regular expression v flag, Promise.withResolvers(), and Mutate array by copy. We’ll explore these features, how they can be used, and why they’re important.

1. Array Grouping

Object.groupBy() is a new JavaScript method that groups the elements of an array according to some criteria defined by a callback function that is supplied to it. This method is useful when you want to classify elements of an array based on the names of one or more properties of objects within the array.

Group elements within an array

Developers often need to group items from a database and display them to users via the UI. Object.groupBy() simplifies grouping such items.

Consider the common scenario of grouping inventory products in an array according to their category:

const shoppingCart = [
  { product: "iPhone X", quantity: 25, color: "black" },
  { product: "MacBook Pro 14'", quantity: 6, color: "white" },
  { product: "HP Spectre", quantity: 0, color: "black" },
];

// Using object.groupBy()
const groupedObjects = Object.groupBy(shoppingCart, (element, index) => element.color);
console.log(groupedObjects);

// Output:
/*
{
  "black": [
    {
      "product": "iPhone X",
      "quantity": 25,
      "color": "black"
    },
    {
      "product": "HP Spectre",
      "quantity": 0,
      "color": "black"
    }
  ],
  "white": [
    {
      "product": "MacBook Pro 14'",
      "quantity": 6,
      "color": "white"
    }
  ]
}
*/

The code above groups the products in shoppingCart by the value of their color property.

  • Object.groupBy accepts two arguments: the array to be grouped and a callback function.
  • The callback function takes two arguments as well: the current item (element) and the index of the current item.

The callback function returns a key that corresponds to the color property (‘black’ or ‘white’) of each object each time it is called. The returned key is then used to group the elements of the array.

Notice that the example above did not use the callback’s index argument. It is only there to demonstrate the callback’s full definition.

Group elements within an array conditionally

The scenario below considers the case where you might want to group the elements of an array depending on the size of a property in each object.

const teams = [
  { name: "Man City", points: 63 },
  { name: "Real Madrid", points: 50 },
  { name: "Bayern Munich", points: 70 },
  { name: "AC Milan", points: 59 },
  { name: "Everton", points: 70 },
];

// groups objects by the number of points
const groupings = Object.groupBy(teams, (team) => {
  return team.points > 60 ? "qualified" : "disqualified";
});

console.table(groupings);
// Output Below:
<table>
<thead>
  <tr>
    <th>index</th>
    <th>0</th>
    <th>1</th>
    <th>2</th>
  </tr>
</thead>
<tbody>
  <tr>
    <td>qualified</td>
    <td>{name: 'Man City', points: 63}</td>
    <td>{name: 'Bayern Munich', points: 70}</td>
    <td>{name: 'Everton', points: 70}</td>
  </tr>
  <tr>
    <td>disqualified</td>
    <td>{name: 'Real Madrid', points: 50}</td>
    <td>{name: 'AC Milan', points: 59}</td>
    <td></td>
  </tr>
</tbody>
</table>

Here, `Object.groupBy()` groups the teams
into `qualified` and `unqualified` teams, depending on each team's points.

* `Object.groupBy()` accepts two arguments: the array to be grouped and a callback function.
* The callback returns a string, either "qualified" or "disqualified," that represents the key for grouping each object.
* The ternary operator determines which string is supplied to `Object.groupBy()` for each element in the array.
* If the team's points exceed 60, the key is set to `qualified`; otherwise, it is set to `disqualified`.

Subsequently, `Object.groupBy()` organizes the objects (teams) according to the magnitude of their points.

### Group individual elements in an array

  What if you have an array that only contains primitives like numbers or strings, and you want
  to group them?
  You can do that by using `Object.groupBy()` like this:

```javascript
let array = [20, 2, 37, 50, 50];

console.log(Object.groupBy(array, (num) => {
  return num > 35 ? 'big' : 'small';
}));

// Output: {small: [20, 2], big: [37,50,50]}

In this example,

The callback function passed to Object.groupBy() takes each number in the array and returns a string based on the condition that the number is greater than 35. If the number exceeds 35, the key is set to ‘big’; otherwise, it is set to ‘small’.

Note: Object.groupBy() was originally implemented as a typical array method. It was originally meant to be used like this:

let myArray = [a, b, c]
myArray.groupBy(callbackFunction)

However, due to web compatibility issues that were encountered when the ECMAScript technical committee was implementing the method, they decided to implement it as a static method (Object.groupBy) instead.

Object.groupBy() streamlines the process of grouping objects within an array by requiring only two arguments: the array itself and a callback function.

In the past, you would have had to write a custom function that groups the array elements or import a grouping method from an external library.

Availability: Object.groupBy() is now supported in all the major browser platforms. You can start using it in your new and existing projects.

Visit MDN Web Docs to learn more about Object.groupBy().

2. Regular Expression v flag

You might be familiar with the regular expression Unicode flag (u flag), which lets you enable support for Unicode characters. The v flag is an extension of most of the capabilities of the u flag.

In addition to being mostly backward compatible with the u flag, the v flag introduces these new features:

Intersection operator

The intersection operator enables the matching of characters that must be present in two character sets. Its syntax is [operand-one&&operand-two], where && denotes the intersection operator, and operand-one and operand-two represent the respective character sets.

Here is an example illustrating the use of the intersection operator:

let txt = "ECMAScript ES 5 and ES 2015 revolutionized web development";

// match lowercase letters that are not vowels
let myRegex = /[[a-z]&&[^aeiuo]]/gv;
console.log(txt.match(myRegex));

//Output: ["c","r","p","t","n","d","r","v","l","t","n","z","d","w","b","d","v","l","p","m","n","t"]
  • The code above defines a regex that matches the intersection of lowercase letters [a-z] and non-vowel characters [^aeiuo].
  • The && operator ensures that only characters common to both sets are matched.
  • The gv flags enable global search (find all matches) and regex v-mode.

The intersection operator lets you create more powerful regular expressions in your code. Consider this scenario:

let txt = `Some Arabic letters include ق ط ب ج د while 7, 6, and 4 in Arabic are: ٧,٦, and ٤, respectively.
ء,  ُ, and  ِ represent some Arabic diacritic marks while ؛, ؟, ، are Arabic punctuation marks`;

let myRegex = /[\p{Script_Extensions=Arabic}&&[\p{Letter}\p{Mark}\p{Decimal_Number}]]/gv
console.log(txt.match(myRegex));

// Output: ["ق","ط","ب","ج","د","٧","٦","٤","ء","ُ","ِ"]

This regex matches:

  • Entries that are common to the Arabic script (e.g., ق ط ب ج د etc.) and
  • The set of entries consisting of letters, diacritic marks, and Arabic numbers (e.g., ٧,٦, and ء, ُ)

Notice that the Arabic punctuation marks are absent in the output above because they are not common to both sets.

Difference operator

The difference operator, represented by two consecutive hyphens (--), provides a convenient way to specify exclusions in your regex. The regex engine will ignore any character set that comes after --. For example,

// Looks for non-ASCII numbers
let myRegex = /[\p{Decimal_Number}--[0-9]]/gv;
let numbers = '1 and 2 in Arabic and Bengali are represented as ٢, ١, and ১, ২, respectively';

console.log(numbers.match(myRegex));

//Output ["٢","١","১","২"]

Here, the regex matches all numbers except ASCII (0-9). Therefore, the output logged only Arabic and Bengali numbers.

The example below looks for non-ASCII emoji characters:

let myEmojis = "😁,😍,😴,☉‿⊙,:O";

// Excludes ASCII Emoji characters
let myRegex = /[\p{Emoji}--\p{ASCII}]/gv;
console.log(myEmojis.match(myRegex));

//Output: ["😁","😍","😴"]

Note that the first example that showed the use of the intersection operator can also be accomplished with the difference operator, like this:

let txt = "ECMAScript ES 5 and ES 2015 revolutionized web development";

// Excludes non-vowel lowercase characters
let myRegex = /[[a-z]--[aeiuo]]/gv;
console.log(txt.match(myRegex));

["c","r","p","t","n","d","r","v","l","t","n","z","d","w","b","d","v","l","p","m","n","t"]

Without using either the difference operator or the intersection operator, achieving the same result would require listing all consonants individually, like this: [b-df-hj-np-tv-z].

Union operator

The union operation retains its previous syntax even with the introduction of the v flag. Expressions like [A-Z-a-z] or [A-Z|a-z] still encompass all characters from A-Z and a-z. Simply put, the absence of both && and -- implies a union.

Operator mixing

The v flag lets you combine operators to create powerful regular expressions via nesting. But you should be careful when combining the operators. For instance, you can do this:

let txt = 'An Arabic word: غلص and an Arabic number: ٩٧'

// Looks for Arabic letters only
let regex = /[\p{Script_Extensions=Arabic}&&\p{Letter}]/gv
console.log(txt.match(regex));

// Output: ["غ","ل","ص"]

Notice that the operands in the code above are not nested, but the regex still works. This is because there’s only one operator in the regex.

Now, let’s consider the case below:

let txt = "Latin forms of letter A include: Ɑɑ ᴀ Ɐɐ ɒ A, a, A";

let regex = /[\p{Script_Extensions=Latin}&&[\p{Letter}]--[A-z]/gv;

// Output:
// Syntax Error: Invalid Set Operation in character class

The code above logs a syntax error because you can only combine operators up to one level. To get the regex to work, you have to be explicit by writing it like this:

let txt = "Latin forms of letter A include: Ɑɑ ᴀ Ɐɐ ɒ A, a, A";

let regex = /[[\p{Script_Extensions=Latin}]&&[[\p{Letter}]--[A-z]]]/gv;

console.log(txt.match(regex));

// Output: ["Ɑ","ɑ","ᴀ","Ɐ","ɐ","ɒ","A"]

We have created a subset [[\p{Letter}]--[A-z]] that combines the last two character sets using --, then the result is combined with the first character set [p{Script_Extensions=Latin}] through the && operator.

That’s it for the Regular Expression v flag. Visit MDN Web Docs to learn more about the v flag.

Availability: The v flag is supported in all the major JavaScript environments.

3. Promise.withResolvers()

Promise.withResolvers() is a static method that returns an object that contains three properties:

  • A promise
  • A resolve function and
  • A reject function
let {promise, resolve, reject} = Promise.withResolvers();

Promise.withResolvers() provides equivalent functionality to the following JavaScript code:

let resolve, reject;
const promise = new Promise((res, rej) => {
  resolve = res;
  reject = rej;
});

The Promise.withResolvers() approach is more concise, and it integrates the resolution and rejection functions directly within the promise’s scope, contrasting with their creation and single-use application within the executor.

This adjustment tends to reduce code nesting compared to enclosing extensive logic within the executor.

Suppose you want to get a todo item from an API with Node.js. You could do it the conventional way, like this:

// Import the 'https' module
import * as https from "https"

function fetchData(url) {
  return new Promise((resolve, reject) => {
    https
      .get(url, (res) => {
        let data = "";
        
        // As data is received, append it to the 'data' variable
        res.on("data", (chunk) => {
          data += chunk;
        });
        
        // When the response is complete, resolve the promise with the accumulated data
        res.on("end", () => {
          resolve(data);
        });
      })
      
      // If an error occurs during the request, reject the promise with the error
      .on("error", (err) => {
        reject(err);
      });
  });
}

// Usage example, fetching data from a URL
fetchData("https://jsonplaceholder.typicode.com/todos/1").then((data) => {
  console.log(data);
});

/* Output:
  { 
  "userId": 1, 
  "id": 1, 
  "title": "delectus aut autem", 
  "completed": false 
}
*/

The fetchData() function fetches data from a specified URL using the HTTPS module in Node.js. It returns a promise that resolves with the accumulated data when the HTTP GET request is successful and rejects with an error if any issues occur during the request.

Below is the same function implemented with Promise.withResolvers():

import * as https from "https"

function fetchTodo(url) {
const { resolve, reject, promise } = Promise.withResolvers();
    https
      .get(url, (res) => {
        let data = "";
        res.on("data", (chunk) => {
          data += chunk;
        });
        res.on("end", () => {
          resolve(data);
        });
      })
      .on("error", (err) => {
        reject(err);
      });
    
    // Return the created promise to the caller
    return promise;
}

fetchTodo("https://jsonplaceholder.typicode.com/todos/1").then((data) => {
  console.log(data);
});


/* Output:
  { 
  "userId": 1, 
  "id": 1, 
  "title": "delectus aut autem", 
  "completed": false 
}
*/

The function above achieves the same goal of fetching todo data from a specified URL. The difference lies in the approach used to implement the function. Here, promise.withResolvers() eliminates one of the nested scopes and the need to pass a function into the promise constructor.

Promise.withResolvers() is also applicable to situations where you might want to pass resolve or reject to more than one caller.

In the example below, resolve is passed to one event listener — socket.on('response'), ... while reject is passed to two different event listeners: socket.on('response'), ... and socket.on('error'), ...

// Initialize empty functions for resolving and rejecting promises
let resolve = () => {};
let reject = () => {};

function request(type, message) {
  if (socket) {
    const promise = new Promise((res, rej) => {
      resolve = res;
      reject = rej;
    });

    // Emit the specified request type and message through the socket
    socket.emit(type, message);

    // Return the created promise to the caller
    return promise;
  }

  // If socket is unavailable, reject the promise with an error
  return Promise.reject(new Error("Socket unavailable"));
}

// Handle 'response' events from the server
socket.on("response", (response) => {
  if (response.status === 200) {
    resolve(response);
  } else {
    reject(new Error(response));
  }
});

// Handle 'error' events from the server
socket.on("error", (err) => {
  reject(err);
});

In this example:

Two variables, resolve and reject, are initialized with empty functions that will later be assigned to the corresponding functions provided by the promise constructor to resolve or reject the promise.

The request function sends a message through a socket and returns a promise. The promise’s resolvers are dynamically assigned to the global variables resolve and reject.

The code listens for ‘response’ and ‘error’ events on the socket, resolving or rejecting the promise based on the received data. This structure allows external code like event handlers, to handle the socket responses asynchronously.

Below is the same function implemented with Promise.withResolvers():

// Create an object with resolve, reject, and promise properties using `Promise.withResolvers()`
const { resolve, reject, promise } = Promise.withResolvers();

function request(type, message) {
  if (socket) {
    socket.emit(type, message);
    return promise;
  }
    
  return Promise.reject(new Error('Socket unavailable'));
}

socket.on('response', response => {
  if (response.status === 200) {
    resolve(response);
  } else {
    reject(new Error(response));
  }
});

socket.on('error', err => {
  reject(err);
});

Overall, the Promise.withResolvers() method aims to enhance the readability and conciseness of asynchronous code, especially in scenarios involving external event handling.

Visit MDN Web Docs to learn more about the Promise.withResolvers() method.

Availability:

Available in all the major browsers. Currently under Technology Preview on Safari 17.3+

Note: As of January 2024, Promise.withResolvers() is not yet included in Node.js. Consequently, the provided examples may not function as intended in Node.js

4. Mutate Array By Copy

Mutate array by copy introduces four new non-mutating array methods: toReversed(), toSpliced(), toSorted(), and with()

The first three are functionally equivalent to their mutating counterparts: reverse(), splice(), and sort().

The non-mutating variants create copies of an array before modifying it.

These three new methods eliminate the need to write code that copies arrays before mutating them. You can read this article that explores the new methods in detail: Exploring the New Array Methods from ECMAScript

with() is the fourth new array method. It allows you to easily replace elements at specific locations within arrays without mutating them.

const numbers = [5, 10, 139];

// replaces 139 with 15
const  multiplesOfFive = numbers.with(2, 15);
console.log(multiplesOfFive); 

// Output: [5, 10, 15]

This is akin to doing numbers[1] = 10. But this time, with() will not mutate the original array. It will make a copy with the associated change instead.

Availability: Present in all major JavaScript runtimes and browsers.

Other Features that Might Land in JavaScript in 2024

The features explored in this article are currently available in most of the major JavaScript runtimes and web browsers. The following are ECMAScript stage 3 proposals, which means they are in the process of being finalized:

Conclusion

In this article, we explored four new JavaScript features: Object.groupBy(), regex v flag, Promise.withResolvers(), and Mutate Array Copy. These features enable you to write more concise and efficient code.

That’s it for the new features coming to JavaScript in 2024. Start exploring them now and make your coding experience more efficient and enjoyable!

Further Reading

Visit the following links to further explore the features mentioned in this article:

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay