The Complete Guide to ES10 Features

ES10 is no longer just a draft! All features have already been implemented in major browsers. Here is a non-alien guide for anyone interested in exploring ES10, released June 2019.

JavaScript Teacher
Feb 20 · 11 min read

Follow me on Twitter for JavaScript tips and book announcements.

In ES6 arrow functions were hands down the most popular new feature.

What will it be in ES10?

BigInt — Arbitrary precision integers

BigInt is the 7th primitive type.

BigInt is an arbitrary-precision integer. What this means is that variables can now represent 2⁵³ numbers. And not just max out at 9007199254740992.

const b = 1n; // append n to create a BigInt

In the past integer values greater than 9007199254740992 were not supported. If exceeded, the value would simply lock to MAX_SAFE_INTEGER + 1:

const limit = Number.MAX_SAFE_INTEGER;
9007199254740991
limit + 1;
9007199254740992
limit + 2;
9007199254740992 <--- MAX_SAFE_INTEGER + 1 exceeded
const larger = 9007199254740991n;
9007199254740991n
const integer = BigInt(9007199254740991); // initialize with number
9007199254740991n
const same = BigInt("9007199254740991"); // initialize with "string"
9007199254740991n

typeof

typeof 10;
'number'
typeof 10n;
'bigint'
10n === BigInt(10);
true
10n == 10;
true
200n / 10n
20n
200n / 20
Uncaught TypeError:
Cannot mix BigInt and other types, use explicit conversions <
-100n
-100n
+100n
Uncaught TypeError:
Cannot convert a BigInt value to a number

By the time you read this matchAll will probably be officially implemented in Chrome C73 – if not, it’s still worth taking a look at. Especially if you’re a regular expression (regex) junkie.

string.prototype.matchAll()

If you run a Google search for javascript string match all the first result will be something like: How do I write a regular expression to “match all”?

Top results will suggest using String.match with a regular expression and /g

…or RegExp.exec and/or RegExp.test with /g

First, let’s take a look at how the older spec worked.

String.match with string argument only returns the first match:

let string = “Hello”;
let matches = string.match(“l”);
console.log(matches[0]); // "l"

Result is single “l” (Note: the match is stored in matches[0] not matches.)

Only “l” is returned from a search for “l” in the word “hello”.

The same goes for using string.match with a regex argument:

Let’s locate “l” character in string “hello” using regular expression /l/:

let string = "Hello";
let matches = string.match(/l/);
console.log(matches[0]); // "l"

But String.match with a regular expression and the /g flag does return multiple matches:

let string = "Hello";
let ret = string.match(/l/g); // (2) [“l”, “l”];

Great… we‘ve got our multiple matches using < ES10. It worked all along.

So why bother with a completely new matchAll method? Well, before we can answer this question in more detail, let’s take a look at capture groups. If nothing else, you might learn something new about regular expressions.

Capturing groups in regex is simply extracting a pattern from () parenthesis.

You can capture groups with /regex/.exec(string) and with string.match.

Regular capture group is created by wrapping a pattern in (pattern).

But to create groups property on resulting object it is: (?<name>pattern).

To create a new group name, simply prepend ?<name> inside brackets and in the result it the grouped (pattern) match will become groups.name attached to the match object. Here’s a practical example.

String specimen to match:

Here match.groups.color & match.groups.bird are created:

const string = 'black*raven lime*parrot white*seagull';
const regex = /(?<color>.*?)\*(?<bird>[a-z0-9]+)/g;
while (match = regex.exec(string))
{
let value = match[0];
let index = match.index;
let input = match.input;
console.log(`${value} at ${index} with '${input}'`);
console.log(match.groups.color);
console.log(match.groups.bird);
}

regex.exec method needs to be called multiple times to walk the entire set of the search results. During each iteration when .exec is called, the next result is revealed (it doesn’t return all matches right away.) Hence while loop.

Console Output:

black*raven at 0 with 'black*raven lime*parrot white*seagull'
black
raven
lime*parrot at 11 with 'black*raven lime*parrot white*seagull'
lime
parrot
white*seagull at 23 with 'black*raven lime*parrot white*seagull'
white
seagull

But there is the quirk:

If you remove /g from this regex, you will create an infinite loop cycling on the first result forever. This has been a huge pain in the past. Imagine receiving a regex from some database where you are unsure of whether it has /g at the end or not. You’d have to check for it first, etc.

Now we have enough background to answer the question:

Good reasons to use .matchAll()

  1. It can be more elegant when using with capture groups. A capture group is simply the part of regular expression with ( ) that extracts a pattern.
  2. It returns an iterator instead of array. Iterators on their own are useful.
  3. An iterator can be converted to an array using spread operator ()
  4. It avoids regular expressions with /g flag… useful when an unknown regular expression is retrieved from database or outside source and used together with the archaic RegEx object.
  5. Regular expressions created using RegEx object cannot be chained using the dot (.) operator.
  6. Advanced: RegEx object changes internal .lastIndex property that tracks last matching position. This can wreck havoc in complex cases.

The simple case

Let’s try to match all instances of letter e and l in the word hello. Because an iterator is returned we can walk it with a for…of loop:

