JavaScript 发展到今天,已经很难再用“脚本语言”三个字概括。
从 ES2016 开始,ECMAScript 正式进入了每年发布一个版本的节奏。过去十年里,JavaScript 增加了大量新语法、新 API 和新的异步编程能力。很多今天看起来理所当然的写法,其实都是近几年才加入标准的。
例如:
user?.profile?.name
const name = user?.name ?? 'Unknown'
const result = users
.filter(user => user.enabled)
.map(user => user.name)
const data = await fetch(url).then(res => res.json())
这些现代 JavaScript 写法,如果放到十年前,很多都无法直接使用。
本文从 ES2016 一直梳理到 ES2026,不追求把每一个标准变更都罗列出来,而是重点介绍那些实际开发中值得掌握、值得迁移、值得写进项目里的特性。
ES2016 是新的年度 ECMAScript 发布机制下的第一版;之后 ECMAScript 基本按照每年一个版本持续演进。
一、先搞清楚 ES、ECMAScript 和 JavaScript
经常有人问:
ES6、ES2016、ES2020 和 JavaScript 到底是什么关系?
可以简单理解:
ECMAScript
↓
JavaScript 遵循的语言标准
↓
浏览器 / Node.js / Deno / Bun 等运行时实现
而:
ES6
其实就是:
ECMAScript 2015
从 ES2016 开始,版本命名逐渐采用年份:
ES2016
ES2017
ES2018
ES2019
...
ES2026
所以现在再说:
ES6
更多是在说 JavaScript 的一次巨大语言升级,而不是最新标准。
二、ES2016:两个小功能,却非常实用
ES2016 是第一个按照年度节奏发布的 ECMAScript 版本,规模并不大,最值得记住的是两个功能:
Array.prototype.includes()
指数运算符 **
includes()
过去判断数组是否包含某个元素,经常使用:
[1, 2, 3].indexOf(2) !== -1
ES2016 后:
[1, 2, 3].includes(2)
结果:
true
代码明显更符合语义。
特别是判断字符串数组:
const roles = ['admin', 'editor', 'user']
if (roles.includes('admin')) {
// ...
}
相比:
roles.indexOf('admin') !== -1
includes() 更容易阅读。
指数运算符
以前:
Math.pow(2, 10)
现在:
2 ** 10
结果:
1024
虽然这个功能不如后面的 async/await、可选链重要,但它标志着 JavaScript 开始逐渐补齐现代语言常见的语法能力。
三、ES2017:async/await 彻底改变异步 JavaScript
如果要选一个对现代 JavaScript 影响最大的特性之一,那么:
async/await
绝对排得上号。
ES2017 正式加入 Async Functions。
在 async/await 出现之前,异步代码经常写成:
getUser(id)
.then(user => {
return getOrders(user.id)
})
.then(orders => {
return getProducts(orders)
})
.then(products => {
console.log(products)
})
.catch(error => {
console.error(error)
})
现在:
async function getUserData(id) {
try {
const user = await getUser(id)
const orders = await getOrders(user.id)
const products = await getProducts(orders)
return products
} catch (error) {
console.error(error)
}
}
代码从:Promise 链变成了:看起来像同步代码实际上仍然是异步代码这对 Node.js 后端开发尤其重要。
如果你使用过 NestJS,那么现代 NestJS 代码中几乎到处都是:
async createUser() {
const user = await this.userRepository.save(data)
return user
}
Object.values() 和 Object.entries()
ES2017 还增加了:
Object.values()
Object.entries()
以前:
const user = {
name: 'Tom',
age: 18
}
Object.keys(user)
现在可以直接:
Object.values(user)
得到:
['Tom', 18]
以及:
Object.entries(user)
得到:
[
['name', 'Tom'],
['age', 18]
]
这和后来的:
Object.fromEntries()
组合起来非常好用。
四、ES2018:对象展开、异步迭代和更强的正则
ES2018 开始,JavaScript 的现代代码风格开始越来越明显。
最常用的两个:
对象展开
异步迭代
对象 Spread
以前复制对象经常:
Object.assign({}, user)
现在:
const newUser = {
...user
}
修改某一个属性:
const newUser = {
...user,
age: 20
}
这已经成为现代 JavaScript 中最常见的对象操作方式之一。
数组和对象的 Rest
例如:
const user = {
id: 1,
name: 'Tom',
age: 18
}
const {
id,
...profile
} = user
得到:
id === 1
profile === {
name: 'Tom',
age: 18
}
这在接口参数处理中特别常见:
const {
password,
...userInfo
} = user
可以方便地把敏感字段排除出去。
for await...of
ES2018 支持异步迭代:
for await (const item of asyncIterable) {
console.log(item)
}
这个能力在:
流式数据
异步数据源
Node.js Stream
分页数据
网络数据
等场景非常有用。
ES2018 同时引入了 async generator:
async function* getUsers() {
yield await getUser(1)
yield await getUser(2)
yield await getUser(3)
}
然后:
for await (const user of getUsers()) {
console.log(user)
}
五、ES2019:数组处理开始变得舒服
ES2019 有几个非常实用的 API:
Array.flat()
Array.flatMap()
Object.fromEntries()
flat()
例如:
const arr = [
[1, 2],
[3, 4],
[5, 6]
]
arr.flat()
得到:
[1, 2, 3, 4, 5, 6]
多层数组:
const arr = [1, [2, [3, [4]]]]
arr.flat(Infinity)
可以直接展开所有层级。
flatMap()
例如:
const users = [
{
name: 'Tom',
roles: ['admin', 'user']
},
{
name: 'Jack',
roles: ['user']
}
]
可以:
const roles = users.flatMap(user => user.roles)
得到:
[
'admin',
'user',
'user'
]
它相当于:
map()
+
flat()
Object.fromEntries()
如果:
Object.entries(user)
是:
Object
↓
Array
那么:
Object.fromEntries()
就是:
Array
↓
Object
例如:
const entries = [
['name', 'Tom'],
['age', 18]
]
const user = Object.fromEntries(entries)
得到:
{
name: 'Tom',
age: 18
}
这在对象转换、过滤属性时特别好用:
const user = {
name: 'Tom',
age: 18,
password: '123456'
}
const safeUser = Object.fromEntries(
Object.entries(user)
.filter(([key]) => key !== 'password')
)
六、ES2020:现代 JavaScript 真正的分水岭
如果说 ES2017 让异步代码发生了变化,那么 ES2020 则让现代 JavaScript 的日常代码风格发生了非常明显的变化。
这一版最重要的几个特性:
?. 可选链
?? 空值合并
Promise.allSettled()
BigInt
globalThis
dynamic import()
可选链 ?.
以前:
const name =
user &&
user.profile &&
user.profile.name
现在:
const name = user?.profile?.name
如果:
user === null
整个表达式不会继续抛出:
Cannot read properties of null
而是得到:
undefined
可选方法调用
不仅可以访问属性:
user?.profile?.name
还可以调用方法:
user?.sayHello?.()
如果方法不存在,不会直接报错。
空值合并 ??
这个和 || 非常容易混淆。
例如:
const count = 0
const result = count || 10
结果:
10
因为 0 被认为是假值。
但是:
const result = count ?? 10
结果:
0
?? 只有在:
null
undefined
时才使用默认值。
因此:
const page = options.page ?? 1
通常比:
const page = options.page || 1
更加准确。
可选链 + 空值合并
现代 JavaScript 最经典的组合之一:
const name = user?.profile?.name ?? 'Unknown'
意思就是:
如果 user、profile 或 name 不存在,就使用 Unknown。
这已经成为现代 Node.js / Vue / React / TypeScript 项目中的标配写法。
七、Promise.allSettled()
以前:
await Promise.all(tasks)
只要其中一个 Promise 失败:
Promise.all
↓
一个失败
↓
整个 Promise reject
但有时候我们需要:
不管每个任务成功还是失败,都把结果收集回来。
这时候:
const results = await Promise.allSettled(tasks)
例如:
const results = await Promise.allSettled([
sendEmail(),
sendSms(),
sendWebhook()
])
结果类似:
[
{
status: 'fulfilled',
value: ...
},
{
status: 'rejected',
reason: ...
},
{
status: 'fulfilled',
value: ...
}
]
特别适合:
批量请求
批量通知
批量任务
批量 API 调用
八、BigInt
JavaScript 传统的 Number 对整数精度存在限制。
例如:
Number.MAX_SAFE_INTEGER
是:
9007199254740991
超过这个范围以后,整数精度可能出现问题。
ES2020 引入:
BigInt
例如:
const id = 9007199254740992n
注意末尾:
n
运算:
const a = 100n
const b = 200n
console.log(a + b)
结果:
300n
但是:
100n + 1
是不允许的。
必须保证两边类型一致:
100n + 1n
在处理:
超大整数
数据库 BIGINT
高精度 ID
金融计算中的整数场景
时非常有用。
九、dynamic import()
ES2020 支持:
const module = await import('./module.js')
这和传统:
import module from './module.js'
不同。
传统 import 通常在模块加载阶段就确定。
动态 import 可以根据运行时条件加载:
if (needEditor) {
const editor = await import('./editor.js')
}
这对:
代码分割
懒加载
插件系统
按需加载
非常重要。
十、ES2021:让对象和 Promise 操作进一步简洁
ES2021 最值得记住:
String.replaceAll()
Promise.any()
逻辑赋值
数字分隔符
replaceAll()
以前:
str.replace(/foo/g, 'bar')
现在:
str.replaceAll('foo', 'bar')
例如:
const text = 'foo foo foo'
text.replaceAll('foo', 'bar')
结果:
bar bar bar
语义非常直接。
Promise.any()
const result = await Promise.any([
requestFromServerA(),
requestFromServerB(),
requestFromServerC()
])
只要其中一个成功:
返回第一个成功结果
如果全部失败:
AggregateError
这特别适合:
多节点请求
多 CDN
多数据源
容灾请求
十一、逻辑赋值运算符
ES2021 增加:
&&=
||=
??=
例如:
let name = null
name ??= 'Tom'
等价于:
if (name == null) {
name = 'Tom'
}
再比如:
user.name ||= 'Unknown'
如果 user.name 是假值,就赋值。
而:
user.name ??= 'Unknown'
只在:
null
undefined
时赋值。
这个特性在配置对象处理中非常方便。
十二、ES2022:现代 JavaScript 类开始真正成熟
ES2022 有几个很值得掌握的能力:
类字段
私有字段 #
static block
top-level await
Error cause
Array.at()
Object.hasOwn()
类字段
以前:
class User {
constructor() {
this.name = 'Tom'
}
}
现在:
class User {
name = 'Tom'
age = 18
}
更清晰。
私有字段
ES2022 正式支持真正的私有字段:
class User {
#password
constructor(password) {
this.#password = password
}
checkPassword(password) {
return this.#password === password
}
}
外部无法直接:
user.#password
这是语法层面的私有字段,而不是简单的命名约定。
static block
类中可以定义:
class Config {
static value
static {
Config.value = loadConfig()
}
}
适合:
复杂静态初始化
类级别初始化逻辑
十三、Top-level await
以前 await 通常只能出现在:
async function
内部。
ES2022 允许 ES Module 顶层直接使用:
const config = await loadConfig()
console.log(config)
例如:
const response = await fetch(
'https://example.com/config.json'
)
const config = await response.json()
这对于 Node.js ESM、现代前端构建工具和模块初始化非常方便。
十四、Error Cause
现代系统排查错误时,经常需要知道:
当前错误到底是由哪个底层错误引起的?
ES2022 支持:
try {
await database.query()
} catch (error) {
throw new Error(
'Failed to load user',
{
cause: error
}
)
}
这样就可以保留错误链:
业务异常
↓
数据库异常
↓
网络异常
这对于后端服务尤其有价值。
十五、Array.at()
以前获取最后一个元素:
arr[arr.length - 1]
现在:
arr.at(-1)
例如:
const numbers = [1, 2, 3]
numbers.at(-1)
得到:
3
同样支持:
numbers.at(-2)
得到:
2
这个 API 很小,但非常舒服。
十六、Object.hasOwn()
以前判断对象自身属性:
Object.prototype.hasOwnProperty.call(
user,
'name'
)
现在:
Object.hasOwn(user, 'name')
简单很多。
这也是 ES2022 中一个很值得记住的实用 API。
十七、ES2023:数组终于有了一套“不修改原数组”的 API
这是 ES2023 非常值得关注的一组变化。
新增:
toSorted()
toReversed()
toSpliced()
with()
findLast()
findLastIndex()
其中最重要的思想是:
返回新数组,而不是修改原数组。
toSorted()
传统:
const arr = [3, 1, 2]
arr.sort()
sort() 会修改原数组。
现在:
const arr = [3, 1, 2]
const sorted = arr.toSorted()
得到:
arr → [3, 1, 2]
sorted → [1, 2, 3]
这对于:
React
Vue
状态管理
函数式编程
非常友好。
toReversed()
传统:
arr.reverse()
会修改原数组。
现在:
const reversed = arr.toReversed()
原数组保持不变。
toSpliced()
传统:
arr.splice(1, 1)
直接修改原数组。
ES2023:
const result = arr.toSpliced(1, 1)
返回新数组。
with()
修改数组指定位置:
const arr = ['a', 'b', 'c']
const result = arr.with(1, 'x')
结果:
['a', 'x', 'c']
原数组:
['a', 'b', 'c']
不会改变。
findLast()
以前:
arr.find(...)
从前往后找。
现在:
arr.findLast(...)
从后往前找。
例如:
const users = [
{ name: 'Tom', active: true },
{ name: 'Jack', active: false },
{ name: 'Lucy', active: true }
]
const user = users.findLast(
user => user.active
)
得到:
Lucy
十八、ES2024:Promise 和对象数据处理继续进化
ES2024 中非常值得关注:
Promise.withResolvers()
Object.groupBy()
Map.groupBy()
/v 正则
ArrayBuffer resize
String well-formed Unicode
Promise.withResolvers()
以前创建一个可以从外部控制的 Promise:
let resolve
let reject
const promise = new Promise((res, rej) => {
resolve = res
reject = rej
})
ES2024:
const {
promise,
resolve,
reject
} = Promise.withResolvers()
代码明显更加清晰。
这个 API 在:
事件驱动
回调转 Promise
任务队列
复杂异步控制
等场景很有用。
十九、Object.groupBy()
以前我们经常手写:
const groups = {}
for (const user of users) {
const key = user.department
if (!groups[key]) {
groups[key] = []
}
groups[key].push(user)
}
现在:
const groups = Object.groupBy(
users,
user => user.department
)
例如:
const users = [
{ name: 'Tom', department: 'dev' },
{ name: 'Jack', department: 'dev' },
{ name: 'Lucy', department: 'hr' }
]
得到:
{
dev: [
{ name: 'Tom', department: 'dev' },
{ name: 'Jack', department: 'dev' }
],
hr: [
{ name: 'Lucy', department: 'hr' }
]
}
这对于数据处理非常实用。
Map.groupBy()
如果分组 Key 本身需要保持对象等复杂类型,可以使用:
Map.groupBy()
区别可以简单理解为:
Object.groupBy()
→ Object
Map.groupBy()
→ Map
二十、ES2025:继续补齐现代开发中的“小痛点”
ES2025 已经进入标准,里面有一些非常值得注意的 API。
例如:
RegExp.escape()
Set 新操作
Promise.try()
Iterator helpers
Float16Array
其中有些属于非常实用的工具型能力。
RegExp.escape()
以前如果需要把用户输入放进正则表达式,经常需要自己处理特殊字符:
function escapeRegExp(value) {
return value.replace(
/[.*+?^${}()|[\]\\]/g,
'\\$&'
)
}
现在:
const pattern = RegExp.escape(userInput)
再拼进正则:
const regex = new RegExp(
RegExp.escape(userInput)
)
语义清晰很多。
特别是在:
搜索
高亮
动态正则
用户输入
这些场景中非常有价值。
Set 新增集合运算
现代 JavaScript 的 Set 越来越像真正的集合类型。
可以直接进行:
setA.union(setB)
以及:
setA.intersection(setB)
还有:
setA.difference(setB)
和:
setA.symmetricDifference(setB)
以及集合关系判断:
setA.isSubsetOf(setB)
setA.isSupersetOf(setB)
setA.isDisjointFrom(setB)
以前我们经常需要手动写循环,现在表达集合关系会更加自然。
二十一、ES2026:JavaScript 开始进一步减少“工具函数”
ES2026 是 ECMAScript 第 17 版。
这里需要特别说明:
ES2026 已经正式进入标准,但“标准已经确定”和“所有 Node.js / 浏览器版本都支持”是两个不同的问题。
ES2026 中正式进入标准的重点能力包括:
Array.fromAsync()
Error.isError()
Iterator.concat()
Map / WeakMap getOrInsert
Uint8Array Base64 / Hex
JSON.parse source access
Math.sumPrecise()
这些功能有一个共同特点:
很多以前需要自己写工具函数或者依赖第三方库的事情,现在逐渐被 JavaScript 标准库吸收。
二十二、Array.fromAsync()
如果你处理异步 Iterable:
const result = []
for await (const item of asyncIterable) {
result.push(item)
}
ES2026 可以:
const result = await Array.fromAsync(
asyncIterable
)
例如:
async function* getUsers() {
yield { id: 1 }
yield { id: 2 }
yield { id: 3 }
}
const users = await Array.fromAsync(
getUsers()
)
最终:
[
{ id: 1 },
{ id: 2 },
{ id: 3 }
]
对于:
AsyncIterator
异步分页
异步数据流
Node.js 流式数据
会非常方便。
二十三、Error.isError()
以前判断一个值是不是 Error,很多代码会写:
value instanceof Error
但是跨 Realm、不同执行上下文等情况下,instanceof 并不总是最理想的判断方式。
ES2026 提供:
Error.isError(value)
例如:
if (Error.isError(error)) {
console.log(error.message)
}
语义更加明确:
判断这个值是不是一个 Error。
二十四、Map.getOrInsert()
一个非常常见的代码:
let list = map.get(key)
if (!list) {
list = []
map.set(key, list)
}
list.push(value)
这种代码在:
分组
缓存
索引
计数
图结构
中非常常见。
新的 API 可以简化这种模式:
const list = map.getOrInsert(key, [])
然后:
list.push(value)
这就是典型的:
Get or Insert。
它可以明显减少 Map 初始化模板代码。
二十五、Uint8Array 的 Base64 和 Hex
以前 Node.js 中处理 Base64,经常使用:
Buffer
浏览器里又经常看到:
btoa()
atob()
不同运行环境的处理方式并不统一。
ES2026 给 Uint8Array 增加了相关能力:
bytes.toBase64()
以及:
Uint8Array.fromBase64(value)
同时支持 Hex:
bytes.toHex()
和:
Uint8Array.fromHex(value)
对于:
二进制数据
文件
加密
签名
网络协议
Base64
Hex
都会比较实用。
二十六、Math.sumPrecise()
JavaScript 的浮点数一直存在经典问题:
0.1 + 0.2
得到:
0.30000000000000004
对于大量数字累加,还会遇到浮点误差累积问题。
ES2026 引入:
Math.sumPrecise()
用于对可迭代数字进行更加精确的求和。
例如:
Math.sumPrecise([
1e17,
1,
-1e17
])
可以得到更符合数学预期的结果。
需要注意:
Math.sumPrecise()解决的是浮点求和精度问题,并不意味着 JavaScript 的 Number 突然变成了任意精度小数类型。
金融场景依然需要根据具体需求考虑:
整数化
BigInt
Decimal 类库
专门的高精度方案
二十七、从 ES2016 到 ES2026,JavaScript 到底发生了什么变化?
如果把这十年压缩成一条时间线,大概是:
ES2016
↓
includes / **
ES2017
↓
async / await
ES2018
↓
对象展开 / async iterator
ES2019
↓
flat / flatMap / fromEntries
ES2020
↓
?. / ?? / BigInt / Promise.allSettled
↓
现代 JS 的重要分水岭
ES2021
↓
replaceAll / Promise.any / ??= / ||=
ES2022
↓
class fields / #private / top-level await
ES2023
↓
toSorted / toReversed / toSpliced / with
ES2024
↓
groupBy / withResolvers / 新 ArrayBuffer 能力
ES2025
↓
RegExp.escape / Set operations / Promise.try 等
ES2026
↓
Array.fromAsync / Error.isError
Map.getOrInsert / Uint8Array Base64
Math.sumPrecise 等
从这里其实可以看出一个非常明显的趋势。
二十八、JavaScript 正在从“语法糖”走向“标准库能力”
早期 JavaScript 的问题之一是:
很多常见操作都需要开发者自己封装。
例如:
数组是否包含
对象分组
对象转换
字符串替换
正则转义
Map 初始化
异步数据转换
二进制编码
于是项目里会出现大量:
utils.js
helper.js
lodash
underscore
自研工具库
而 ECMAScript 的发展方向越来越明显:
语言语法
↓
标准 API
↓
解决日常开发中的重复问题
例如以前:
indexOf() !== -1
现在:
includes()
以前:
Object.keys()
.reduce(...)
现在:
Object.groupBy()
以前:
arr.slice().sort()
现在:
arr.toSorted()
以前:
new Promise((resolve, reject) => {
...
})
现在某些场景:
Promise.withResolvers()
以前:
if (!map.has(key)) {
map.set(key, [])
}
现在:
map.getOrInsert(key, [])
JavaScript 正在不断把这些高频模式变成语言标准的一部分。
二十九、TypeScript 和 ES 标准是什么关系?
很多现代 Node.js 开发者使用:TypeScript容易产生一个误区:
TypeScript 新语法是不是 ES 新语法?
其实不是。
TypeScript 自己增加了很多类型系统能力:
interface User {
id: number
name: string
}
:
type UserId = string | number
这些属于 TypeScript。
而:
user?.name
:
user ?? defaultUser
属于 ECMAScript。
可以简单理解:
TypeScript
↓
类型系统 + JavaScript
↓
编译 / 转换
↓
JavaScript
↓
Node.js / Browser
所以学习 TypeScript 时,JavaScript 的 ES 标准依然是基础。
三十、结语
从 ES2016 到 ES2026,JavaScript 已经发生了非常大的变化。
如果把这十年的变化总结成几个关键词:
ES2016
更实用的数组 API
ES2017
异步编程革命
ES2018
对象操作 + 异步迭代
ES2019
数据处理能力增强
ES2020
?. + ?? + BigInt
现代 JavaScript 分水岭
ES2021
Promise 和赋值操作增强
ES2022
类、私有字段、顶层 await
ES2023
不修改原数组的新 API
ES2024
数据分组和 Promise 工具
ES2025
RegExp、Set、Promise 等进一步完善
ES2026
异步 Iterable、Map、二进制、错误处理等能力继续标准化
真正值得学习的并不是:
ES2016 有几个 API
ES2017 有几个 API
ES2018 有几个 API
而是理解 JavaScript 这十年的演进方向:
回调
↓
Promise
↓
async / await
手写工具函数
↓
标准 API
修改原对象
↓
不可变操作
简单对象
↓
更强的数据处理能力
运行时差异
↓
更加统一的标准能力
对于现在的 Node.js / TypeScript 开发者来说,至少应该把:
async / await
?
??
Promise.allSettled
Promise.any
Object.entries
Object.fromEntries
Array.flat
Array.flatMap
Array.at
toSorted
toReversed
toSpliced
Object.groupBy
Map.groupBy
Promise.withResolvers
Set operations
这些现代能力真正用熟。
这样你再回头看几年前的 JavaScript 代码,会明显感觉到:
现在的 JavaScript,已经不是当年的 JavaScript 了。
