跪拜 Guibai
← Back to the summary

JavaScript from ES6 to ES2026: What Each Version Actually Changed

Comprehensive Advanced Guide from ES6 to ES2026: New Features, Principles, Examples, and Engineering Implementation

Author: 王林不想说话 Tags: JavaScript, Interview, Frontend

Many people call let, arrow functions, and Promise "ES6," and also loosely refer to optional chaining, async/await, and even all modern JavaScript as ES6. While convenient for communication, this easily confuses the version a feature belongs to, the degree of runtime environment support, and the responsibilities of build tools.

This article starts from ECMAScript 2015, commonly known as ES6, and goes all the way to ECMAScript 2026. For every important feature, it answers five questions:

  1. What problem does it solve?
  2. What is the basic syntax?
  3. What are the operational rules?
  4. Where does it apply in work?
  5. What are the compatibility issues and common misconceptions?

The article only includes capabilities that have entered the official annual standard in the ES2015 to ES2026 mainline. Syntax still in the TC39 proposal stage is explained separately at the end, avoiding confusion between Babel experimental plugins, framework syntax, and the ECMAScript standard.

1. Before We Begin: What Exactly Is ES6

1. ECMAScript, JavaScript, and the Host Environment

ECMAScript is the language specification, defining language rules such as syntax, types, objects, modules, and Promise. JavaScript is the most widespread implementation and usage name for ECMAScript.

Browsers also provide host APIs outside the ECMAScript specification:

ECMAScript: Array, Map, Promise, Proxy, import, class
Web API: DOM, fetch, setTimeout, localStorage, Web Worker
Node.js API: fs, http, process, Buffer

Therefore, Promise is an ECMAScript feature, but fetch is not; async/await is language syntax, but whether a network request can be canceled depends on host capabilities like fetch and AbortController.

2. Why ES6 Is Also Called ES2015

The sixth edition of ECMAScript was released in 2015, so it is simultaneously called ES6 and ES2015. Since then, the specification has adopted an annual release cadence: ES2016, ES2017, all the way to ES2026, officially released in June 2026.

ES2015 was a very large upgrade, adding block scope, arrow functions, classes, modules, Promise, iterators, generators, Map, Set, Proxy, and many other capabilities. Subsequent versions are usually more focused, absorbing a batch of mature proposals each year.

3. What TC39 Proposal Stages Mean

New language features typically go through the following stages:

Stage 0: Idea
Stage 1: The committee is willing to discuss the problem and direction
Stage 2: Core design has formed, but significant changes are still possible
Stage 2.7: Specification text is complete, awaiting verification and testing
Stage 3: Specification is basically stable, awaiting implementation feedback
Stage 4: Complete, meets the conditions for entering the formal standard

Engineering judgment cannot just look at "Chrome can run it": browsers may implement experimental features early, and Babel may provide transformation plugins. Production code should comprehensively confirm whether a feature is Stage 4, whether target browsers and Node.js support it, whether build tools can transform it, and whether runtime polyfills are still needed.

4. Transpilation and Polyfilling Are Not the Same Thing

Transpilation is responsible for rewriting new syntax into old syntax, for example, turning arrow functions into regular functions; polyfilling is responsible for supplementing runtime APIs that do not exist in old environments, such as Array.prototype.includes.

// New syntax: can be converted by build tools
const add = (a, b) => a + b

// New API: old environments need a polyfill; simply rewriting syntax cannot create this method
[1, 2, 3].includes(2)

Low-level semantics like Proxy are very difficult to polyfill completely. When targeting old environments, you must analyze specific features case by case and cannot assume "using Babel means full compatibility."

2. ES2015 (ES6): The Foundation of Modern JavaScript

2.1 let, const, and Block Scope

Concept

var only has function scope and allows redeclaration; let and const introduce block scope. Blocks include function bodies, if, for, while, and standalone {}.

if (true) {
  let message = 'inside'
  const version = 2015
}

// console.log(message) // ReferenceError

const means the binding cannot be reassigned to another value, not that the inside of an object cannot be modified:

const user = { name: 'Alice' }
user.name = 'Bob' // legal

// user = {} // TypeError

Temporal Dead Zone

Bindings for let and const are created upon entering the scope, but are not initialized before the declaration statement is executed. This region is called the Temporal Dead Zone (TDZ):

{
  // console.log(count) // ReferenceError
  let count = 1
}

Don't simply memorize "let does not hoist." More accurately: the binding is created, but cannot be read before initialization.

Independent Bindings in Loops

let in a for loop creates an independent binding for each iteration:

for (let index = 0; index < 3; index += 1) {
  setTimeout(() => console.log(index), 0)
}

// 0 1 2

If var is used, the three callbacks share the same binding, and by the time they execute the loop has already finished, typically outputting 3 3 3.

Practical Application

2.2 Arrow Functions

Basic Syntax

const add = (a, b) => a + b

const normalizeUser = (user) => ({
  id: String(user.id),
  name: user.name.trim(),
})

When returning an object literal, parentheses are needed; otherwise, {} will be parsed as the function body.

Arrow Functions Do Not Have Their Own this

Arrow functions capture this from the defining location and do not change due to call, apply, or object method invocation:

class Timer {
  seconds = 0

  start() {
    this.id = setInterval(() => {
      this.seconds += 1
    }, 1000)
  }
}

Using an arrow function here is very appropriate because the callback needs to continue using the instance this from the start method.

But when an object method needs to dynamically receive the caller, arrow functions should not be used:

const user = {
  name: 'Alice',
  show: () => console.log(this.name),
}

user.show() // usually not Alice

Arrow functions also lack their own arguments, super, and new.target, cannot be used as constructors, and do not have a prototype usable by new.

Practical Application

2.3 Default Parameters, Rest Parameters, and Spread Syntax

Default Parameters

Default values only take effect when the argument is undefined or not passed; passing null does not trigger the default:

function createRequest(url, timeout = 5000) {
  return { url, timeout }
}

createRequest('/users')            // timeout: 5000
createRequest('/users', undefined) // timeout: 5000
createRequest('/users', null)      // timeout: null

Default parameters can reference preceding parameters:

function createRange(start, end = start + 10) {
  return { start, end }
}

Object configuration parameters are often paired with destructuring and an overall default value:

function request(url, { timeout = 5000, retry = 0 } = {}) {
  // ...
}

request('/api/users')

The trailing = {} is important; otherwise, destructuring undefined when the second argument is missing will throw an error.

Rest Parameters

Rest parameters collect excess arguments into a true array:

function sum(first, ...rest) {
  return rest.reduce((total, value) => total + value, first)
}

sum(1, 2, 3) // 6

Rest parameters must be last, and a function can only have one. They are more explicit than arguments and can directly call array methods.

Spread Syntax

Spread syntax expands an iterable object into arguments or an array:

const base = [1, 2]
const result = [0, ...base, 3]

Math.max(...result) // 3

Array spread in ES2015 requires the value to be iterable. Object spread is a feature officially added in ES2018.

Spread is not a deep copy:

const source = [{ id: 1 }]
const copied = [...source]

copied[0].id = 2
console.log(source[0].id) // 2

In practice, it's commonly used for merging arguments, constructing new arrays, and shallow immutable updates. For very large arrays, avoid fn(...items) directly, as engines have limits on the number of arguments.

2.4 Destructuring Assignment

Array Destructuring by Position

const [first, second = 'default', ...rest] = ['A']

console.log(first)  // A
console.log(second) // default
console.log(rest)   // []

You can skip unwanted positions and swap variables:

const [, secondItem] = items

let left = 1
let right = 2
;[left, right] = [right, left]

