Skip to content

JavaScript 中的 ECMAScript 版本演进与兼容性

很多人学 JavaScript 时,都会遇到一类很常见的说法:

  1. ES5 很重要
  2. ES6 改变很大
  3. 现在已经是 ES202x 时代了

但如果只停在这些名字上,实际工程里还是很容易混。

真正要回答的问题通常不是:这个特性属于 ES 几。

而是:这个能力到底是语言层的新特性、运行时新增的 API,还是需要构建工具帮忙处理的兼容性问题。

1. ECMAScript 和 JavaScript 到底是什么关系

可以把两者关系理解成:

  1. JavaScript 是我们日常使用的这门语言
  2. ECMAScript 是这门语言核心语法和对象体系的标准规范

实际写项目时,更多是这样:ECMAScript 更像标准,JavaScript 更像这个标准在真实世界里的实现和使用形态。

这也是为什么大家讨论版本时,经常会说:

  1. ES5
  2. ES6
  3. ES2017
  4. ES2020

这些名字,本质上是在说:ECMAScript 标准演进到了哪个阶段。

2. 为什么会有这么多版本名

版本名看起来复杂,主要是因为 ECMAScript 的命名经历过两个阶段。

2.1 早期常见叫法:ES3、ES5、ES6

最常见的历史说法包括:

  1. ES3
  2. ES5
  3. ES6

其中 ES6 特别有名,是因为它带来的变化非常集中,工程影响也非常大。

2.2 后来的年度命名:ES2015、ES2016、ES2017...

ES6 开始,官方更常见的命名方式变成了年份。

也就是说:

  1. ES6 基本等同于 ES2015
  2. 后面依次有 ES2016ES2017ES2018 ...

所以工程里常见的说法:

  1. “支持 ES6”
  2. “支持 ES2015”

很多时候说的是同一波核心新能力。

3. 为什么 ES5 仍然是一个重要分界线

虽然现在早就不只 ES5 了,但 ES5 仍然很重要,因为它可以看作:现代 JavaScript 的一个稳定历史基线。

很多旧浏览器时代的兼容性讨论,都是拿 ES5 做参考。

3.1 ES5 提供了哪些关键基础

ES5 本身没有后来的语法革新那么显眼,但它补上了很多现代写法的基础土壤,比如:

  1. 严格模式 use strict
  2. 数组迭代方法,如 forEachmapfilter
  3. Object.defineProperty
  4. JSON
  5. 更稳定的对象属性控制方式

可以直接把它理解成:ES5 更像是在把 JavaScript 从“早期脚本语言”往“可工程化语言”方向继续推进。

3.2 为什么很多兼容性工具会提到 ES5

因为:

  1. 很多转译工具会把较新的语法降级到 ES5
  2. 很多旧环境至少能把 ES5 当作可运行下限

这也是为什么工程里常常会听到:把代码编译到 ES5。

这句话的意思通常不是“项目只会用 ES5 写”,而是:最终产物尽量能被只支持到 ES5 左右能力的环境执行。

4. 为什么 ES2015 是真正的大拐点

如果说 ES5 是稳定基线,那 ES2015 更像是现代 JavaScript 真正全面起势的分界线。

它带来的变化,不只是多几个语法糖,而是:变量、函数、对象、异步、模块化、面向对象写法,几乎都出现了更现代的表达方式。

4.1 ES2015 最值得先掌握的能力

工程里影响最大的通常包括:

  1. letconst
  2. 箭头函数
  3. 模板字符串
  4. 解构赋值
  5. 默认参数
  6. 剩余参数和扩展运算符
  7. class
  8. Promise
  9. Symbol
  10. MapSet
  11. import/export

这些能力的共同意义是:

  1. 代码可读性更强
  2. 边界表达更清楚
  3. 工程组织更自然

4.2 为什么 ES2015 不只是“语法升级”

很多人会把 ES2015 只理解成:

  1. var 换成 let
  2. 普通函数换成箭头函数

这其实太窄了。

更贴近工程感受的说法是,ES2015 改变了很多核心写法习惯:

  1. 变量声明方式变了
  2. 函数参数和 this 语义的表达更自然了
  3. 对象和类的组织方式更清晰了
  4. 模块化第一次以语言标准形式站稳了
  5. 异步结果模型有了 Promise