// Match all occurrences of the letters: "e" or "l" 
let
iterator = "hello".matchAll(/[el]/);
for (const match of iterator)
console.log(match);

You can skip /g this time, it’s not required by .matchAll method. Result:

[ 'e', index: 1, input: 'hello' ] // Iteration 1
[ 'l', index: 2, input: 'hello' ] // Iteration 2
[ 'l', index: 3, input: 'hello' ] // Iteration 3

Capture Groups example with .matchAll():

.matchAll has all of the benefits listed above. It’s an iterator, so we can walk it with for…of loop. And that’s the whole syntactic difference.

const string = 'black*raven lime*parrot white*seagull';
const regex = /(?<color>.*?)\*(?<bird>[a-z0-9]+)/;
for (const match of string.matchAll(regex)) {
let value = match[0];
let index = match.index;
let input = match.input;
console.log(`${value} at ${index} with '${input}'`);
console.log(match.groups.color);
console.log(match.groups.bird);
}

Note that /g flag is missing because it is already implied by .matchAll().

Console Output:

black*raven at 0 with 'black*raven lime*parrot white*seagull'
black
raven
lime*parrot at 11 with 'black*raven lime*parrot white*seagull'
lime
parrot
white*seagull at 23 with 'black*raven lime*parrot white*seagull'
white
seagull

Perhaps aesthetically it is very similar to the original regex.exec while loop implementation. But as stated earlier this is the better way for many reasons mentioned above. And removing /g won’t cause an infinite loop.

Dynamic import

Imports can now be assigned to a variable:

element.addEventListener('click', async () => {
const module = await import(`./api-scripts/button-click.js`);
module.clickEvent();
});

Array.flat()

Flattening of a multi-dimensional array:

let multi = [1,2,3,[4,5,6,[7,8,9,[10,11,12]]]];multi.flat();               // [1,2,3,4,5,6,Array(4)]
multi.flat().flat(); // [1,2,3,4,5,6,7,8,9,Array(3)]
multi.flat().flat().flat(); // [1,2,3,4,5,6,7,8,9,10,11,12]
multi.flat(Infinity); // [1,2,3,4,5,6,7,8,9,10,11,12]

Array.flatMap()

let array = [1, 2, 3, 4, 5];array.map(x => [x, x * 2]);

Becomes:

[Array(2), Array(2), Array(2)]
0: (2)[1, 2]
1: (2)[2, 4]
2: (2)[3, 6]
3: (2)[4, 8]
4: (2)[5, 10]

Flatten the map again:

array.flatMap(v => [v, v * 2]);
[1, 2, 2, 4, 3, 6, 4, 8, 5, 10]

Object.fromEntries()

Transform a list of key & value pairs into an object:

let obj = { apple : 10, orange : 20, banana : 30 };
let entries = Object.entries(obj);
entries;
(3) [Array(2), Array(2), Array(2)]
0: (2) ["apple", 10]
1: (2) ["orange", 20]
2: (2) ["banana", 30]
let fromEntries = Object.fromEntries(entries);
{ apple: 10, orange: 20, banana: 30 }

String.trimStart() & String.trimEnd()

let greeting = "     Space around     ";greeting.trimEnd();   // "     Space around";greeting.trimStart(); // "Space around     ";

Well-formed JSON.stringify()

This update fixes processing of characters U+D800 through U+DFFF that sometimes can make their way into your JSON strings. It’s can be a problem because JSON.stringify may return these numbers formatted as values for which there are no equivalent UTF-8 characters. But JSON format requires UTF-8 encoding.

JSON object can be used to parse JSON format (but also more.) The JavaScript JSON object also has stringify and parse methods.

The parse method works with a well-formed JSON string, like:

'{ “prop1” : 1, "prop2" : 2 }'; // A well-formed JSON format string

Note that double quotes surrounding property names are absolutely required to create a string in correct JSON format. Absence of… or any other types of quotes will not produce a well-formed JSON.

'{ “prop1” : 1, "meth" : () => {}}'; // Not JSON format string

JSON string format is different from Object Literal… which looks almost the same but can use any type of quotes around property names and can also include methods (JSON format does not allow methods):

let object_literal = { property: 1, meth: () => {} };

Anyway, everything seems just fine. The first examples looks compliant. But they are also simple examples and most of the time will work without a hitch!

Here is the catch. EcmaScript prior to ES10 does not actually fully support JSON format. The unescaped line separator U+2028 and paragraph separator U+2029 characters are not accepted in pre-ES10 era:

U+2029 is the line separator.
U+2029 is the paragraph separator. Sometimes it can lurk into your JSON formatted string.

If these characters crept into your JSON formatted string (let’s say from a database record) you might have ended up spending hours trying to figure out why the rest of your program generates parsing errors.

So if you pass eval a string like “console.log(‘hello’)” it will execute that JavaScript statement (by trying to convert the string to actual code.) This also similar to how JSON.parse will process your JSON string.

Stable Array.prototype.sort()

Previous implementation of V8 used an unstable quick sort algorithm for arrays containing more than 10 items.