If the previous line might connect with (, [, template strings, etc., pay attention to Automatic Semicolon Insertion. The semicolon before the example is used to explicitly terminate the previous statement.

Object Destructuring by Property Name

const user = { id: 1, profile: { nickname: 'Alice' } }

const {
  id: userId,
  profile: { nickname },
  role = 'guest',
} = user

id: userId means read id and store it in the variable userId; it does not simultaneously declare id.

Default values also only take effect when the result is undefined:

const { count = 10 } = { count: null }
console.log(count) // null

Practical Application and Misconceptions

2.5 Template Strings and Tagged Templates

Template Strings

const message = `User ${user.name} has ${orders.length} orders`

const html = `
  <section>
    <h2>${title}</h2>
  </section>
`

Interpolation expressions are evaluated normally and converted to strings. Template strings do not automatically prevent XSS; inserting user input into HTML and then passing it to innerHTML is still dangerous.

Tagged Templates

Tagged templates separate static segments and dynamic values, passing them to a function:

function sql(strings, ...values) {
  return {
    text: strings.join('?'),
    values,
  }
}

const query = sql`
  SELECT * FROM users
  WHERE id = ${userId} AND status = ${status}
`

The tag function receives strings as an array of static text and values as the interpolation results. Actual database queries should use the parameterized APIs of mature drivers; this example demonstrates how tagged templates separate structure from values, not that hand-writing a SQL security library is safe.

In practice, this is seen in CSS-in-JS, GraphQL queries, internationalization, and security escaping tools.

2.6 Object Literal Enhancements

Property Shorthand and Method Shorthand

const id = 1
const name = 'Alice'

const user = {
  id,
  name,
  greet() {
    return `Hi, ${this.name}`
  },
}

Method shorthand is not just about writing less function; it also allows the use of super in object inheritance scenarios.

Computed Property Names

const field = 'mobile'

const form = {
  [field]: '13800138000',
  [`${field}Error`]: '',
}

Practical applications include dynamic forms, indexing by ID, state updates, and protocol field generation:

function updateField(state, name, value) {
  return {
    ...state,
    [name]: value,
  }
}

2.7 class, Inheritance, and super

class Is Syntax for the Prototype Mechanism

class User {
  constructor(name) {
    this.name = name
  }

  greet() {
    return `Hi, ${this.name}`
  }

  static createGuest() {
    return new User('Guest')
  }
}

Instance methods are actually located on User.prototype:

const a = new User('A')
const b = new User('B')

a.greet === b.greet // true
Object.getPrototypeOf(a) === User.prototype // true

Class declarations have a TDZ, class bodies are strict mode by default, and classes must be called with new.

Inheritance

class Admin extends User {
  constructor(name, permissions) {
    super(name)
    this.permissions = permissions
  }

  greet() {
    return `${super.greet()}, Permissions: ${this.permissions.join(',')}`
  }
}

Derived class constructors must call super() before accessing this, because the base class construction process is responsible for creating and initializing the instance.

Practical Application

2.8 Symbol

Symbol() creates a unique primitive value, commonly used to avoid property name conflicts:

const cacheKey = Symbol('cache')
const anotherKey = Symbol('cache')

cacheKey === anotherKey // false

const target = {
  [cacheKey]: new Map(),
}

Global Symbol registry:

const a = Symbol.for('app.user')
const b = Symbol.for('app.user')

a === b // true
Symbol.keyFor(a) // 'app.user'

Built-in well-known symbols provide extension points for language protocols, such as Symbol.iterator, Symbol.toPrimitive, Symbol.toStringTag, and Symbol.hasInstance.

Symbol properties do not appear in Object.keys and ordinary for...in, but can be obtained via Object.getOwnPropertySymbols or Reflect.ownKeys, so they are not an absolute privacy mechanism.

2.9 Iterator, Iterable, and for...of

Two Protocols

An iterable object implements [Symbol.iterator](); this method returns an iterator. An iterator implements next(), returning { value, done } each time.

const range = {
  from: 1,
  to: 3,

  [Symbol.iterator]() {
    let current = this.from
    const end = this.to

    return {
      next() {
        if (current <= end) {
          return { value: current++, done: false }
        }
        return { value: undefined, done: true }
      },
    }
  },
}

console.log([...range]) // [1, 2, 3]

Arrays, strings, Map, and Set are all built-in iterable objects. for...of consumes values, while for...in enumerates property keys; the two cannot be mixed:

for (const value of ['a', 'b']) {
  console.log(value) // a, b
}

for (const key in ['a', 'b']) {
  console.log(key) // 0, 1, and the type is string
}

Practical Application

When implementing custom pagination cursors, tree traversal, data streams, and lazy sequences, you can uniformly connect to language capabilities like spread syntax, destructuring, and for...of. Note: iterators are usually one-time consumption; re-traversal may require re-creation.

2.10 Generator

Calling a generator function does not immediately execute the function body; instead, it returns a generator object. Each next() call runs the function to the next yield:

function* createIds() {
  let id = 1

  while (true) {
    yield id
    id += 1
  }
}

const ids = createIds()
ids.next() // { value: 1, done: false }
ids.next() // { value: 2, done: false }

yield* can delegate to another iterable object:

function* combined() {
  yield 'start'
  yield* [1, 2, 3]
  yield 'end'
}

The value passed into next(value) becomes the result of the previous yield expression, allowing generators to express bidirectional control flow, but also increasing the cost of understanding.

In practice, they can be used for lazy sequences, state machines, tree traversal, test data generation, and flow control. For ordinary asynchronous business, async/await is usually preferred now; Generator should not be used just because it seems "advanced."

2.11 Promise

State Model

A Promise represents a future result:

pending -> fulfilled
pending -> rejected

Once the state is determined, it cannot change. The executor in the constructor executes synchronously immediately, while reactions registered via then execute asynchronously as microtasks:

console.log('A')

const promise = new Promise((resolve) => {
  console.log('B')
  resolve(1)
})

promise.then(() => console.log('C'))
console.log('D')

// A B D C

Chaining

Each then returns a new Promise:

loadUser(id)
  .then((user) => loadOrders(user.id))
  .then((orders) => orders.filter((order) => order.paid))
  .then(renderOrders)
  .catch(showError)

If the callback returns a plain value, the new Promise fulfills with that value; if it returns a Promise or thenable, the new Promise adopts its final state; if it throws an exception, the new Promise rejects.

Forgetting return causes the subsequent chain to continue prematurely:

loadUser(id).then((user) => {
  return loadOrders(user.id)
})

ES2015 Static Methods

Promise.resolve(value)
Promise.reject(error)
Promise.all([requestA(), requestB()])
Promise.race([request(), timeout(5000)])

Promise.all is suitable for tasks that are independent of each other and must all succeed. It rejects quickly when one item fails, but does not automatically cancel the remaining tasks. Promise.race also does not cancel the losing tasks.

Promise solves the problems of asynchronous result composition and error propagation, but does not automatically parallelize CPU computation, nor does it have a unified cancellation protocol.

2.12 ES Module

Named Exports and Default Exports

// math.js
export const PI = 3.14159

export function add(a, b) {
  return a + b
}
// app.js
import { add, PI } from './math.js'
// createApp.js
export default function createApp() {}

// main.js
import createApp from './createApp.js'

Named exports facilitate automatic refactoring, static analysis, and unified naming; default exports are suitable when a module truly has only one core concept.

Static Structure and Live Bindings

Static import and export are at the module top level, allowing tools to build a dependency graph before execution. Imports are live bindings, not simple copies of values:

// counter.js
export let count = 0
export const increment = () => {
  count += 1
}
import { count, increment } from './counter.js'

console.log(count) // 0
increment()
console.log(count) // 1

The importing side cannot directly assign to count, but can observe updates to the binding made by the exporting module.

Practical Application

2.13 Map, Set, WeakMap, and WeakSet

Map

Map keys can be any value, preserve insertion order, and provide a clear size:

const userById = new Map()

userById.set('1001', { name: 'Alice' })
userById.set(1001, { name: 'Bob' })

userById.size // 2, string and number are different keys
userById.get('1001')

Map is suitable for dynamic key-value collections, frequent additions/deletions, object key caching, and indexing. Domain objects with fixed fields are still better suited to plain objects.

Set

const selectedIds = new Set(['1', '2'])

selectedIds.add('3')
selectedIds.has('2') // true
selectedIds.delete('1')

Set is suitable for unique value collections, selection states, permission sets, and deduplication:

const uniqueIds = [...new Set(ids)]

Weak Collections

WeakMap and WeakSet hold weak references to objects, do not prevent objects from being garbage collected, and are not enumerable:

const metadata = new WeakMap()

function getMetadata(element) {
  if (!metadata.has(element)) {
    metadata.set(element, { clickCount: 0 })
  }
  return metadata.get(element)
}

Suitable for attaching lifecycle-consistent metadata to DOM nodes, class instances, or third-party objects. Non-enumerability is to avoid observing garbage collection timing.

2.14 Proxy and Reflect

Proxy

Proxy can intercept fundamental internal operations on objects:

const state = new Proxy(
  { count: 0 },
  {
    get(target, key, receiver) {
      console.log('Reading', key)
      return Reflect.get(target, key, receiver)
    },

    set(target, key, value, receiver) {
      if (key === 'count' && value < 0) {
        throw new RangeError('count cannot be less than 0')
      }
      return Reflect.set(target, key, value, receiver)
    },
  },
)

Common traps include get, set, has, deleteProperty, ownKeys, apply, and construct.

Reflect

Reflect turns many low-level object operations into functions and returns more unified results:

Reflect.get(target, key, receiver)
Reflect.set(target, key, value, receiver)
Reflect.has(target, key)
Reflect.ownKeys(target)
Reflect.construct(User, ['Alice'])

Using Reflect in Proxy traps preserves default semantics and correctly passes receiver.

Practical Application and Boundaries

2.15 Common New Built-in APIs in ES2015

Array

Array.from(arrayLike, mapFn)
Array.of(1, 2, 3)

users.find((user) => user.id === id)
users.findIndex((user) => user.id === id)

new Array(3).fill(0)
items.copyWithin(target, start, end)

Array.from can turn array-like or iterable objects into arrays and map during creation:

const indexes = Array.from({ length: 5 }, (_, index) => index)
// [0, 1, 2, 3, 4]

fill with objects repeats the same reference:

const wrong = new Array(3).fill({ selected: false })
wrong[0].selected = true
console.log(wrong[1].selected) // true

const correct = Array.from(
  { length: 3 },
  () => ({ selected: false }),
)

Object

Object.assign(target, sourceA, sourceB)
Object.is(NaN, NaN) // true
Object.setPrototypeOf(target, prototype)

Object.assign copies own enumerable properties and invokes setters on the target object; it is a shallow operation. For immutable updates, prefer writing to a new object:

const next = Object.assign({}, defaults, options)

String and Unicode

text.includes(keyword)
text.startsWith('https://')
text.endsWith('.json')
'ha'.repeat(3) // hahaha

'😀'.codePointAt(0)
String.fromCodePoint(0x1f600)
'\u{1F600}' // 😀, requires u semantic support

ES2015's Unicode improvements alleviate UTF-16 surrogate pair handling issues, but string.length still counts UTF-16 code units, not the number of characters a user sees.

Number and Math

Number.isNaN(value)
Number.isFinite(value)
Number.isInteger(value)
Number.isSafeInteger(value)
Number.parseInt('10', 10)

Math.trunc(3.9) // 3
Math.sign(-5)   // -1
Math.cbrt(8)    // 2
Math.hypot(3, 4) // 5

Number.isNaN and Number.isFinite do not perform implicit conversion first, usually making them more suitable for type-explicit business validation than the global functions of the same name.

Binary and octal literals:

0b1010 // 10
0o755  // 493

RegExp u and y Flags

u enables more complete Unicode semantics; y is sticky mode, matching only starting from the position specified by lastIndex, suitable for continuous parsing scenarios like lexers.

const token = /\d+/y
token.lastIndex = 2
token.exec('ab12cd') // matches 12

These capabilities are not as high-frequency as arrays and Promise in ordinary pages, but are important in parsers, editors, internationalized text processing, and low-level libraries.

2.16 Engineering Summary of ES2015

What ES2015 truly changed was not just syntax length, but the way programs are organized:

let/const       -> clearer scope and bindings
Arrow functions -> lighter callbacks and lexical this
Destructuring/default params -> clearer data boundaries
class           -> unified construction and inheritance expression
Iterator        -> unified data consumption protocol
Promise         -> composable asynchronous results
ES Module       -> static dependencies and module boundaries
Map/Set         -> more accurate data structures
Proxy/Reflect   -> metaprogramming and framework infrastructure

Modern projects should not cram every feature into code just to "use ES6." The correct approach is to let syntax express real semantics: use const for stable bindings, Map for dynamic key-value collections, Set for unique collections, ESM when a static dependency graph is needed, and Promise when asynchronous composition is needed.

3. ES2016: Two High-Frequency Capabilities in a Small Release

3.1 Exponentiation Operator **

2 ** 3 // 8
2 ** 3 ** 2 // 512

The exponentiation operator is right-associative; the second expression is equivalent to:

2 ** (3 ** 2)

It corresponds to the common use of Math.pow:

Math.pow(base, exponent)
base ** exponent

Compound assignment:

let value = 3
value **= 2
console.log(value) // 9

Parentheses are needed when combining unary negation with exponentiation:

-(2 ** 2) // -4
(-2) ** 2 // 4

In practice, it can be used for compound interest, distance, area, statistics, and algorithmic calculations. Ordinary business logic should not sacrifice readability for fewer characters; complex mathematical expressions should be broken into intermediate variables with business names.

3.2 Array.prototype.includes

includes determines whether an array contains a certain value:

const roles = ['admin', 'editor']

roles.includes('admin') // true
roles.includes('guest') // false

The second parameter specifies the starting search position and supports negative numbers:

['a', 'b', 'c'].includes('a', 1)  // false
['a', 'b', 'c'].includes('c', -1) // true

An important difference from indexOf is that it can find NaN:

[NaN].indexOf(NaN)  // -1
[NaN].includes(NaN) // true

The reason is that includes uses SameValueZero comparison, where NaN is considered equal to itself, and 0 and -0 are also considered equal.

Practical Application

const ALLOWED_STATUSES = ['draft', 'published', 'archived']

if (!ALLOWED_STATUSES.includes(status)) {
  throw new Error('Unsupported status')
}

4. ES2017: Asynchronous Code Enters the Era of Linear Expression

4.1 async/await

Basic Concept

async/await is built on top of Promise, allowing asynchronous flows to be written in a structure close to synchronous code. async functions always return a Promise:

async function getValue() {
  return 42
}

getValue().then(console.log) // 42

If the function throws an error, the returned Promise rejects:

async function fail() {
  throw new Error('failed')
}

fail().catch((error) => console.log(error.message))

Pausing and Resuming with await

await expression first processes the expression according to Promise semantics. The current async function pauses, yielding control back to the caller; once the result is determined, the second half of the function resumes via a microtask:

async function run() {
  console.log('B')
  const value = await Promise.resolve(1)
  console.log('D', value)
}

console.log('A')
run()
console.log('C')

// A B C D 1

await does not block the entire JavaScript thread; it only pauses the current async function. Other synchronous code, events, and tasks can still proceed.

Error Handling

async function loadUserPage(id) {
  startLoading()

  try {
    const user = await fetchUser(id)
    const orders = await fetchOrders(user.id)
    return { user, orders }
  } catch (error) {
    throw new Error('Failed to load user page', { cause: error })
  } finally {
    stopLoading()
  }
}

A rejected Promise from await manifests as an exception thrown in the current function, so try/catch/finally can be used. Do not catch and just log at every layer, otherwise the upper layer will mistakenly think the operation succeeded.

Serial, Parallel, and Dependencies

Serial when there are dependencies:

const user = await fetchUser(id)
const orders = await fetchOrders(user.id)

Parallel when there are no dependencies:

const [user, config, permissions] = await Promise.all([
  fetchUser(id),
  fetchConfig(),
  fetchPermissions(id),
])

The following writing unintentionally serializes:

const user = await fetchUser(id)
const config = await fetchConfig()

If two requests take 800ms and 600ms respectively, serial takes about 1400ms, while parallel is determined by the slower 800ms.

await in Loops

Serial processing:

for (const file of files) {
  await upload(file)
}

Full parallel:

await Promise.all(files.map((file) => upload(file)))

Do not use forEach(async () => {}) to wait for tasks:

// The outer scope will not wait for the Promise returned by the callback
files.forEach(async (file) => {
  await upload(file)
})

When the number of batch tasks is large, concurrency should be limited instead of firing all requests at once:

async function runPool(tasks, limit = 4) {
  const results = new Array(tasks.length)
  let nextIndex = 0

  async function worker() {
    while (nextIndex < tasks.length) {
      const index = nextIndex
      nextIndex += 1
      results[index] = await tasks[index]()
    }
  }

  const workers = Array.from(
    { length: Math.min(limit, tasks.length) },
    () => worker(),
  )

  await Promise.all(workers)
  return results
}

Practical Application

async/await is just control flow syntax for Promise; it does not provide timeout, cancellation, retry, or concurrency limiting. These capabilities still need to be implemented through AbortSignal, schedulers, and business strategies.

4.2 Object.values and Object.entries

const scoreByUser = {
  alice: 90,
  bob: 80,
}

Object.values(scoreByUser)  // [90, 80]
Object.entries(scoreByUser) // [['alice', 90], ['bob', 80]]

Paired with for...of:

for (const [name, score] of Object.entries(scoreByUser)) {
  console.log(`${name}: ${score}`)
}

They only process the object's own enumerable string-keyed properties, excluding inherited properties and Symbol keys. Numeric property keys remain strings in the results.

Practical Application

const query = new URLSearchParams(
  Object.entries(filters).filter(([, value]) => value != null),
)

Suitable for configuration traversal, dictionary conversion, query parameters, form error lists, and statistical aggregation. When keys of arbitrary types are needed, use Map instead of repeatedly converting between objects and entries.

4.3 String Padding

'7'.padStart(2, '0') // '07'
'JS'.padEnd(5, '.')  // 'JS...'

The target length is calculated by UTF-16 code units; the padding string is repeated and truncated if necessary:

'A'.padStart(5, '01') // '0101A'

In practice, it can be used for times, serial numbers, table text, and log formatting:

const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')

For currency and localized number display, prefer Intl.NumberFormat; do not hand-write padding and separator logic.

4.4 Object.getOwnPropertyDescriptors

Object.assign and object spread mainly copy values, not fully preserving property descriptors like getters, setters, and enumerability:

const source = {
  get total() {
    return 100
  },
}

const descriptors = Object.getOwnPropertyDescriptors(source)
const clone = Object.defineProperties({}, descriptors)

Suitable for frameworks, decorators, mixins, and low-level tools that need to precisely copy property semantics. Ordinary business objects usually do not need to add this complexity.

4.5 Trailing Commas in Function Parameters and Calls

function createUser(
  id,
  name,
) {}

createUser(
  1,
  'Alice',
)

Trailing commas make Git diffs cleaner when adding new parameters and facilitate formatters unifying multi-line structures. No comma can be added after a rest parameter.

4.6 SharedArrayBuffer and Atomics

SharedArrayBuffer allows multiple execution agents to share the same block of memory, and Atomics provides atomic read/write and synchronization capabilities:

const shared = new SharedArrayBuffer(4)
const counter = new Int32Array(shared)

Atomics.add(counter, 0, 1)
Atomics.load(counter, 0) // 1

It solves the problem of truly concurrent reads and writes, not ordinary page state management. Typical scenarios include parallel computation, WebAssembly, low-level runtimes, and high-performance processing.

Shared memory brings risks of race conditions, visibility issues, and deadlocks. Browsers also typically require cross-origin isolation headers. Ordinary forms, lists, and business state should not use it.

5. ES2018: Asynchronous Streams, Object Spread, and Comprehensive Regex Enhancements

5.1 Asynchronous Iterators and for await...of

A synchronous iterator's next() returns a plain result; an asynchronous iterator's next() returns a Promise:

const asyncIterable = {
  [Symbol.asyncIterator]() {
    let page = 1

    return {
      async next() {
        if (page > 3) {
          return { done: true, value: undefined }
        }

        const value = await fetchPage(page)
        page += 1
        return { done: false, value }
      },
    }
  },
}

Consumption method:

for await (const pageData of asyncIterable) {
  renderPage(pageData)
}

More common is the async generator:

async function* paginate(loadPage) {
  let page = 1

  while (true) {
    const result = await loadPage(page)

    yield* result.items

    if (!result.hasNext) return
    page += 1
  }
}

for await (const item of paginate(fetchOrders)) {
  await processOrder(item)
}

Practical Application

for await...of waits for each item by default, suitable for streams with ordering or backpressure needs. For finite, mutually independent tasks where throughput is the goal, controlled concurrency should be used.

5.2 Object Rest and Spread

Object Spread

const defaults = {
  timeout: 5000,
  retry: 0,
}

const options = {
  ...defaults,
  retry: 2,
}

Later properties with the same name override earlier ones. Object spread copies own enumerable properties, including enumerable Symbol keys, but does not copy prototype and non-enumerable properties.

Object Rest Properties

const { password, token, ...publicUser } = user

Suitable for excluding sensitive fields from an object, but note this is a shallow new object: nested objects still share references.

undefined Overrides Default Values

const merged = {
  timeout: 5000,
  ...{ timeout: undefined },
}

console.log(merged.timeout) // undefined

Object spread overrides based on "whether the property exists," not automatically ignoring undefined. If configuration merging should only accept valid values, filter first or explicitly use ??.

Practical Application

const payload = {
  name: form.name,
  ...(form.mobile ? { mobile: form.mobile } : {}),
}

Deep state still requires copying each changed level; do not treat object spread as a deep copy.

5.3 Promise.prototype.finally

finally executes regardless of whether the Promise succeeds or fails, suitable for cleanup and ending states:

startLoading()

saveForm()
  .then(showSuccess)
  .catch(showError)
  .finally(stopLoading)

The finally callback does not receive the success value or failure reason; returning normally preserves the original result:

Promise.resolve('data')
  .finally(() => console.log('cleanup'))
  .then(console.log) // data

If finally throws an error or returns a rejected Promise, the new error overrides the original result. Therefore, the cleanup function itself should also be reliable; do not execute unrelated high-risk business logic within it.

5.4 Regular Expression Enhancements

dotAll s Flag

By default, . does not match newline characters; the s flag makes it match newlines:

/start.*end/.test('start\nend')  // false
/start.*end/s.test('start\nend') // true

For parsing HTML, JSON, and code, prefer structured parsers; do not rely on fragile giant regexes just because dotAll is more convenient.

Named Capture Groups

const result = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/.exec(
  '2026-08-13',
)

console.log(result.groups.year)  // 2026
console.log(result.groups.month) // 08

Names can be used in replacements:

'2026-08-13'.replace(
  /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/,
  '$<month>/$<day>/$<year>',
)

Names express business meaning better than $1, $2, and are less likely to misalign when capture groups are added.

Lookbehind Assertions

/(?<=¥)\d+/.exec('Price ¥100')[0] // 100
/(?<!¥)\d+/.exec('Quantity 20')[0]   // 20

(?<=...) is a positive lookbehind assertion, (?<!...) is a negative lookbehind assertion. Assertions check the context but do not include the context itself in the match result.

Unicode Property Escapes

/\p{Script=Han}+/u.test('中文') // true
/\p{Letter}+/u.test('Hello世界') // true
/\p{Emoji}/u.test('😀') // true

Compared to hand-writing character ranges, Unicode properties better express semantics like "letter," "Chinese character," "number." However, Unicode data evolves with the specification; input validation should still be based on the business-allowed range.

5.5 Template String Escape Rule Revision

ES2018 allows tagged templates to receive raw text containing certain illegal escape sequences, where the corresponding cooked value is undefined, while strings.raw still retains the original content. This mainly serves DSL, LaTeX, and other tagged template libraries.

function inspect(strings) {
  console.log(strings[0])
  console.log(strings.raw[0])
}

Ordinary template strings do not thereby allow arbitrary illegal escapes; the practical application of this capability is mainly for tool and library authors.

6. ES2019: Data Transformation and Language Detail Refinements

6.1 Array.prototype.flat

const nested = [1, [2, [3, [4]]]]

nested.flat()         // [1, 2, [3, [4]]]
nested.flat(2)        // [1, 2, 3, [4]]
nested.flat(Infinity) // [1, 2, 3, 4]

The default depth is 1. flat returns a new array and removes sparse positions in the levels it processes.

In practice, it can be used for limited-level flattening of tree data, merging API batch results, and matrix processing. Before using Infinity on large, deeply nested data of unknown depth, consider memory, recursion depth, and whether the data design is reasonable.

6.2 Array.prototype.flatMap

flatMap is equivalent to the common semantics of map followed by a one-level flat, but the implementation can avoid an intermediate array:

const departments = [
  { name: 'R&D', users: ['Alice', 'Bob'] },
  { name: 'Design', users: ['Carol'] },
]

const users = departments.flatMap((department) =>
  department.users.map((name) => ({
    department: department.name,
    name,
  })),
)

It can also express both filtering and mapping by returning an empty array, a single-item array, or a multi-item array:

const positiveDoubles = numbers.flatMap((number) =>
  number > 0 ? [number * 2] : [],
)

If only a one-to-one transformation is needed, map is clearer; the value of flatMap lies in one-to-zero, one-to-many, and nested result merging.

6.3 Object.fromEntries

It is the reverse transformation of Object.entries:

const object = Object.fromEntries([
  ['name', 'Alice'],
  ['role', 'admin'],
])

Transforming object values:

const normalized = Object.fromEntries(
  Object.entries(form).map(([key, value]) => [
    key,
    typeof value === 'string' ? value.trim() : value,
  ]),
)

Filtering query parameters:

const cleanFilters = Object.fromEntries(
  Object.entries(filters).filter(([, value]) => value != null),
)

Keys are converted to strings or Symbols according to object property rules. Use new Map(entries) when object keys need to be preserved.

6.4 Optional catch Binding

When the error object is not needed, the parameter can be omitted:

try {
  return JSON.parse(text)
} catch {
  return fallback
}

Only use this when the error truly does not need to be distinguished or reported. Silently falling back on failures to parse critical configuration, payments, or data writes can mask serious problems.

6.5 trimStart and trimEnd

'  hello  '.trimStart() // 'hello  '
'  hello  '.trimEnd()   // '  hello'

trimLeft and trimRight usually exist as compatibility aliases; new code should prefer trimStart and trimEnd, which do not depend on writing direction.

Commonly used in practice for text input and file content processing. Whether fields like usernames and passwords allow or preserve spaces should be decided by business rules, not by uniformly trimming all fields.

6.6 Symbol.prototype.description

const key = Symbol('cache')
key.description // 'cache'

Symbol().description // undefined

description is mainly for debugging and logging; it does not participate in Symbol uniqueness and should not be used as a stable business ID.

6.7 Consistency Improvements for JSON, Sorting, and Function Source Code

ES2019 also includes several specification improvements that may not directly introduce new APIs but affect reliability:

Practical significance of stable sorting:

const users = [
  { name: 'A', level: 2 },
  { name: 'B', level: 1 },
  { name: 'C', level: 2 },
]

users.sort((a, b) => a.level - b.level)
// The relative order of A and C remains unchanged

However, sort mutates the original array; for state data, use toSorted in ES2023 environments, or copy before sorting.

7. ES2020: The Most Common Batch of Enhancements in Modern Business Code

7.1 Optional Chaining ?.

Property, Index, and Call Access

user?.profile?.address?.city
users?.[selectedIndex]
onSuccess?.(result)

When the left side of optional chaining is null or undefined, the current continuous chain short-circuits and returns undefined, without continuing to access subsequent properties.

const user = null
let index = 0

user?.items[index++]
console.log(index) // 0, the index expression was not executed

Continuous Chain Boundaries

Parentheses can break optional chaining:

const value = user?.profile?.name

// (user?.profile).name
// If profile does not exist, .name after the parentheses will still throw an error

Method calls also need to distinguish between "the object might be null" and "the method might be null":

service?.save()  // service can be null; if it exists, save must be callable
service.save?.() // service must exist; save can be null

Practical Application and Misconceptions

Optional chaining should not mask system invariants. When core configuration and required API fields are missing, validate early and throw clear errors; getting undefined all the way down is harder to debug.

7.2 Nullish Coalescing ??

?? uses the right-side default value only when the left side is null or undefined:

0 ?? 10     // 0
'' ?? 'N/A' // ''
false ?? true // false
null ?? 10  // 10

Compared to ||:

0 || 10 // 10
0 ?? 10 // 0

Page numbers, prices, toggles, and empty strings can all be legitimate values, so ?? is usually preferred for configuration defaults.

?? cannot be directly mixed with && and || without parentheses:

const result = enabled && (value ?? fallback)

This syntax restriction is to prevent readers from misjudging precedence.

7.3 BigInt

BigInt represents arbitrary-precision integers:

const id = 900719925474099312345n
const next = id + 1n

BigInt('900719925474099312345')

BigInt and Number cannot be directly mixed in arithmetic:

// 1n + 1 // TypeError
1n + BigInt(1)

Division truncates the fractional part:

5n / 2n // 2n

BigInt cannot be directly passed to Math methods, and JSON.stringify cannot serialize it by default:

const json = JSON.stringify(
  { id },
  (key, value) => typeof value === 'bigint' ? value.toString() : value,
)

Practical Application

APIs must return strings or text that BigInt can safely construct from the source. If a very long ID is first parsed as a JSON Number, precision is already lost, and converting to BigInt afterwards cannot recover it.

7.4 Dynamic import()

Dynamic import returns a Promise, allowing modules to be loaded on demand at runtime:

async function openEditor() {
  const { createEditor } = await import('./heavy-editor.js')
  return createEditor()
}

Practical applications:

const localeModule = await import(`./locales/${locale}.js`)

Dynamic paths can affect the static analysis of build tools; the path range should remain enumerable and controlled. Loading on first click introduces a wait; critical features can be combined with preloading strategies.

Static import is suitable for definite dependencies; dynamic import() is for conditional or deferred dependencies. Do not change all modules to dynamic imports just for "lazy loading."

7.5 Promise.allSettled

It waits for all inputs to settle, without rejecting early due to a single failure:

const results = await Promise.allSettled([
  upload(fileA),
  upload(fileB),
  upload(fileC),
])

for (const result of results) {
  if (result.status === 'fulfilled') {
    console.log('Success', result.value)
  } else {
    console.error('Failure', result.reason)
  }
}

The result is a discriminated union structure:

{ status: 'fulfilled', value }
{ status: 'rejected', reason }

Suitable for batch uploads, batch notifications, initializing multiple optional modules, and background operations that allow partial failures. If any failure should abort the entire process, Promise.all is more semantically correct.

7.6 globalThis

Different environments historically required using different global objects: window in browsers, self in Workers, global in Node.js. globalThis provides a unified access method:

globalThis.setTimeout
globalThis.console

Universal libraries, polyfills, and cross-runtime tools use it. Business modules should not casually attach state to the global object, as this causes naming conflicts, test pollution, and hidden dependencies.

7.7 String.prototype.matchAll

matchAll returns an iterator containing full match details; the regex needs the g flag:

const text = 'id=12&count=30'
const pattern = /(?<key>\w+)=(?<value>\d+)/g

for (const match of text.matchAll(pattern)) {
  console.log(match.groups.key, match.groups.value)
}

Compared to match in global mode, which only returns matched text, matchAll preserves capture groups, indices, and input information, and avoids the error-prone manual management of lastIndex in exec loops.

In practice, suitable for log extraction, lightweight template tagging, and text analysis. For structured formats, still use URL, JSON, HTML, or language parsers.

7.8 import.meta

import.meta provides host-related metadata about the current module. Common in browsers and modern build tools is import.meta.url:

const worker = new Worker(
  new URL('./worker.js', import.meta.url),
  { type: 'module' },
)

It allows relative resource URLs to be resolved relative to the current module, suitable for Workers, images, WASM, and module-adjacent resources. Fields like import.meta.env are usually build tool conventions, not part of the unified ECMAScript standard; do not treat specific tool behavior as a language capability.

7.9 for...in Enumeration Order Standardization

ES2020 further standardized the constraints on property enumeration order for for...in. However, business logic should still not overly rely on the enumeration order of complex object prototype chains.

For own data, use Object.keys, Object.entries; when a stable business order is needed, use arrays or Map; avoid using for...in to iterate over arrays.

7.10 Module Namespace Re-export

All exports of another module can be re-exported as a single namespace:

// index.js
export * as userApi from './user-api.js'
export * as orderApi from './order-api.js'

Consumer side:

import { userApi, orderApi } from './index.js'

await userApi.getUser(id)
await orderApi.getOrders(id)

Previously, this required importing first, then exporting:

import * as userApi from './user-api.js'
export { userApi }

Suitable for SDK entry points and public modules organized by domain. Do not pile all files into one giant namespace, as this weakens the Tree Shaking, auto-import, and dependency visibility of named exports.

8. ES2021: Default Values, Batch Replacement, and Promise Racing

8.1 Logical Assignment Operators

ES2021 adds:

value ||= fallback
value &&= nextValue
value ??= fallback

They are not just character abbreviations for ordinary assignment and logical expressions; they only execute the right side and assign when the condition is met.

||=: Assign When Left Side Is Falsy

let title = ''
title ||= 'Untitled'

console.log(title) // Untitled

0, false, and empty strings all trigger ||=, so it's suitable for cases where "all falsy values represent absence."

??=: Assign When Left Side Is Nullish

const config = {
  retry: 0,
}

config.retry ??= 3
config.timeout ??= 5000

console.log(config) // { retry: 0, timeout: 5000 }

??= only treats null and undefined as absence, making it more suitable for form numbers, toggles, and configuration defaults.

&&=: Assign When Left Side Is Truthy

let callback = () => 'done'
callback &&= wrapWithLogging(callback)

It calculates and writes the right-side result only when the left side is truthy.

The Left Side Is Evaluated Only Once

This is important for getters, setters, and computed properties:

const state = {
  _value: null,
  get value() {
    console.log('get')
    return this._value
  },
  set value(next) {
    console.log('set', next)
    this._value = next
  },
}

state.value ??= 10
// get
// set 10

If the original value already satisfies the short-circuit condition, neither the right side nor the setter will execute. In practice, do not place side effects on the right side of a logical assignment that depend on guaranteed execution.

8.2 String.prototype.replaceAll

const path = 'users/:id/orders/:id'
path.replaceAll(':id', '1001')
// users/1001/orders/1001

Common past approaches were split(search).join(replacement) or global regex. replaceAll directly expresses replacing all literal text.

If a RegExp is passed, it must have the g flag:

'a1b2'.replaceAll(/\d/g, '#') // a#b#
// 'a1b2'.replaceAll(/\d/, '#') // TypeError

A string search value is not interpreted as a regex, so it's usually safer when replacing user input:

function highlightPlainText(text, keyword) {
  return text.replaceAll(keyword, `[${keyword}]`)
}

But this does not equal HTML safety. If the result goes into innerHTML, proper escaping or sanitization is still required.

Special patterns like $&, $1 in the replacement string still have special meaning; when complex replacement values need to be inserted literally, use a function:

text.replaceAll(keyword, () => replacement)

8.3 Promise.any and AggregateError

Promise.any returns the first successful result, rejecting only if all fail:

const data = await Promise.any([
  fetchFromPrimary(),
  fetchFromBackupA(),
  fetchFromBackupB(),
])

Comparison with Promise.race:

Promise.race: the first settled result, regardless of success or failure
Promise.any: ignores preceding failures, waits for the first success

When all fail, an AggregateError is obtained:

try {
  await Promise.any([
    Promise.reject(new Error('A failed')),
    Promise.reject(new Error('B failed')),
  ])
} catch (error) {
  console.log(error instanceof AggregateError) // true
  console.log(error.errors) // the two failure reasons
}

Practical Application

It does not cancel other still-running tasks. To save resources after getting the first successful result, the business needs to cancel the remaining requests itself via AbortController. Accessing multiple services in parallel also increases traffic; don't just look at response speed.

8.4 Numeric Separators

const budget = 1_000_000
const mask = 0b1111_0000
const color = 0xff_00_cc
const huge = 9_007_199_254_740_993n

Underscores only improve source code readability and do not change the numeric value. They cannot be placed at the beginning or end of a number, next to a decimal point, or used consecutively.

It only belongs to numeric literal syntax:

Number('1_000') // NaN

User input and API text will not automatically support separators. The most practical use cases are amounts, permission bits, time constants, and large integer source code.

8.5 WeakRef and FinalizationRegistry

WeakRef

let target = { id: 1 }
const weakRef = new WeakRef(target)

weakRef.deref() // may get target
target = null

// After some future GC, deref() may get undefined

WeakRef does not prevent an object from being collected, but garbage collection timing is unpredictable. Within the same synchronous job, an object usually won't suddenly disappear between observations, but business logic must still treat the deref() result as nullable.

FinalizationRegistry

const registry = new FinalizationRegistry((heldValue) => {
  console.log('Object has been collected', heldValue)
})

let object = {}
registry.register(object, 'debug-id-1')
object = null

Whether and when the callback executes is uncertain; it may even not execute at all before the program exits.

Practical Application and Limitations

9. ES2022: Class System Completion and Module-Level Async

9.1 Public Class Fields

class Counter {
  count = 0
  label = 'Default Counter'

  increment() {
    this.count += 1
  }
}

Public fields become own properties of the instance, while class methods are on the prototype:

const counter = new Counter()

Object.hasOwn(counter, 'count') // true
Object.hasOwn(counter, 'increment') // false

Field initialization occurs in source order. Base class instance fields are initialized before the base class constructor body executes; derived class instance fields are initialized after super() returns.

Arrow function class fields create a new function for each instance and capture the instance this:

class Form {
  handleSubmit = () => {
    this.submit()
  }
}

This is suitable for directly using methods as callbacks, but when there are many instances, it generates more function objects and cannot be naturally shared and overridden like prototype methods. Do not mechanically change all methods to arrow fields.

9.2 Private Fields, Methods, and Accessors

class Wallet {
  #balance = 0

  deposit(amount) {
    if (amount <= 0) throw new RangeError('Amount must be greater than 0')
    this.#balance += amount
  }

  get balance() {
    return this.#balance
  }

  #audit(action) {
    console.log(action)
  }
}

#balance is enforced as private by the language, not a regular string property:

const wallet = new Wallet()

// wallet.#balance // SyntaxError: Cannot reference this private name outside the class
wallet['#balance'] = 100 // just creates a new regular string property

Private fields must be declared in the class body, cannot be dynamically added, and do not participate in normal prototype chain property lookups.

Private Brand Check

class Wallet {
  #balance = 0

  static isWallet(value) {
    return #balance in value
  }
}

#balance in value checks if an object possesses the private brand defined by this class, clearer than trying to access and catching an exception.

Practical Application

It restricts test replacement, serialization, and inheritance extension. Ordinary page DTOs do not need to be changed to private classes entirely for "encapsulation."

9.3 Static Fields and Static Initialization Blocks

class Config {
  static environment = 'production'
  static #values

  static {
    const raw = globalThis.APP_CONFIG ?? {}
    this.#values = Object.freeze({ ...raw })
  }

  static get(key) {
    return this.#values[key]
  }
}

Static blocks execute when the class definition is evaluated, can access private static fields, and use statements, exception handling, and local variables for complex initialization.

Suitable for registries, compatibility layers, and class-level configuration requiring one-time computation. Static blocks containing heavy side effects like network or file access make module loading unpredictable; asynchronous operations also cannot be directly awaited; such initialization should remain lightweight.

9.4 Top-Level await

ES Modules can use await directly at the top level:

// config.js
const response = await fetch('/config.json')
export const config = await response.json()

Modules that depend on this module will wait for its evaluation to complete:

// app.js
import { config } from './config.js'

startApp(config)

Practical Application

Risks

Top-level await propagates asynchronous waiting through the dependency graph, potentially delaying a large set of module execution; in circular dependencies, it can also create hard-to-analyze waiting relationships. For non-critical resources, prefer exporting an initialization Promise or async function, letting the caller decide when to wait.

Top-level await is only allowed in Modules, not applicable to ordinary Scripts.

9.5 .at() Relative Indexing

Array, String, and TypedArray all support at:

const items = ['a', 'b', 'c']

items.at(0)  // a
items.at(-1) // c
items.at(-2) // b

'JavaScript'.at(-1) // t

Compared to items[items.length - 1], negative indexing is more direct. Bracket notation items[-1] reads a regular property named '-1', not the last item.

at(-1) on an empty array returns undefined; handling of empty values is still needed before calling subsequent methods.

9.6 Object.hasOwn

Object.hasOwn(object, key)

It checks for own properties, not traversing the prototype chain, and is applicable to objects without a prototype or those that have overridden hasOwnProperty:

const dictionary = Object.create(null)
dictionary.name = 'Alice'

Object.hasOwn(dictionary, 'name') // true

Comparison:

key in object            true if exists on own or prototype chain
Object.hasOwn(object,key) checks own properties only

When handling configuration merging, untrusted input, dictionaries, and serialized data, it's important to be clear whether inherited properties are accepted.

9.7 Error Cause

When wrapping errors, the original cause can be preserved:

async function loadConfig() {
  try {
    return await requestConfig()
  } catch (error) {
    throw new Error('Application config loading failed', {
      cause: error,
    })
  }
}

The upper layer gets business context, while the logging system can still follow error.cause to see the underlying network or parsing error.

Custom errors can also forward options:

class DataAccessError extends Error {
  constructor(message, options) {
    super(message, options)
    this.name = 'DataAccessError'
  }
}

Do not put tokens, full request bodies, or personal sensitive data into the error chain and report them.

9.8 RegExp Match Indices /d

The d flag makes match results include the start and end positions of each capture group:

const pattern = /(?<key>\w+)=(?<value>\d+)/d
const match = pattern.exec('count=42')

match.indices[0]           // [0, 8]
match.indices.groups.key   // [0, 5]
match.indices.groups.value // [6, 8]

Suitable for editor highlighting, syntax analysis, search marking, and error location. Do not add d when only needing to check for a match, to avoid generating unnecessary index data.

10. ES2023: Immutable Array Operations Become Standard Capabilities

10.1 findLast and findLastIndex

const logs = [
  { type: 'info', id: 1 },
  { type: 'error', id: 2 },
  { type: 'error', id: 3 },
]

logs.findLast((log) => log.type === 'error')
// { type: 'error', id: 3 }

logs.findLastIndex((log) => log.type === 'error')
// 2

They search from the end of the array forward, stopping once found. Suitable for reading the last state, recent events, and undo records; more efficient than filter(...).at(-1) by saving an intermediate array and more accurately expressing intent.

10.2 Change Array by Copy

Traditional reverse, sort, and splice mutate the original array; ES2023 adds corresponding methods that return new arrays.

toReversed

const original = [1, 2, 3]
const reversed = original.toReversed()

console.log(original) // [1, 2, 3]
console.log(reversed) // [3, 2, 1]

toSorted

const sortedUsers = users.toSorted(
  (a, b) => b.score - a.score,
)

Like sort, it defaults to string comparison; a compare function must still be provided for numeric sorting.

toSpliced

const next = items.toSpliced(index, 1, replacement)

It returns a new array after deletion or insertion, whereas splice returns the deleted elements and mutates the original array.

const inserted = items.toSpliced(2, 0, newItem)
const removed = items.toSpliced(2, 1)

with

const next = items.with(index, updatedItem)

Supports negative indexing:

[1, 2, 3].with(-1, 9) // [1, 2, 9]

Index out of bounds throws a RangeError, which can expose state calculation errors earlier.

Practical Application

These methods are very suitable for React, Vue state, Redux reducers, cache snapshots, and any scenario relying on reference change detection:

function updateUser(users, index, patch) {
  return users.with(index, {
    ...users[index],
    ...patch,
  })
}

They only copy the array container; element objects still share references. When immutably updating nested objects, the changed objects must still be copied.

10.3 Hashbang Grammar

The first line of a JavaScript file can use a Unix shebang:

#!/usr/bin/env node

console.log('CLI started')

This allows Node.js CLI scripts to be directly executable files. Hashbang can only be at the very beginning of the source code; it mainly serves command-line runtimes and has no practical use for browser business.

10.4 Weak Collections Support More Symbol Keys

ES2023 allows non-registered Symbols as WeakMap keys and WeakSet members:

const key = Symbol('temporary')
const cache = new WeakMap()

cache.set(key, { value: 1 })

Globally registered Symbols created via Symbol.for cannot be weakly held, because the registry itself holds them long-term.

This is an enhancement for low-level metadata and capability token scenarios; using objects as WeakMap keys is still more common in ordinary business.

11. ES2024: Grouping, Promise Construction, and Binary Memory Enhancements

11.1 Object.groupBy

It groups iterable data by the property key returned by a callback:

const orders = [
  { id: 1, status: 'paid', amount: 100 },
  { id: 2, status: 'pending', amount: 80 },
  { id: 3, status: 'paid', amount: 120 },
]

const grouped = Object.groupBy(
  orders,
  (order) => order.status,
)

console.log(grouped.paid)
// orders with id 1, 3

The result is a null prototype object, avoiding conflicts with inherited properties like __proto__:

Object.getPrototypeOf(grouped) === null // true

Callback return values are converted to property keys, so object keys typically become the string "[object Object]"; use Map.groupBy when object identity is needed as the key.

Practical Application

Grouped elements are still references to the original objects:

grouped.paid[0].amount = 999
console.log(orders[0].amount) // 999

groupBy only creates the grouping container, not copies of the elements.

11.2 Map.groupBy

const paidGroup = { name: 'Paid' }
const pendingGroup = { name: 'Pending' }

const groupedByObject = Map.groupBy(orders, (order) =>
  order.status === 'paid' ? paidGroup : pendingGroup,
)

groupedByObject.get(paidGroup)

Map preserves the type and identity of keys, suitable for grouping by objects, DOM nodes, instances, or non-string values.

Selection principle:

Result to be read by string/Symbol properties -> Object.groupBy
Result keys are arbitrary values or object identity -> Map.groupBy

11.3 Promise.withResolvers

Previously, capturing resolve/reject outside the Promise constructor required:

let resolveTask
let rejectTask

const task = new Promise((resolve, reject) => {
  resolveTask = resolve
  rejectTask = reject
})

Now, a triplet can be obtained directly:

const {
  promise,
  resolve,
  reject,
} = Promise.withResolvers()

Real Scenario: Turning an Event into a Promise

function waitForMessage(target, type) {
  const { promise, resolve, reject } = Promise.withResolvers()

  const controller = new AbortController()

  target.addEventListener(type, resolve, {
    once: true,
    signal: controller.signal,
  })

  target.addEventListener('error', reject, {
    once: true,
    signal: controller.signal,
  })

  return {
    promise: promise.finally(() => controller.abort()),
    cancel: () => {
      controller.abort()
      reject(new DOMException('Cancelled', 'AbortError'))
    },
  }
}

Suitable for event bridging, queues, locks, test control, and low-level async primitives. For ordinary request flows, still prefer directly returning existing Promises; exposing resolve/reject to widely shared state makes state changes hard to track.

11.4 Well-Formed Unicode Strings

JavaScript strings are composed of UTF-16 code units and may contain lone surrogates. These can cause problems at certain encoding, URL, and system boundaries.

const malformed = '\uD800'

malformed.isWellFormed() // false
malformed.toWellFormed() // '\uFFFD', replaced with replacement character

Normal surrogate pairs remain unchanged:

'😀'.isWellFormed() // true

Practical Application

toWellFormed loses the original code units that cannot be legally interpreted, so critical forensic data should not overwrite the original text without recording.

11.5 RegExp /v Flag

v is an enhanced Unicode sets mode, supporting stronger character set operations and string properties. It is mutually exclusive with u.

Set intersection:

const greekLetters = /[\p{Script=Greek}&&\p{Letter}]/v
greekLetters.test('α') // true
greekLetters.test('A') // false

Set difference:

const nonAsciiDigits = /[\p{Decimal_Number}--[0-9]]/v

It's suitable for internationalized validation, editors, lexical analysis, and Unicode data processing. Business fields like usernames cannot just loosely allow "Unicode Letter"; a whitelist should be established based on security, normalization, and product rules.

11.6 Resizable and Transferable ArrayBuffer

Resizable ArrayBuffer

const buffer = new ArrayBuffer(8, {
  maxByteLength: 1024,
})

buffer.resizable      // true
buffer.maxByteLength  // 1024

buffer.resize(64)
buffer.byteLength // 64

Length-tracking views and fixed-length views behave differently after resize; low-level libraries must be clear about view creation methods and out-of-bounds semantics.

ArrayBuffer transfer

const source = new ArrayBuffer(1024)
const moved = source.transfer()

source.detached // true
moved.byteLength // 1024

transfer moves the ownership of the underlying data to a new buffer; the original buffer is detached. Suitable for Worker communication, file processing, and parsing pipelines to avoid copying large blocks of memory.

buffer.transferToFixedLength()

Can convert a resizable buffer to a fixed-length buffer.

These are performance and low-level data processing capabilities. Ordinary JSON business does not need to introduce the lifecycle complexity of ArrayBuffer.

Growable SharedArrayBuffer

SharedArrayBuffer can declare a maximum length and grow when needed:

const shared = new SharedArrayBuffer(16, {
  maxByteLength: 1024,
})

shared.growable     // true
shared.maxByteLength // 1024

shared.grow(64)

It can only grow, not shrink, which prevents other execution agents from seeing the shared memory suddenly truncated. Suitable for Workers, WASM, and parallel runtimes dynamically expanding shared areas; still requires Atomics and explicit concurrency protocols.

11.7 Atomics.waitAsync

Atomics.wait blocks the execution agent and usually cannot be used on the main thread; waitAsync returns an asynchronous wait result:

const result = Atomics.waitAsync(
  int32Array,
  index,
  expectedValue,
  timeout,
)

if (result.async) {
  const status = await result.value
  console.log(status) // ok, not-equal, or timed-out
} else {
  console.log(result.value)
}

Suitable for shared memory concurrency coordination, Worker runtimes, and WebAssembly. It requires a full understanding of atomic operations and memory models; it is not a replacement for ordinary asynchronous waiting.

11 Supplement: Engineering Choices for ES2024

Group array by string key          Object.groupBy
Group array by object identity     Map.groupBy
Need external Promise control      Promise.withResolvers
Check UTF-16 data boundaries       isWellFormed/toWellFormed
Complex Unicode character sets     RegExp /v
Large binary memory expand/transfer Resizable/Transferable ArrayBuffer
Shared memory async sync           Atomics.waitAsync

The abstraction levels of these APIs vary greatly. Grouping and Promise helpers are suitable for daily business; shared memory and buffer migration mainly belong to frameworks, Workers, audio/video, WASM, and low-level tools.

12. ES2025: Iterator Pipelines, Set Operations, and Module Attributes

12.1 Iterator Helpers

ES2025 introduces the global Iterator and a set of iterator helper methods, enabling lazy data processing without first converting to arrays.

Creating an Iterator

const iterator = Iterator.from([1, 2, 3, 4])

Lazy Transformation Methods

const result = Iterator.from([1, 2, 3, 4, 5])
  .filter((value) => value % 2 === 1)
  .map((value) => value * 10)
  .take(2)
  .toArray()

console.log(result) // [10, 30]

map, filter, take, drop, flatMap return new helper iterators, computing item by item only upon consumption:

function* infiniteIds() {
  let id = 1
  while (true) yield id++
}

const firstThreeEvenIds = infiniteIds()
  .filter((id) => id % 2 === 0)
  .take(3)
  .toArray()

// [2, 4, 6]

If an infinite sequence is first converted to an array, it will never complete; lazy take can close the upstream iterator after the required count is met.

Terminal Methods

Iterator.from(values).reduce((sum, value) => sum + value, 0)
Iterator.from(values).some((value) => value > 100)
Iterator.from(values).every((value) => value >= 0)
Iterator.from(values).find((value) => value.id === id)
Iterator.from(values).forEach(handleValue)
Iterator.from(values).toArray()

Practical Application

Iterators are typically one-time consumption; reusing the same iterator may yield empty results. Iterator Helpers are synchronous capabilities; Async Iterator Helpers must be confirmed separately in the current formal standard mainline and should not be mixed in writing.

12.2 Set Operations

Union, Intersection, and Difference

const frontend = new Set(['Alice', 'Bob'])
const backend = new Set(['Bob', 'Carol'])

frontend.union(backend)
// Alice, Bob, Carol

frontend.intersection(backend)
// Bob

frontend.difference(backend)
// Alice

frontend.symmetricDifference(backend)
// Alice, Carol

Relationship Checks

const required = new Set(['read', 'write'])
const actual = new Set(['read', 'write', 'delete'])

required.isSubsetOf(actual)     // true
actual.isSupersetOf(required)   // true
required.isDisjointFrom(new Set(['admin'])) // true

Practical Application

function hasAllPermissions(userPermissions, requiredPermissions) {
  return new Set(requiredPermissions).isSubsetOf(
    new Set(userPermissions),
  )
}

Set expresses set semantics more directly than array includes loops. Object elements are still compared by identity; two objects with the same content are not the same member; business entity sets typically use IDs.

12.3 Import Attributes and JSON Modules

Module type attributes can be declared upon import:

import config from './config.json' with { type: 'json' }

Dynamic import:

const module = await import('./config.json', {
  with: { type: 'json' },
})

Attributes let the host explicitly know what type a module should be loaded and validated as, avoiding reliance solely on file extensions or server MIME inference.

Practical Application and Notes

12.4 RegExp.escape

When dynamically constructing regexes, special characters in user text must be escaped:

const keyword = 'price (C++)'
const pattern = new RegExp(RegExp.escape(keyword), 'gi')

pattern.test('PRICE (C++)') // true

You cannot just hand-replace characters like .*+?, because safe escaping also involves boundaries like the first character, punctuation, control characters, whitespace, and Unicode.

Practical search highlighting:

function createLiteralSearchPattern(keyword) {
  return new RegExp(RegExp.escape(keyword), 'giu')
}

RegExp.escape solves the problem of "embedding text as a literal in a regex," not all input security issues. Very long or complex regexes can still pose performance risks; for pure literal search, includes and indexOf can be used directly.

12.5 Regex Inline Modifiers

Flags like i, m, s can be enabled or disabled locally within a pattern:

const pattern = /prefix-(?i:hello)-suffix/

pattern.test('prefix-HELLO-suffix') // true

Can also be locally disabled:

const pattern = /(?i:product)-(?-i:SKU)/

Suitable for complex patterns combining multiple sources, different casing, or multi-line rules, making it easier to maintain a single match context than splitting into multiple regexes. Ordinary validation should prefer simple regexes; avoid turning business parsing into an unmaintainable pattern language.

12.6 Promise.try

Promise.try calls a function that may return synchronously, throw synchronously, or return a Promise, and uniformly obtains a Promise:

function executePlugin(plugin, input) {
  return Promise.try(plugin, input)
}

The equivalent goal is to unify synchronous and asynchronous implementations:

Promise.try(() => parseConfig(raw))
  .then(useConfig)
  .catch(handleError)

If parseConfig throws synchronously, the error becomes a Promise rejection; if it returns a Promise, its state is adopted.

It has a timing difference from the following:

Promise.resolve().then(() => parseConfig(raw))

The latter schedules the function call to a microtask; Promise.try executes the function at call time and normalizes the result to a Promise.

Practical Application

If the function contract already clearly returns a Promise, there is no need for extra wrapping.

12.7 Float16Array, DataView Float16, and Math.f16round

Half-precision floating-point numbers use 16 bits of storage, with significantly lower precision and range than ordinary Number:

const values = new Float16Array([1.5, 3.1415926])

console.log(values[0]) // 1.5
console.log(values[1]) // will be rounded according to float16

DataView:

const buffer = new ArrayBuffer(2)
const view = new DataView(buffer)

view.setFloat16(0, 1.5, true)
view.getFloat16(0, true) // 1.5

Simulated rounding:

Math.f16round(3.1415926)

Practical Application

Amounts, business counts, and high-precision calculations should not use Float16. Its value is compact representation, not greater precision.

12.8 Duplicate Named Capture Groups

In mutually exclusive regex branches, the same capture group name can be reused, allowing different input formats to yield a unified result field:

const datePattern = /(?:(?<year>\d{4})-\d{2}-\d{2}|\d{2}\/\d{2}\/(?<year>\d{4}))/

datePattern.exec('2026-08-13').groups.year // 2026
datePattern.exec('08/13/2026').groups.year // 2026

For branches that do not participate in the match, the value of the same-named group is undefined; the participating branch provides the final value. This capability only allows patterns that do not create ambiguity on the same match path; arbitrarily repeating names in the same branch is still a syntax error.

Suitable for simultaneously accommodating multiple log, date, or protocol formats while exposing a unified field name to subsequent processing. When formats become too numerous, switch to explicit parsers instead of infinitely expanding a single regex.

12.9 Specification Consistency Corrections

ES2025 also corrected some redeclaration rules between var introduced by global eval and existing declarations in non-strict mode, making behavior consistent across different implementations. Such changes mainly affect legacy scripts and engine conformance tests.

Modern projects should use ESM, strict mode, and avoid direct eval. It is not a business capability worth actively adopting, but part of the annual specification changes.

13. ES2026: Precise Summation, Async Collections, and Binary Interop

ES2026 is the 17th edition of ECMAScript, officially released in June 2026. The following capabilities already belong to the annual formal standard, but the actual implementation status of target browsers, Node.js, and build runtimes still needs to be checked before deploying to production.

13.1 Math.sumPrecise

Ordinary accumulation can be affected by floating-point rounding and numerical magnitude:

[1e20, 1, -1e20].reduce((sum, value) => sum + value, 0)
// 0, the small 1 is lost in intermediate calculations

Math.sumPrecise performs a more stable summation on an iterable of Numbers, minimizing precision loss from numbers of different magnitudes:

Math.sumPrecise([1e20, 1, -1e20]) // 1

It accepts an iterable, not limited to arrays:

Math.sumPrecise(new Set([0.1, 0.2, 0.3]))

Practical Application and Boundaries

13.2 Iterator.concat

It lazily concatenates multiple iterable objects in order:

const iterator = Iterator.concat(
  [1, 2],
  new Set([3, 4]),
  function* () {
    yield 5
    yield 6
  }(),
)

iterator.toArray() // [1, 2, 3, 4, 5, 6]

Compared to array spread:

const eager = [...first, ...second, ...third]

Array spread immediately consumes and creates a full array; Iterator.concat enters each source one by one only when the downstream consumes, suitable for large data, generators, and combinations of infinite/finite streams.

const firstTen = Iterator.concat(sourceA, sourceB)
  .filter(isValid)
  .take(10)
  .toArray()

If a preceding source is an infinite iterator, subsequent sources will never be visited; this is the necessary semantics of sequential concatenation.

13.3 Array.fromAsync

It creates an array asynchronously from an async iterable, sync iterable, or array-like:

async function* loadPages() {
  yield 1
  yield Promise.resolve(2)
  yield 3
}

const values = await Array.fromAsync(loadPages())
// [1, 2, 3]

With an async mapper:

const users = await Array.fromAsync(
  userIds,
  async (id) => fetchUser(id),
)

Difference from Promise.all(Array.from(...))

Array.fromAsync is oriented towards async iteration and item-by-item waiting, typically consuming in source order, and is not equivalent to firing all mapping tasks in parallel at once.

// Tends towards serial consumption/mapping semantics
await Array.fromAsync(ids, fetchUser)

// Explicitly fires all in parallel
await Promise.all(Array.from(ids, fetchUser))

Practical Application

13.4 Error.isError

Error.isError(value)

It is used to reliably determine Error objects, including cross-Realm scenarios:

const iframeError = iframe.contentWindow.eval(
  'new Error("failed")',
)

iframeError instanceof Error // may be false
Error.isError(iframeError)    // true

instanceof depends on whether the current Realm's Error.prototype appears on the object's prototype chain; Error.isError uses an internal language brand check, making it more suitable for iframe, Worker boundaries, plugin sandboxes, and cross-context logging systems.

Ordinary objects like { name: 'Error', message: 'x' } will not become true Errors because of this. Errors coming from external JSON still need to be parsed according to the data structure.

13.5 Map and WeakMap get-or-insert

Direct Default Value

const cache = new Map()

const list = cache.getOrInsert('users', [])
list.push('Alice')

If the key exists, the existing value is returned; if not, the given default value is inserted and returned.

Lazily Computed Default Value

const permissions = cache.getOrInsertComputed(
  userId,
  (key) => loadDefaultPermissions(key),
)

The callback only executes when the key is missing, suitable for default objects with higher creation cost:

function groupByKey(map, key, item) {
  map.getOrInsertComputed(key, () => []).push(item)
}

WeakMap also provides corresponding methods, suitable for instance metadata:

const metadata = new WeakMap()
const meta = metadata.getOrInsertComputed(
  element,
  () => ({ clicks: 0 }),
)

Notes

13.6 Uint8Array Base64 and Hex Conversion

Base64

const bytes = new Uint8Array([
  72, 101, 108, 108, 111,
])

bytes.toBase64() // SGVsbG8=

Uint8Array.fromBase64('SGVsbG8=')
// Uint8Array [72, 101, 108, 108, 111]

URL-safe alphabet and omitting padding:

bytes.toBase64({
  alphabet: 'base64url',
  omitPadding: true,
})

Hex

bytes.toHex() // 48656c6c6f

Uint8Array.fromHex('48656c6c6f')

Writing to an Existing Buffer

const target = new Uint8Array(16)
const result = target.setFromBase64('SGVsbG8=')

console.log(result.read)
console.log(result.written)

setFromHex provides the corresponding capability.

Practical Application

Base64 is just encoding, not encryption; data volume typically expands. For binary uploads, prefer Blob, ArrayBuffer, or multipart instead of unconditionally stuffing into JSON.

13.7 JSON.parse Source Text Access

The reviver gets an extra context, allowing access to the original JSON text fragment:

const json = '{"id":900719925474099312345}'

const data = JSON.parse(json, (key, value, context) => {
  if (key === 'id') {
    return BigInt(context.source)
  }
  return value
})

console.log(data.id)
// 900719925474099312345n

Why not use BigInt(value)? Because the JSON number may have already lost precision when converted to Number, whereas context.source retains the matched original numeric text.

context only provides source when the corresponding value is still related to the original JSON node; the processing semantics for objects and arrays differ from primitive values. Actual revivers must be tested according to the specification and runtime environment.

Practical Application

The most robust cross-system contract is usually still transmitting very long integers as JSON strings, reducing differences across old runtimes and other language implementations.

13.8 JSON.rawJSON and JSON.isRawJSON

JSON.rawJSON creates a controlled wrapper; JSON.stringify will directly write the contained legal raw JSON primitive value text into the result:

const payload = {
  id: JSON.rawJSON('900719925474099312345'),
}

JSON.stringify(payload)
// {"id":900719925474099312345}

Check:

JSON.isRawJSON(payload.id) // true

The input must be legal JSON primitive value text; the outer cannot be an object or array. Illegal text throws a SyntaxError:

JSON.rawJSON('true')
JSON.rawJSON('null')
JSON.rawJSON('123.45')

// JSON.rawJSON('{"x":1}') // SyntaxError

Practical Application and Risks

Do not pass unvalidated user strings directly as raw JSON. This API validates JSON syntax, but the business still needs to validate the value range and type. Most APIs can continue using ordinary JSON.stringify.

14. Choosing Modern Features by Work Scenario

Memorizing APIs by year is suitable for building a historical map; when writing code, choosing tools by problem is more important.

14.1 Configuration and Defaults

function normalizeOptions(input = {}) {
  return {
    timeout: input.timeout ?? 5000,
    retry: input.retry ?? 0,
    enabled: input.enabled ?? true,
  }
}

Recommended combination: default parameters, object destructuring, object spread, ??, ??=, optional chaining.

Key distinction:

||  / ||=  treats all falsy values as absence
??  / ??=  treats only null, undefined as absence

14.2 List Querying, Transformation, and State Updates

const visible = users
  .filter((user) => user.enabled)
  .toSorted((a, b) => a.name.localeCompare(b.name))

const nextUsers = users.with(index, {
  ...users[index],
  enabled: false,
})

Recommended combination: find, findLast, includes, flatMap, toSorted, toSpliced, with.

When data scale is large, the source is a generator, or only the first few items are consumed, consider Iterator Helpers to avoid intermediate arrays.

14.3 Indexing, Grouping, and Set Relationships

const userById = new Map(
  users.map((user) => [user.id, user]),
)

const usersByDepartment = Object.groupBy(
  users,
  (user) => user.department,
)

const allowed = requiredPermissions.isSubsetOf(userPermissions)
Fixed-field entities       Object
Dynamic arbitrary key index Map
Unique member set           Set
String key grouping         Object.groupBy
Object identity key grouping Map.groupBy
Object lifecycle cache      WeakMap

14.4 Asynchronous Task Selection

All must succeed              Promise.all
Wait for all, keep each result Promise.allSettled
First to settle, win or lose  Promise.race
First success                 Promise.any
Unify sync/async function     Promise.try
External Promise control      Promise.withResolvers
Sequential async stream       for await...of
Collect async stream to array Array.fromAsync

No aggregation method automatically cancels tasks. Network request cancellation, timeouts, and resource release require AbortController or cancellation protocols provided by the host.

14.5 Modules and On-Demand Loading

Stable static dependencies     import/export
Low-frequency heavy features   dynamic import()
Module-level startup deps      top-level await (cautiously)
Static JSON resources          import attributes + JSON module
Module-adjacent resource loc   import.meta.url

Module boundaries should be designed by responsibility; dynamic imports cannot be used as a tool to fix circular dependencies.

14.6 Text, Regex, and Unicode

const escaped = RegExp.escape(keyword)
const pattern = new RegExp(escaped, 'giu')

if (!text.isWellFormed()) {
  text = text.toWellFormed()
}

For plain text search, prefer includes, startsWith, replaceAll; for dynamic regex literal text, use RegExp.escape; for internationalized character sets, use Unicode properties and /v. HTML, URL, JSON, and programming languages should use structured parsers.

14.7 Binary and High-Precision Data

Very long integer ops         BigInt
Very long business IDs        usually String
More stable float summation   Math.sumPrecise
Half-precision compact data   Float16Array
Base64/Hex byte conversion    Uint8Array new APIs
Large memory expand/transfer  ArrayBuffer resize/transfer
JSON large integer recovery   reviver context.source
JSON raw number output        JSON.rawJSON

"Precision" must first be defined by business meaning: BigInt only handles integers; Math.sumPrecise reduces float summation error but is not a decimal amount model; Float16 actively sacrifices precision.

15. Compatibility and Engineering Implementation

15.1 Don't Use "Supports ES6" to Describe Compatibility Scope

A runtime environment might support arrow functions but not Proxy; might support optional chaining but not yet have ES2025 Iterator Helpers. The correct approach is to judge by feature and target version.

A project should at least clarify:

Browsers: which versions are supported, whether WebViews are included, whether old Safari is needed
Node.js: minimum major version and module mode
Build tools: syntax transformation target and module output
Runtime: whether polyfills are injected, on-demand or full
Published packages: whether compatibility strategies differ for apps, libraries, Node services

Application code can be tailored to its own user environment; when publishing npm libraries, avoid silently polluting the global scope, and clearly declare the build artifacts, module format, and minimum runtime environment.

15.2 browserslist and Target Environment

Frontend projects usually express the target environment through configurations like browserslist, which Babel, Autoprefixer, and build tools use to decide the transformation scope.

Example configuration:

> 0.5%
last 2 versions
not dead

This is just an example and should not be directly copied to all projects. Internal backend systems might only support the company's unified browser; public consumer products might need to cover more mobile devices; embedded WebViews must also be tested against the actual host version.

Older targets usually mean:

Compatibility scope is a product decision, not a choice made by developer habit.

15.3 Which Capabilities Can Be Transpiled

Syntax-layer capabilities can usually be rewritten to older syntax:

Arrow functions
Destructuring and default parameters
Optional chaining and nullish coalescing
Class fields
async/await (usually requires helper runtime code)
Object spread

But the transformed code does not necessarily have the same cost as the native implementation. For example, transpiling async/await into generators and helper functions increases code size; private field transformation might rely on WeakMap, with performance, debugging, and reflection behavior differing from native semantics.

15.4 Which Capabilities Need Polyfills

Runtime APIs usually need implementations provided for old environments:

Array.prototype.includes
Object.entries / Object.fromEntries
Promise.allSettled / Promise.any
Array.prototype.flat / toSorted
Object.groupBy
New Set methods
Iterator Helpers

Polyfill strategies mainly fall into two types:

  1. Global injection: Modifying or supplementing global constructors and prototypes; convenient for application use, but may pollute the host environment.
  2. Pure function/ponyfill: Importing implementations by module, not modifying globals; more controllable for libraries, but the calling form may differ.

On-demand injection must be based on reliable static analysis and target environment data. Dynamic property access, third-party dependencies, and runtime-generated code can cause automatic detection to miss cases.

15.5 Which Capabilities Are Difficult to Fully Polyfill

Proxy
WeakRef / FinalizationRegistry
SharedArrayBuffer / Atomics
True private field semantics
Module loading and top-level await
Ownership transfer of ArrayBuffer transfer
Some regex engine semantics

These capabilities involve engine internal slots, garbage collection, parsers, module loaders, or memory models. Tools may provide limited alternatives or transformations, but cannot guarantee equivalence in all observable behaviors.

Before using, a choice must be made: raise the minimum runtime environment, provide a fallback path, complete the related work server-side, or not adopt it for now.

15.6 Feature Detection

Check if an API exists:

if (typeof Promise.withResolvers === 'function') {
  // Use native capability
}
if ('toSorted' in Array.prototype) {
  // ...
}

You cannot use try/catch to dynamically detect certain new syntax in unsupported engines, because the source code fails to parse before execution. Syntax compatibility should be addressed by build targets, module distribution, or separate script entry points.

Feature Detection can determine "existence," but does not guarantee that early implementations are bug-free, nor does it replace testing on real devices.

15.7 Different Strategies for Adopting New Features in Apps and Libraries

Applications

Libraries

15.8 Performance Is Not "New Syntax Is Always Faster"

The primary value of language features is usually semantics and reliability. Performance needs to be measured by data volume and implementation:

First identify the problem, then measure the bottleneck, and finally choose the feature. Don't treat API year as a performance grade.

16. Centralized Explanation of Common Misconceptions

1. const equals object immutability

Wrong. It only prohibits reassigning the binding; nested properties can still be modified.

2. Arrow functions are more modern, so use them for all functions

Wrong. Dynamic this, constructor calls, generators, and prototype-shared methods still require other function forms.

3. Spread syntax equals deep copy

Wrong. Both array and object spread only handle one level; nested objects continue to share references.

4. async/await makes tasks parallel

Wrong. It expresses waiting; whether tasks are parallel depends on when they are started. Consecutive awaits are usually serial.

5. Promise can be cancelled

Promise itself represents a result and has no unified cancellation protocol. Cancellation depends on the task API, such as fetch's AbortSignal.

6. More optional chaining is more robust

Wrong. It is suitable for legitimately missing values; abusing it on data that the system must have turns clear errors into distant undefined.

7. || and ?? can be swapped casually

Wrong. || handles all falsy values, ?? only handles nullish values. Whether 0, empty string, false are valid is decided by the business.

8. Map is definitely faster than Object

Wrong. Performance depends on the engine, key type, data scale, and operation pattern. Choose based on data semantics first, then measure at hotspots.

9. WeakMap is an auto-expiring cache

Incomplete. It only allows collection when the key object is unreachable; it has no TTL, capacity limit, or enumerability.

10. Babel can solve all compatibility problems

Wrong. Parsing and syntax transformation, runtime APIs, and engine internal semantics are different layers.

11. Stage 3 is safe for production

Not necessarily. The design is usually relatively stable, but it is still not formally complete; environment support, toolchain, and future change risks must be assessed separately.

12. All browsers support a new standard immediately after the annual release

Wrong. There is a time lag between the standard, engine implementation, browser release, enterprise updates, and WebView upgrades.

17. ES2027 Candidates and How to Judge Future Proposals

As of August 2026, ES2026 is the most recent formal annual version. TC39 already has some Stage 4 completed proposals expected to enter ES2027, but they are not yet part of the ES2026 mainline.

17.1 Temporal

Temporal aims to provide a clearer model for dates, times, time zones, and calendars than Date, distinguishing concepts like instants, plain dates, zoned date-times, and durations.

Illustration:

const date = Temporal.PlainDate.from('2026-08-13')
const nextWeek = date.add({ days: 7 })

Real applications include appointments, cross-timezone meetings, accounting periods, and calendar calculations. Before entering production, check the runtime environment, or use an official/mature polyfill and evaluate the bundle size.

17.2 Explicit Resource Management

Through mechanisms like using, await using, Symbol.dispose, Symbol.asyncDispose, resources are reliably released when leaving scope:

class Subscription {
  [Symbol.dispose]() {
    this.unsubscribe()
  }
}

// In environments supporting this syntax:
// using subscription = createSubscription()

It is suitable for file handles, locks, subscriptions, and connections, not a replacement for garbage collection.

17.3 Joint Iteration and Atomics.pause

Joint Iteration targets the joint advancement of multiple iterable sequences; Atomics.pause provides optimization hints for low-level concurrency loops like spin-waiting. The user base and risks for these two types of capabilities differ; the former leans towards data processing, the latter towards runtimes and concurrency libraries.

17.4 Decorators, Pipeline, Async Iterator Helpers

These capabilities have long attracted attention, but as of this article's time point, they cannot be claimed to have entered ES2026 just because TypeScript, Babel, or some browsers support them.

Proposal Adoption Principles

Stage 0/1: Understand the direction, do not bind production architecture
Stage 2: Can experiment, avoid strong dependency in external public APIs
Stage 2.7/3: Specification is relatively stable, still need tool and risk assessment
Stage 4: Wait/confirm annual attribution and target environment implementation
Formal Annual Version: Still need to check runtime environment compatibility

18. Annual Quick Reference Table

ES2015  let/const, arrows, destructuring, templates, class, Symbol, Iterator,
        Generator, Promise, ESM, Map/Set, Proxy/Reflect, numerous built-in APIs

ES2016  Exponentiation operator, Array.includes

ES2017  async/await, Object.values/entries, string padding,
        property descriptors, function trailing commas, SharedArrayBuffer/Atomics

ES2018  Async iteration, object rest/spread, Promise.finally,
        regex dotAll/named groups/lookbehind/Unicode properties

ES2019  flat/flatMap, Object.fromEntries, optional catch, trimStart/End,
        Symbol.description, stable sort, JSON and function source improvements

ES2020  Optional chaining, nullish coalescing, BigInt, dynamic import, allSettled,
        globalThis, matchAll, import.meta, module namespace re-export

ES2021  Logical assignment, replaceAll, Promise.any/AggregateError,
        numeric separators, WeakRef/FinalizationRegistry

ES2022  Class fields and private elements, static blocks, top-level await, at, hasOwn,
        Error cause, RegExp match indices

ES2023  findLast/findLastIndex, toSorted/toReversed/toSpliced/with,
        Hashbang, Weak collection Symbol keys

ES2024  Object/Map.groupBy, Promise.withResolvers, well-formed strings,
        RegExp /v, resizable/transferable ArrayBuffer, growable SharedArrayBuffer,
        Atomics.waitAsync

ES2025  Iterator Helpers, Set operations, Import Attributes/JSON Modules,
        RegExp.escape, regex inline modifiers, Promise.try, Float16,
        duplicate named capture groups and spec consistency corrections

ES2026  Math.sumPrecise, Iterator.concat, Array.fromAsync,
        Error.isError, Map/WeakMap get-or-insert, Uint8Array Base64/Hex,
        JSON.parse source context, JSON.rawJSON

19. Learning and Migration Order

If a project still mainly uses ES5 style, migration can proceed in the following order:

  1. First master let/const, functions, destructuring, template strings, and object shorthand.
  2. Use array methods, Map, Set to express correct data structures.
  3. Master Promise, async/await, parallelism, and error propagation.
  4. Use ESM to establish module boundaries, understand static and dynamic imports.
  5. Use optional chaining, nullish coalescing, and non-destructive array methods to simplify business state.
  6. Learn grouping, Iterator Helpers, Set operations, and new Promise APIs based on real scenarios.
  7. Only delve into SharedArrayBuffer, Atomics, Float16, and ArrayBuffer transfer when dealing with binary, Workers, WASM, or low-level frameworks.
  8. Establish compatibility targets and automated tests for new projects; do not judge environment support by memory.

Migration does not require changing all old code at once. Prioritize modules that have test coverage, are under active development, and are error-prone; do not mix pure formatting changes with major business modifications in the same commit.

Summary

From ES2015 to ES2026, the evolution of JavaScript can be summarized into several main threads:

The truly valuable way to learn is not memorizing method names by year, but understanding which semantics each feature changes, what kind of repetitive code it solves, and what constraints it introduces. When choosing new features at work, confirm in order: whether it accurately expresses the business, whether the team can maintain it, whether the target environment supports it, and whether the transformation and polyfill costs are acceptable.

References