What's new in ECMAScript 2023?
Another year, another version of our beloved ECMAScript. For anyone who doesn’t know, ECMA is the core spec that JavaScript is based on. It gets tweaked every year, with small changes shipping throughout the months and bigger changes over the years.
In this video I explain a bit more about the process behind shipping new JavaScript features. If you haven’t watched it yet, I strongly recommend it so you can understand how the whole thing works!
Most of these changes live in the TC39 notes, the technical committee #39, which is the technical committee that evaluates and discusses the future of the language specifically. These notes are open and you can see all of them in the committee’s official repository.
Just like I do with other technologies, JS is no different! In 2022 I published an article about that year’s news and even dared to predict what would come this year, let’s see if I got it right.
Array.findLast#
One of the proposals open on TC39 was the ability to flip the initial direction of a search on an Array. Today, the find and findIndex methods will always start searching an array from its first element (which is 0, as it should be).
But in the case of sorted arrays, having the ability to search backwards, starting from the tail toward the head, is a lot faster and more efficient. And here you might argue: “Come on, can’t you just reverse the array first and then use find?”, basically yes, but no.
When we reverse the array using the reverse method, we run an operation on it that will invariably iterate over half of that array’s items. Today this method is quite fast, the spec says it should work like this:
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}This implementation was taken directly from the ECMA262 spec, section 23.1.3.26, and turned from pseudo-code into JavaScript
As you can see, the implementation starts from the beginning and the end at the same time, swapping each position until the two pointers finally meet in the middle. You’d think it’s not that fast, so let’s run a test with this function:
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')}Our output will look something like this (trimmed to fit):
i: 1 -> reverse: 0.006msi: 10 -> reverse: 0.046msi: 100 -> reverse: 0.392msi: 1000 -> reverse: 2.335msi: 10000 -> reverse: 4.026msi: 100000 -> reverse: 68.4msi: 1000000 -> reverse: 150.565msi: 10000000 -> reverse: 1.542sNotice it’s exponential. Sure, in modern implementations reverse is a lot more optimized and runs a lot faster, but the point stands: we’re going to have processing overhead.
The new proposal creates two new methods, findLast and findLastIndex, that do exactly the same as the original methods but starting from the end. That way we avoid having to reverse the array. Here’s an example:
const isEven = (number) => number % 2 === 0;const numbers = [1, 2, 3, 4];
// Existing methodconsole.log(numbers.find(isEven)); // 2console.log(numbers.findIndex(isEven)); // 1
// new methodconsole.log(numbers.findLast(isEven)); // 4console.log(numbers.findLastIndex(isEven)); // 3Using Hashbang#
The grammar known as hashbang (or shebang) is the well known sequence of characters that starts with #! at the top of a script. What it does is define which interpreter that script is going to use to run.
This is already possible today with Node through scripts like:
#!/usr/bin/env/node
console.log('Hello')Under the hood, your shell strips the first line and passes the rest of the file to the interpreter you selected, in this case the result of the env node command, which is the location of the Node.js executable.
The proposal doesn’t change the behavior, it just creates a standard for how this should be done everywhere.
Copy-on-write Arrays#
This is one of the main features, and also one of the ones I predicted would show up in JS this year, and one of the most requested. As we saw before, the reverse method does an in place replacement, meaning it replaces the elements of the very same array that was passed in, returning the same reference. It essentially mutates the original object, and that’s not cool.
To work around this kind of behavior, what we usually do is something like this:
const original = [1,2,3]const novo = [...original]console.log(novo.reverse()) // [3, 2, 1]console.log(original) // [1, 2, 3]The proposal adds four new methods to Array.prototype, all of them a variation of the original reverse, sort and splice methods, and it also creates a new method called with, which returns a new array with a new element at a specific position swapped for another value, which avoids us doing in place changes too, like with the well known a[0] = 1 notation.
The new methods are called toReversed, toSorted and toSpliced (and, of course, 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 with Symbols#
This is a proposal that’s pretty hard to explain, if only because it’s a really specific case of other pretty specific cases… But, in short, what happens is that the spec now allows Symbols to be used as keys for WeakMaps.
I won’t go into detail about WeakMaps here, but you can see more about real world uses in this StackOverflow answer
Previously, only objects were allowed as keys, but there’s another case where we have a unique value that can’t be recreated with the same value, and that’s Symbols. This proposal makes WeakMaps accept these values as keys too.
const weak = new WeakMap();const key = Symbol("ref");weak.set(key, "ECMAScript 2023");
console.log(weak.get(key));// ECMAScript 2023Wrapping up#
A lot of what I predicted in the previous article ended up getting promoted to a later stage, some got dropped, some even earned their own articles, while others are exactly where they were a year ago.
This update to the spec doesn’t touch much of what we use day to day, but it promises to be a big quality of life improvement for anyone using more specific features that can definitely have a big impact on efficiency and performance.