探索-JavaScript-ES2025-版--一-
探索 JavaScript(ES2025 版)(一)
原文:
exploringjs.com/js/book/index.html译者:飞龙
I 背景
原文:exploringjs.com/js/book/pt_background.html
1 在购买本书之前
-
1.1 关于内容
-
1.1.1 本书包含什么?
-
1.1.2 本书未涵盖的内容?
-
-
1.2 预览和购买本书
-
1.2.1 如何预览本书及其配套材料?
-
1.2.2 如何购买本书的数字版?
-
1.2.3 如何购买本书的印刷版?
-
-
1.3 关于作者
-
1.4 致谢
1.1 关于内容
1.1.1 本书包含什么?
本书通过提供尽可能一致的现代视角,使 JavaScript 对新来者来说更容易学习。
突出显示:
-
通过最初专注于现代功能,快速开始学习。
-
大多数章节都提供了测试驱动的练习。
-
涵盖 JavaScript 的所有基本特性,直至包括 ES2022。
-
可选的高级部分让您可以深入了解。
不需要具备 JavaScript 的先验知识,但你应该知道如何编程。
1.1.2 本书未涵盖的内容?
-
一些高级语言特性没有解释,但提供了适当的参考资料——例如,到我的其他 JavaScript 书籍在
ExploringJS.com,这些书籍可以免费在线阅读。 -
本书故意专注于语言。不描述仅浏览器特有的功能等。
1.2 预览和购买本书
1.2.1 如何预览本书及其配套材料?
访问本书的主页:
-
本书的所有章节都可以免费在线阅读。
-
大多数材料都有免费的预览版本(约包含 50%的内容),可在主页上找到。
1.2.2 如何购买本书的数字版?
《探索 JavaScript》的主页解释了如何购买其数字包之一。
1.2.3 如何购买本书的印刷版?
《探索 JavaScript》的旧版本被称为《JavaScript for impatient programmers》。其纸质版可在 Amazon 上购买。
1.3 关于作者
Axel Rauschmayer 博士专注于 JavaScript 和 Web 开发。自 1995 年以来一直在开发 Web 应用程序。1999 年,他在一家德国互联网初创公司担任技术经理,该公司后来扩展到国际市场。2006 年,他举办了第一次关于 Ajax 的演讲。2010 年,他在慕尼黑大学获得信息学博士学位。
自 2011 年以来,他一直在 2ality.com 上撰写关于 Web 开发的博客,并撰写了多本关于 JavaScript 的书籍。他为 eBay、美国银行和 O'Reilly Media 等公司提供过培训和演讲。
他住在德国慕尼黑。
1.4 感谢
-
封面图片由Fran Caye提供
-
感谢回答问题,讨论语言话题等:
-
Allen Wirfs-Brock
-
Benedikt Meurer
-
Brian Terlson
-
Daniel Ehrenberg
-
Jordan Harband
-
Maggie Johnson-Pint
-
Mathias Bynens
-
Myles Borins
-
Rob Palmer
-
Šime Vidas
-
以及许多其他人
-
-
感谢审阅:
- Johannes Weber
2 常见问题解答:书籍和补充材料
-
2.1 如何阅读这本书
-
2.1.1 我应该按照什么顺序阅读这本书的内容?
-
2.1.2 为什么有些章节和部分被标记为“(高级)”?
-
-
2.2 我拥有数字版
-
2.2.1 我如何提交反馈和更正?
-
2.2.2 我如何获取在 Payhip 购买的下载内容的更新?
-
2.2.3 我如何从小型包升级到大型包,或从旧包升级到新包?
-
-
2.3 我拥有印刷版(“JavaScript for impatient programmers”)
-
2.3.1 我可以为数字版获得折扣吗?
-
2.3.2 我如何提交反馈和更正?
-
-
2.4 符号和约定
-
2.4.1 类型签名是什么?为什么我在这本书中有时会看到静态类型?
-
2.4.2 图标注释的笔记代表什么意思?
-
本章回答了你可能有的问题,并提供了阅读这本书的技巧。
2.1 如何阅读这本书
2.1.1 我应该按照什么顺序阅读这本书的内容?
这本书是一本集三本书于一体的作品:
-
你可以用它来尽可能快速地开始学习 JavaScript:
-
从“使用 JavaScript:大局观”(§8)开始阅读。
-
跳过所有标记为“高级”的章节和部分,以及所有快速参考。
-
-
它为你提供了对当前 JavaScript 的全面了解。在这种“模式”下,你阅读一切内容,不跳过高级内容和快速参考。
-
它作为参考。如果你对某个主题感兴趣,你可以通过目录或索引找到相关信息。由于基本和高级内容混合,你所需的一切通常都在一个地方。
练习在帮助你练习和保留所学内容方面发挥着重要作用。
2.1.2 为什么有些章节和部分被标记为“(高级)”?
几个章节和部分被标记为“(高级)”。其理念是你可以最初跳过它们。也就是说,你只需阅读基本(非高级)内容,就可以快速获得 JavaScript 的工作知识。
随着你的知识不断进步,你可以稍后返回并阅读一些或全部的高级内容。
2.2 我拥有数字版
2.2.1 我如何提交反馈和更正?
这本书的 HTML 版本(在线版,或在付费版本的广告免费存档中)在每个章节的末尾都有一个链接,允许你提供反馈。
2.2.2 我如何在 Payhip 购买的下载中获取更新?
-
购买收据电子邮件中包含一个链接。你将始终能够从该位置下载文件的最新版本。
-
如果你购买时选择了电子邮件,你将在有新内容时收到电子邮件。要稍后选择,你必须联系 Payhip(见
payhip.com的底部)。
2.2.3 我如何从小型包升级到大型包,或从旧包升级到新包?
书的主页 解释了如何做到这一点。
2.3 我拥有印刷版(“JavaScript for impatient programmers”)
2.3.1 我可以为数字版获得折扣吗?
如果你购买了印刷版,你可以为数字版获得折扣。主页解释了如何操作。
然而,反过来是不可能的:如果你购买了数字版,就不能为印刷版获得折扣。
2.3.2 我如何提交反馈和更正?
-
在报告错误之前,请访问 “探索 JavaScript”的在线版本 并检查这本书的最新版本。错误可能已经在网上被更正。
-
如果错误仍然存在,你可以使用每个章节末尾的评论链接来报告它。
-
你也可以使用评论来提供反馈。
2.4 符号和约定
2.4.1 什么是类型签名?为什么我有时会在这本书中看到静态类型?
例如,你可能看到:
Number.isFinite(num: number): boolean
这被称为 Number.isFinite() 的 类型签名。这种表示法,特别是 num 的静态类型 number 和结果的 boolean 类型,并不是真正的 JavaScript。这种表示法是从编译到 JavaScript 的语言 TypeScript(它主要是 JavaScript 加上静态类型)借用的。
为什么使用这种表示法?这有助于快速了解函数的工作方式。这种表示法在 “处理 TypeScript” 中有详细解释,但通常相对直观。
2.4.2 带有图标的注释意味着什么?
阅读说明
解释了如何最好地阅读内容。
外部内容
指向额外的、外部的、内容。
提示
提供与当前内容相关的提示。
问题
提出并回答与当前内容相关的问题(想想常见问题解答)。
警告
警告关于陷阱等。
详情
提供额外的细节,补充当前内容。它类似于脚注。
练习
提及在那个点可以进行的测试驱动练习的路径。
3 为什么是 JavaScript?
-
3.1 JavaScript 的缺点
-
3.2 JavaScript 的优点
-
3.2.1 语言
-
3.2.2 实用
-
3.2.3 语言
-
-
[3.3 JavaScript 的利弊:创新]
在本章中,我们探讨了 JavaScript 的优缺点。
“ECMAScript 6”(简称“ES6”)指的是 JavaScript 的一个版本
ECMAScript 是语言标准的名称;在“ECMAScript X”和“ESX”中的数字 X 指的是该标准的版本。最初版本是序数(ES1–ES6)。后来,它们是年份(ES2016+)。有关更多信息,请参阅“标准化:JavaScript 与 ECMAScript”(§5.2)。
3.1 JavaScript 的缺点
在程序员中,JavaScript 并非总是受到喜爱。一个原因是它有很多怪癖。其中一些只是做某事的不寻常方式。其他被认为是错误。无论如何,了解 JavaScript 为什么以这种方式做事,有助于处理怪癖和接受 JavaScript(甚至可能喜欢它)。希望这本书能有所帮助。
此外,许多传统的怪癖现在已经被消除。例如:
-
传统上,JavaScript 变量不是块作用域。ES6 引入了
let和const,这使得我们可以声明块作用域变量。 -
在 ES6 之前,通过
function和.prototype实现对象工厂和继承是笨拙的。ES6 引入了类,这为这些机制提供了更方便的语法。 -
传统上,JavaScript 没有内置模块。ES6 将其添加到语言中。
最后,JavaScript 的标准库有限,但:
3.2 JavaScript 的优点
在正面方面,JavaScript 提供了许多好处。
3.2.1 社区
JavaScript 的流行意味着它得到了良好的支持和文档。无论我们用 JavaScript 创建什么,我们都可以依赖很多人(可能)对此感兴趣。如果我们需要,我们可以从庞大的 JavaScript 程序员群体中招聘。
没有单一的组织控制 JavaScript - 它是由 TC39,一个由许多组织组成的委员会所发展的。语言是通过一个鼓励公众反馈的开放过程来发展的。
3.2.2 实用
使用 JavaScript,我们可以为许多客户端平台编写应用程序。以下是一些示例技术:
-
Progressive Web Apps可以原生安装在 Android、iOS 和许多桌面操作系统上。
-
Electron让我们能够构建跨平台的桌面应用程序。
-
React Native让我们能够编写具有原生用户界面的 iOS 和 Android 应用程序。
-
Node.js提供了广泛的 shell 脚本(除了作为 Web 服务器平台之外)编写支持。
JavaScript 被许多服务器平台和服务支持——例如:
-
Node.js(以下许多服务基于 Node.js 或支持其 API)
-
Amazon Web Services Lambda
-
Cloudflare Workers
-
Google Cloud Functions
-
Microsoft Azure Functions
-
Vercel Functions
对于 JavaScript 来说,有许多数据技术可用:许多数据库支持它,并且存在中间层(如 GraphQL)。此外,标准数据格式JSON(JavaScript 对象表示法)基于 JavaScript,并由其标准库支持。
最后,大多数 JavaScript 工具都是用 JavaScript 编写的,或者至少支持用 JavaScript 编写的插件。即使是原生工具也是以与其他 JavaScript 相关软件相同的方式安装的——通过包管理器,如 npm。
3.2.3 语言
-
通过 JavaScript 生态系统的事实上的标准,npm 软件注册表提供了许多库。
-
如果我们对“纯”JavaScript 不满意,相对容易添加更多功能:
-
我们可以将未来的和现代的语言特性编译到当前的以及过去的 JavaScript 版本中,通过Babel。
-
我们可以通过TypeScript添加静态类型。
-
-
语言是灵活的:它是动态的,并且支持面向对象编程和函数式编程。
-
对于这样一个动态语言来说,JavaScript 变得出奇地快。
- 当它不够快时,我们可以切换到 WebAssembly,这是一种内置在大多数 JavaScript 引擎中的通用虚拟机。它可以在接近原生速度下运行静态代码。
3.3 JavaScript 的优缺点:创新
JavaScript 生态系统中有许多创新:实现用户界面的新方法、优化软件交付的新方式,等等。好处是我们将不断学习新事物。坏处是这种不断的改变有时会让人感到疲惫。幸运的是,许多事情已经变得稳定——例如,内置模块(ECMAScript 模块)于 2015 年引入,但几乎花了 10 年时间才比非内置替代品 CommonJS 模块更受欢迎。
4 JavaScript 的本质
-
4.1 JavaScript 的影响
-
4.2 JavaScript 的本质
- 4.2.1 JavaScript 经常静默失败
-
4.3 开始使用 JavaScript 的技巧
4.1 JavaScript 的影响
当 JavaScript 在 1995 年被创建时,它受到了几种编程语言的影响:
-
JavaScript 的语法在很大程度上基于 Java。
-
自身启发了 JavaScript 的原型继承。
-
闭包和环境是从 Scheme 中借鉴的。
-
AWK 影响了 JavaScript 的函数(包括
function关键字)。 -
JavaScript 的字符串、数组和正则表达式受到了 Perl 的影响。
-
HyperTalk 通过
onclick在 Web 浏览器中启发了事件处理。
随着 ECMAScript 6 的推出,JavaScript 获得了新的影响:
-
生成器是从 Python 中借鉴的。
-
箭头函数的语法来自 CoffeeScript。
-
C++ 贡献了
const关键字。 -
解构是从 Lisp 的 解构绑定 中获得的灵感。
-
模板字面量来自 E 语言(在那里它们被称为 准字面量)。
4.2 JavaScript 的本质
这些是语言的一些特性:
-
它的语法是 C 家族语言的一部分(花括号等)。
-
它是一种动态语言:大多数对象可以在运行时以各种方式更改,可以直接创建对象等。
-
它是一种动态类型语言:变量没有固定的静态类型,你可以将任何值分配给给定的(可变的)变量。
-
它具有函数式编程特性:一等函数、闭包、通过
bind()的部分应用等。 -
它具有面向对象特性:可变状态、对象、继承、类等。
-
它经常静默失败:请参阅下一小节以获取详细信息。
-
它以源代码的形式部署。但那个源代码通常会被 压缩(重写以减少存储需求)。并且有 关于二进制源代码格式的计划。
-
JavaScript 是 Web 平台的一部分——它是嵌入到 Web 浏览器中的语言。但它也被用于其他地方——例如,在 Node.js 中用于服务器端,以及 shell 脚本。
-
JavaScript 引擎通常在底层优化效率较低的语言机制。例如,原则上,JavaScript 数组是字典。但在底层,如果它们有连续的索引,引擎会连续存储数组。
4.2.1 JavaScript 经常静默失败
JavaScript 经常静默失败。让我们看看两个例子。
第一个例子:如果运算符的操作数没有适当的类型,它们将按需转换。
> '3' * '5'
15
第二个例子:如果算术计算失败,你会得到一个错误值,而不是异常。
> 1 / 0
Infinity
无声失败的原因是历史性的:JavaScript 直到 ECMAScript 3 才有异常。从那时起,其设计者一直试图避免无声失败。
4.3 开始使用 JavaScript 的技巧
这些是一些帮助您开始学习 JavaScript 的技巧:
-
请花时间真正了解这门语言。传统的 C 风格语法隐藏了这是一个非常非常不寻常的语言的事实。特别学习其怪癖及其背后的理由。然后您将更好地理解和欣赏这门语言。
- 除了细节之外,这本书还教授了一些简单的经验法则,以确保安全——例如,“始终使用
===来确定两个值是否相等,而不是==。”
- 除了细节之外,这本书还教授了一些简单的经验法则,以确保安全——例如,“始终使用
-
语言工具使得使用 JavaScript 更加容易。例如:
-
您可以通过 TypeScript 静态类型化 JavaScript。
-
您可以通过像 ESLint 这样的 linters 检查问题和反模式。
-
您可以通过像 Prettier 这样的代码格式化工具自动格式化您的代码。
-
关于 JavaScript 工具的更多信息,请参阅“下一步:Web 开发概述”(§49)“Next steps: overview of web development” (§49)。
-
-
与社区取得联系:
-
在 JavaScript 程序员中,像 Mastodon 这样的社交媒体服务很受欢迎。作为一种介于口语和书面语之间的交流方式,它非常适合交换知识。
-
许多城市都有定期的免费聚会,人们聚集在一起学习与 JavaScript 相关的主题。
-
JavaScript 会议是结识其他 JavaScript 程序员的另一种便捷方式。
-
-
阅读书籍和博客。许多资料都可以在线免费获取!
5 JavaScript 的历史和演变
-
5.1 JavaScript 的创建过程
-
5.2 标准化:JavaScript 与 ECMAScript
-
5.3 ECMAScript 版本的时间线
-
5.4 JavaScript 的发展:TC39
-
5.5 提案 ECMAScript 特性的 TC39 流程
-
5.5.1 小贴士:思考单个特性和阶段,而不是 ECMAScript 版本
-
5.5.2 TC39 流程的细节(高级)
-
-
5.6 如何在更改 JavaScript 的同时不破坏网络
-
5.7 常见问题解答:ECMAScript 和 TC39
-
5.7.1 我在哪里可以查找给定 ECMAScript 版本中添加了哪些特性?
-
5.7.2 我的 favorite 提案 JavaScript 特性进展如何?
-
5.7.3 为什么阶段 2.7 有这样一个奇特的数量?
-
5.1 JavaScript 的创建过程
JavaScript 是由 Brendan Eich 在 1995 年 5 月用 10 天时间创建的。Eich 在 Netscape 工作,并为他们的网络浏览器 Netscape Navigator 实现了 JavaScript。
想法是,客户端 Web 的主要交互部分应该用 Java 实现。JavaScript 应该是这些部分的粘合语言,并使 HTML 略微更具交互性。鉴于其辅助 Java 的角色,JavaScript 必须看起来像 Java。这排除了像 Perl、Python、TCL 等现有的解决方案。
初始时,JavaScript 的名称更改了好几次:
-
它的代号是 Mocha。
-
在 Netscape Navigator 2.0 测试版(1995 年 9 月),它被称为 LiveScript。
-
在 Netscape Navigator 2.0 测试版 3(1995 年 12 月),它获得了最终名称,JavaScript。
5.2 标准化:JavaScript 与 ECMAScript
有两个 JavaScript 标准:
-
ECMA-262 由 Ecma International 托管。它是主要标准。
-
ISO/IEC 16262 由国际标准化组织(ISO)和国际电工委员会(IEC)托管。这是一个二级标准。
这些标准所描述的语言被称为 ECMAScript,而不是 JavaScript。选择了一个不同的名称,因为 Sun(现在是 Oracle)对后者名称拥有商标。ECMAScript 中的“ECMA”来自托管主要标准的组织。
该组织的原始名称是 ECMA,代表 European Computer Manufacturers Association。后来它被改为 Ecma International(“Ecma”是一个专有名词,不是一个缩写),因为该组织的活动已经扩展到欧洲之外。最初的全部大写缩写解释了 ECMAScript 的拼写。
通常,JavaScript 和 ECMAScript 是同义的。有时会做出以下区分:
-
术语JavaScript指的是语言及其实现。
-
术语ECMAScript指的是语言标准和语言版本。
因此,ECMAScript 6是语言的版本(其第 6 版)。
5.3 ECMAScript 版本时间线
这是一份 ECMAScript 版本的简要时间线:
-
ECMAScript 1(1997 年 6 月):标准的第一个版本。
-
ECMAScript 2(1998 年 6 月):对 ECMA-262 的小更新,以保持与 ISO 标准同步。
-
ECMAScript 3(1999 年 12 月):增加了许多核心功能——“[...]正则表达式、更好的字符串处理、新的控制语句[do-while、switch]、try/catch 异常处理[...]”
-
ECMAScript 4(2008 年 7 月废弃):本应是一次重大的升级(具有静态类型、模块、命名空间等),但最终过于雄心勃勃,导致语言管理者之间产生分歧。
-
ECMAScript 5(2009 年 12 月):带来了一些小的改进——一些标准库功能和严格模式。
-
ECMAScript 5.1(2011 年 6 月):对 Ecma 和 ISO 标准的又一次小更新,以保持同步。
-
ECMAScript 6(2015 年 6 月):这是一次重大的更新,实现了 ECMAScript 4 的许多承诺。这个版本的官方名称——ECMAScript 2015——是基于出版年份命名的。
-
ECMAScript 2016(2016 年 6 月):第一个年度发布。较短的发布生命周期导致与大型 ES6 相比,新功能较少。
-
ECMAScript 2017(2017 年 6 月)。第二个年度发布。
-
后续的 ECMAScript 版本(ES2018 等)总是在 6 月获得批准。
5.4 JavaScript 的发展:TC39
TC39(Ecma 技术委员会 39)是推动 JavaScript 发展的委员会。其成员严格来说都是公司:Adobe、Apple、Facebook、Google、Microsoft、Mozilla、Opera、Twitter 以及其他公司。也就是说,通常是竞争对手的公司正在一起合作开发 JavaScript。
每两个月,TC39 都会举行会议,由成员指定的代表和受邀的专家参加。这些会议的纪要公开在GitHub 仓库中。
在会议之外,TC39 还与 JavaScript 社区的各个成员和团体合作。
5.5 提议的 ECMAScript 特性的 TC39 流程
在 ECMAScript 6 中,当时使用的发布流程出现了两个问题:
-
如果发布之间的时间过长,那么准备较早的功能必须等待很长时间才能发布。而准备较晚的功能则面临匆忙赶工以完成截止日期的风险。
-
功能通常在实现和使用之前就已经设计好。因此,与实现和使用相关的设计缺陷发现得太晚。
针对这些问题,TC39 实施了新的TC39 流程:
-
ECMAScript 特性是独立设计的,并经过六个阶段:一个草稿阶段 0 和五个“成熟”阶段(1、2、2.7、3、4)。
-
尤其是后期阶段需要原型实现和实际测试,导致设计和实现之间的反馈循环。
-
ECMAScript 版本每年发布一次,包括在发布截止日期前达到阶段 4 的所有特性。
结果:更小、更渐进的发布,其特性已经过现场测试。
ES2016 是第一个按照 TC39 流程设计的 ECMAScript 版本。
5.5.1 提示:思考个体特性和阶段,而非 ECMAScript 版本
直到包括 ES6 在内,人们通常根据 ECMAScript 版本来考虑 JavaScript – 例如,“这个浏览器支持 ES6 吗?”
从 ES2016 开始,最好考虑个体特性:一旦特性达到阶段 4,我们就可以安全地使用它(如果它是我们目标 JavaScript 引擎所支持的)。我们不必等到下一个 ECMAScript 发布。
5.5.2 TC39 流程的细节(高级)
ECMAScript 特性是通过 提案 设计的,这些提案经过所谓的 TC39 流程。该流程包括六个阶段:
-
阶段 0 表示提案尚未进入实际流程。大多数提案都是从这里开始的。
-
然后,提案将经过五个成熟阶段 1、2、2.7、3 和 4。如果它达到阶段 4,它就是完整的,并准备好包含在 ECMAScript 标准中。
5.5.2.1 与 ECMAScript 提案相关的工件
以下工件与 ECMAScript 提案相关:
-
提案文档:用英文散文和代码示例描述提案给 JavaScript 程序员,通常是 GitHub 仓库的 README。
-
规范:用 Ecmarkup 编写,这是一个由工具链支持的 HTML 和 Markdown 方言。该工具链检查 Ecmarkup,并将其渲染为具有针对阅读规范的功能的 HTML(交叉引用、变量出现的突出显示等)。
-
HTML 也可以打印成 PDF。
-
如果一个提案达到阶段 4,其规范将被集成到完整的 ECMAScript 规范中(该规范也用 Ecmarkup 编写)。
-
-
测试:用 JavaScript 编写的代码,用于检查实现是否符合规范。
- 如果一个提案达到阶段 4,其测试将被集成到 Test262,这是官方 ECMAScript 兼容性测试套件。
-
实现:提案的功能,在引擎和转换器(如 Babel 和 TypeScript)中实现。
每个阶段都有关于工件状态的进入标准:
| 阶段 | 提案 | 规范 | 测试 | 实现 |
|---|---|---|---|---|
| 0 | ||||
| 1 | 草稿 | |||
| 2 | 完成 | 草稿 | ||
| 2.7 | 完成 | |||
| 3 | 完成 | 原型 | ||
| 4 | 2 个实现 |
5.5.2.2 管理提案的人员角色
-
作者:由一个或多个作者撰写提案。
-
倡导者:每个提案都有一名或多名 TC39 代表,他们指导提案通过 TC39 流程。如果作者没有流程经验,这一点尤为重要。
-
审稿人:审稿人在第二阶段对规范提供反馈,并在提案达到 2.7 阶段之前必须签署它。他们由 TC39 任命(不包括提案的作者和倡导者)。
-
编辑:负责管理 ECMAScript 规范的人员。当前编辑人员列在ECMAScript 规范的开始处。
5.5.2.3 提案的阶段
-
阶段 0:构思和探索
- 不属于常规推进流程的一部分。任何作者都可以创建一个草案提案并将其分配到阶段 0。
-
阶段 1:设计解决方案
-
进入标准:
-
选择倡导者
-
包含提案的仓库
-
-
状态:
- 提案正在考虑中。
-
-
阶段 2:完善解决方案
-
进入标准:
-
提案已完成。
-
规范草案。
-
-
状态:
- 提案可能(但不保证)会被标准化。
-
-
阶段 2.7:测试和验证
-
进入标准:
- 规范已完成并获得审稿人和编辑的批准。
-
状态:
-
规范已完成。现在是时候通过测试和符合规范的原型来验证它了。
-
除了通过验证发现的问题外,不再进行更多更改。
-
-
-
阶段 3:获得实施经验
-
进入标准:
- 测试已完成。
-
状态:
-
提案准备就绪,可以实施。
-
除非发现 Web 不兼容性,否则没有变化。
-
-
-
阶段 4:集成到草案规范并最终纳入标准
-
进入标准:
-
两个通过测试的实现
-
在航运实施方面具有显著的实际经验
-
向 TC39 仓库提交的拉取请求,已由编辑批准
-
-
状态:
-
提出的功能完整:
-
其规范准备就绪,可包含在 ECMAScript 规范中。
-
其测试准备就绪,可包含在 ECMAScript 一致性测试套件 Test262 中。
-
-
-
图 5.1 说明了 TC39 流程。