A stable sorting algorithm is when two objects with equal keys appear in the same order in the sorted output as they appear in the unsorted input.

But this is no longer the case. ES10 offers a stable array sort:

var fruit = [
{ name: "Apple", count: 13, },
{ name: "Pear", count: 12, },
{ name: "Banana", count: 12, },
{ name: "Strawberry", count: 11, },
{ name: "Cherry", count: 11, },
{ name: "Blackberry", count: 10, },
{ name: "Pineapple", count: 10, }
];
// Create our own sort criteria function:
let
my_sort = (a, b) => a.count - b.count;
// Perform stable ES10 sort:
let
sorted = fruit.sort(my_sort);
console.log(sorted);

Console Output (items appear in reverse order):

New Function.toString()

Functions are objects. And every object has a .toString() method because it originally exists on Object.prototype.toString(). All objects (including functions) are inherited from it via prototype-based class inheritance.

This means we’ve already had funcion.toString() method before.

But ES10 further tries to standardize string representation of all objects and built-in functions. Here is the big picture with samples for various new cases:

function () { console.log('Hello there.'); }.toString();

Console Output (body of the function in string format:)

 function () { console.log('Hello there.'); }

And here are the rest of the cases:

Number.parseInt.toString();
function parseInt() { [native code] }
function () { }.bind(0).toString();
function () { [native code] }
Symbol.toString();
function Symbol() { [native code] }
Function().toString();
function anonymous() {}
function* () { }.toString();
function* () { }
Function.prototype.toString.call({});
Function.prototype.toString requires that 'this' be a Function"

Just standardized across many more different situations.

Optional Catch Binding

In the past catch clause in a try / catch statement required a variable.

The try / catch statement helps us intercept errors on terminal level:

Here’s a refresher.

try {
// Call a non-existing function undefined_Function
undefined_Function("I'm trying");
}
catch(error) {
// Display the error if statements inside try above fail
console.log(
error ); // undefined_Function is undefined
}

But in some cases required error variable was left unused:

try {
JSON.parse(text); // <--- this will fail with "text not defined"
return true; <--- exit without error even if there is one
}
catch (redundant_sometmes) <--- this makes error variable redundant
{
return false;
}

Whoever wrote this code exits from the try clause by trying to forcing true. But… this isn’t actually what happens (as pointed out by Douglas Massolari.)

(() => {
try {
JSON.parse(text)
return true
} catch(err) {
return false
}
})()
=> false

You can now skip error variable:

try {
JSON.parse(text);
return true;
}
catch
{
return false;
}

There is currently no way to test what the try statement evaluate to like in previous example. But once it comes out I’ll update this part.

Standardized globalThis object

The global this was not standardized before ES10.

In production code you would “standardize” it across multiple platforms on your own by writing this monstrosity:

var getGlobal = function () {
if (typeof self !== 'undefined') { return self; }
if (typeof window !== 'undefined') { return window; }
if (typeof global !== 'undefined') { return global; }
throw new Error('unable to locate global object');
};

But even this didn‘t always work. So ES10 added the globalThis object which should be used from now on to access global scope on any platform:

// Access global array constructor
globalThis.Array(0, 1, 2);
[0, 1, 2]

// Similar to window.v = { flag: true } in <= ES5
globalThis.v = { flag: true };

console.log(globalThis.v);
{ flag: true }

Symbol.description

description is a read-only property that returns optional description of Symbol objects.

let mySymbol = 'My Symbol';
let symObj = Symbol(mySymbol);
symObj; // Symbol(My Symbol)
String(symObj) === 'Symbol(${mySymbol})`); // true
symObj.description; // "My Symbol"

Hashbang Grammar

AKA the shebang unix users will be familiar with.

It specifies an interpreter (what will execute your JavaScript file?)

ES10 standardizes this. I won’t go into details on this because this is technically not really a language feature. But it basically unifies how JavaScript is executed on server-side end.

$ ./index.js

Instead of:

$ node index.js

Under Unix-like operating systems.

ES10 Classes: private, static & public members

Here is a rather meaningless example that focuses only on new syntax:

class Raven extends Bird {    #state = { eggs: 10};    // getter
get #eggs() {
return state.eggs;
}
// setter
set #eggs(value) {
this.#state.eggs = value;
}
#lay() {
this.#eggs++;
}
constructor() {
super();
this.#lay.bind(this);
}
#render() {
/* paint UI */
}
}

To be honest I think it makes the language a bit harder to read.

It is still my favorite new feature because I love classes from C++ days.

I will update the article when it is actually implemented. Until then… don’t shoot the messenger.

ES10 is a new set of features that hasn’t had the chance to be fully explored in production environment yet. Let me know if you have any corrections, suggestions or any other feedback.

Often times I write a tutorial because I want to learn some of the subjects myself. This was one of those times. But not without the help of other resources already compiled by others:

Thanks to Sergey Podgornyy who wrote this ES10 tutorial.

And Selvaganesh who wrote this ES10 tutorial.

JavaScript Teacher

Written by

Issues. Every webdev has them. Published author of CSS Visual Dictionary https://amzn.to/2JMWQP3 few others…