Unlocking ES2024 Must Know Javascript Features
Introduction
As JavaScript development, ES2024 introduces a range of exciting new features that are set to transform web development. These enhancements not only streamline asynchronous operations and complex data handling but also boost development efficiency. In this article, we’ll take a deep dive into these new capabilities, complete with practical code examples, to help you quickly master them and elevate the performance and maintainability of your web applications.
1. Promise.withResolvers
Promise.withResolvers
offers a more streamlined way to create a Promise
while simultaneously accessing its resolve
and reject
functions. This addition simplifies control over Promises, especially in asynchronous workflows.
In the past, we often had to declare additional variables to store these functions, as shown below:
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
Now, we can simply use const { promise, resolve, reject } = Promise.withResolvers();
to achieve the same result.
Here’s a practical example of a cancelable timer using this feature:
const cancelableTimeout = (ms: number) => {
let cancel = () => {};
const promise = new Promise((resolve, reject) => {
const timeoutId = setTimeout(resolve, ms);
cancel = () => {
clearTimeout(timeoutId);
reject(new Error('Cancelled'));
};
});
return { cancel, promise };
};
With Promise.withResolvers()
:
const cancelableTimeoutWithPromises = (ms: number) => {
const { promise, resolve, reject } = Promise.withResolvers();
const timeoutId = setTimeout(resolve, ms);
const cancel = () => {
clearTimeout(timeoutId);
reject(new Error('Cancelled'));
};
return { cancel, promise };
};
2. RegExp v Flag
The RegExp v
flag builds on the u
flag, adding two powerful features:
- Unicode Property Escapes: Enables matching based on Unicode properties of strings.
- Set Operations: Supports set operations between character classes.
This functionality expands what’s possible with regular expressions. For example:
- Matching Alphabetic Characters:
const regex = /^\p{Letter}+$/v;
console.log(regex.test('Hello')); // true (Latin letters)
console.log(regex.test('こんにちは')); // true (Japanese kana)
console.log(regex.test('123')); // false (digits)
- Set Operations:
const regex = /[\p{Letter}&&\p{Number}]/v;
console.log(regex.test('a')); // false
console.log(regex.test('1')); // false
3. Object.groupBy and Map.groupBy
In the past, grouping elements in an array required manual implementation using reduce
or forEach
. For example:
const array: { type: string, name: string }[] = [
{ type: 'fruit', name: 'apple' },
{ type: 'vegetable', name: 'carrot' },
{ type: 'fruit', name: 'banana' },
];
const grouped = array.reduce((acc: any, item) => {
const key = item.type;
if (!acc[key]) acc[key] = [];
acc[key].push(item);
return acc;
}, {});
console.log(grouped);
With the introduction of Object.groupBy
and Map.groupBy
, we can achieve the same result more elegantly:
const grouped = Object.groupBy(array, (item) => item.type);
console.log(grouped);
const groupedMap = Map.groupBy(array, (item) => item.type);
console.log(groupedMap);
These methods also allow for custom grouping logic via callback functions. For instance, grouping based on multiple conditions or transforming data before grouping:
const array = [
{ category: 'fruit', name: 'apple', color: 'red' },
{ category: 'vegetable', name: 'carrot', color: 'orange' },
{ category: 'fruit', name: 'banana', color: 'yellow' },
];
const grouped = Object.groupBy(array, (item) => `${item.category}-${item.color}`);
console.log(grouped);
4. String.prototype.isWellFormed and String.prototype.toWellFormed
String.prototype.isWellFormed
and String.prototype.toWellFormed
address Unicode encoding issues in strings. They solve the following problems:
- Detecting whether a string is valid UTF-16 encoded.
- Fixing invalid UTF-16 encoded strings.
In everyday development, we often need to encode URLs. However, if a string contains invalid surrogate pairs (e.g., standalone high or low surrogates), it can cause errors.
In the past, we had to use workarounds like this:
function isWellFormed(str: string) {
return !/[\uD800-\uDBFF]([^\uDC00-\uDFFF]|$)|([^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/.test(str);
}
Or have a method to ensure proper URL encoding:
function toWellFormed(str: string) {
return str.replace(/[\uD800-\uDBFF]([^\uDC00-\uDFFF]|$)|([^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/g, '�');
}
With String.prototype.isWellFormed
and String.prototype.toWellFormed
, these workarounds are no longer necessary, as the built-in methods handle these cases seamlessly.
5. Atomics.waitAsync()
Atomics.waitAsync()
introduces a non-blocking asynchronous waiting mechanism for monitoring changes in shared memory (SharedArrayBuffer
). It returns a Promise
, and it’s very useful for multi-threaded environments like the main thread and Web Workers.
Unlike Atomics.wait()
, it is non-blocking, so it can be used in the main thread without freezing the page.
Here’s a simple example demonstrating its usage in the main thread:
// Create SharedArrayBuffer and Int32Array
const sab = new SharedArrayBuffer(1024);
const int32 = new Int32Array(sab);
// Asynchronously wait for a change at position 0
const result = Atomics.waitAsync(int32, 0, 0, 1000); // Wait for 1000ms
console.log(result); // { async: true, value: Promise { <pending> } }
// Modify the value in another thread or operation
setTimeout(() => {
Atomics.store(int32, 0, 1); // Modify the value
Atomics.notify(int32, 0); // Notify waiting threads
}, 500);
// Handle the Promise
result.value.then((res) => {
console.log(res); // Outputs "ok" (value changed) or "time-out" (timeout)
});
6. ArrayBuffer.prototype.transfer()
In the past, copying an ArrayBuffer
required manually creating a new ArrayBuffer
and using a TypedArray
or DataView
to copy the data. This approach was not only cumbersome but also led to unnecessary memory allocation and data duplication. Moreover, even after copying, the original buffer remained accessible and modifiable. To address these issues, ArrayBuffer.prototype.transfer()
was introduced.
// Create an ArrayBuffer and write some data
const buffer = new ArrayBuffer(8);
const view = new Uint8Array(buffer);
view[1] = 2;
view[7] = 4;
// Transfer the buffer to a new ArrayBuffer
const buffer2 = buffer.transfer();
console.log(buffer.detached); // true (original buffer is detached)
console.log(buffer2.byteLength); // 8 (size of the new buffer)
// Verify that the data was correctly transferred
const view2 = new Uint8Array(buffer2);
console.log(view2[1]); // 2
console.log(view2[7]); // 4