图 5.1:每个 ECMAScript 功能提案都要经过从 0 到 4 编号的阶段。
本节来源:
-
“TC39 流程”(TC39 的官方文件)
-
ECMAScript 规范的 扉页。扉页是书籍结尾的内容。它通常包含有关书籍制作的信息。
5.6 如何在更改 JavaScript 的同时不破坏网络
有时候会提出一个想法,即通过移除旧特性和怪癖来清理 JavaScript。虽然这个想法的吸引力很明显,但它有显著的缺点。
假设我们创建了一个不向后兼容且修复了所有缺陷的新版本的 JavaScript。结果,我们会遇到以下问题:
-
JavaScript 引擎变得臃肿:它们需要支持旧版本和新版本。对于 IDE 和构建工具等工具也是如此。
-
程序员需要了解,并且持续意识到版本之间的差异。
-
我们可以选择将现有的所有代码库迁移到新版本(这可能是一项大量工作)。或者我们可以混合版本,重构变得更加困难,因为我们不能在不更改代码的情况下在版本之间移动代码。
-
我们必须以某种方式指定每段代码的版本——无论是文件还是嵌入网页中的代码——每个可行的解决方案都有其优缺点。例如,严格模式 是 ES5 的一个稍微干净一点的版本。它没有像应该的那样受欢迎的原因之一是:通过文件或函数开头的指令进行选择加入是一个麻烦。
那么,解决方案是什么?这是 JavaScript 的发展方式:
-
新版本总是完全向后兼容(但偶尔可能会有一些微小的、几乎察觉不到的清理)。
-
旧特性不会被移除或修复。相反,会引入它们的更好版本。一个例子是通过
let和const声明变量——它们是var的改进版本。 -
如果语言的一些方面发生了变化,它是在新的语法结构中完成的。也就是说,我们隐式地选择加入——例如:
-
yield只是在生成器(在 ES6 中引入)内部的关键字。 -
所有在模块和类(两者都是在 ES6 中引入)内部的代码都隐式地处于严格模式。
-
5.7 FAQ:ECMAScript 和 TC39
5.7.1 在哪个地方可以查找给定 ECMAScript 版本中添加了哪些特性?
你可以在几个地方查找每个 ECMAScript 版本中的新特性:
-
在这本书中,有一个章节列出了每个 ECMAScript 版本中的新特性[(ch_new-javascript-features.html#ch_new-javascript-features)]。它还提供了链接到解释。
-
TC39 存储库有一个表格,列出了完成的提案,说明它们是在哪个 ECMAScript 版本中(或将要)引入的。
-
ECMAScript 语言规范“引言”部分的“介绍”列出了每个 ECMAScript 版本的新特性。
-
ECMA-262 仓库有一个 发布页面。
5.7.2 我的最爱提议的 JavaScript 功能进展如何?
如果你想知道各种提议的功能处于哪个阶段,请参阅 GitHub 仓库的提议部分。
5.7.3 为什么第 2.7 阶段有这样一个奇特的数量?
第 2.7 阶段是在 2023 年晚些时候加入的,在 0、1、2、3、4 阶段已经使用多年之后。
-
Q: 为什么不重新编号阶段?
- A: 重新编号不在计划之中,因为这会使旧文档难以阅读。
-
Q: 为什么不使用另一个数字,比如 2.5?
- .7 反映出阶段 2.7 比阶段 2 更接近阶段 3。
-
Q: 新阶段用 3a,旧阶段 3 用 3b 怎么样?
- A: 如果你在旧文档中看到“阶段 3”,可能会弄不清楚这是指新的阶段 3a 还是新的阶段 3b。
6 新 JavaScript 特性
-
6.1 ECMAScript 2025 中的新特性
-
6.2 ECMAScript 2024 中的新特性
-
6.3 ECMAScript 2023 中的新特性
-
6.4 ECMAScript 2022 中的新特性
-
6.5 ECMAScript 2021 中的新特性
-
6.6 ECMAScript 2020 中的新特性
-
6.7 ECMAScript 2019 中的新特性
-
6.8 ECMAScript 2018 中的新特性
-
6.9 ECMAScript 2017 中的新特性
-
6.10 ECMAScript 2016 中的新特性
-
6.11 本章来源
本章按时间顺序反向列出最近 ECMAScript 版本的新特性。它结束于 ES6(ES2015)之前:ES2016 是 ECMAScript 的第一个真正增量发布版本——这就是为什么 ES6 在这里列出的功能太多。如果您想了解更早的版本:
-
我的书“Exploring ES6”描述了 ES6(ES2015)中添加了哪些内容。
-
我的书“Speaking JavaScript”描述了 ES5 的所有特性——因此它是一个有用的时间胶囊。
6.1 ECMAScript 2025 中的新特性
-
导入属性为导入非 JavaScript 工件提供了语法基础。首先支持的这类工件是 JSON 模块:
// Static import import configData1 from './config-data.json' with { type: 'json' }; // Dynamic import const configData2 = await import( './config-data.json', { with: { type: 'json' } } );with之后的对象字面量语法用于指定导入属性。type是一个导入属性。 -
迭代器辅助方法让我们可以更灵活地使用迭代器:
const arr = ['a', '', 'b', '', 'c', '', 'd', '', 'e']; assert.deepEqual( arr.values() // creates an iterator .filter(x => x.length > 0) .drop(1) .take(3) .map(x => `=${x}=`) .toArray() , ['=b=', '=c=', '=d='] );迭代器方法是如何在数组方法之上进行改进的?
-
迭代器方法可以与任何可迭代数据结构一起使用——例如,它们允许我们过滤和映射
Set和Map数据结构。 -
迭代器方法不会创建中间数组并增量计算数据。这对于大量数据来说很有用:
-
使用迭代器方法,所有方法都应用于第一个值,然后是第二个值,依此类推。
-
使用数组方法,首先将第一个方法应用于所有值,然后第二个方法应用于所有结果,依此类推。
-
-
-
用于合并集合和检查集合关系的方法:
-
合并集合:
-
Set.prototype.intersection(other) -
Set.prototype.union(other) -
Set.prototype.difference(other) -
Set.prototype.symmetricDifference(other)
-
-
检查集合关系:
-
Set.prototype.isSubsetOf(other) -
Set.prototype.isSupersetOf(other) -
Set.prototype.isDisjointFrom(other)
-
示例:
assert.deepEqual( new Set(['a', 'b', 'c']).union(new Set(['b', 'c', 'd'])), new Set(['a', 'b', 'c', 'd']) ); assert.deepEqual( new Set(['a', 'b', 'c']).intersection(new Set(['b', 'c', 'd'])), new Set(['b', 'c']) ); assert.deepEqual( new Set(['a', 'b']).isSubsetOf(new Set(['a', 'b', 'c'])), true ); assert.deepEqual( new Set(['a', 'b', 'c']).isSupersetOf(new Set(['a', 'b'])), true ); -
-
RegExp.escape()将文本转义,以便可以在正则表达式中使用——例如,以下代码移除了str中所有未引用的text出现:function removeUnquotedText(str, text) { const regExp = new RegExp( `(?<!“)${RegExp.escape(text)}(?!”)`, 'gu' ); return str.replaceAll(regExp, '•'); } assert.equal( removeUnquotedText('“yes” and yes and “yes”', 'yes'), '“yes” and • and “yes”' ); -
正则表达式模式修饰符(内联标志) 允许我们将标志应用于正则表达式的部分(而不是整个正则表达式)——例如,在以下正则表达式中,标志
i仅应用于“HELLO”:
> /^x(?i:HELLO)x$/.test('xHELLOx')
true
> /^x(?i:HELLO)x$/.test('xhellox')
true
> /^x(?i:HELLO)x$/.test('XhelloX')
false
-
重复命名的捕获组:我们现在可以使用相同的组名两次——只要它出现在不同的备选方案中:
const RE = /(?<chars>a+)|(?<chars>b+)/v; assert.deepEqual( RE.exec('aaa').groups, { chars: 'aaa', __proto__: null, } ); assert.deepEqual( RE.exec('bb').groups, { chars: 'bb', __proto__: null, } ); -
Promise.try() 允许我们以非纯异步的代码开始 Promise 链——例如:
function computeAsync() { return Promise.try(() => { const value = syncFuncMightThrow(); return asyncFunc(value); }); } -
支持 16 位浮点数(float16):
-
Math.f16round() 方法
-
Typed Arrays API 的新元素类型:
-
Float16Array -
DataView.prototype.getFloat16() -
DataView.prototype.setFloat16()
-
-
6.2 新增于 ECMAScript 2024
-
分组同步可迭代对象:
Map.groupBy()将可迭代对象的项分组到 Map 条目中,其键由回调函数提供:assert.deepEqual( Map.groupBy([0, -5, 3, -4, 8, 9], x => Math.sign(x)), new Map() .set(0, [0]) .set(-1, [-5,-4]) .set(1, [3,8,9]) );此外,还有
Object.groupBy(),它生成一个对象而不是 Map:assert.deepEqual( Object.groupBy([0, -5, 3, -4, 8, 9], x => Math.sign(x)), { '0': [0], '-1': [-5,-4], '1': [3,8,9], __proto__: null, } ); -
Promise.withResolvers() 提供了一种创建我们想要解决的 Promise 的新方法:
const { promise, resolve, reject } = Promise.withResolvers(); -
新的正则表达式标志
/v(.unicodeSets) 启用这些功能:-
Unicode 字符串属性的转义(😵💫 由三个代码点组成):
// Previously: Unicode code point property `Emoji` via /u assert.equal( /^\p{Emoji}$/u.test('😵💫'), false ); // New: Unicode string property `RGI_Emoji` via /v assert.equal( /^\p{RGI_Emoji}$/v.test('😵💫'), true ); -
字符类中的字符串字面量通过
\q{}:> /^[\q{😵💫}]$/v.test('😵💫') true > /^[\q{abc|def}]$/v.test('abc') true -
字符类集合的集合操作:
> /^[\w--[a-g]]$/v.test('a') false > /^[\p{Number}--[0-9]]$/v.test('٣') true > /^[\p{RGI_Emoji}--\q{😵💫}]$/v.test('😵💫') false -
如果通过
[^···]取消了 Unicode 属性转义,则与/i的匹配得到改进
-
-
ArrayBuffer 获得了两个新功能:
-
它们可以在原地 调整大小:
const buf = new ArrayBuffer(2, {maxByteLength: 4}); // `typedArray` starts at offset 2 const typedArray = new Uint8Array(buf, 2); assert.equal( typedArray.length, 0 ); buf.resize(4); assert.equal( typedArray.length, 2 ); -
它们获得一个
.transfer()方法来 转让 它们。
-
-
SharedArrayBuffers 可以调整大小,但它们只能增长,不能缩小。它们不可转让,因此不获得
ArrayBuffers获得的方法.transfer()。 -
两个新方法帮助我们确保字符串格式良好(与 UTF-16 代码单元相关):
-
String 方法
.isWellFormed()检查 JavaScript 字符串是否格式良好,并且不包含任何 单独的代理字符。 -
String 方法
.toWellFormed()返回一个副本,其中每个单独的代理字符被代码单元 0xFFFD 替换(它代表具有相同数字的代码点,其名称为“替换字符”)。因此,结果是格式良好的。
-
-
Atomics.waitAsync()允许我们异步等待共享内存的变化。其功能超出了本书的范围。有关更多信息,请参阅 MDN 网络文档。
6.3 新增于 ECMAScript 2023
-
“通过复制更改数组”:数组和类型化数组获得新的非破坏性方法,在更改之前复制接收器:
-
.toReversed()是.reverse()的非破坏性版本:const original = ['a', 'b', 'c']; const reversed = original.toReversed(); assert.deepEqual(reversed, ['c', 'b', 'a']); // The original is unchanged assert.deepEqual(original, ['a', 'b', 'c']); -
.toSorted()是.sort()的非破坏性版本:const original = ['c', 'a', 'b']; const sorted = original.toSorted(); assert.deepEqual(sorted, ['a', 'b', 'c']); // The original is unchanged assert.deepEqual(original, ['c', 'a', 'b']); -
.toSpliced()是.splice()的非破坏性版本:const original = ['a', 'b', 'c', 'd']; const spliced = original.toSpliced(1, 2, 'x'); assert.deepEqual(spliced, ['a', 'x', 'd']); // The original is unchanged assert.deepEqual(original, ['a', 'b', 'c', 'd']); -
.with()是使用方括号设置值的非破坏性版本:const original = ['a', 'b', 'c']; const updated = original.with(1, 'x'); assert.deepEqual(updated, ['a', 'x', 'c']); // The original is unchanged assert.deepEqual(original, ['a', 'b', 'c']);
-
-
“从数组末尾开始查找数组”:数组和类型化数组获得两个新方法:
-
.findLast()与.find()类似,但开始从数组的末尾搜索:> ['', 'a', 'b', ''].findLast(s => s.length > 0) 'b' -
.findLastIndex()与.findIndex()类似,但开始从数组的末尾搜索:> ['', 'a', 'b', ''].findLastIndex(s => s.length > 0) 2
-
-
符号作为 WeakMap 键:在此功能之前,只有对象可以用作 WeakMap 中的键。此功能还允许我们使用符号——除了 已注册的符号(通过
Symbol.for()创建)。 -
“Hashbang 语法”:JavaScript 现在忽略文件的第一行,如果它以哈希 (
#) 和感叹号 (!) 开头。一些 JavaScript 运行时,如 Node.js,已经这样做了很长时间。现在它也成为语言本身的一部分。这是一个“hashbang”行的例子:#!/usr/bin/env node
6.4 新增于 ECMAScript 2022
-
类的新成员:
-
属性(公共槽)现在可以通过以下方式创建:
-
实例公共字段
-
静态公共字段
-
-
私有槽是新的,可以通过以下方式创建:
-
私有字段(实例私有字段和静态私有字段)
-
私有方法和访问器(非静态和静态)
-
-
静态初始化块
-
-
私有槽检查(“私有字段的舒适品牌检查”):以下表达式检查
obj是否有一个私有槽#privateSlot:#privateSlot in obj -
模块中的顶层
await:我们现在可以在模块的顶层使用await,不再需要进入异步函数或方法。 -
错误原因:
Error及其子类现在允许我们指定哪个错误导致了当前错误:new Error('Something went wrong', {cause: otherError}) -
索引值的方法
.at()允许我们在给定的索引处读取一个元素(类似于中括号操作符[]),并支持负索引(与中括号操作符不同)。> ['a', 'b', 'c'].at(0) 'a' > ['a', 'b', 'c'].at(-1) 'c'以下“可索引”类型具有
.at()方法:-
string -
Array -
所有类型化数组类:
Uint8Array等。
-
-
正则表达式匹配索引:如果我们向正则表达式添加一个标志,使用它会产生记录每个分组捕获的起始和结束索引的匹配对象。
-
Object.hasOwn(obj, propKey)提供了一种安全的方式来检查对象obj是否具有键propKey的自有属性。
6.5 ECMAScript 2021 新特性
-
String.prototype.replaceAll()允许我们替换正则表达式或字符串的所有匹配项(.replace()只替换字符串的第一个出现):> 'abbbaab'.replaceAll('b', 'x') 'axxxaax' -
Promise.any()和AggregateError:Promise.any()返回一个 Promise,当可迭代中的第一个 Promise 完成(fulfilled)时,它就会完成。如果只有拒绝(rejections),它们会被放入一个AggregateError中,该错误成为拒绝值。当我们只对多个 Promise 中第一个完成的 Promise 感兴趣时,我们使用
Promise.any()。 -
逻辑赋值运算符:
a ||= b a &&= b a ??= b -
下划线 (
_) 作为分隔符在:-
数字字面量:
123_456.789_012 -
大整数字面量:
6_000_000_000_000_000_000_000_000n
-
-
WeakRefs:这个特性超出了本书的范围。引用其提案[(https://github.com/tc39/proposal-weakrefs)]表示:
-
[此提案] 包含两个主要的新功能:
-
使用
WeakRef类创建对对象的弱引用 -
在对象被垃圾回收后运行用户定义的终结器,使用
FinalizationRegistry类
-
-
正确使用需要仔细思考,如果可能的话,最好避免使用。
-
-
Array.prototype.sort自 ES2019 起就是稳定的。在 ES2021 中,“[它]变得更加精确,减少了导致实现定义排序顺序的情况数量” [来源]。更多信息,请参阅此改进的 pull request。
6.6 ECMAScript 2020 新特性
-
新模块功能:
-
通过
import()动态导入:正常的import语句是静态的:我们只能在模块的顶层使用它,并且其模块指定器是一个固定的字符串。import()改变了这一点。它可以在任何地方使用(包括条件语句),并且我们可以计算其参数。 -
import.meta包含当前模块的元数据。它第一个广泛支持的属性是import.meta.url,其中包含当前模块文件的 URL。 -
命名空间重新导出:以下表达式将模块
'mod'的所有导出导入到命名空间对象ns中,并导出该对象。export * as ns from 'mod';
-
-
可选链用于属性访问和方法调用。可选链的一个例子是:
value?.prop如果
value是undefined或null,则此表达式求值为undefined。否则,它求值为value.prop。这个特性在属性读取链中特别有用,其中一些属性可能不存在。 -
空值合并运算符(
??):value ?? defaultValue如果
value是undefined或null,则此表达式为defaultValue,否则为value。这个运算符允许我们在某些东西缺失时使用默认值。之前在这个情况下使用的是逻辑或运算符(
||),但在这里它有缺点,因为它在左侧是假值时返回默认值(这并不总是正确的)。 -
大整数 – 可任意精度的整数:BigInt 是一种新的原始类型。它支持可以任意大的整数(它们的存储空间会根据需要增长)。
-
String.prototype.matchAll():如果标志/g没有设置,这个方法会抛出异常,并返回一个包含给定字符串中所有匹配对象的可迭代对象。 -
Promise.allSettled()接收一个 Promise 的可迭代对象。它返回一个 Promise,一旦所有输入的 Promise 都已解决,它就会实现。实现值是一个包含每个输入 Promise 的对象的数组 - 要么是:-
{ status: 'fulfilled', value: «fulfillment value» } -
{ status: 'rejected', reason: «rejection value» }
-
-
globalThis提供了一种在浏览器和服务器端平台(如 Node.js 和 Deno)上访问全局对象的方法。 -
for-in机制:这个特性超出了本书的范围。有关更多信息,请参阅其提案。 -
命名空间重新导出:
export * as ns from './internal.mjs';
6.7 新增于 ECMAScript 2019
-
数组方法
.flatMap()的行为类似于.map(),但允许回调返回包含零个或多个值的数组。然后返回的数组会被连接,成为.flatMap()的结果。用例包括:-
同时进行过滤和映射
-
将单个输入值映射到多个输出值
-
-
数组方法
.flat()将嵌套数组转换为扁平数组。可选地,我们可以告诉它应该在哪个嵌套深度停止扁平化。 -
Object.fromEntries()从一个遍历 entries 的可迭代对象创建一个对象。每个条目是一个包含属性键和属性值的两个元素的数组。 -
字符串方法:
.trimStart()和.trimEnd()的行为类似于.trim(),但只删除字符串开头或结尾的空白字符。 -
可选的
catch绑定:如果我们不使用它,现在可以省略catch子句的参数。 -
Symbol.prototype.description 是一个用于读取符号描述的 getter。之前,描述包含在
.toString()的结果中,但无法单独访问。 -
.sort()方法对于数组和类型化数组现在是稳定的:如果元素在排序时被认为是相等的,那么排序不会改变这些元素的顺序(相对于彼此)。
这些 ES2019 功能超出了本书的范围:
-
JSON 超集:参见 2ality 博客文章。
-
有效的
JSON.stringify():参见 2ality 博客文章。 -
Function.prototype.toString()修订:参见 2ality 博客文章。
6.8 新增于 ECMAScript 2018
-
异步迭代 是同步迭代的异步版本。它基于 Promises:
-
对于同步迭代器,我们可以立即访问每个项目。对于异步迭代器,我们必须
await之后才能访问一个项目。 -
对于同步迭代器,我们使用
for-of循环。对于异步迭代器,我们使用for-await-of循环。
-
-
对象字面量扩展:通过在对象字面量中使用扩展(
...),我们可以将另一个对象的属性复制到当前对象中。一个用例是创建对象obj的浅拷贝:const shallowCopy = {...obj}; -
剩余属性(解构):当对值进行对象解构时,我们现在可以使用剩余语法(
...)来获取对象中所有之前未提及的属性。const {a, ...remaining} = {a: 1, b: 2, c: 3}; assert.deepEqual(remaining, {b: 2, c: 3}); -
Promise.prototype.finally() 与 try-catch-finally 语句的
finally子句相关——类似于 Promise 方法.then()与try子句以及.catch()与catch子句的关系。换句话说:
.finally()的回调无论 Promise 是否被实现或拒绝都会执行。 -
新的正则表达式功能:
-
正则表达式命名字符组:除了通过数字访问组之外,我们现在可以命名它们并通过名称访问:
const matchObj = '---756---'.match(/(?<digits>[0-9]+)/) assert.equal(matchObj.groups.digits, '756'); -
正向前瞻断言的
RegExp补充了前瞻断言:-
正向前瞻:
(?<=X)匹配如果当前位置之前是'X'。 -
负向前瞻:
(?<!X)匹配如果当前位置不是由'(?<!X)'预先。
-
-
正则表达式的
s(点全部)标志。如果此标志处于活动状态,点匹配行终止符(默认情况下不匹配)。 -
RegExp Unicode 属性转义 在匹配 Unicode 代码点集合时赋予我们更多能力 - 例如:
> /^\p{Lowercase_Letter}+$/u.test('aüπ') true > /^\p{White_Space}+$/u.test('\n \t') true > /^\p{Script=Greek}+$/u.test('ΩΔΨ') true
-
-
模板字面量修订 允许在标记模板中使用包含反斜杠的文本,这在字符串字面量中是非法的 - 例如:
windowsPath`C:\uuu\xxx\111` latex`\unicode`
6.9 新增于 ECMAScript 2017
-
异步函数 (
async/await) 允许我们使用类似同步的语法来编写异步代码。 -
Object.values() 返回一个包含给定对象所有可枚举字符串键属性值的数组。
-
Object.entries() 返回一个包含给定对象所有可枚举字符串键属性键值对的数组。每个键值对编码为一个包含两个元素的数组。
-
字符串填充:字符串方法
.padStart()和.padEnd()在接收器足够长之前插入填充文本:> '7'.padStart(3, '0') '007' > 'yes'.padEnd(6, '!') 'yes!!!' -
函数参数列表和调用中的尾随逗号:自 ES3 起允许在数组字面量中使用尾随逗号,自 ES5 起允许在对象字面量中使用,现在也允许在函数调用和方法调用中使用。
-
Object.getOwnPropertyDescriptors() 允许我们通过具有属性描述符的对象来定义属性:
-
功能“共享内存和原子操作”超出了本书的范围。有关更多信息,请参阅:
-
MDN Web Docs 上的 SharedArrayBuffer 和 Atomics 文档。
-
6.10 新增于 ECMAScript 2016
-
Array.prototype.includes() 检查数组是否包含指定的值。
-
指数运算符 (
**):> 4 ** 2 16
6.11 本章来源
ECMAScript 功能列表来自 TC39 完成提案页面。
7 FAQ: JavaScript
-
7.1 什么是 JavaScript 的良好参考资料?
-
7.2 我如何了解 JavaScript 在哪些地方支持哪些功能?
-
7.3 我在哪里可以查找 JavaScript 计划中的功能?
-
7.4 为什么 JavaScript 经常无声失败?
-
7.5 为什么我们不能通过删除怪癖和过时的功能来清理 JavaScript?
-
7.6 如何快速尝试一段 JavaScript 代码?
7.1 什么是 JavaScript 的良好参考资料?
请参阅 “JavaScript 参考资料” (§8.3)。
7.2 我如何了解 JavaScript 在哪些地方支持哪些功能?
本书通常会在提到一个功能是否是 ECMAScript 5(由旧版浏览器要求)或更新版本的一部分。对于更详细的信息(包括 ES5 之前的版本),网上有多个良好的兼容性表格可供参考。
-
Mozilla 的 MDN 网页文档为每个功能提供表格,描述相关的 ECMAScript 版本和浏览器支持。
-
“Can I use…” 文档说明了哪些功能(包括 JavaScript 语言功能)被网络浏览器支持。
7.3 我在哪里可以查找 JavaScript 计划中的功能?
请参阅以下资源:
-
“提议的 ECMAScript 功能的 TC39 流程” (§5.5)
-
“FAQ: ECMAScript 和 TC39” (§5.7)
7.4 为什么 JavaScript 经常无声失败?
JavaScript 经常无声失败。让我们看看两个例子。
第一个例子:如果运算符的操作数没有适当的类型,它们将按需转换。
> '3' * '5'
15
第二个例子:如果算术计算失败,你会得到一个错误值,而不是异常。
> 1 / 0
Infinity
无声失败的原因是历史性的:JavaScript 在 ECMAScript 3 之前没有异常处理。从那时起,其设计者一直试图避免无声失败。
7.5 为什么我们不能通过删除怪癖和过时的功能来清理 JavaScript?
这个问题在“在更改 JavaScript 时如何不破坏网页” (§5.6)中得到了解答。
7.6 如何快速尝试一段 JavaScript 代码?
“尝试运行 JavaScript 代码” (§10.1) 解释了如何做到这一点。
II 第一步骤
原文:exploringjs.com/js/book/pt_first-steps.html
8 使用 JavaScript:整体图景
-
8.1 本书你学到了什么?
-
8.2 浏览器和 Node.js 的结构
-
8.3 JavaScript 参考
-
8.4 进一步阅读
在本章中,我想描绘一个整体图景:你在本书中学到了什么,以及它如何融入网络开发的整体格局?
8.1 本书你学到了什么?
本书教授 JavaScript 语言。它专注于语言本身,但偶尔会涉及两个 JavaScript 可以使用的平台:
-
网页浏览器
-
Node.js
Node.js 在网络开发中有三个重要方面:
-
你可以用它来用 JavaScript 编写服务器端软件。
-
你也可以用它来编写命令行软件(例如 Unix shell、Windows PowerShell 等)。许多 JavaScript 相关的工具都是基于(并通过)Node.js 来实现的。
-
Node 的软件注册库 npm 已经成为安装工具(如编译器和构建工具)和库的主要方式——甚至对于客户端开发也是如此。
8.2 浏览器和 Node.js 的结构

图 8.1:两个 JavaScript 平台(网页浏览器和 Node.js)的结构。API “标准库”和“平台 API”位于包含 JavaScript 引擎和特定平台“核心”的基础层之上。
两个 JavaScript 平台(网页浏览器和 Node.js)的结构相似(图 8.1):
-
基础层由 JavaScript 引擎和特定平台的“核心”功能组成。
-
在这个基础上,有两个 API:
-
JavaScript 标准库是 JavaScript 本身的一部分,并在引擎之上运行。
-
平台 API 也从 JavaScript 中提供——它提供了对特定平台功能的访问。例如:
-
在浏览器中,如果你想进行任何与用户界面相关的工作(如响应用户点击、播放声音等),你需要使用特定平台的 API。
-
在 Node.js 中,特定平台的 API 允许你读写文件、通过 HTTP 下载数据等。
-
-
8.3 JavaScript 参考
如果你关于 JavaScript 有任何疑问,我可以推荐以下在线资源:
-
MDN Web 文档:涵盖各种网络技术,如 CSS、HTML、JavaScript 等。是一本优秀的参考书。
-
Node.js 文档:记录 Node.js API。
-
ExploringJS.com:我的其他书籍涵盖了网络开发的各个方面:
-
“深入 JavaScript:理论与技巧” 描述了 JavaScript 的细节程度,超出了“探索 JavaScript”的范围。
-
8.4 进一步阅读
- “下一步:网络开发概述” (§49) 提供了更全面的网络开发视角。
9 语法
-
9.1 JavaScript 语法的概述
-
9.1.1 基本结构
-
9.1.2 模块
-
9.1.3 类
-
9.1.4 异常处理
-
9.1.5 合法的变量和属性名
-
9.1.6 命名风格
-
9.1.7 名称的大小写
-
9.1.8 更多的命名约定
-
9.1.9 分号应该放在哪里?
-
-
9.2 (高级)
-
9.3 哈希行(Unix shell 脚本)
-
9.4 标识符
-
9.4.1 有效的标识符(变量名等)
-
9.4.2 保留字
-
-
9.5 语句与表达式
-
9.5.1 语句
-
9.5.2 表达式
-
9.5.3 在哪里允许什么?
-
-
9.6 模糊语法
-
9.6.1 相同的语法:函数声明和函数表达式
-
9.6.2 相同的语法:对象字面量和代码块
-
9.6.3 消除歧义
-
-
9.7 分号
-
9.7.1 分号规则
-
9.7.2 分号:控制语句
-
-
9.8 自动分号插入(ASI)
-
9.8.1 自动分号插入意外触发
-
9.8.2 自动分号插入(ASI)意外未触发
-
-
9.9 分号:最佳实践
-
9.10 严格模式与宽松模式
-
9.10.1 开启严格模式
-
9.10.2 严格模式中的改进
-
9.1 JavaScript 语法的概述
这是 JavaScript 语法的第一次全面了解。如果有些东西现在还不明白,不用担心。它们将在本书的后面部分详细解释。
这个概述也不是详尽的。它专注于基本内容。
9.1.1 基本结构
9.1.1.1 注释
// single-line comment
/*
Comment with
multiple lines
*/
9.1.1.2 原始(原子)值
布尔值:
true
false
数字:
1.141
-123
基本数字类型用于浮点数(双精度)和整数。
大整数:
17n
-49n
基本数字类型只能正确表示 53 位加符号范围内的整数。大整数可以任意增长大小。
字符串:
'abc'
"abc"
`String with interpolated values: ${256} and ${true}`
JavaScript 没有额外的字符类型。它使用字符串来表示它们。
9.1.1.3 断言
断言描述了计算结果预期应该是什么样子,如果那些期望不正确,则会抛出异常。例如,以下断言指出,计算 7 加 1 的结果必须是 8:
assert.equal(7 + 1, 8);
assert.equal()是一个方法调用(对象是assert,方法是.equal()),带有两个参数:实际结果和预期结果。它是 Node.js 断言 API 的一部分,该 API 将在本书后面解释。
还有assert.deepEqual(),它可以深度比较对象。
9.1.1.4 记录到控制台
将日志记录到浏览器的控制台或 Node.js:
// Printing a value to standard out (another method call)
console.log('Hello!');
// Printing error information to standard error
console.error('Something went wrong!');
9.1.1.5 运算符
// Operators for booleans
assert.equal(true && false, false); // And
assert.equal(true || false, true); // Or
// Operators for numbers
assert.equal(3 + 4, 7);
assert.equal(5 - 1, 4);
assert.equal(3 * 4, 12);
assert.equal(10 / 4, 2.5);
// Operators for bigints
assert.equal(3n + 4n, 7n);
assert.equal(5n - 1n, 4n);
assert.equal(3n * 4n, 12n);
assert.equal(10n / 4n, 2n);
// Operators for strings
assert.equal('a' + 'b', 'ab');
assert.equal('I see ' + 3 + ' monkeys', 'I see 3 monkeys');
// Comparison operators
assert.equal(3 < 4, true);
assert.equal(3 <= 4, true);
assert.equal('abc' === 'abc', true);
assert.equal('abc' !== 'def', true);
JavaScript 还有一个==比较运算符。我建议避免使用它——原因在“建议:始终使用严格相等”(§15.5.3)中解释。
9.1.1.6 声明变量
const创建不可变变量绑定:每个变量必须立即初始化,以后不能分配不同的值。然而,值本身可能是可变的,我们可能能够更改其内容。换句话说:const并不使值不可变。
// Declaring and initializing x (immutable binding):
const x = 8;
// Would cause a TypeError:
// x = 9;
let创建可变变量绑定:
// Declaring y (mutable binding):
let y;
// We can assign a different value to y:
y = 3 * 5;
// Declaring and initializing z:
let z = 3 * 5;
9.1.1.7 普通函数声明
// add1() has the parameters a and b
function add1(a, b) {
return a + b;
}
// Calling function add1()
assert.equal(add1(5, 2), 7);
9.1.1.8 箭头函数表达式
箭头函数表达式特别用于函数调用和方法调用的参数:
const add2 = (a, b) => { return a + b };
// Calling function add2()
assert.equal(add2(5, 2), 7);
// Equivalent to add2:
const add3 = (a, b) => a + b;
之前的代码包含以下两个箭头函数(术语表达式和语句将在本章后面解释):
// An arrow function whose body is a code block
(a, b) => { return a + b }
// An arrow function whose body is an expression
(a, b) => a + b
9.1.1.9 普通对象
// Creating a plain object via an object literal
const obj = {
first: 'Jane', // property
last: 'Doe', // property
getFullName() { // property (method)
return this.first + ' ' + this.last;
},
};
// Getting a property value
assert.equal(obj.first, 'Jane');
// Setting a property value
obj.first = 'Janey';
// Calling the method
assert.equal(obj.getFullName(), 'Janey Doe');
9.1.1.10 数组
// Creating an Array via an Array literal
const arr = ['a', 'b', 'c'];
assert.equal(arr.length, 3);
// Getting an Array element
assert.equal(arr[1], 'b');
// Setting an Array element
arr[1] = 'β';
// Adding an element to an Array:
arr.push('d');
assert.deepEqual(
arr, ['a', 'β', 'c', 'd']);
9.1.1.11 控制流语句
条件语句:
if (x < 0) {
x = -x;
}
for-of循环:
const arr = ['a', 'b'];
for (const element of arr) {
console.log(element);
}
输出:
a
b
9.1.2 模块
每个模块都是一个单独的文件。例如,考虑以下包含模块的两个文件:
file-tools.mjs
main.mjs
file-tools.mjs中的模块导出其函数isTextFilePath():
export function isTextFilePath(filePath) {
return filePath.endsWith('.txt');
}
main.mjs中的模块导入了整个模块path和函数isTextFilePath():
// Import whole module as namespace object `path`
import * as path from 'node:path';
// Import a single export of module file-tools.mjs
import {isTextFilePath} from './file-tools.mjs';
9.1.3 类
class Person {
constructor(name) {
this.name = name;
}
describe() {
return `Person named ${this.name}`;
}
static logNames(persons) {
for (const person of persons) {
console.log(person.name);
}
}
}
class Employee extends Person {
constructor(name, title) {
super(name);
this.title = title;
}
describe() {
return super.describe() +
` (${this.title})`;
}
}
const jane = new Employee('Jane', 'CTO');
assert.equal(
jane.describe(),
'Person named Jane (CTO)');
9.1.4 异常处理
function throwsException() {
throw new Error('Problem!');
}
function catchesException() {
try {
throwsException();
} catch (err) {
assert.ok(err instanceof Error);
assert.equal(err.message, 'Problem!');
}
}
注意:
-
支持
try-finally和try-catch-finally。 -
我们可以抛出任何值,但只有
Error及其子类支持堆栈跟踪等特性。
9.1.5 合法变量和属性名称
变量名和属性名的语法类别称为标识符。
标识符允许包含以下字符:
-
Unicode 字母:
A–Z、a–z(等等) -
$,_ -
Unicode 数字:
0–9(等等)- 变量名不能以数字开头
一些单词在 JavaScript 中有特殊含义,被称为保留字。例如:if、true、const。
保留字不能用作变量名:
const if = 123;
// SyntaxError: Unexpected token if
但它们可以用作属性名:
> const obj = { if: 123 };
> obj.if
123
9.1.6 大小写风格
拼接单词的常见大小写风格有:
-
驼峰式:
threeConcatenatedWords -
下划线大小写(也称为 snake case):
three_concatenated_words -
破折号大小写(也称为 kebab case):
three-concatenated-words
9.1.7 名称的资本化
通常,JavaScript 使用驼峰式,但常量除外。
小写:
-
函数、变量:
myFunction -
方法:
obj.myMethod -
CSS:
-
CSS 名称:
my-utility-class(破折号大小写) -
对应的 JavaScript 名称:
myUtilityClass
-
-
模块文件名通常是破折号大小写:
import * as theSpecialLibrary from './the-special-library.mjs';
大写:
- 类:
MyClass
全大写:
- 常量(如模块之间的共享等):
MY_CONSTANT(下划线大小写)
9.1.8 更多命名约定
以下命名约定在 JavaScript 中很流行。
如果参数的名称以下划线开头(或本身就是下划线),则表示该参数未使用——例如:
arr.map((_x, i) => i)
如果对象的属性名称以下划线开头,则该属性被认为是私有的:
class ValueWrapper {
constructor(value) {
this._value = value;
}
}
9.1.9 分号放置的位置?
在语句的末尾:
const x = 123;
func();
但如果该语句以花括号结尾,则不是这样:
while (false) {
// ···
} // no semicolon
function func() {
// ···
} // no semicolon
然而,在这样一个语句后添加分号并不是语法错误——它被解释为一个空语句:
// Function declaration followed by empty statement:
function func() {
// ···
};
9.2 (高级)
本章的其余部分都是高级内容。
9.3 Hashbang 行(Unix shell 脚本)
在 Unix shell 脚本中,我们可以添加一个以 #! 开头的第一行,以告诉 Unix 应使用哪个可执行文件来运行脚本。这两个字符有几个名字,包括 hashbang、sharp-exclamation、sha-bang(“sha”就像“sharp”)和 shebang。否则,大多数 shell 脚本语言和 JavaScript 都将其视为注释。这是 Node.js 的常见 hashbang 行:
#!/usr/bin/env node
如果我们要向 node 传递参数,我们必须使用 env 选项 -S(为了安全起见,某些 Unix 系统可能不需要它):
#!/usr/bin/env -S node --enable-source-maps --no-warnings=ExperimentalWarning
9.4 标识符
9.4.1 有效标识符(变量名等)
第一个字符:
-
Unicode 字母(包括带重音的字符,如
é和ü以及来自非拉丁字母表的字符,如α) -
$ -
_
后续字符:
-
合法的第一个字符
-
Unicode 数字(包括东阿拉伯数字)
-
一些其他的 Unicode 标记和标点符号
例子:
const ε = 0.0001;
const строка = '';
let _tmp = 0;
const $foo2 = true;
9.4.2 保留字
保留字不能是变量名,但可以是属性名。
所有 JavaScript 关键字 都是保留字:
awaitbreakcasecatchclassconstcontinuedebuggerdefaultdeletedoelseexportextendsfinallyforfunctionifimportininstanceofletnewreturnstaticsuperswitchthisthrowtrytypeofvarvoidwhilewithyield
以下标记也是关键字,但目前未在语言中使用:
enumimplementspackageprotectedinterfaceprivatepublic
以下字面量是保留字:
truefalsenull
从技术上讲,这些词不是保留的,但你应该避免使用它们,因为它们实际上也是关键字:
InfinityNaNundefinedasync
你也不应该使用全局变量的名称(String、Math等)来为自己的变量和参数命名。
9.5 语句与表达式
在本节中,我们探讨 JavaScript 如何区分两种语法结构:语句和表达式。之后,我们将看到这可能会引起问题,因为相同的语法在不同的上下文中可能意味着不同的事情。
我们假装只有语句和表达式
为了简化起见,我们假装 JavaScript 中只有语句和表达式。
9.5.1 语句
语句是一段可以执行并执行某种动作的代码。例如,if是一个语句:
let myStr;
if (myBool) {
myStr = 'Yes';
} else {
myStr = 'No';
}
另一个语句的例子:函数声明。
function twice(x) {
return x + x;
}
9.5.2 表达式
表达式是一段可以评估以产生值的代码。例如,括号之间的代码是一个表达式:
let myStr = (myBool ? 'Yes' : 'No');
在括号之间使用的操作符_?_:_被称为三元操作符。它是if语句的表达式版本。
让我们看看更多表达式的例子。我们输入表达式,REPL 会为我们评估它们:
> 'ab' + 'cd'
'abcd'
> Number('123')
123
> true || false
true
9.5.3 允许在哪里?
JavaScript 源代码中的当前位置决定了你可以使用哪种语法结构:
-
函数体必须是一系列语句:
function max(x, y) { if (x > y) { return x; } else { return y; } } -
函数调用或方法调用的参数必须是表达式:
console.log('ab' + 'cd', Number('123'));
然而,表达式可以用作语句。这时,它们被称为表达式语句。相反的情况不成立:当上下文需要表达式时,你不能使用语句。
以下代码演示了任何表达式bar()可以是表达式或语句——这取决于上下文:
function f() {
console.log(bar()); // bar() is expression
bar(); // bar(); is (expression) statement
}
9.6 混淆的语法
JavaScript 有几个在语法上模糊的编程结构:相同的语法在不同的上下文中被解释为不同,这节将探讨这种现象及其带来的问题。
9.6.1 同样的语法:函数声明和函数表达式
函数声明是一个语句:
function id(x) {
return x;
}
函数表达式是一个表达式(等号的右侧):
const id = function me(x) {
return x;
};
9.6.2 同样的语法:对象字面量和代码块
在以下代码中,{}是一个对象字面量:一个创建空对象的表达式。
const obj = {};
这是一个空的代码块(一个语句):
{
}
9.6.3 消歧义
这些歧义仅在语句上下文中是问题:如果 JavaScript 解析器遇到歧义性语法,它不知道它是一个普通语句还是一个表达式语句。例如:
-
如果一个语句以
function开头:它是函数声明还是函数表达式? -
如果一个语句以
{开头:它是对象字面量还是代码块?
为了解决歧义,以 function 或 { 开头的语句永远不会被解释为表达式。如果你想使一个表达式语句以这两个标记之一开始,你必须将其括在括号中:
(function (x) { console.log(x) })('abc');
输出:
abc
在此代码中:
-
我们首先通过函数表达式创建一个函数:
function (x) { console.log(x) } -
然后我们调用该函数:
('abc')
在(1)中显示的代码片段仅被解释为表达式,因为我们将其括在括号中。如果我们没有这样做,我们会得到一个语法错误,因为那时 JavaScript 期望一个函数声明并抱怨缺少函数名。此外,你不能在函数声明后立即放置一个函数调用。
在本书的后面部分,我们将看到更多由语法歧义引起的陷阱示例:
-
通过对象解构赋值
-
从箭头函数返回对象字面量
9.7 分号
9.7.1 分号的经验法则
每个语句都以分号结束:
const x = 3;
someFunction('abc');
i++;
除了以块结尾的语句:
function foo() {
// ···
}
if (y > 0) {
// ···
}
以下情况稍微有些棘手:
const func = () => {}; // semicolon!
整个 const 声明(一个语句)以分号结束,但其中包含一个箭头函数表达式。也就是说,结束于花括号的不是语句本身,而是嵌入的箭头函数表达式。这就是为什么在末尾有一个分号的原因。
9.7.2 分号:控制语句
控制语句的主体本身就是一个语句。例如,这是 while 循环的语法:
while (condition)
statement
主体可以是一个单独的语句:
while (a > 0) a--;
但块也是语句,因此是控制语句的有效主体:
while (a > 0) {
a--;
}
如果你想让循环有一个空的主体,你的第一个选项是一个空语句(它只是一个分号):
while (processNextItem() > 0);
你的第二个选项是一个空块:
while (processNextItem() > 0) {}
9.8 自动分号插入(ASI)
虽然我建议始终写分号,但在 JavaScript 中大多数情况下它们是可选的。使这成为可能的是称为 自动分号插入(ASI)的机制。从某种意义上说,它纠正了语法错误。
ASI 的工作原理如下。语句的解析继续进行,直到以下情况之一:
-
一个分号
-
一个行终止符后面跟着一个非法标记
换句话说,自动分号插入(ASI)可以看作是在行尾插入分号。接下来的小节将介绍 ASI 的陷阱。
9.8.1 意外触发的 ASI
关于自动分号插入(ASI)的好消息是——如果你不依赖它并且总是写分号——你只需要注意一个陷阱。那就是 JavaScript 禁止在某些标记后换行。如果你插入换行,也会插入分号。
这在return标记中最为实用。例如,考虑以下代码:
return
{
first: 'jane'
};
这段代码被解析为:
return;
{
first: 'jane';
}
;
那就是:
-
没有操作数的返回语句:
return; -
代码块开始:
{ -
表达式语句
'jane';带有标签first: -
代码块结束:
} -
空语句:
;
为什么 JavaScript 要这样做?它是为了防止在return语句之后的行中意外返回一个值。
9.8.2 ASI 意外未触发
在某些情况下,当你认为应该触发 ASI 时,它并没有触发。这使得不喜欢分号的人的生活更加复杂,因为他们需要意识到这些情况。以下有三个例子。还有更多。
示例 1:意外的函数调用。
a = b + c
(d + e).print()
解析为:
a = b + c(d + e).print();
示例 2:意外的除法。
a = b
/hi/g.exec(c).map(d)
解析为:
a = b / hi / g.exec(c).map(d);
示例 3:意外的属性访问。
someFunction()
['ul', 'ol'].map(x => x + x)
执行为:
const propKey = ('ul','ol'); // comma operator
assert.equal(propKey, 'ol');
someFunction()[propKey].map(x => x + x);
9.9 分号:最佳实践
我建议你总是写分号:
-
我喜欢它给代码带来的视觉结构——你可以清楚地看到一个语句在哪里结束。
-
需要记住的规则更少。
-
大多数 JavaScript 程序员使用分号。
然而,也有很多人不喜欢分号带来的额外视觉混乱。如果你是其中之一:没有分号的代码是合法的。我建议你使用工具来帮助你避免错误。以下有两个例子:
-
自动代码格式化工具Prettier可以配置为不使用分号。然后它会自动修复问题。例如,如果它遇到以方括号开头的行,它会在该行前加上分号。
9.10 strict 模式与宽松模式
从 ECMAScript 5 开始,JavaScript 有两种模式,JavaScript 可以在其中执行:
-
正常的“宽松”模式在脚本中是默认的(模块的前身,由浏览器支持)。
-
在模块和类中,strict 模式是默认的,可以在脚本中打开(如何操作将在后面解释)。在这种模式下,消除了正常模式的一些陷阱,并抛出了更多异常。
在现代 JavaScript 代码中,你很少会遇到宽松模式,它几乎总是位于模块中。在这本书中,我假设始终打开 strict 模式。
9.10.1 打开 strict 模式
在脚本文件和 CommonJS 模块中,你通过在第一行放置以下代码来为一个完整的文件打开 strict 模式:
'use strict';
这个“指令”的妙处在于,ECMAScript 5 之前的版本简单地忽略它:它是一个什么也不做的表达式语句。
你也可以只为单个函数切换到严格模式:
function functionInStrictMode() {
'use strict';
}
9.10.2 严格模式中的改进
让我们看看严格模式比宽松模式做得更好的三个方面。仅仅在这一节中,所有代码片段都是在宽松模式下执行的。
9.10.2.1 宽松模式陷阱:更改未声明的变量会创建全局变量
在非严格模式下,更改未声明的变量会创建一个全局变量。
function sloppyFunc() {
undeclaredVar1 = 123;
}
sloppyFunc();
// Created global variable `undeclaredVar1`:
assert.equal(undeclaredVar1, 123);
严格模式做得更好,并抛出一个ReferenceError。这使得检测拼写错误变得更容易。
function strictFunc() {
'use strict';
undeclaredVar2 = 123;
}
assert.throws(
() => strictFunc(),
{
name: 'ReferenceError',
message: 'undeclaredVar2 is not defined',
});
assert.throws()表明其第一个参数,一个函数,在调用时会抛出一个ReferenceError。
9.10.2.2 严格模式中函数声明是块作用域的,宽松模式中是函数作用域的
在严格模式下,通过函数声明创建的变量仅存在于最内层的封装块中:
function strictFunc() {
'use strict';
{
function foo() { return 123 }
}
return foo(); // ReferenceError
}
assert.throws(
() => strictFunc(),
{
name: 'ReferenceError',
message: 'foo is not defined',
});
在宽松模式下,函数声明是函数作用域的:
function sloppyFunc() {
{
function foo() { return 123 }
}
return foo(); // works
}
assert.equal(sloppyFunc(), 123);
9.10.2.3 宽松模式在更改不可变数据时不会抛出异常
在严格模式下,如果你尝试更改不可变数据,你会得到一个异常:
function strictFunc() {
'use strict';
true.prop = 1; // TypeError
}
assert.throws(
() => strictFunc(),
{
name: 'TypeError',
message: "Cannot create property 'prop' on boolean 'true'",
});
在宽松模式下,赋值操作会静默失败:
function sloppyFunc() {
true.prop = 1; // fails silently
return true.prop;
}
assert.equal(sloppyFunc(), undefined);
进一步阅读:宽松模式
更多关于宽松模式与严格模式差异的信息,请参阅MDN。


浙公网安备 33010602011771号