要注意的是:ES2015 不是一次局部补丁,而是现代 JavaScript 编程风格的重要起点。

5. ES2016 之后为什么看起来“每年都在更新”

从 ES2016 往后,标准演进进入了一种更稳定的节奏:每年持续加一些新能力,但单次变化通常没有 ES2015 那么剧烈。

这也是为什么:

  1. 很多人知道 ES6
  2. 但对 ES2017、ES2018、ES2019 的感知没那么强

5.1 这并不意味着后续版本不重要

后续版本依然带来了很多工程里常用的能力,比如:

  1. async/await
  2. Object.entriesObject.values
  3. Promise.finally
  4. 可选链 ?.
  5. 空值合并 ??
  6. Array.prototype.flat
  7. globalThis

只是这些能力更像:在已经现代化的 JavaScript 之上,继续把常见写法补得更顺手。

5.2 更适合学习的方式,不是背年份

工程上更实用的方式通常不是背:

  1. 哪个特性属于哪一年

而是先按能力分类去理解:

  1. 变量与作用域
  2. 函数表达
  3. 对象与类
  4. 异步编程
  5. 模块化
  6. 标准内置对象增强

这样更容易知道:这个特性到底在解决什么问题。

6. 版本差异通常分成哪几类

实际工程里,“版本差异”并不是一件单一的事。

至少要拆成下面三类来看。

6.1 语法层差异

例如:

  1. letconst
  2. 箭头函数
  3. 解构赋值
  4. 可选链

这类差异的特点是:代码连解析都可能过不去。

如果目标环境太老,连源码都读不懂,就更别说执行了。

6.2 标准对象和方法差异

例如:

  1. Promise
  2. Map
  3. Set
  4. Array.prototype.includes
  5. Object.fromEntries

这类差异的特点是:语法可能能跑,但真正执行时会发现某个对象或方法不存在。

6.3 宿主环境能力差异

例如:

  1. 某个浏览器是否支持 fetch
  2. 某个 Node.js 版本是否原生支持 fetch
  3. 某个环境里是否存在 window
  4. 某个环境里是否存在 document

这类差异已经不只是 ECMAScript 标准本身,而是:运行时环境到底给了哪些额外能力。

🌟 所以“版本兼容性”真正至少要回答三件事:

  1. 语法能不能被解析
  2. 标准对象和方法在不在
  3. 宿主环境 API 在不在

7. Babel 在解决什么问题

Babel 可以看成 把较新的 JavaScript 语法转换成较老环境更容易理解的写法

例如:

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

经过转译后,可能变成更传统的函数表达式形式。

7.1 Babel 最擅长处理什么

Babel 更擅长处理的是:

  1. 语法层的新特性
  2. 一部分语义可以通过辅助代码模拟的能力

也就是说,它最擅长解决的是:老环境看不懂这段源码怎么办。

7.2 Babel 不是万能兼容器

这点特别重要。

Babel 不能简单理解成“开了就什么都兼容了”。

例如:

  1. 语法可以转
  2. 但某些运行时对象和原生能力,并不是单靠转语法就能凭空变出来

比如:

  1. 老环境没有 Promise
  2. 老环境没有 Map
  3. 老环境没有 fetch

这时仅靠语法转译通常不够。

8. Polyfill 在解决什么问题

Polyfill 可以看成 在老环境里补上一些原本没有的标准对象、方法或 API 实现

例如:

  1. Promise
  2. 补数组新方法
  3. 补对象新方法

它更像是在回答:环境里缺这个能力,能不能补一个近似或标准实现。

8.1 Polyfill 和 Babel 的分工

这两个词经常一起出现,但解决的问题不完全一样。

工具主要解决什么
Babel新语法老环境看不懂的问题
Polyfill环境里缺少对象、方法或部分能力的问题

把这层分工拆开看:

  1. Babel 更偏“把写法改成老环境能读懂的样子”
  2. Polyfill 更偏“把老环境没有的能力补进来”

8.2 Polyfill 也有边界

不是所有能力都能通过 Polyfill 完整补出来。

例如:

  1. 纯语言层对象方法,通常更容易补
  2. 真正依赖浏览器或宿主环境底层实现的能力,就不一定能等价补齐

所以工程里不能简单理解成:缺什么都补一个 Polyfill 就行。

