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

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

## ES2018 (ES9)

### Rest/Spread Properties for Objects

```javascript
// 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

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

### Promise.prototype.finally()

Run cleanup code regardless of Promise outcome.

```javascript
showSpinner();

fetch('/api/data')
  .then(res => res.json())
  .then(data => process(data))
  .catch(err => showError(err))
  .finally(() => hideSpinner());  // Always runs
```

### RegExp Named Capture Groups

```javascript
// Named groups — self-documenting
const dateRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const { year, month, day } = '2024-03-15'.match(dateRegex).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

```javascript
// 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
/(?<!€)\d+(\.\d{2})?/.test('€19.99');  // false
```

### RegExp dotAll Flag (s)

Make `.` match newlines too.

```javascript
/foo.bar/.test('foo\nbar');   // false
/foo.bar/s.test('foo\nbar');  // true
```

### RegExp Unicode Property Escapes

Match characters by Unicode property.

```javascript
/\p{Script=Greek}/u.test('π');   // true
/\p{Letter}/u.test('Ω');         // true
/\p{Emoji}/u.test('😀');          // true
/\p{Decimal_Number}/u;            // digits 0-9 in any script
/\p{Script=Han}/u;                // Chinese characters
```

### Template Literal Revision

Allow invalid escape sequences in tagged templates.

```javascript
function tag(strings) {
  console.log(strings[0]);      // undefined (invalid escape → cooked is undefined)
  console.log(strings.raw[0]);  // '\\unicode'
}
tag`\unicode`;  // No longer throws SyntaxError
```

---

## ES2019 (ES10)

### Array.prototype.flat()

Flatten nested arrays.

```javascript
[1, [2, [3, [4]]]].flat();           // [1, 2, [3, [4]]]  (default: 1 level)
[1, [2, [3, [4]]]].flat(2);          // [1, 2, 3, [4]]
[1, [2, [3, [4]]]].flat(Infinity);   // [1, 2, 3, 4]
[1, , 3, , 5].flat();                // [1, 3, 5]  (removes holes)
```

### Array.prototype.flatMap()

Map then flatten one level.

```javascript
const sentences = ['Hello world', 'How are you'];

sentences.map(s => s.split(' '));
// [['Hello', 'world'], ['How', 'are', 'you']]

sentences.flatMap(s => s.split(' '));
// ['Hello', 'world', 'How', 'are', 'you']

// Filter by returning empty array
[1, 2, 3, 4].flatMap(n => n % 2 ? [n] : []);  // [1, 3]
```

### Object.fromEntries()

Create object from key-value pairs (inverse of Object.entries).

```javascript
// 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 values
const prices = { apple: 1, banana: 2 };
Object.fromEntries(Object.entries(prices).map(([k, v]) => [k, v * 2]));
// { apple: 2, banana: 4 }

// Filter object entries
Object.fromEntries(Object.entries(obj).filter(([, v]) => v > 0));
```

### String.prototype.trimStart() and trimEnd()

```javascript
const str = '  Hello World  ';
str.trimStart();  // 'Hello World  '
str.trimEnd();    // '  Hello World'
str.trim();       // 'Hello World'
```

### Optional Catch Binding

Omit the error parameter if not needed.

```javascript
// Before: always needed parameter
try { JSON.parse(input); } catch (e) { return null; }

// Now optional
try { JSON.parse(input); } catch { return null; }
```

### Symbol.prototype.description

```javascript
const sym = Symbol('mySymbol');
sym.toString();    // 'Symbol(mySymbol)'
sym.description;   // 'mySymbol'  (direct access, ES2019+)
Symbol().description;   // undefined
```

### Stable Array.prototype.sort()

Sort is now guaranteed stable — equal elements maintain relative order.

```javascript
const items = [
  { name: 'Alice', age: 30 },
  { name: 'Bob', age: 30 },
  { name: 'Charlie', age: 25 },
];

// Alice and Bob maintain relative order after sort by age
// Note: Use .toSorted() in ES2023+ to avoid mutation
items.toSorted((a, b) => a.age - b.age);
// [{ Charlie, 25 }, { Alice, 30 }, { Bob, 30 }]
```

### Well-formed JSON.stringify()

Properly encode lone surrogates as escape sequences.

```javascript
JSON.stringify('\uD800');  // '"\\ud800"' (valid — was invalid before)
```

### Function.prototype.toString() Revision

Returns exact source code including whitespace and comments.

```javascript
function /* comment */ foo() { }
foo.toString();  // 'function /* comment */ foo() { }'
```
