<!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2025-2026 Marcus Quinn -->

# 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.

## ES2022 (ES13)

### Array.prototype.at()

Access elements from the end with negative indices. Works on arrays, strings, and TypedArrays.

```javascript
const arr = ['a', 'b', 'c', 'd', 'e'];
arr.at(-1);   // 'e' (last element)
arr.at(-2);   // 'd'
'hello'.at(-1);  // 'o'
new Uint8Array([1, 2, 3]).at(-1);  // 3

// Migration: arr[arr.length - 1] → arr.at(-1)
```

### Object.hasOwn()

Safe alternative to `hasOwnProperty` — works on null-prototype objects and can't be overridden.

```javascript
const obj = { name: 'Alice' };
Object.hasOwn(obj, 'name');      // true
Object.hasOwn(obj, 'toString');  // false (inherited)

// Safe with null-prototype objects (hasOwnProperty would throw TypeError)
const nullProto = Object.create(null);
nullProto.key = 'value';
Object.hasOwn(nullProto, 'key');  // true

// Can't be spoofed (unlike obj.hasOwnProperty which 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.

```javascript
// config.js — loading configuration
const response = await fetch('/api/config');
export const config = await response.json();

// db.js — initializing connections
import { config } from './config.js';
export const db = await connectDatabase(config.dbUrl);

// app.js — modules wait for dependencies automatically
import { db } from './db.js';  // Waits for db to be ready
```

### Class Private Fields and Methods

True encapsulation with `#` prefix.

```javascript
class BankAccount {
  #balance = 0;
  #history = [];

  constructor(initial) { this.#balance = initial; }

  #log(type, amount) {
    this.#history.push({ type, amount, date: new Date() });
  }

  deposit(amount) { this.#balance += amount; this.#log('deposit', amount); }

  withdraw(amount) {
    if (amount > this.#balance) throw new Error('Insufficient funds');
    this.#balance -= amount;
    this.#log('withdrawal', amount);
  }

  get balance() { return this.#balance; }

  // Private field existence check via `in`
  static isAccount(obj) { return #balance in obj; }
}

const account = new BankAccount(100);
account.deposit(50);
// account.#balance       // SyntaxError: Private field
// account.#log()         // SyntaxError: Private method
```

### Static Class Fields and Blocks

```javascript
class Config {
  static #instance;
  static version = '1.0.0';
  static features = [];

  // Static initialization block — runs once when class is evaluated
  static {
    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 using the `cause` option.

```javascript
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) {
  error.message;        // 'User data unavailable'
  error.cause.message;  // 'Failed to fetch user'
  error.cause.cause;    // { status: 404, userId: 123 }
}
```

### RegExp Match Indices

Get start/end positions with `/d` flag.

```javascript
const match = 'Hello World'.match(/(?<greeting>Hello) (?<subject>World)/d);
match.indices;         // [[0, 11], [0, 5], [6, 11]]
match.indices.groups;  // { greeting: [0, 5], subject: [6, 11] }
```

---

## ES2023 (ES14)

### Array Change-by-Copy Methods

New methods return modified copies without mutating the original. Replaces the clone-then-mutate pattern.

```javascript
const nums = [3, 1, 4, 1, 5];
const months = ['Jan', 'Mar', 'Apr'];
const colors = ['red', 'green', 'blue'];

// toSorted() — immutable sort
const sorted = nums.toSorted((a, b) => a - b);  // [1, 1, 3, 4, 5]
console.log(nums);  // [3, 1, 4, 1, 5] — unchanged

// toReversed() — immutable reverse
const reversed = nums.toReversed();  // [5, 1, 4, 1, 3]

// toSpliced() — immutable splice (insert/remove)
const withFeb = months.toSpliced(1, 0, 'Feb');  // ['Jan', 'Feb', 'Mar', 'Apr']
const withoutMar = months.toSpliced(1, 1);       // ['Jan', 'Apr']

// with() — immutable index replacement (supports negative indices)
const updated = colors.with(1, 'yellow');  // ['red', 'yellow', 'blue']
const last = colors.with(-1, 'purple');    // ['red', 'green', 'purple']
```

**Migration patterns:**

```javascript
// Sorting: [...arr].sort(fn) → arr.toSorted(fn)
const sortedUsers = users.toSorted((a, b) => a.name.localeCompare(b.name));

// State updates (React/Redux): spread-copy-mutate → .with()
// Before: const copy = [...items]; copy[index] = newValue; return copy;
const updateItem = (items, index, val) => items.with(index, val);
```

### findLast() and findLastIndex()

Search from the end of the array.

```javascript
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');
// 2

// Migration: manual reverse loop → .findLast()
const lastError = logs.findLast(entry => entry.level === 'error');
```

### Hashbang Grammar

Executable scripts with shebang — now valid JavaScript syntax.

```javascript
#!/usr/bin/env node
console.log('Hello from CLI!');
```

### Symbols as WeakMap Keys

Use symbols (except registered) as WeakMap keys.

```javascript
const privateData = new WeakMap();
const key = Symbol('private');
privateData.set(key, { secret: 'data' });
privateData.get(key);  // { secret: 'data' }

// Registered symbols (Symbol.for()) still not allowed as WeakMap keys
```
