ES2017~ES2020 Key Features
We'll look at the major features introduced from ES2017 through ES2020. This period added async/await, Optional Chaining, Nullish Coalescing, and other core features of modern JavaScript.
ES2017 (ES8)
async/await
Syntax that allows writing asynchronous code in a synchronous style. (Covered in detail in Chapter 6)
// Promise approach
function fetchUser(id) {
return fetch(`/api/users/${id}`)
.then(r => r.json())
.catch(err => console.error(err));
}
// async/await approach
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
return await response.json();
} catch (err) {
console.error(err);
}
}
Object.entries() / Object.values()
const user = { name: 'Alice', age: 30, city: 'Seoul' };
// Object.entries: array of [key, value] pairs
const entries = Object.entries(user);
// [['name', 'Alice'], ['age', 30], ['city', 'Seoul']]
// Convert object to Map
const map = new Map(Object.entries(user));
// Iterate over an object
for (const [key, value] of Object.entries(user)) {
console.log(`${key}: ${value}`);
}
// Filter and create new object
const filtered = Object.fromEntries(
Object.entries(user).filter(([_, v]) => typeof v === 'string')
);
// { name: 'Alice', city: 'Seoul' }
// Object.values: values only as an array
const values = Object.values(user); // ['Alice', 30, 'Seoul']
// Calculate average
const scores = { math: 90, english: 85, science: 92 };
const avg = Object.values(scores).reduce((a, b) => a + b) / Object.values(scores).length;
String.padStart() / padEnd()
// padStart(targetLength, fillString)
'5'.padStart(3, '0'); // '005'
'42'.padStart(5, '0'); // '00042'
'hello'.padStart(10); // ' hello' (default: spaces)
// padEnd
'hello'.padEnd(10, '.'); // 'hello.....'
'100'.padEnd(8, '%'); // '100%%%%%'
// Real-world: formatting
function formatTime(hours, minutes, seconds) {
return [hours, minutes, seconds]
.map(n => String(n).padStart(2, '0'))
.join(':');
}
formatTime(9, 5, 3); // '09:05:03'
formatTime(14, 30, 0); // '14:30:00'
// Padding for number alignment
const items = [
{ id: 1, name: 'Apple' },
{ id: 42, name: 'Banana' },
{ id: 100, name: 'Cherry' }
];
items.forEach(({ id, name }) => {
console.log(`${String(id).padStart(3)}: ${name}`);
});
// 1: Apple
// 42: Banana
// 100: Cherry
Object.getOwnPropertyDescriptors()
const source = {
get name() { return 'Alice'; },
set name(value) { this._name = value; }
};
// The existing Object.assign cannot copy getters/setters
const copy1 = Object.assign({}, source);
// getters/setters are called and only the values are copied
// Complete copy with getOwnPropertyDescriptors
const copy2 = Object.create(
Object.getPrototypeOf(source),
Object.getOwnPropertyDescriptors(source)
);
// Full copy including getters/setters
// Useful in mixin patterns
const mixin = (target, ...sources) => {
sources.forEach(source => {
Object.defineProperties(
target,
Object.getOwnPropertyDescriptors(source)
);
});
return target;
};
ES2018 (ES9)
Object Spread/Rest Operator
// Object spread (previously only arrays were supported)
const defaults = { theme: 'light', language: 'en', fontSize: 16 };
const userPrefs = { theme: 'dark', fontSize: 18 };
// Merge (later properties take precedence)
const settings = { ...defaults, ...userPrefs };
// { theme: 'dark', language: 'en', fontSize: 18 }
// Object rest (in destructuring)
const { theme, ...rest } = settings;
// theme: 'dark', rest: { language: 'en', fontSize: 18 }
// Real-world: Redux-style immutable update
function reducer(state = initialState, action) {
switch (action.type) {
case 'UPDATE_USER':
return {
...state,
users: state.users.map(user =>
user.id === action.id
? { ...user, ...action.updates }
: user
)
};
default:
return state;
}
}
Promise.finally()
// Always runs regardless of success or failure
async function uploadFile(file) {
showProgressBar();
try {
const result = await fetch('/api/upload', {
method: 'POST',
body: file
});
return await result.json();
} catch (err) {
showErrorMessage(err.message);
throw err;
} finally {
hideProgressBar(); // always runs
cleanupTempFiles(); // always runs
}
}
// Chaining
fetch('/api/data')
.then(r => r.json())
.then(processData)
.catch(handleError)
.finally(() => {
isLoading = false;
updateUI();
});
for-await-of
Iterating over async iterables (covered in detail in Chapter 6)
// Async API pagination
async function* getPages(url) {
let nextUrl = url;
while (nextUrl) {
const { data, next } = await fetch(nextUrl).then(r => r.json());
yield* data;
nextUrl = next;
}
}
for await (const item of getPages('/api/items')) {
process(item);
}
Regular Expression Improvements
// Named Capture Groups (ES2018)
const dateStr = '2024-03-15';
const { groups: { year, month, day } } =
dateStr.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);
console.log(year, month, day); // 2024 03 15
// Lookbehind Assertion
// ?<= : positive lookbehind
// ?<! : negative lookbehind
const prices = ['$100', '€200', '£300'];
const usd = prices.filter(p => /(?<=\$)\d+/.test(p)); // ['$100']
// dotAll flag (s)
const multiline = 'Hello\nWorld';
/Hello.World/.test(multiline); // false (by default . doesn't match newlines)
/Hello.World/s.test(multiline); // true (resolved with s flag)
ES2019 (ES10)
Array.flat() / flatMap()
// flat(): flatten nested arrays
const nested = [1, [2, 3], [4, [5, 6]]];
nested.flat(); // [1, 2, 3, 4, [5, 6]] - one level only
nested.flat(2); // [1, 2, 3, 4, 5, 6] - two levels
nested.flat(Infinity); // [1, 2, 3, 4, 5, 6] - fully flatten
// flatMap(): combines map + flat(1)
const sentences = ['Hello World', 'Foo Bar'];
const words = sentences.flatMap(s => s.split(' '));
// ['Hello', 'World', 'Foo', 'Bar']
// Old way (less efficient)
const words2 = sentences.map(s => s.split(' ')).flat();
// Real-world: handle nested arrays in API responses
const categories = [
{ name: 'Fruits', items: ['Apple', 'Banana'] },
{ name: 'Veggies', items: ['Carrot', 'Potato'] }
];
const allItems = categories.flatMap(cat => cat.items);
// ['Apple', 'Banana', 'Carrot', 'Potato']
// Filtering effect with empty arrays
const numbers = [1, 2, -3, 4, -5];
const positiveDoubled = numbers.flatMap(n => n > 0 ? [n * 2] : []);
// [2, 4, 8]
Object.fromEntries()
// Map → object
const map = new Map([['name', 'Alice'], ['age', 30]]);
const obj = Object.fromEntries(map);
// { name: 'Alice', age: 30 }
// [key, value] array → object
const entries = [['a', 1], ['b', 2], ['c', 3]];
Object.fromEntries(entries); // { a: 1, b: 2, c: 3 }
// Object transformation pipeline
const prices = { apple: 1000, banana: 500, cherry: 2000 };
// Apply 10% discount to all prices
const discounted = Object.fromEntries(
Object.entries(prices).map(([key, value]) => [key, value * 0.9])
);
// { apple: 900, banana: 450, cherry: 1800 }
// URLSearchParams → object
const params = new URLSearchParams('name=Alice&age=30&city=Seoul');
const paramsObj = Object.fromEntries(params);
// { name: 'Alice', age: '30', city: 'Seoul' }
String.trimStart() / trimEnd()
const str = ' Hello World ';
str.trim(); // 'Hello World' (both sides)
str.trimStart(); // 'Hello World ' (start only)
str.trimEnd(); // ' Hello World' (end only)
// Aliases: trimLeft/trimRight (previously non-standard, now standardized)
str.trimLeft(); // same as trimStart
str.trimRight(); // same as trimEnd
// Real-world: user input processing
function parseCSV(line) {
return line.split(',').map(cell => cell.trim());
}
parseCSV('Alice , 30 , Seoul '); // ['Alice', '30', 'Seoul']
Optional catch binding
// Before: had to declare catch variable even if not used
try {
JSON.parse(invalidJson);
} catch (e) { // had to declare e even without using it
isValidJson = false;
}
// ES2019: variable can be omitted
try {
JSON.parse(invalidJson);
} catch { // can be used without e
isValidJson = false;
}
// Real-world example
function isJson(str) {
try {
JSON.parse(str);
return true;
} catch {
return false;
}
}
Array.prototype.sort Stability Guarantee
// From ES2019, stable sort is guaranteed in all JS engines
const users = [
{ name: 'Charlie', age: 30 },
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 },
];
// When sorting by age, items with the same age preserve original order (stable sort)
users.sort((a, b) => a.age - b.age);
// Charlie and Bob have the same age, so their original order (Charlie → Bob) is preserved
ES2020 (ES11)
Optional Chaining (?.)
Prevents null/undefined errors when accessing nested objects.
const user = {
profile: {
address: {
city: 'Seoul'
}
}
};
// Old way
const city1 = user && user.profile && user.profile.address && user.profile.address.city;
// Optional Chaining
const city2 = user?.profile?.address?.city; // 'Seoul'
const zip = user?.profile?.address?.zip; // undefined (no error)
// Method calls
user?.profile?.getFullAddress?.(); // safe even if method doesn't exist
// Array access
const firstTag = user?.tags?.[0]; // safe even if array doesn't exist
// Dynamic property
const key = 'name';
const value = user?.profile?.[key];
// Optional Chaining + Nullish Coalescing combination
const displayName = user?.profile?.name ?? 'Anonymous';
const userCity = user?.address?.city ?? 'Location not set';
// In function calls
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
const data = await response?.json(); // returns undefined if response is null
return data?.user ?? null;
}
Nullish Coalescing (??)
// ?? : use default value only when null or undefined
// || : use default value for any falsy value (0, '', false, null, undefined)
const count = 0;
const name = '';
// || problem
count || 10; // 10 (count is 0, which is falsy → unintended result)
name || 'default'; // 'default' (empty string is also falsy)
// ?? solution
count ?? 10; // 0 (not null/undefined so original value is kept)
name ?? 'default'; // '' (empty string is also a valid value)
// Real-world
const userSettings = {
volume: 0, // 0 is a valid value
username: '', // empty string is a valid value
timeout: null, // null should use default
debug: undefined // undefined should use default
};
const volume = userSettings.volume ?? 50; // 0
const username = userSettings.username ?? 'Guest'; // ''
const timeout = userSettings.timeout ?? 5000; // 5000
const debug = userSettings.debug ?? false; // false
// ??= (Nullish Assignment) - ES2021
userSettings.timeout ??= 5000; // null, so set to 5000
userSettings.volume ??= 50; // 0, so no change
BigInt
// Handle integers larger than Number.MAX_SAFE_INTEGER
Number.MAX_SAFE_INTEGER; // 9007199254740991
// BigInt literal: n after the number
const bigNum = 9007199254740992n;
const trillion = 1_000_000_000_000n;
// BigInt constructor
BigInt(9007199254740992);
BigInt('9007199254740992');
// Operations (only between BigInts)
const a = 100n;
const b = 200n;
a + b; // 300n
a * b; // 20000n
b / a; // 2n (decimal part discarded)
b % a; // 0n
// Cannot mix with Number
// 100n + 100; // TypeError!
// Comparison is possible
100n == 100; // true (loose equality)
100n === 100; // false (different types)
100n > 99; // true
// Real-world: cryptography, large IDs
const id = 9007199254740993n;
const timestamp = BigInt(Date.now());
function factorial(n) {
if (n <= 1n) return 1n;
return n * factorial(n - 1n);
}
factorial(100n); // enormously large number
Promise.allSettled()
// Waits for all Promises to complete (regardless of success/failure)
const results = await Promise.allSettled([
fetch('/api/users').then(r => r.json()),
fetch('/api/invalid').then(r => r.json()), // fails
fetch('/api/products').then(r => r.json()),
]);
// Each result includes a status
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('Success:', result.value);
} else {
console.log('Failure:', result.reason);
}
});
// Filter only successes
const successes = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
globalThis
// Access the global object regardless of environment
// Browser: window
// Node.js: global
// Web Worker: self
// Old way (differs by environment)
const global = typeof window !== 'undefined' ? window
: typeof global !== 'undefined' ? global
: typeof self !== 'undefined' ? self
: this;
// ES2020: unified with globalThis
globalThis.setTimeout(() => {}, 0);
globalThis.fetch('/api/data');
console.log(globalThis === window); // true in browser
// Detect polyfill environment
if (typeof globalThis.Proxy === 'undefined') {
// add Proxy polyfill
}
String.matchAll()
// Returns an iterator of all regex matches (requires g flag)
const text = '2024-01-15, 2024-06-20, 2024-12-31';
const dateRegex = /(\d{4})-(\d{2})-(\d{2})/g; // g flag required
// Old exec approach (repetitive)
let match;
const dates = [];
while ((match = dateRegex.exec(text)) !== null) {
dates.push({ full: match[0], year: match[1], month: match[2], day: match[3] });
}
// matchAll approach (concise)
const allMatches = [...text.matchAll(dateRegex)];
const parsedDates = allMatches.map(match => ({
full: match[0],
year: match[1],
month: match[2],
day: match[3],
index: match.index
}));
// Combined with Named Groups
const namedRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/g;
for (const match of text.matchAll(namedRegex)) {
const { year, month, day } = match.groups;
console.log(`${year}-${month}-${day}`);
}
Version Summary
| Feature | Version | Description |
|---|---|---|
async/await | ES2017 | Async syntax revolution |
Object.entries/values | ES2017 | Convenient object iteration |
String.padStart/padEnd | ES2017 | String padding |
| Object spread/rest | ES2018 | Object merging/destructuring |
Promise.finally | ES2018 | Cleanup operations |
for await...of | ES2018 | Async iteration |
Array.flat/flatMap | ES2019 | Nested array processing |
Object.fromEntries | ES2019 | Entries → object conversion |
String.trim{Start,End} | ES2019 | Whitespace removal |
| Optional catch binding | ES2019 | Omit catch variable |
Optional Chaining ?. | ES2020 | Safe property access |
Nullish Coalescing ?? | ES2020 | Null default value |
| BigInt | ES2020 | Large integers |
Promise.allSettled | ES2020 | Wait for all to complete |
globalThis | ES2020 | Unified global object |
String.matchAll | ES2020 | All regex matches |
Expert Tips
1. Short-circuit evaluation with Optional Chaining
// Optional Chaining uses short-circuit evaluation
// If user is null in user?.profile, profile is not evaluated
const result = user?.profile?.expensiveMethod();
// If user is null, expensiveMethod() itself is never called
// Function arguments are also not evaluated
user?.log(expensiveCalculation());
// If user is null, expensiveCalculation() is also not executed
2. Nullish Coalescing Assignment chain
// Apply multiple default values in sequence
const config = {};
config.timeout ??= 5000;
config.retries ??= 3;
config.baseUrl ??= 'https://api.example.com';
// Or combine with Object.assign
const defaults = { timeout: 5000, retries: 3 };
const merged = { ...defaults, ...config };
3. BigInt serialization caution
// BigInt cannot be used with JSON.stringify
JSON.stringify({ id: 100n }); // TypeError!
// Solution: toJSON or replacer
const obj = { id: 100n };
JSON.stringify(obj, (key, value) =>
typeof value === 'bigint' ? value.toString() : value
);
// '{"id":"100"}'