ES2018 & ES2019 Features
Object rest/spread, for-await-of, Promise.finally(), RegExp named capture groups, lookbehind assertions, dotAll flag, Unicode property escapes, Array.flat(), Array.flatMap(), Object.fromEntries(), String.trimStart(), String.trimEnd(), optional catch binding, Symbol.description, stable sort.
Contents
ES2018 (ES9)
Rest/Spread Properties for Objects
// Object spread (copying)
const defaults = { theme: 'dark', lang: 'en' };
const userPrefs = { theme: 'light' };
const settings = { ...defaults, ...userPrefs };
// { theme: 'light', lang: 'en' }
// Object rest (destructuring)
const { name, ...rest } = { name: 'Alice', age: 30, city: 'NYC' };
// name = 'Alice'
// rest = { age: 30, city: 'NYC' }
// Function parameters
function update({ id, ...changes }) {
return { id, ...changes, updatedAt: new Date() };
}Asynchronous Iteration
// Async generator
async function* fetchPages(url) {
let page = 1;
while (true) {
const res = await fetch(`${url}?page=${page}`);
const data = await res.json();
if (data.length === 0) break;
yield data;
page++;
}
}
// for-await-of
for await (const page of fetchPages('/api/items')) {
processPage(page);
}
// Async iterables
const asyncIterable = {
[Symbol.asyncIterator]() {
let i = 0;
return {
async next() {
if (i >= 3) return { done: true };
await delay(100);
return { value: i++, done: false };
}
};
}
};Promise.prototype.finally()
Run cleanup code regardless of Promise outcome.
showSpinner();
fetch('/api/data')
.then(res => res.json())
.then(data => process(data))
.catch(err => showError(err))
.finally(() => {
hideSpinner(); // Always runs
});
// With async/await
async function fetchData() {
showSpinner();
try {
const res = await fetch('/api/data');
return await res.json();
} catch (err) {
showError(err);
} finally {
hideSpinner(); // Always runs
}
}RegExp Named Capture Groups
// ❌ Before: numbered groups
const oldRegex = /(\d{4})-(\d{2})-(\d{2})/;
const oldMatch = '2024-03-15'.match(oldRegex);
const y = oldMatch[1]; // Which is which?
const m = oldMatch[2];
const d = oldMatch[3];
// ✅ Named groups
const dateRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = '2024-03-15'.match(dateRegex);
const { year, month, day } = match.groups;
// year = '2024', month = '03', day = '15'
// In replace()
const result = '2024-03-15'.replace(
/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/,
'$<month>/$<day>/$<year>'
);
// '03/15/2024'RegExp Lookbehind Assertions
// Lookahead (existed before): (?=...) and (?!...)
// Lookbehind (new): (?<=...) and (?<!...)
// Positive lookbehind: match if preceded by pattern
const priceRegex = /(?<=\$)\d+(\.\d{2})?/;
'$19.99'.match(priceRegex); // ['19.99']
'€19.99'.match(priceRegex); // null
// Negative lookbehind: match if NOT preceded by pattern
const notEuroRegex = /(?<![€\d.])\d+(?:\.\d{2})?(?![\d.])/;
'€19.99'.match(notEuroRegex); // null
'$19.99'.match(notEuroRegex); // ['19.99']RegExp dotAll Flag (s)
Make . match newlines too.
// ❌ Before: dot doesn't match newlines
/foo.bar/.test('foo\nbar'); // false
// ✅ With s flag
/foo.bar/s.test('foo\nbar'); // true
// Check flag
/foo.bar/s.dotAll; // trueRegExp Unicode Property Escapes
Match characters by Unicode property.
// Match Greek letters
/\p{Script=Greek}/u.test('π'); // true
/\p{Script=Greek}/u.test('p'); // false
// Match any letter
/\p{Letter}/u.test('Ω'); // true
/\p{Letter}/u.test('5'); // false
// Match emoji
/\p{Emoji}/u.test('😀'); // true
// General category properties
/\p{Lowercase}/u; // Lowercase letters
/\p{Uppercase}/u; // Uppercase letters
/\p{Alphabetic}/u; // Alphabetic characters
/\p{Number}/u; // Any number (Nd, Nl, No)
/\p{Decimal_Number}/u; // Decimal digits (0-9 in any script)
/\p{Punctuation}/u; // Punctuation marks
/\p{White_Space}/u; // Whitespace characters
/\p{Math}/u; // Mathematical symbols
// Script properties
/\p{Script=Latin}/u; // Latin script
/\p{Script=Greek}/u; // Greek script
/\p{Script=Cyrillic}/u; // Cyrillic script
/\p{Script=Han}/u; // Chinese characters
/\p{Script=Hiragana}/u; // Japanese Hiragana
/\p{Script=Katakana}/u; // Japanese Katakana
/\p{Script=Arabic}/u; // Arabic script
/\p{Script=Hebrew}/u; // Hebrew scriptTemplate Literal Revision
Allow invalid escape sequences in tagged templates.
// Before ES2018, this would throw SyntaxError
function latex(strings) {
return strings.raw[0];
}
latex`\unicode`; // Now works, returns '\\unicode'
// Cooked value is undefined for invalid escapes
function tag(strings) {
console.log(strings[0]); // undefined
console.log(strings.raw[0]); // '\\unicode'
}
tag`\unicode`;ES2019 (ES10)
Array.prototype.flat()
Flatten nested arrays.
// Default: flatten one level
[1, [2, [3, [4]]]].flat(); // [1, 2, [3, [4]]]
// Specify depth
[1, [2, [3, [4]]]].flat(2); // [1, 2, 3, [4]]
// Flatten completely
[1, [2, [3, [4]]]].flat(Infinity); // [1, 2, 3, 4]
// Removes holes
[1, , 3, , 5].flat(); // [1, 3, 5]Array.prototype.flatMap()
Map then flatten (one level).
// Equivalent to .map().flat()
const sentences = ['Hello world', 'How are you'];
// ❌ map returns nested arrays
sentences.map(s => s.split(' '));
// [['Hello', 'world'], ['How', 'are', 'you']]
// ✅ flatMap flattens
sentences.flatMap(s => s.split(' '));
// ['Hello', 'world', 'How', 'are', 'you']
// Can also filter by returning empty array
const nums = [1, 2, 3, 4];
nums.flatMap(n => n % 2 ? [n] : []); // [1, 3]Object.fromEntries()
Create object from key-value pairs (inverse of Object.entries).
// From entries array
Object.fromEntries([['a', 1], ['b', 2]]);
// { a: 1, b: 2 }
// From Map
const map = new Map([['name', 'Alice'], ['age', 30]]);
Object.fromEntries(map);
// { name: 'Alice', age: 30 }
// Transform object
const prices = { apple: 1, banana: 2 };
const doubled = Object.fromEntries(
Object.entries(prices).map(([k, v]) => [k, v * 2])
);
// { apple: 2, banana: 4 }
// Filter object
const filtered = Object.fromEntries(
Object.entries(obj).filter(([k, v]) => v > 0)
);
// URL search params to object
const params = new URLSearchParams('name=Alice&age=30');
Object.fromEntries(params);
// { name: 'Alice', age: '30' }String.prototype.trimStart() and trimEnd()
const str = ' Hello World ';
str.trimStart(); // 'Hello World '
str.trimEnd(); // ' Hello World'
str.trim(); // 'Hello World'
// Aliases (for compatibility)
str.trimLeft(); // Same as trimStart()
str.trimRight(); // Same as trimEnd()Optional Catch Binding
Omit the error parameter if not needed.
// ❌ Before: always needed parameter
function parseOld(input) {
try {
return JSON.parse(input);
} catch (e) { // 'e' is unused
return null;
}
}
// ✅ Now optional
function parseNew(input) {
try {
return JSON.parse(input);
} catch {
return null;
}
}Symbol.prototype.description
Access symbol description directly.
const sym = Symbol('mySymbol');
// ❌ Before: had to parse toString()
sym.toString(); // 'Symbol(mySymbol)'
// ✅ Now: direct access
sym.description; // 'mySymbol'
Symbol().description; // undefined
Symbol('').description; // ''Stable Array.prototype.sort()
Sort is now guaranteed to be stable (equal elements maintain order).
const items = [
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 30 },
{ name: 'Charlie', age: 25 },
];
// Sort by age - Alice and Bob maintain relative order
// Note: Use .toSorted() in ES2023+ to avoid mutation
const sorted = items.toSorted((a, b) => a.age - b.age);
// [
// { name: 'Charlie', age: 25 },
// { name: 'Alice', age: 30 }, <- Alice before Bob (stable)
// { name: 'Bob', age: 30 },
// ]Well-formed JSON.stringify()
Properly encode lone surrogates as escape sequences.
// Before: could produce invalid Unicode
JSON.stringify('\uD800'); // '"\uD800"' (invalid)
// After: escape sequences for lone surrogates
JSON.stringify('\uD800'); // '"\\ud800"' (valid)Function.prototype.toString() Revision
Returns exact source code including whitespace and comments.
function /* comment */ foo() { }
foo.toString();
// 'function /* comment */ foo() { }'
// (Before, comments/whitespace might be stripped)