9. 为什么“语法能转”不等于“功能一定能跑”

这是兼容性里最容易踩的坑之一。

比如某段代码用了:

  1. 箭头函数
  2. Promise
  3. fetch

这里其实混着三层问题:

  1. 箭头函数是语法问题
  2. Promise 更偏标准对象问题
  3. fetch 还涉及宿主环境支持问题

这时就不能只说一句:我已经用 Babel 处理过了。

因为 Babel 主要解决的是第一层,不一定自动解决后两层。

10. 浏览器兼容性为什么总要配合目标范围来谈

兼容性从来不是抽象讨论“支不支持”,而是要结合目标环境。

真正的工程问题通常是:

  1. 我们要支持哪些浏览器版本
  2. 要不要兼容较老的移动端 WebView
  3. Node.js 的最低版本是多少

因为:

  1. 目标版本不同,构建策略不同
  2. 能不能放心使用某些新特性,也会不同

这也是为什么工程里经常会出现:

  1. browserslist
  2. 构建目标配置
  3. 不同环境的产物策略

它们本质上都在回答:我们的代码,最终要交给谁来运行。

11. Node.js 版本兼容性为什么也要单独看

很多人只会把兼容性理解成浏览器兼容,但现代前端工程里,Node.js 版本也很关键。

因为 Node.js 不只是服务端运行时,它还是很多工程工具的底座。

例如:

  1. 某些构建工具要求更高版本的 Node.js
  2. 某些 Node.js 版本原生支持 fetch
  3. 某些 ESM 行为在不同 Node.js 版本里差异更明显

所以工程里经常要同时关心两套兼容性:

  1. 浏览器产物兼容性
  2. Node.js 开发和构建环境兼容性

12. 一张版本与兼容性的总关系图

mermaid
flowchart TD
    A[ECMAScript 标准演进] --> B[新语法]
    A --> C[新对象与新方法]
    B --> D[运行时能否解析]
    C --> E[运行时是否内置实现]
    D --> F[Babel 转译]
    E --> G[Polyfill 或放弃低版本兼容]
    A --> H[宿主环境实现支持]
    H --> I[浏览器版本]
    H --> J[Node.js 版本]

这张图最重要的意思是:

  1. 标准在演进
  2. 运行时支持有快有慢
  3. 工程上通常要用转译、Polyfill 和目标环境配置一起兜住兼容性

13. 工程里最常见的误区

13.1 误把 ECMAScript 版本名当成“会不会写现代 JavaScript”

知道 ES2018ES2020 这些名字当然有用,但更重要的是知道:

  1. 这个特性解决什么问题
  2. 它属于语法、标准对象,还是宿主 API

13.2 误把 Babel 当成所有兼容问题的终点

很多兼容问题并不是“代码写法太新”,而是:

  1. 运行时根本没有这个对象
  2. 当前环境根本没有这个宿主能力

13.3 误把宿主环境 API 当成 ECMAScript 标准能力

例如把:

  1. fetch
  2. document
  3. localStorage

都混进“ES 新特性”里,这会让理解越来越乱。

13.4 误以为所有新能力都值得为了低版本完全兼容去补

工程上更常见的做法其实是:

  1. 先定义目标用户环境
  2. 再决定哪些能力要转译
  3. 再决定哪些能力值得引入 Polyfill
  4. 超出收益的兼容成本就不再硬扛

14. 一份检查清单

当你判断某个新特性能不能用时,可以问自己:

  1. 这是语法、标准对象方法,还是宿主 API
  2. 目标浏览器和 Node.js 版本分别是什么
  3. 这类问题该靠 Babel 解决,还是靠 Polyfill 解决
  4. 这个能力能不能被等价转译或补齐
  5. 为了兼容低版本,引入的成本是否值得

15. 小结

这一篇最想建立的核心认识是:

  1. ECMAScript 版本演进说的是 JavaScript 标准在持续前进
  2. 兼容性问题至少要区分语法、标准对象和宿主环境三层
  3. BabelPolyfill 分工不同,不能混成一回事
  4. “能不能用”永远要结合目标浏览器和 Node.js 版本来讨论

如果只压缩成一句话,可以记住:

不要只问这个特性属于 ES 几,更要问当前目标环境到底能不能解析、能不能实现、值不值得兼容。

基于 VitePress 构建的个人技术笔记。