Appearance
JavaScript 中的 ECMAScript 版本演进与兼容性
很多人学 JavaScript 时,都会遇到一类很常见的说法:
ES5很重要ES6改变很大- 现在已经是
ES202x时代了
但如果只停在这些名字上,实际工程里还是很容易混。
真正要回答的问题通常不是:这个特性属于 ES 几。
而是:这个能力到底是语言层的新特性、运行时新增的 API,还是需要构建工具帮忙处理的兼容性问题。
1. ECMAScript 和 JavaScript 到底是什么关系
可以把两者关系理解成:
JavaScript是我们日常使用的这门语言ECMAScript是这门语言核心语法和对象体系的标准规范
实际写项目时,更多是这样:ECMAScript 更像标准,JavaScript 更像这个标准在真实世界里的实现和使用形态。
这也是为什么大家讨论版本时,经常会说:
ES5ES6ES2017ES2020
这些名字,本质上是在说:ECMAScript 标准演进到了哪个阶段。
2. 为什么会有这么多版本名
版本名看起来复杂,主要是因为 ECMAScript 的命名经历过两个阶段。
2.1 早期常见叫法:ES3、ES5、ES6
最常见的历史说法包括:
ES3ES5ES6
其中 ES6 特别有名,是因为它带来的变化非常集中,工程影响也非常大。
2.2 后来的年度命名:ES2015、ES2016、ES2017...
从 ES6 开始,官方更常见的命名方式变成了年份。
也就是说:
ES6基本等同于ES2015- 后面依次有
ES2016、ES2017、ES2018...
所以工程里常见的说法:
- “支持 ES6”
- “支持 ES2015”
很多时候说的是同一波核心新能力。
3. 为什么 ES5 仍然是一个重要分界线
虽然现在早就不只 ES5 了,但 ES5 仍然很重要,因为它可以看作:现代 JavaScript 的一个稳定历史基线。
很多旧浏览器时代的兼容性讨论,都是拿 ES5 做参考。
3.1 ES5 提供了哪些关键基础
ES5 本身没有后来的语法革新那么显眼,但它补上了很多现代写法的基础土壤,比如:
- 严格模式
use strict - 数组迭代方法,如
forEach、map、filter Object.definePropertyJSON- 更稳定的对象属性控制方式
可以直接把它理解成:ES5 更像是在把 JavaScript 从“早期脚本语言”往“可工程化语言”方向继续推进。
3.2 为什么很多兼容性工具会提到 ES5
因为:
- 很多转译工具会把较新的语法降级到 ES5
- 很多旧环境至少能把 ES5 当作可运行下限
这也是为什么工程里常常会听到:把代码编译到 ES5。
这句话的意思通常不是“项目只会用 ES5 写”,而是:最终产物尽量能被只支持到 ES5 左右能力的环境执行。
4. 为什么 ES2015 是真正的大拐点
如果说 ES5 是稳定基线,那 ES2015 更像是现代 JavaScript 真正全面起势的分界线。
它带来的变化,不只是多几个语法糖,而是:变量、函数、对象、异步、模块化、面向对象写法,几乎都出现了更现代的表达方式。
4.1 ES2015 最值得先掌握的能力
工程里影响最大的通常包括:
let和const- 箭头函数
- 模板字符串
- 解构赋值
- 默认参数
- 剩余参数和扩展运算符
classPromiseSymbolMap和Setimport/export
这些能力的共同意义是:
- 代码可读性更强
- 边界表达更清楚
- 工程组织更自然
4.2 为什么 ES2015 不只是“语法升级”
很多人会把 ES2015 只理解成:
var换成let- 普通函数换成箭头函数
这其实太窄了。
更贴近工程感受的说法是,ES2015 改变了很多核心写法习惯:
- 变量声明方式变了
- 函数参数和
this语义的表达更自然了 - 对象和类的组织方式更清晰了
- 模块化第一次以语言标准形式站稳了
- 异步结果模型有了
Promise
要注意的是:ES2015 不是一次局部补丁,而是现代 JavaScript 编程风格的重要起点。
5. ES2016 之后为什么看起来“每年都在更新”
从 ES2016 往后,标准演进进入了一种更稳定的节奏:每年持续加一些新能力,但单次变化通常没有 ES2015 那么剧烈。
这也是为什么:
- 很多人知道 ES6
- 但对 ES2017、ES2018、ES2019 的感知没那么强
5.1 这并不意味着后续版本不重要
后续版本依然带来了很多工程里常用的能力,比如:
async/awaitObject.entries、Object.valuesPromise.finally- 可选链
?. - 空值合并
?? Array.prototype.flatglobalThis
只是这些能力更像:在已经现代化的 JavaScript 之上,继续把常见写法补得更顺手。
5.2 更适合学习的方式,不是背年份
工程上更实用的方式通常不是背:
- 哪个特性属于哪一年
而是先按能力分类去理解:
- 变量与作用域
- 函数表达
- 对象与类
- 异步编程
- 模块化
- 标准内置对象增强
这样更容易知道:这个特性到底在解决什么问题。
6. 版本差异通常分成哪几类
实际工程里,“版本差异”并不是一件单一的事。
至少要拆成下面三类来看。
6.1 语法层差异
例如:
let、const- 箭头函数
- 解构赋值
- 可选链
这类差异的特点是:代码连解析都可能过不去。
如果目标环境太老,连源码都读不懂,就更别说执行了。
6.2 标准对象和方法差异
例如:
PromiseMapSetArray.prototype.includesObject.fromEntries
这类差异的特点是:语法可能能跑,但真正执行时会发现某个对象或方法不存在。
6.3 宿主环境能力差异
例如:
- 某个浏览器是否支持
fetch - 某个 Node.js 版本是否原生支持
fetch - 某个环境里是否存在
window - 某个环境里是否存在
document
这类差异已经不只是 ECMAScript 标准本身,而是:运行时环境到底给了哪些额外能力。
🌟 所以“版本兼容性”真正至少要回答三件事:
- 语法能不能被解析
- 标准对象和方法在不在
- 宿主环境 API 在不在
7. Babel 在解决什么问题
Babel 可以看成 把较新的 JavaScript 语法转换成较老环境更容易理解的写法。
例如:
js
const add = (a, b) => a + b经过转译后,可能变成更传统的函数表达式形式。
7.1 Babel 最擅长处理什么
Babel 更擅长处理的是:
- 语法层的新特性
- 一部分语义可以通过辅助代码模拟的能力
也就是说,它最擅长解决的是:老环境看不懂这段源码怎么办。
7.2 Babel 不是万能兼容器
这点特别重要。
Babel 不能简单理解成“开了就什么都兼容了”。
例如:
- 语法可以转
- 但某些运行时对象和原生能力,并不是单靠转语法就能凭空变出来
比如:
- 老环境没有
Promise - 老环境没有
Map - 老环境没有
fetch
这时仅靠语法转译通常不够。
8. Polyfill 在解决什么问题
Polyfill 可以看成 在老环境里补上一些原本没有的标准对象、方法或 API 实现。
例如:
- 补
Promise - 补数组新方法
- 补对象新方法
它更像是在回答:环境里缺这个能力,能不能补一个近似或标准实现。
8.1 Polyfill 和 Babel 的分工
这两个词经常一起出现,但解决的问题不完全一样。
| 工具 | 主要解决什么 |
|---|---|
Babel | 新语法老环境看不懂的问题 |
Polyfill | 环境里缺少对象、方法或部分能力的问题 |
把这层分工拆开看:
Babel更偏“把写法改成老环境能读懂的样子”Polyfill更偏“把老环境没有的能力补进来”
8.2 Polyfill 也有边界
不是所有能力都能通过 Polyfill 完整补出来。
例如:
- 纯语言层对象方法,通常更容易补
- 真正依赖浏览器或宿主环境底层实现的能力,就不一定能等价补齐
所以工程里不能简单理解成:缺什么都补一个 Polyfill 就行。
9. 为什么“语法能转”不等于“功能一定能跑”
这是兼容性里最容易踩的坑之一。
比如某段代码用了:
- 箭头函数
Promisefetch
这里其实混着三层问题:
- 箭头函数是语法问题
Promise更偏标准对象问题fetch还涉及宿主环境支持问题
这时就不能只说一句:我已经用 Babel 处理过了。
因为 Babel 主要解决的是第一层,不一定自动解决后两层。
10. 浏览器兼容性为什么总要配合目标范围来谈
兼容性从来不是抽象讨论“支不支持”,而是要结合目标环境。
真正的工程问题通常是:
- 我们要支持哪些浏览器版本
- 要不要兼容较老的移动端 WebView
- Node.js 的最低版本是多少
因为:
- 目标版本不同,构建策略不同
- 能不能放心使用某些新特性,也会不同
这也是为什么工程里经常会出现:
browserslist- 构建目标配置
- 不同环境的产物策略
它们本质上都在回答:我们的代码,最终要交给谁来运行。
11. Node.js 版本兼容性为什么也要单独看
很多人只会把兼容性理解成浏览器兼容,但现代前端工程里,Node.js 版本也很关键。
因为 Node.js 不只是服务端运行时,它还是很多工程工具的底座。
例如:
- 某些构建工具要求更高版本的 Node.js
- 某些 Node.js 版本原生支持
fetch - 某些 ESM 行为在不同 Node.js 版本里差异更明显
所以工程里经常要同时关心两套兼容性:
- 浏览器产物兼容性
- 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 版本]这张图最重要的意思是:
- 标准在演进
- 运行时支持有快有慢
- 工程上通常要用转译、Polyfill 和目标环境配置一起兜住兼容性
13. 工程里最常见的误区
13.1 误把 ECMAScript 版本名当成“会不会写现代 JavaScript”
知道 ES2018、ES2020 这些名字当然有用,但更重要的是知道:
- 这个特性解决什么问题
- 它属于语法、标准对象,还是宿主 API
13.2 误把 Babel 当成所有兼容问题的终点
很多兼容问题并不是“代码写法太新”,而是:
- 运行时根本没有这个对象
- 当前环境根本没有这个宿主能力
13.3 误把宿主环境 API 当成 ECMAScript 标准能力
例如把:
fetchdocumentlocalStorage
都混进“ES 新特性”里,这会让理解越来越乱。
13.4 误以为所有新能力都值得为了低版本完全兼容去补
工程上更常见的做法其实是:
- 先定义目标用户环境
- 再决定哪些能力要转译
- 再决定哪些能力值得引入 Polyfill
- 超出收益的兼容成本就不再硬扛
14. 一份检查清单
当你判断某个新特性能不能用时,可以问自己:
- 这是语法、标准对象方法,还是宿主 API
- 目标浏览器和 Node.js 版本分别是什么
- 这类问题该靠 Babel 解决,还是靠 Polyfill 解决
- 这个能力能不能被等价转译或补齐
- 为了兼容低版本,引入的成本是否值得
15. 小结
这一篇最想建立的核心认识是:
- ECMAScript 版本演进说的是 JavaScript 标准在持续前进
- 兼容性问题至少要区分语法、标准对象和宿主环境三层
Babel和Polyfill分工不同,不能混成一回事- “能不能用”永远要结合目标浏览器和 Node.js 版本来讨论
如果只压缩成一句话,可以记住:
不要只问这个特性属于 ES 几,更要问当前目标环境到底能不能解析、能不能实现、值不值得兼容。