JavaScript — What’s new with ECMAScript® 2024 (ES15) — In Depth Guide
Live Article — Last Updated: February 07, 2024.
Discovering new functionality in programming languages is like the holidays or a birthday — it’s an exciting time filled with anticipation and the joy of exploring new gifts. With the proposed features of ES2024®, developers are on the brink of unwrapping a variety of enhancements that promise to make coding in JavaScript more efficient, readable, and robust. From the intuitive syntax of top-level await
to the expressive power of the pipeline operator, and the reliability of immutable records and tuples, each new feature is like a carefully chosen present, designed to enrich the JavaScript ecosystem and empower developers with more tools at their disposal.
According to the ECMAScript 2024 Internationalization API Specification (ECMA-402 11th Edition) several features are slated for inclusion in ES2024.
Just keep in mind that some of these are still “proposals” so things will probably adapt a bit, but lucky for you, this article will adapt with the approved changes!
Without further ado…
Well-formed Unicode Strings
This feature aims to improve how JavaScript handles Unicode strings. Unicode strings are essential for representing a wide range of characters from different languages and symbols. This update would ensure consistent and accurate processing of these strings across different JavaScript environments.
const sampleStrings = [
// Examples with lone surrogates
"igor\uD800", // Leading surrogate
"igor\uD800komolov", // Leading surrogate followed by text
"\uDC00yourfuse", // Trailing surrogate
"your\uDC00fuse", // Trailing surrogate followed by text
// Well-formed examples
"yourFuse", // Regular string without surrogates
"emoji\uD83D\uDE00", // String with a complete surrogate pair (emoji)
];
sampleStrings.forEach(str => {
console.log(`Processed String: ${str.toWellFormed()}`);
});
// Expected output:
// "Processed String: igor�"
// "Processed String: igor�komolov"
// "Processed String: �yourfuse"
// "Processed String: your�fuse"
// "Processed String: yourFuse"
// "Processed String: emoji😀"
In the example above, the toWellFormed()
method is applied to an array of strings, including some with lone surrogates and some well-formed strings. The method converts strings with lone surrogates into well-formed Unicode strings by replacing invalid sequences with a replacement character, while leaving already well-formed strings unchanged.
const problematicURL = "https://yourfuse.com/query=\uDC00data";
try {
encodeURI(problematicURL);
} catch (e) {
console.log('Error:', e.message); // Expected: URIError: URI malformed
}
// Using toWellFormed() to prevent the error
console.log('Well Formed URI:', encodeURI(problematicURL.toWellFormed()));
// Expected output: "https://yourfuse.com/query=%EF%BF%BDdata"
- The variable
problematicURL
contains a URL with a lone trailing surrogate (\uDC00
). - Attempting to encode this URL with
encodeURI()
throws a URIError due to the malformed Unicode string. - By applying
toWellFormed()
, the lone surrogate is replaced with the Unicode replacement character (U+FFFD
, encoded as%EF%BF%BD
), allowingencodeURI()
to process it without errors.
Atomic waitSync
This addition targets concurrent operations, particularly in shared memory contexts. It provides a synchronization mechanism that’s vital for ensuring data integrity and preventing race conditions in multi-threaded operations. For example, waitSync can be used to synchronize access to a shared buffer between multiple workers.
Since we dont have an example yet as the docs are still being fleshed out, I can’t show you how it will be implement . However, we can take a guess based on the existing Atomics methods. Here goes…
// Assuming sharedArray is a SharedArrayBuffer
const sharedArray = new Int32Array(new SharedArrayBuffer(1024));
function performSynchronizedOperation(index, value) {
// The waitSync method would block execution until a certain condition is met.
// For example, it could wait until the value at the specified index is no longer equal to 0.
Atomics.waitSync(sharedArray, index, 0);
// Perform operations on shared memory
sharedArray[index] = value;
// Notify other threads or workers that the value at index has been updated
Atomics.notify(sharedArray, index, 1);
}
// In a web worker or another thread
performSynchronizedOperation(0, 123);
RegExp v flag with set notation + properties of strings
This improvement to regular expressions in JavaScript allows for more complex pattern matching and string manipulations. The ‘v’ flag and set notation enable more precise and expressive regex patterns. For instance, you could use this feature to match a set of characters with specific Unicode properties.
// difference/subtraction
[A--B]
// intersection
[A&&B]
// nested character class
[A--[0-9]]
A
and B
can be thought of as placeholders for a character class (e.g. [a-z]
) or a property escape. Illustrative examples and FAQ for the proposal.
Top-level await
This “Just Do It” feature allows the await keyword to be used outside of async functions, making asynchronous code easier to write and read. For example, you can directly await a promise at the top level of a module, streamlining the code for importing modules or fetching data asynchronously.
// With top-level await
const data = await fetchData();
console.log(data);
Sure does breath some fresh air into heavy async/await structures!
Pipeline Operator
The pipeline operator (|>) improves the readability of code with multiple function calls. It allows for a functional-style syntax where the result of an expression is passed as an argument to the next function. For instance, you can refactor nested function calls into a clear sequence of operations:
// Without pipeline operator
const calculatedValue = Math.ceil(Math.pow(Math.max(0, -10), 1/3));
// With pipeline operator
const calculatedValue = -10
|> (n => Math.max(0, n)) // Replacing Math.max
|> (n => Math.pow(n, 1/3)) // Replacing Math.pow
|> Math.ceil; // Using Math.ceil
In this example:
- The
Math.max
function ensures the number is not negative. - The
Math.pow
function calculates the cube root (raised to the power of 1/3). - The
Math.ceil
function rounds the number up to the nearest integer.
The pipeline operator (|>
) simplifies chaining these operations, making the code more readable.
Now, the following is an example demonstrating the usefulness of the pipeline operator for data transformations:
// The pipeline operator simplifies complex data manipulations by allowing a series of functions to be applied in a clear, concise manner.
const numbers = [10, 20, 30, 40, 50];
const processedNumbers = numbers
|> (_ => _.map(n => n / 2)) // Halving each number
|> (_ => _.filter(n => n > 10)); // Filtering out numbers less than or equal to 10
console.log(processedNumbers); // [15, 20, 25]
In this example:
- The
map
function halves each number in the array. - The
filter
function then removes any numbers that are 10 or less. - The pipeline operator (
|>
) elegantly chains these transformations, enhancing code readability.
Remember, the pipeline operator is still in “Draft”, Stage 2 of TC39.
Records and Tuples
These immutable data structures are similar to objects and arrays, respectively, but cannot be modified after creation. For example, updating a record or tuple results in a new instance:
// Creating an immutable record
const userProfile = #{
username: "IgorKomolov",
age: 39,
};
// Creating an immutable tuple
const numberSequence = #[10, 20, 30];
// Updating these structures creates new instances
const updatedProfile = userProfile.with({ age: 40});
console.log(updatedProfile); // #{ username: "IgorKomolov", age: 40 }
console.log(userProfile); // #{ username: "IgorKomolov", age: 39 } (remains the same)
const newNumberSequence = numberSequence.with(1, 25);
console.log(newNumberSequence); // #[10, 25, 30]
console.log(numberSequence); // #[10, 20, 30] (remains the same)
Records would function similarly to objects, and tuples would be akin to arrays. However, their defining feature is immutability.
Records and tuples can enhance performance in certain situations and enforce immutability in a codebase. While they are in Stage 2 of proposal and not yet implemented in JavaScript engines, developers can experiment with them using transpilers like Babel.
Decorators
Thank TypeScript for this one, it has been long awaited, and now out of the box! These allow for modifying or augmenting the behavior of classes, methods, properties, or parameters. They are particularly useful for adding metadata, logging, or modifying behaviors in a declarative manner:
// Applying a decorator to track the execution of a method
class SampleClass {
@trackExecution
performAction(parameter1, parameter2) {
// Method implementation goes here
}
}
In this example:
SampleClass
is the class being defined.@trackExecution
is the decorator used to log or track the calls to theperformAction
method.performAction
is the method withinSampleClass
that takes two parameters (parameter1
andparameter2
). The decorator will log or track every call made to this method.
Pattern Matching
This feature introduces a concise syntax for destructuring and matching complex data structures, enhancing code readability and reducing boilerplate.
(Researching) More coming soon, please check back later!
Temporal
Although it has been drafted for a while, the updated Temporal is a modern and comprehensive date-time API proposed for JavaScript and currently in Stage 3, designed to address many of the limitations and complexities of the existing Date
object. Here are some examples of using Temporal in ES2024:
This object offers several factory methods to create Temporal values for the current time.
Getting the current instant in UTC
Temporal.Now.instant().toString()
Getting the current zoned date-time in a specific timezone
Temporal.Now.zonedDateTimeISO('Asia/Shanghai').toString()
Getting the current plain date-time in ISO format
Temporal.Now.plainDateTimeISO().toString()
Getting the current plain time in ISO format
Temporal.Now.plainTimeISO().toString()
.
Properties of ZonedDateTime.prototype
The ZonedDateTime
class in Temporal has several properties and methods that allow detailed manipulation and retrieval of date-time information.
- These include getters for calendar, timezone, year, month, day, hour, minute, second, and even nanoseconds.
- It also includes methods like
.with()
,.add()
,.subtract()
,.until()
,.since()
, and.round()
, providing extensive functionality for working with zoned date-time values.
Plain Time Classes in Temporal
Temporal introduces “plain” classes, which are abstract representations of time without a time zone.
- These classes include
PlainDateTime
,PlainDate
, andPlainTime
. - They are useful for displaying wall-clock time in a given time zone or for time computations where the time zone is irrelevant, like finding the first Tuesday of June 1984.
These examples demonstrate how Temporal in ES2024 can simplify and enhance date-time handling in JavaScript, providing more robust and versatile tools for developers.
Want to use it now? No problem!
Import the proposal or use a Babel Polyfil, heres how to import the proposal…
//Thats right you can import proposals :)
import { Temporal } from '@std/proposal-temporal';
//Basic Ops
const now = Temporal.Now.zonedDateTimeISO('America/New_York');
console.log(now.toString());
//Manipulation & COmparison
const date = Temporal.PlainDate.from('2024-01-01');
const newDate = date.add({ days: 10 });
console.log(newDate.toString()); // Outputs '2024-01-11'
Ergonomic Brand Checks
Simplifies checking an object’s type in custom classes and data structures, making type verification more intuitive and less error-prone. See you later boilerplate!
Traditional Method (Before ES2024)
class Book {
#author;
constructor(author) {
this.#author = author;
}
static hasAuthorField(obj) {
try {
obj.#author; // Attempt to access the private field
return true; // Access successful
} catch (err) {
if (err instanceof TypeError) {
return false; // Access failed, field does not exist
}
throw err; // Other errors are re-thrown
}
}
}
// Example usage:
const myBook = new Book("Igor Komolov");
console.log(Book.hasAuthorField(myBook)); // Expected output: true
const otherObject = {};
console.log(Book.hasAuthorField(otherObject)); // Expected output: false
New ES2024 Method
class BookES2024 {
#author;
constructor(author) {
this.#author = author;
}
static hasAuthorField(obj) {
return #author in obj; // New ES2024 syntax for checking private field
}
}
// Example usage:
const myBook2024 = new BookES2024("Igor Komolov");
console.log(BookES2024.hasAuthorField(myBook2024)); // Expected output: true
const otherObject2024 = {};
console.log(BookES2024.hasAuthorField(otherObject2024)); // Expected output: false
In this example, the class Book
demonstrates the traditional method, while BookES2024
uses the new ES2024 syntax. The hasAuthorField
static method checks for the presence of the private field #author
in an object, using different approaches in each class.
Realms API
This API offers a mechanism for creating isolated JavaScript environments. It’s useful for secure code execution and sandboxing, allowing you to run code in a controlled and isolated context. Plus, the name sounds so awesome!
Creating a Realm and Evaluating Simple Expressions
const igorsRealm = new Realm();
igorsRealm.evaluate('3 * 5'); // Evaluates to 15 in Igor's realm
Sharing Symbols Between Realms
const igorsRealm = new Realm();
Symbol.for('y') === igorsRealm.evaluate('Symbol.for("y")'); // returns true, shared symbol 'y'
Using Auto-Wrapped Functions
When a callable object is sent from one realm to another, a Wrapped Function Exotic Object is created in the target realm. This wrapped function, when called, chains the call to its connected function in the original realm.
const igorsRealm = new Realm();
const doubleFunction = igorsRealm.evaluate('num => num * 2');
doubleFunction(10); // returns 20
Function Evaluation with Callbacks
const igorsRealm = new Realm();
const processNumber = igorsRealm.evaluate('(number, callback) => callback(number + 5)');
processNumber(5, (result => console.log(result))); // Logs 10 (5 + 5)
Restricted Global Context Access
Accessing global objects like globalThis
, arrays, or Object.prototype
directly through realm.evaluate
throws a TypeError.
const igorsRealm = new Realm();
igorsRealm.evaluate('this'); // Throws a TypeError
igorsRealm.evaluate('new Array()'); // Throws a TypeError
igorsRealm.evaluate('Object.keys({})'); // Throws a TypeError
The upcoming features in ES2024 are set to revolutionize the way we approach JavaScript coding. These enhancements not only promise to improve code readability and efficiency but also introduce powerful new paradigms like immutable data structures and advanced pattern matching. As these features move from proposal to implementation, they open up new possibilities for developers to write cleaner, more maintainable, and more expressive JavaScript code. The future of JavaScript looks bright with these advancements, signaling a continued evolution of a language that has become a cornerstone of modern web development.