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:
- What problem does it solve?
- What is the basic syntax?
- What are the operational rules?
- Where does it apply in work?
- 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
- Use
constby default, only useletwhen the binding genuinely needs to change. constcan express "this variable will not be reassigned," reducing the scope of state tracking during reading.- Don't twist simple logic into unreadable code just to pursue
const; usingletfor local counters and state machines is normal. - When immutable objects are needed, use immutable updates,
Object.freeze, or corresponding state management constraints;constitself is not responsible for deep immutability.
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
- Array transformations and short callbacks:
items.map((item) => item.id). - Asynchronous callbacks that need to capture the outer
this. - Not suitable for constructors, methods relying on dynamic
this, and large functions requiring clear function names and complex control flow. - Arrow functions are not a complete replacement for regular functions; the choice is based on semantics, not fewer characters.
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
- Receiving configuration objects, extracting API fields, swapping variables, reading return values from React/Vue Composition API.
- Deep destructuring makes optional paths prone to errors; external data should be validated first, or provide default objects for intermediate layers.
- Destructuring a dozen fields in the parameter list reduces readability; complex functions can receive
optionsfirst, then extract groups within the function body. - Destructuring is a read and assignment syntax; it does not automatically clone nested objects.
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
- Custom Errors, domain entities, SDK clients, component or framework base classes with stable inheritance relationships.
- Deep inheritance trees scatter behavior across multiple levels; business code usually prefers composing small objects and functions.
- Class fields, private fields, and static initialization blocks were only officially added in ES2022; do not mistakenly remember them as part of the original ES2015 class content.
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
- Split modules by responsibility, establishing clear public APIs.
- Build tools perform Tree Shaking based on the static structure, but whether code is actually removed depends on side-effect analysis and package configuration.
- Circular dependencies may run, but the initialization order becomes complex; they should usually be eliminated through responsibility splitting.
- Dynamic
import()is an official feature of ES2020, not part of the original ES2015 module syntax.
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
- Reactive systems like Vue 3, validation layers, API clients, virtual objects, and low-level libraries.
- Proxy by default only proxies the current object. Deep reactivity usually requires continuing to wrap nested objects upon reading, and handling caching and identity consistency.
- Built-in objects have internal slots; for example, some Map methods cannot be arbitrarily borrowed for use with proxy objects and require correct receiver binding.
- Proxy cannot be fully polyfilled; the target runtime environment must be confirmed.
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')
}
- For small fixed lists,
includescan be used directly; its semantics are clearer thanindexOf(...) !== -1. - For large collections requiring frequent queries, pre-build a
Set; the average query cost is more appropriate. - To find an object in an array by ID, you cannot pass a new object directly; use
someor a Map:users.some((user) => user.id === id).
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
- Combining multiple APIs during page initialization.
- Validation, upload, save, and navigation in form submission.
- Database and remote service calls in Node.js servers.
- Sequential flows in scheduled tasks and scripts.
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
- Cursor pagination, streaming responses, log streams, and message queues.
- Chunk-by-chunk processing of Node.js readable streams and Web Streams.
- Avoid loading all data into memory at once.
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
- Shallow immutable updates for React/Vue state.
- Merging request parameters and component props.
- Removing fields from DTOs, conditionally adding fields.
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:
- JSON becomes a syntactic subset of JavaScript source text, with more consistent handling of related line separator characters.
JSON.stringifyuses well-formed escaping for lone UTF-16 surrogates, reducing encoding/transmission issues.Array.prototype.sortis required to be stable: elements with a comparison result of 0 retain their original relative order.Function.prototype.toStringmore accurately returns the function's source text representation.
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
- Reading nested fields that the backend indeed allows to be missing.
- Calling optional callbacks and plugin hooks.
- Accessing subsequent properties of Map query results that might be missing.
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
- Integer calculations exceeding
Number.MAX_SAFE_INTEGER. - Cryptography, blockchain, precise counting, and low-level protocols.
- For very long business IDs used only for identification and transmission, using strings directly is usually safer, without needing BigInt operations.
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:
- Route-level code splitting.
- Low-frequency, heavy features like rich text editors, charts, and maps.
- Loading implementations based on locale, theme, or runtime environment.
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
- Fetching the first successful response from multiple CDNs or mirror sources.
- Probing primary and fallback services in parallel.
- Multiple alternative data fetching strategies.
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
- Low-level caches, object interop layers, and diagnostic tools.
- Cannot be used for saving data, closing critical connections, network reporting, charging, or any process that must happen.
- For ordinary caches, prefer capacity limits, expiration times, and explicit cleanup; only consider these APIs when object lifecycle truly requires weak association.
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
- Internal state in SDKs that callers should not arbitrarily modify.
- Domain objects that need to maintain invariants.
- Library authors hiding caches and implementation details.
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
- Configuration, WASM, and localization resource initialization that must complete before app startup.
- Reading startup data in Node.js scripts.
- Conditional dynamic imports.
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
- Grouping table data by status, date, or department.
- Report data preprocessing.
- UI partitioned lists and batch operation queues.
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
- Checking untrusted text before handing it to UTF-8 encoders, file systems, or external APIs.
- Logs, crawlers, protocol gateways, and data cleaning.
- Fixing illegal sequences when rich text editors handle truncated strings.
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
- Large data filtering pipelines, reducing intermediate arrays.
- File records, pagination cursors, tree traversal, and generator results.
- Infinite sequences and tasks that only need to consume the first N items.
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
- Permission checks.
- Tag filtering and multi-select state.
- Cache key invalidation scope.
- Set relationships in recommendation systems.
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
- Loading static JSON configuration, language packs, and data tables at build time or runtime.
- Node.js, browsers, and build tools may differ in path, MIME, caching, and support scope.
- JSON modules are suitable for trusted static resources, not a replacement for runtime API response validation.
- Old code might use the earlier
assert { type: 'json' }syntax; the new standard useswith { type: 'json' }. Check the toolchain when migrating.
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
- Plugin systems allowing synchronous or asynchronous hooks.
- Cache layers that may hit synchronously or read asynchronously.
- Test runners and task pipelines unifying error handling.
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
- WebGPU, machine learning models, graphics, audio/video, and large-scale scientific data.
- Binary protocols where precision is traded for memory and bandwidth.
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
- Scientific computing, statistical aggregation, numerical analysis, and summarizing large amounts of floating-point data.
- Improves summation stability, but does not turn binary floating-point into decimal fixed-point.
- Amounts should still use the smallest currency unit, explicit rounding rules, or decimal schemes.
- Input must be Number; BigInt requires separate integer aggregation.
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
- Collecting async generators and streaming pagination results into an array.
- Consuming Web/Node async data sources in tests or when data volume is manageable.
- When data volume is large or the stream is infinite, do not collect everything; use
for await...ofto process item by item to avoid memory growth.
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
getOrInsert(key, expensive())will compute the argument first, even if the key exists; usegetOrInsertComputedfor expensive defaults.- The callback might modify the same Map; the specification defines the corresponding result, but business code should avoid the complexity brought by re-entrancy.
- After storing a mutable array or object, subsequent calls get the same value; this is the purpose of caching, not automatic copying.
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
- File digests, cryptographic material, WebSocket, and binary protocols.
- JWT segments, URL-safe encoding, image and attachment data.
- Browser and Node.js universal libraries reduce environment branching for
atob,btoa, Buffer, and hand-written loops.
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
- Precise recovery of large integer IDs, financial, or scientific data.
- Parsers that need to preserve the original format of numbers.
- Audit tools comparing original text with parsed values.
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
- Precisely outputting JSON numbers exceeding the safe integer range.
- Low-level serialization libraries controlling the representation of specific primitive values.
- Cooperating with
JSON.parsesource context for large integer round-tripping.
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:
- More syntax transformation code.
- More polyfills and bundle size.
- More complex Source Maps and debugging stacks.
- Some low-level capabilities cannot be fully simulated.
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:
- Global injection: Modifying or supplementing global constructors and prototypes; convenient for application use, but may pollute the host environment.
- 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
- Know the user runtime environment, can unify build and inject polyfills.
- The readability benefits of new features can usually be enjoyed first.
- Still need to monitor real user browser and WebView versions.
Libraries
- Do not know the caller's environment and build chain, cannot assume global polyfills definitely exist.
- Should declare the minimum environment in
package.json, documentation, and release notes. - When publishing both ESM/CJS or different syntax targets, ensure the
exportsmap is clear. - Try not to modify global prototypes, and do not implicitly bundle heavy polyfills into every caller.
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:
- Iterator Helpers can reduce intermediate arrays, but the difference may be meaningless on small arrays.
- Map lookup is suitable for large dynamic indexes; not all small fixed objects need to be replaced.
toSortedprovides immutable semantics and necessarily creates a new array.- Dynamic import reduces the initial bundle, but shifts the download wait to the first use of the feature.
- Float16 saves memory, but reduces precision and has conversion costs.
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.
- Decorators still need attention to the final standard semantics and differences from old TypeScript decorators.
- The Pipeline Operator's syntax and design are still in the proposal process.
- Async Iterator Helpers should not be conflated with the synchronous Iterator Helpers formally completed in ES2025.
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:
- First master
let/const, functions, destructuring, template strings, and object shorthand. - Use array methods, Map, Set to express correct data structures.
- Master Promise,
async/await, parallelism, and error propagation. - Use ESM to establish module boundaries, understand static and dynamic imports.
- Use optional chaining, nullish coalescing, and non-destructive array methods to simplify business state.
- Learn grouping, Iterator Helpers, Set operations, and new Promise APIs based on real scenarios.
- Only delve into SharedArrayBuffer, Atomics, Float16, and ArrayBuffer transfer when dealing with binary, Workers, WASM, or low-level frameworks.
- 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:
- Syntax more accurately expresses scope, default values, missing values, and immutable updates.
- Promise, async functions, and async iteration progressively refine asynchronous control flow.
- ESM, dynamic imports, and module attributes make dependency relationships more analyzable.
- Map, Set, groupBy, and Iterator Helpers provide data abstractions closer to the problem.
- Class private elements, Proxy, WeakMap, and other capabilities support framework and library encapsulation.
- Unicode, regex, binary arrays, and precise JSON handling continuously fill in boundary scenarios.
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.