ES2022 & ES2023 Features
.at() indexing, Object.hasOwn(), top-level await, private class fields, static blocks, error cause, RegExp match indices, .toSorted(), .toReversed(), .toSpliced(), .with(), .findLast(), .findLastIndex(), hashbang grammar, symbols as WeakMap keys.
Contents
ES2022 (ES13)
Array.prototype.at()
Access elements from the end with negative indices.
const arr = ['a', 'b', 'c', 'd', 'e'];
// Positive indices (same as bracket notation)
arr.at(0); // 'a'
arr.at(2); // 'c'
// Negative indices (from end)
arr.at(-1); // 'e' (last element)
arr.at(-2); // 'd' (second to last)
// Works on strings too
'hello'.at(-1); // 'o'
// Works on TypedArrays
new Uint8Array([1, 2, 3]).at(-1); // 3Migration pattern:
// ❌ Before
const last = arr[arr.length - 1];
const secondLast = arr[arr.length - 2];// ✅ After
const last = arr.at(-1);
const secondLast = arr.at(-2);Object.hasOwn()
Safe alternative to hasOwnProperty.
const obj = { name: 'Alice' };
// ✅ Safe and concise
Object.hasOwn(obj, 'name'); // true
Object.hasOwn(obj, 'toString'); // false (inherited)
// Works with objects that don't have hasOwnProperty
const nullProto = Object.create(null);
nullProto.key = 'value';
Object.hasOwn(nullProto, 'key'); // true
// nullProto.hasOwnProperty('key') // TypeError!Why not hasOwnProperty:
// hasOwnProperty can be overwritten
const malicious = {
hasOwnProperty: () => false,
secret: 'data'
};
malicious.hasOwnProperty('secret'); // false (wrong!)
Object.hasOwn(malicious, 'secret'); // true (correct!)Top-Level Await
Use await at module top level without async wrapper.
// config.js
const response = await fetch('/api/config');
export const config = await response.json();// db.js
import { config } from './config.js';
export const db = await connectDatabase(config.dbUrl);// app.js
import { db } from './db.js'; // Waits for db to be readyUse cases:
- Loading configuration
- Initializing database connections
- Dynamic module loading
- Conditional imports
Class Private Fields and Methods
True encapsulation with # prefix.
class BankAccount {
#balance = 0; // Private field
#transactionHistory = [];
constructor(initial) {
this.#balance = initial;
}
// Private method
#logTransaction(type, amount) {
this.#transactionHistory.push({ type, amount, date: new Date() });
}
deposit(amount) {
this.#balance += amount;
this.#logTransaction('deposit', amount);
}
withdraw(amount) {
if (amount > this.#balance) throw new Error('Insufficient funds');
this.#balance -= amount;
this.#logTransaction('withdrawal', amount);
}
get balance() {
return this.#balance;
}
// Private field check
static isAccount(obj) {
return #balance in obj;
}
}
const account = new BankAccount(100);
account.deposit(50);
// account.#balance // SyntaxError: Private field
// account.#logTransaction() // SyntaxError: Private methodStatic Class Fields and Blocks
class Config {
static #instance;
static version = '1.0.0';
static features = [];
// Static initialization block
static {
console.log('Initializing Config class');
this.features = ['auth', 'logging'];
if (process.env.NODE_ENV === 'development') {
this.features.push('debug');
}
}
static getInstance() {
if (!this.#instance) {
this.#instance = new Config();
}
return this.#instance;
}
}Error Cause
Chain errors with context.
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user', {
cause: { status: response.status, userId }
});
}
return await response.json();
} catch (error) {
throw new Error('User data unavailable', { cause: error });
}
}
try {
await fetchUserData(123);
} catch (error) {
console.error(error.message); // 'User data unavailable'
console.error(error.cause.message); // 'Failed to fetch user'
console.error(error.cause.cause); // { status: 404, userId: 123 }
}RegExp Match Indices
Get start/end positions with /d flag.
const str = 'Hello World';
const regex = /(?<greeting>Hello) (?<subject>World)/d;
const match = str.match(regex);
console.log(match.indices);
// [[0, 11], [0, 5], [6, 11]]
// [fullMatch, greeting, subject]
console.log(match.indices.groups);
// { greeting: [0, 5], subject: [6, 11] }ES2023 (ES14)
Array Change-by-Copy Methods
New methods that return modified copies without mutating the original.
toSorted()
const numbers = [3, 1, 4, 1, 5];
// ❌ Mutates original
const sorted = numbers.sort((a, b) => a - b);
console.log(numbers); // [1, 1, 3, 4, 5] - mutated!
// ✅ Returns new array
const numbers2 = [3, 1, 4, 1, 5];
const sorted2 = numbers2.toSorted((a, b) => a - b);
console.log(numbers2); // [3, 1, 4, 1, 5] - unchanged
console.log(sorted2); // [1, 1, 3, 4, 5] - new arraytoReversed()
const arr = [1, 2, 3, 4, 5];
// ❌ Mutates original
const reversed = arr.reverse();
// ✅ Returns new array
const arr2 = [1, 2, 3, 4, 5];
const reversed2 = arr2.toReversed();
console.log(arr2); // [1, 2, 3, 4, 5] - unchanged
console.log(reversed2); // [5, 4, 3, 2, 1] - new arraytoSpliced()
const months = ['Jan', 'Mar', 'Apr'];
// ❌ Mutates original
months.splice(1, 0, 'Feb');
// ✅ Returns new array
const months2 = ['Jan', 'Mar', 'Apr'];
const withFeb = months2.toSpliced(1, 0, 'Feb');
console.log(months2); // ['Jan', 'Mar', 'Apr'] - unchanged
console.log(withFeb); // ['Jan', 'Feb', 'Mar', 'Apr'] - new array
// Remove elements
const withoutMar = months2.toSpliced(1, 1);
// ['Jan', 'Apr']with()
Replace element at index.
const colors = ['red', 'green', 'blue'];
// ❌ Mutates original
colors[1] = 'yellow';
// ✅ Returns new array
const colors2 = ['red', 'green', 'blue'];
const updated = colors2.with(1, 'yellow');
console.log(colors2); // ['red', 'green', 'blue'] - unchanged
console.log(updated); // ['red', 'yellow', 'blue'] - new array
// Supports negative indices
const last = colors2.with(-1, 'purple');
// ['red', 'green', 'purple']findLast() and findLastIndex()
Search from the end of the array.
const numbers = [1, 2, 3, 2, 1];
// Find last occurrence
const lastTwo = numbers.findLast(n => n === 2); // 2
const lastTwoIdx = numbers.findLastIndex(n => n === 2); // 3
// Practical example
const transactions = [
{ id: 1, type: 'credit' },
{ id: 2, type: 'debit' },
{ id: 3, type: 'credit' },
{ id: 4, type: 'debit' }
];
const lastCredit = transactions.findLast(t => t.type === 'credit');
// { id: 3, type: 'credit' }
const lastCreditIdx = transactions.findLastIndex(t => t.type === 'credit');
// 2Hashbang Grammar
Executable scripts with shebang.
#!/usr/bin/env node
// This is now valid JavaScript syntax
console.log('Hello from CLI!');Symbols as WeakMap Keys
Use symbols (except registered) as WeakMap keys.
const privateData = new WeakMap();
const key = Symbol('private');
const obj = {};
privateData.set(key, { secret: 'data' });
console.log(privateData.get(key)); // { secret: 'data' }
// Registered symbols still not allowed
const registered = Symbol.for('global');
// privateData.set(registered, 'value'); // TypeErrorPractical Migrations
Sorting Collections
// Before: Clone then sort
const sortedUsers = [...users].sort((a, b) => a.name.localeCompare(b.name));// After: Single method
const sortedUsers = users.toSorted((a, b) => a.name.localeCompare(b.name));State Updates (React/Redux style)
// Before
function updateItem(items, index, newValue) {
const copy = [...items];
copy[index] = newValue;
return copy;
}// After
function updateItem(items, index, newValue) {
return items.with(index, newValue);
}Finding Last Match
// Before
const logs = getLogEntries();
let lastError = null;
for (let i = logs.length - 1; i >= 0; i--) {
if (logs[i].level === 'error') {
lastError = logs[i];
break;
}
}// After
const lastError = logs.findLast(entry => entry.level === 'error');