写给不耐烦程序员的 JavaScript 指南(一)

第一部分:背景

原文:exploringjs.com/impatient-js/pt_background.html

译者:飞龙

协议:CC BY-NC-SA 4.0

下一步:1 在购买书之前

一、在购买本书之前

原文:exploringjs.com/impatient-js/ch_about-book.html

译者:飞龙

协议:CC BY-NC-SA 4.0


  • 1.1 关于内容

    • 1.1.1 本书包含什么?

    • 1.1.2 本书未涵盖的内容是什么?(ch_about-book.html#what-is-not-covered-by-this-book)

    • 1.1.3 这本书对于急躁的人来说不是太长了吗?(ch_about-book.html#isnt-this-book-too-long-for-impatient-people)

  • 1.2 预览和购买本书

    • 1.2.1 我如何预览本书、练习和测验?

    • 1.2.2 我如何购买本书的数字版本?(ch_about-book.html#how-can-i-buy-a-digital-version-of-this-book)

    • 1.2.3 我如何购买本书的印刷版本?

  • 1.3 关于作者(ch_about-book.html#about-the-author)

  • 1.4 致谢


1.1 关于内容

1.1.1 本书包含什么?

本书通过提供尽可能一致的现代视角,使 JavaScript 对于新手来说变得不那么具有挑战性。

亮点:

  • 通过最初专注于现代特性来快速入门。

  • 大多数章节都提供了测试驱动的练习和测验。

  • 涵盖了 JavaScript 的所有基本特性,包括 ES2022。

  • 可选的高级部分让您深入了解。

不需要 JavaScript 的先验知识,但您应该知道如何编程。

1.1.2 本书未涵盖的内容是什么?
  • 一些高级语言特性没有解释,但提供了适当材料的参考资料,例如我的其他 JavaScript 书籍ExploringJS.com,可以免费在线阅读。

  • 本书有意专注于语言。仅限于浏览器的特性等未进行描述。

1.1.3 这本书对于急躁的人来说不是太长了吗?

有几种方式可以阅读本书。其中一种方式是跳过大部分内容,以便快速入门。有关详情,请参阅§2.1.1“我应该以什么顺序阅读本书的内容?”。

1.2 预览和购买本书

1.2.1 我如何预览本书、练习和测验?

转到本书的主页:

  • 本书的所有基本章节都可以免费在线阅读。

  • 测试驱动的练习的前半部分可以下载。

  • 测验的前半部分可以在线尝试。

1.2.2 我如何购买本书的数字版本?

《JavaScript for impatient programmers》有两个数字版本:

  • 电子书:PDF,EPUB,MOBI,HTML(均无 DRM)

  • 电子书加练习和测验

本书的主页描述了您可以如何购买它们。

1.2.3 我如何购买本书的印刷版本?

《JavaScript for impatient programmers》的印刷版本可在亚马逊上购买。

1.3 关于作者

Dr. Axel Rauschmayer 专注于 JavaScript 和网页开发。他自 1995 年以来一直在开发网页应用程序。1999 年,他是德国一家互联网初创公司的技术经理,后来扩展到国际市场。2006 年,他进行了关于 Ajax 的首次演讲。2010 年,他从慕尼黑大学获得了信息学博士学位。

自 2011 年以来,他一直在 2ality.com 上就网页开发进行博客,并撰写了几本关于 JavaScript 的书籍。他曾为 eBay、美国银行和 O’Reilly Media 等公司进行培训和演讲。

他居住在德国慕尼黑。

1.4 致谢

[生成日期:2022 年 01 月 03 日 14:17]

评论

二、FAQ:书籍和补充材料

原文:exploringjs.com/impatient-js/ch_faq-book.html

译者:飞龙

协议:CC BY-NC-SA 4.0


  • 2.1 如何阅读本书

    • 2.1.1 我应该按照什么顺序阅读本书的内容?

    • 2.1.2 为什么有些章节和部分标有“(高级)”?

    • 2.1.3 为什么有些章节标有“(奖励)”?

  • 2.2 我拥有数字版本

    • 2.2.1 如何提交反馈和更正?

    • 2.2.2 我如何获取在 Payhip 购买的下载的更新?

    • 2.2.3 我可以从“电子书”套餐升级到“电子书+练习+测验”套餐吗?

  • 2.3 我拥有印刷版本

    • 2.3.1 我可以获得数字版本的折扣吗?

    • 2.3.2 我可以提交错误或查看已提交的错误吗?

    • 2.3.3 本书中的 URL 是否有在线列表?

  • 2.4 符号和约定

    • 2.4.1 什么是类型签名?为什么我在本书中看到静态类型?

    • 2.4.2 带图标的注释是什么意思?


本章回答了您可能遇到的问题,并提供了阅读本书的建议。

2.1 如何阅读本书

2.1.1 我应该按照什么顺序阅读本书的内容?

这本书是三本书合为一体:

  • 您可以使用它尽快开始学习 JavaScript。这种“模式”适用于急躁的人:

    • 从§6 “使用 JavaScript:全局视角”开始阅读。

    • 跳过所有标有“(高级)”的章节和部分,以及所有快速参考。

  • 它为您提供了对当前 JavaScript 的全面了解。在这种“模式”下,您可以阅读所有内容,不要跳过高级内容和快速参考。

  • 它作为参考。如果有您感兴趣的主题,您可以通过目录或索引找到相关信息。由于基本和高级内容混合在一起,您通常可以在一个地方找到所需的一切。

测验和练习 在帮助您练习和巩固所学内容方面起着重要作用。

2.1.2 为什么有些章节和部分标有“(高级)”?

有几个章节和部分标有“(高级)”。其想法是您最初可以跳过它们。也就是说,您可以通过只阅读基本(非高级)内容来快速掌握 JavaScript 的工作知识。

随着您的知识的进步,您以后可以回顾一些或所有的高级内容。

2.1.3 为什么有些章节标有“(奖励)”?

奖励章节仅在本书的付费版本(印刷版和电子书)中提供。它们在完整目录中列出。

2.2 我拥有数字版本

2.2.1 我如何提交反馈和更正?

本书的 HTML 版本(在线或付费版本中的无广告存档)在每章的末尾都有一个链接,可以让您提供反馈。

2.2.2 我如何获取在 Payhip 购买的下载的更新?
  • 购买收据邮件中包含一个链接。您始终可以在该位置下载文件的最新版本。

  • 如果您在购买时选择了接收电子邮件,每当有新内容时您都会收到一封电子邮件。要稍后选择接收电子邮件,您必须联系 Payhip(请参阅payhip.com底部)。

2.2.3 我可以从“电子书”套餐升级到“电子书+练习+测验”套餐吗?

是的。有关如何做到这一点的说明在本书的主页上。

2.3 我拥有印刷版本

2.3.1 我可以为数字版本获得折扣吗?

如果您购买了印刷版本,您可以获得数字版本的折扣。印刷版本的主页解释了如何做到这一点。

遗憾的是,反之则不可能:如果您购买了数字版本,您无法获得印刷版本的折扣。

2.3.2 我可以提交错误或查看已提交的错误吗?

印刷版本的主页上,您可以提交错误并查看已提交的错误。

2.3.3 这本书中是否有 URL 的在线列表?

印刷版本的主页列有所有脚注中看到的 URL 的列表。

2.4 符号和约定

2.4.1 什么是类型签名?为什么我在这本书中看到静态类型?

例如,您可能会看到:

Number.isFinite(num: number): boolean

这被称为Number.isFinite()类型签名。这种符号,特别是num的静态类型number和结果的boolean,并不是真正的 JavaScript。这种符号是从编译为 JavaScript 的语言 TypeScript 借来的(它基本上只是 JavaScript 加上静态类型)。

为什么使用这种符号?它有助于让您快速了解函数的工作原理。这种符号在“探索 TypeScript”中有详细解释,但通常相对直观。

2.4.2 带有图标的注释是什么意思?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 阅读说明

解释如何最好地阅读内容。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 外部内容

指向额外的外部内容。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 提示

提供与当前内容相关的提示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 问题

提出并回答与当前内容相关的问题(想想常见问题)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 警告

警告有关陷阱等。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 详情

提供额外的细节,补充当前内容。这类似于脚注。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 练习

提到您可以在那时进行的基于测试驱动的练习的路径。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 测验

表示当前(部分)章节有测验。

评论

三、JavaScript 的历史和演变

原文:exploringjs.com/impatient-js/ch_history.html

译者:飞龙

协议:CC BY-NC-SA 4.0


  • 3.1 JavaScript 是如何创建的

  • 3.2 JavaScript 标准化

  • 3.3 ECMAScript 版本的时间表

  • 3.4 Ecma 技术委员会 39(TC39)

  • 3.5 TC39 流程

    • 3.5.1 提示:考虑单个功能和阶段,而不是 ECMAScript 版本
  • 3.6 常见问题:TC39 流程

    • 3.6.1 我的最喜欢的提议功能怎么样?

    • 3.6.2 是否有 ECMAScript 功能的官方列表?

  • 3.7 JavaScript 的演变:不要破坏 Web


3.1 JavaScript 是如何创建的

JavaScript 是由 Brendan Eich 于 1995 年 5 月在 10 天内创建的。Eich 在 Netscape 工作,并为他们的 Web 浏览器Netscape Navigator实现了 JavaScript。

这个想法是客户端 Web 的主要交互部分应该用 Java 实现。JavaScript 应该是这些部分的粘合语言,并且也使 HTML 稍微更具交互性。鉴于它作为 Java 的辅助角色,JavaScript 必须看起来像 Java。这排除了现有的解决方案,如 Perl,Python,TCL 等。

最初,JavaScript 的名称多次更改:

  • 它的代号是Mocha

  • 在 Netscape Navigator 2.0 beta 版(1995 年 9 月),它被称为LiveScript

  • 在 Netscape Navigator 2.0 beta 3(1995 年 12 月),它得到了最终的名字,JavaScript

3.2 JavaScript 标准化

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 版)。

3.3 ECMAScript 版本的时间表

这是 ECMAScript 版本的简要时间表:

  • ECMAScript 1(1997 年 6 月):标准的第一个版本。

  • ECMAScript 2(1998 年 6 月):与 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 月份正式通过。

3.4 Ecma 技术委员会 39(TC39)

TC39 是推动 JavaScript 发展的委员会。严格来说,它的成员是公司:Adobe、Apple、Facebook、Google、Microsoft、Mozilla、Opera、Twitter 等。也就是说,通常是激烈竞争的公司正在为了语言的利益而共同合作。

每两个月,TC39 都会举行会议,由成员指定的代表和受邀专家参加。这些会议的记录是公开的,存储在GitHub 存储库中。

3.5 TC39 流程

随着 ECMAScript 6,当时使用的发布流程出现了两个问题:

  • 如果发布之间的时间太长,那么早期准备好的功能就必须等待很长时间才能发布。而准备晚的功能则面临着为了赶上截止日期而被匆忙发布的风险。

  • 功能通常是在实现和使用之前设计的。因此,与实现和使用相关的设计缺陷通常太晚才被发现。

为了解决这些问题,TC39 制定了新的TC39 流程

  • ECMAScript 功能是独立设计的,并经历从 0(“草案”)到 4(“完成”)的阶段。

  • 特别是后期阶段需要原型实现和实际测试,从而在设计和实现之间形成反馈循环。

  • ECMAScript 版本每年发布一次,并包括在发布截止日期之前达到第 4 阶段的所有功能。

结果是:更小的、增量式的发布,其功能已经经过了现场测试。图 1 说明了 TC39 的流程。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1:每个 ECMAScript 功能提案都经历了从 0 到 4 的阶段。冠军是支持功能作者的 TC39 成员。Test 262 是一套测试,用于检查 JavaScript 引擎是否符合语言规范。

ES2016 是第一个根据 TC39 流程设计的 ECMAScript 版本。

3.5.1 提示:以单独的功能和阶段为思考对象,而不是 ECMAScript 版本

直到 ES6 为止,最常见的是根据 ECMAScript 版本来思考 JavaScript - 例如,“这个浏览器支持 ES6 吗?”

从 ES2016 开始,最好是考虑单独的功能:一旦一个功能达到第 4 阶段,你就可以安全地使用它(如果它受到你所针对的 JavaScript 引擎的支持)。你不必等到下一个 ECMAScript 发布版本。

3.6 常见问题:TC39 流程

3.6.1 我最喜欢的提议功能进展如何?

如果你想知道各种提议功能处于哪个阶段,请查阅GitHub 存储库proposals

3.6.2 是否有一个官方的 ECMAScript 功能列表?

是的,TC39 存储库列出了已完成的提案,并提到它们是在哪个 ECMAScript 版本中引入的。

3.7 JavaScript 的发展:不要破坏网络

有时会提出一个想法,即通过删除旧功能和怪癖来清理 JavaScript。虽然这个想法的吸引力是显而易见的,但它有重大的缺点。

假设我们创建了一个不向后兼容并修复了所有缺陷的 JavaScript 的新版本。结果,我们会遇到以下问题:

  • JavaScript 引擎变得臃肿:它们需要支持旧版本和新版本。对于 IDE 和构建工具也是如此。

  • 程序员需要了解,并不断意识到,不同版本之间的差异。

  • 你可以选择将现有的全部代码迁移到新版本(这可能是很多工作)。或者你可以混合使用版本,重构变得更加困难,因为你不能在不改变代码的情况下在不同版本之间移动代码。

  • 你必须以某种方式指定每一段代码的版本 - 无论是文件还是嵌入在网页中的代码 - 它是用哪个版本编写的。每种可行的解决方案都有利有弊。例如,严格模式是 ES5 的一个稍微更清洁的版本。它之所以没有像应该的那样受欢迎的一个原因是:通过在文件或函数开头的指令中选择进入是一件麻烦事。

那么解决方案是什么?我们可以两全其美吗?ES6 选择的方法称为“一种 JavaScript”:

  • 新版本始终完全向后兼容(但偶尔可能会有一些微小的、几乎察觉不到的清理)。

  • 旧功能不会被移除或修复。相反,它们的更好版本被引入。一个例子是通过let声明变量 - 这是var的改进版本。

  • 如果语言的某些方面发生了变化,那是在新的语法结构内完成的。也就是说,你是隐式选择进入的。例如,yield只是 ES6 中生成器内的关键字。并且所有模块和类中的代码(都是在 ES6 中引入的)都隐式地处于严格模式。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 测验

查看测验应用。

评论

四、新的 JavaScript 功能

原文:exploringjs.com/impatient-js/ch_new-javascript-features.html

译者:飞龙

协议:CC BY-NC-SA 4.0


  • 4.1 ECMAScript 2022 中的新功能

  • 4.2 ECMAScript 2021 中的新功能

  • 4.3 ECMAScript 2020 中的新功能

  • 4.4 ECMAScript 2019 中的新功能

  • 4.5 ECMAScript 2018 中的新功能

  • 4.6 ECMAScript 2017 中的新功能

  • 4.7 ECMAScript 2016 中的新功能

  • 4.8 本章的来源


本章以逆序列出了 ES2016–ES2022 中的新功能。它在 ES2015(ES6)之后开始,因为该版本具有太多功能无法在此列出。

4.1 ECMAScript 2022 中的新功能

ES2022 可能会在 2022 年 6 月成为标准。以下提案已经达到了第 4 阶段,并计划成为该标准的一部分:

  • 类的新成员:

    • 现在可以通过以下方式创建属性(公共槽):

      • 实例公共字段

      • 静态公共字段

    • 私有槽是新的,可以通过以下方式创建:

      • 私有字段(实例私有字段和静态私有字段)

      • 私有方法和访问器(非静态和静态)

    • 静态初始化块

  • 私有槽检查(“私有字段的人性化品牌检查”):以下表达式检查obj是否具有私有槽#privateSlot

    #privateSlot in obj
    
  • 模块中的顶层await:我们现在可以在模块的顶层使用await,不再需要进入异步函数或方法。

  • error.causeError及其子类现在让我们指定导致当前错误的错误:

    new Error('Something went wrong', {cause: otherError})
    
  • 索引值的方法.at()(ch_arrays.html#Array.prototype.at)允许我们在给定索引处读取元素(类似于括号运算符[]),并支持负索引(与括号运算符不同)。

    > ['a', 'b', 'c'].at(0)
    'a'
    > ['a', 'b', 'c'].at(-1)
    'c'
    

    以下“可索引”类型具有方法.at()

    • string

    • Array

    • 所有 Typed Array 类:Uint8Array等。

  • RegExp 匹配索引:如果我们向正则表达式添加一个标志,使用它会产生记录每个组捕获的开始和结束索引的匹配对象。

  • Object.hasOwn(obj, propKey)提供了一种安全的方式来检查对象obj是否具有键propKey的自有属性。与Object.prototype.hasOwnProperty相比,它适用于所有对象。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 ES2022 可能还会添加更多功能

如果发生这种情况,本书将相应地进行更新。

4.2 ECMAScript 2021 中的新功能

以下功能是在 ECMAScript 2021 中添加的:

  • String.prototype.replaceAll()允许我们替换正则表达式或字符串的所有匹配项(.replace()只替换字符串的第一个出现):

    > 'abbbaab'.replaceAll('b', 'x')
    'axxxaax'
    
  • Promise.any()AggregateErrorPromise.any()返回一个 Promise,一旦可迭代的 Promise 中的第一个 Promise 被满足,它就会被满足。如果只有拒绝,它们将被放入一个AggregateError,成为拒绝值。

    当我们只对多个已完成的 Promise 中的第一个感兴趣时,我们使用Promise.any()

  • 逻辑赋值运算符:

    a ||= b
    a &&= b
    a ??= b
    
  • 在以下情况下使用下划线(_)作为分隔符:

    • 数字字面量: 123_456.789_012

    • 大整数字面量: 6_000_000_000_000_000_000_000_000n

  • WeakRefs: 这个特性超出了本书的范围。有关更多信息,请参阅其提案

4.3 ECMAScript 2020 中的新功能

以下功能是在 ECMAScript 2020 中添加的:

  • 新模块功能:

    • 通过import()动态导入:普通的import语句是静态的:我们只能在模块的顶层使用它,其模块说明符是一个固定的字符串。import()改变了这一点。它可以在任何地方使用(包括条件语句),并且我们可以计算其参数。

    • import.meta包含当前模块的元数据。它的第一个广泛支持的属性是import.meta.url,其中包含当前模块文件的 URL 字符串。

    • 命名空间重新导出: 以下表达式将模块'mod'的所有导出导入到命名空间对象ns中,并导出该对象。

      export * as ns from 'mod';
      
  • 可选链式调用用于属性访问和方法调用。一个可选链式调用的例子是:

    value.?prop
    

    如果valueundefinednull,则该表达式的值为undefined。否则,它的值为value.prop。这个特性在属性链中的一些属性可能缺失时特别有用。

  • 空值合并运算符(??):

    value ?? defaultValue
    

    如果valueundefinednull,则该表达式为defaultValue,否则为value。这个运算符让我们在某些东西缺失时使用默认值。

    以前在这种情况下使用逻辑或运算符(||),但它在这里有缺点,因为每当左侧为假值时它就返回默认值(这并不总是正确的)。

  • 大整数 - 任意精度整数: 大整数是一种新的原始类型。它支持可以任意大的整数(存储空间会根据需要增长)。

  • 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机制:这个特性超出了本书的范围。有关更多信息,请参阅其提案

4.4 ECMAScript 2019 中的新功能

以下功能是在 ECMAScript 2019 中添加的:

  • 数组方法.flatMap()类似于.map(),但让回调函数返回零个或多个值的数组,而不是单个值。然后返回的数组被连接在一起,成为.flatMap()的结果。使用案例包括:

    • 同时进行过滤和映射

    • 将单个输入值映射到多个输出值

  • 数组方法.flat()将嵌套的数组转换为扁平数组。可选地,我们可以告诉它在哪个嵌套深度停止扁平化。

  • Object.fromEntries()entries的可迭代对象中创建一个对象。每个条目都是一个包含属性键和属性值的两元素数组。

  • 字符串方法:.trimStart().trimEnd()的工作方式类似于.trim(),但仅删除字符串的开头或结尾处的空格。

  • 可选的catch绑定:如果我们不使用catch子句的参数,现在可以省略它。

  • Symbol.prototype.description是用于读取符号描述的 getter。以前,描述包含在.toString()的结果中,但无法单独访问。

这些新的 ES2019 功能超出了本书的范围:

4.5 ECMAScript 2018 中的新功能

以下功能是在 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 是被兑现还是被拒绝都会执行。

  • 新的正则表达式功能:

    • RegExp命名捕获组:除了通过编号访问组外,我们现在可以对它们进行命名并通过名称访问它们:

      const matchObj = '---756---'.match(/(?<digits>[0-9]+)/)
      assert.equal(matchObj.groups.digits, '756');
      
    • RegExp后瞻断言补充了前瞻断言:

      • 正向后瞻:(?<=X)匹配当前位置是否由'X'前导。

      • 负向后瞻:(?<!X)匹配当前位置是否不是由'(?<!X)'前导。

    • 正则表达式的sdotAll)标志。如果此标志激活,则点匹配行终止符(默认情况下不匹配)。

    • 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`
    

4.6 ECMAScript 2017 中的新功能

以下功能是在 ECMAScript 2017 中添加的:

  • 异步函数(async/await)让我们使用看起来同步的语法来编写异步代码。

  • Object.values()返回一个包含给定对象的所有可枚举字符串键属性的值的数组。

  • Object.entries()返回一个包含给定对象的所有可枚举字符串键属性的键值对的数组。每对都被编码为一个两元素数组。

  • 字符串填充:字符串方法 .padStart().padEnd() 插入填充文本,直到接收者足够长:

    > '7'.padStart(3, '0')
    '007'
    > 'yes'.padEnd(6, '!')
    'yes!!!'
    
  • 函数参数列表和调用中的尾随逗号(ch_callables.html#trailing-commas-parameters):自 ES3 起,数组文字中允许尾随逗号,自 ES5 起,对象文字中也允许。现在也允许在函数调用和方法调用中使用。

  • 以下两个特性超出了本书的范围:

4.7 ECMAScript 2016 新特性

以下特性是在 ECMAScript 2016 中添加的:

  • Array.prototype.includes() 检查数组是否包含给定值。

  • 指数运算符 (**):

    > 4 ** 2
    16
    

4.8 本章来源

ECMAScript 特性列表来自 TC39 关于已完成提案的页面

评论

五、FAQ: JavaScript

原文:exploringjs.com/impatient-js/ch_faq-language.html

译者:飞龙

协议:CC BY-NC-SA 4.0


  • 5.1 JavaScript 有哪些好的参考资料?

  • 5.2 我怎样才能知道 JavaScript 的功能在哪里得到支持?

  • 5.3 我可以在哪里查找 JavaScript 计划中的功能?

  • 5.4 为什么 JavaScript 经常悄悄失败?

  • 5.5 为什么我们不能清理 JavaScript,删除怪癖和过时的功能?

  • 5.6 我怎样才能快速尝试一段 JavaScript 代码?


5.1 JavaScript 有哪些好的参考资料?

请参阅 §6.3 “JavaScript references”。

5.2 我怎样才能知道 JavaScript 的功能在哪里得到支持?

本书通常会提到一个功能是否是 ECMAScript 5 的一部分(老版本浏览器所需),或者是一个更新的版本。有关更详细的信息(包括 ES5 之前的版本),有几个在线的兼容性表格可供参考:

5.3 我可以在哪里查找 JavaScript 计划中的功能?

请参阅以下来源:

  • §3.5 “The TC39 process” 描述了即将推出的功能是如何计划的。

  • §3.6 “FAQ: TC39 process” 回答了关于即将推出的功能的各种问题。

5.4 为什么 JavaScript 经常悄悄失败?

JavaScript 经常悄悄失败。让我们看两个例子。

第一个例子:如果操作数的运算符没有适当的类型,它们会根据需要进行转换。

> '3' * '5'
15

第二个例子:如果算术计算失败,你会得到一个错误值,而不是异常。

> 1 / 0
Infinity

悄悄失败的原因是历史性的:直到 ECMAScript 3 之前,JavaScript 没有异常。从那时起,它的设计者们试图避免悄悄失败。

5.5 为什么我们不能清理 JavaScript,删除怪癖和过时的功能?

这个问题在§3.7 “Evolving JavaScript: Don’t break the web”中得到了回答。

5.6 我怎样才能快速尝试一段 JavaScript 代码?

§8.1 “Trying out JavaScript code” 解释了如何做到这一点。

Comments

第二部分:第一步

原文:exploringjs.com/impatient-js/pt_first-steps.html

译者:飞龙

协议:CC BY-NC-SA 4.0

接下来:6 使用 JavaScript:大局观

六、使用 JavaScript:整体概况

原文:exploringjs.com/impatient-js/ch_big-picture.html

译者:飞龙

协议:CC BY-NC-SA 4.0


  • 6.1 你在这本书中学到了什么?

  • 6.2 浏览器和 Node.js 的结构

  • 6.3 JavaScript 参考

  • 6.4 进一步阅读


在这一章中,我想描绘整体概况:你在这本书中学到了什么,它如何融入 Web 开发的整体格局?

6.1 你在这本书中学到了什么?

这本书教授 JavaScript 语言。它专注于语言本身,但偶尔也会提到 JavaScript 可以使用的两个平台:

  • Web 浏览器

  • Node.js

Node.js 在 Web 开发中有三种重要的用途:

  • 你可以用它来用 JavaScript 编写服务器端软件。

  • 你也可以用它来编写命令行软件(比如 Unix shell、Windows PowerShell 等)。许多与 JavaScript 相关的工具都是基于(并通过)Node.js 执行的。

  • Node 的软件注册表 npm 已成为安装工具(如编译器和构建工具)和库的主要方式-甚至用于客户端开发。

6.2 浏览器和 Node.js 的结构

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2:两个 JavaScript 平台web 浏览器Node.js的结构。API“标准库”和“平台 API”托管在具有 JavaScript 引擎和特定平台“核心”的基础层之上。

两个 JavaScript 平台web 浏览器Node.js的结构相似(图 2):

  • 基础层包括 JavaScript 引擎和特定平台的“核心”功能。

  • 两个 API 托管在这个基础之上:

    • JavaScript 标准库是 JavaScript 的一部分,运行在引擎之上。

    • 平台 API 也可以从 JavaScript 中使用-它提供对特定平台功能的访问。例如:

      • 在浏览器中,如果你想做任何与用户界面相关的事情,你需要使用特定平台 API:响应鼠标点击,播放声音等。

      • 在 Node.js 中,特定平台 API 允许你读写文件,通过 HTTP 下载数据等。

6.3 JavaScript 参考

当你对 JavaScript 有疑问时,通常可以通过网络搜索来解决。我可以推荐以下在线资源:

6.4 进一步阅读

  • 额外章节提供了对 Web 开发的更全面的了解。

评论

七、语法

原文:exploringjs.com/impatient-js/ch_syntax.html

译者:飞龙

协议:CC BY-NC-SA 4.0


  • 7.1 JavaScript 语法概述

    • 7.1.1 基本结构

    • 7.1.2 模块

    • 7.1.3 类

    • 7.1.4 异常处理

    • 7.1.5 合法的变量和属性名称

    • 7.1.6 大小写风格

    • 7.1.7 名称的大写形式

    • 7.1.8 更多命名约定

    • 7.1.9 分号放在哪里?

  • 7.2 (高级)

  • 7.3 标识符

    • 7.3.1 有效的标识符(变量名等)

    • 7.3.2 保留字

  • 7.4 语句 vs 表达式

    • 7.4.1 语句

    • 7.4.2 表达式

    • 7.4.3 允许的位置

  • 7.5 模糊的语法

    • 7.5.1 相同的语法:函数声明和函数表达式

    • 7.5.2 相同的语法:对象字面量和块

    • 7.5.3 消歧

  • 7.6 分号

    • 7.6.1 分号的经验法则

    • 7.6.2 分号:控制语句

  • 7.7 自动分号插入(ASI)

    • 7.7.1 意外触发的 ASI

    • 7.7.2 意外未触发的 ASI

  • 7.8 分号:最佳实践

  • 7.9 严格模式 vs 松散模式

    • 7.9.1 切换到严格模式

    • 7.9.2 严格模式的改进


7.1 JavaScript 语法概述

这是对 JavaScript 语法的第一次简要介绍。如果有些东西还不明白,不要担心。它们将在本书的后面更详细地解释。

这个概述也不是详尽无遗的。它侧重于基本要点。

7.1.1 基本结构
7.1.1.1 注释
// single-line comment

/*
Comment with
multiple lines
*/
7.1.1.2 原始(原子)值

布尔值:

true
false

数字:

1.141
-123

基本数字类型用于浮点数(双精度)和整数。

大整数:

17n
-49n

基本数字类型只能在 53 位加符号的范围内正确表示整数。大整数可以任意增长。

字符串:

'abc'
"abc"
`String with interpolated values: ${256} and ${true}`

JavaScript 没有额外的字符类型。它使用字符串来表示它们。

7.1.1.3 断言

断言描述了计算结果预期的样子,并在这些期望不正确时抛出异常。例如,以下断言说明了计算结果 7 加 1 必须是 8:

assert.equal(7 + 1, 8);

assert.equal()是一个方法调用(对象是assert,方法是.equal()),有两个参数:实际结果和期望结果。它是 Node.js 断言 API 的一部分,将在本书的后面解释。

还有assert.deepEqual()用于深度比较对象。

7.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!');
7.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 还有一个==比较运算符。我建议避免使用它-原因在§13.4.3“建议:始终使用严格相等”中有解释。

7.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;
7.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);
7.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
7.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');
7.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']);
7.1.1.11 控制流语句

条件语句:

if (x < 0) {
 x = -x;
}

for-of循环:

const arr = ['a', 'b'];
for (const element of arr) {
 console.log(element);
}
// Output:
// 'a'
// 'b'
7.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 'path';
// Import a single export of module file-tools.mjs
import {isTextFilePath} from './file-tools.mjs';
7.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)');
7.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-finallytry-catch-finally也受支持。

  • 我们可以抛出任何值,但是堆栈跟踪等功能仅由Error及其子类支持。

7.1.5 合法的变量和属性名

变量名和属性名的语法类别称为标识符

标识符允许具有以下字符:

  • Unicode 字母:AZaz(等等)

  • $_

  • Unicode 数字:09(等等)

    • 变量名不能以数字开头

一些单词在 JavaScript 中具有特殊含义,被称为保留字。例如:iftrueconst

保留字不能用作变量名:

const if = 123;
 // SyntaxError: Unexpected token if

但它们允许作为属性的名称:

> const obj = { if: 123 };
> obj.if
123
7.1.6 大小写风格

连接单词的常见大小写风格有:

  • 驼峰命名法:threeConcatenatedWords

  • 下划线命名法(也称为蛇形命名法):three_concatenated_words

  • 破折号命名法(也称为烤肉串命名法):three-concatenated-words

7.1.7 名称的大写形式

一般来说,JavaScript 使用驼峰命名法,除了常量。

小写:

  • 函数,变量:myFunction

  • 方法:obj.myMethod

  • CSS:

    • CSS 实体:special-class

    • 对应的 JavaScript 变量:specialClass

大写:

  • 类:MyClass

  • 常量:MY_CONSTANT

    • 常量通常也以驼峰命名法编写:myConstant
7.1.8 更多命名约定

以下命名约定在 JavaScript 中很受欢迎。

如果参数名以下划线开头(或者是下划线),则表示该参数未被使用——例如:

arr.map((_x, i) => i)

如果对象的属性名以下划线开头,则该属性被视为私有:

class ValueWrapper {
 constructor(value) {
 this._value = value;
 }
}
7.1.9 分号放在哪里?

在语句结束时:

const x = 123;
func();

但如果该语句以大括号结束,则不是条件语句:

while (false) {
 // ···
} // no semicolon

function func() {
 // ···
} // no semicolon

但在这样的语句后添加分号不是语法错误——它被解释为空语句:

// Function declaration followed by empty statement:
function func() {
 // ···
};

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 测验:基础

参见测验应用程序。

7.2(高级)

本章的所有其余部分都是高级内容。

7.3 标识符

7.3.1 有效标识符(变量名等)

首字符:

  • Unicode 字母(包括重音字符,如éü,以及非拉丁字母的字符,如α

  • $

  • _

后续字符:

  • 合法的首字符

  • Unicode 数字(包括东阿拉伯数字)

  • 一些其他 Unicode 标记和标点符号

例子:

const ε = 0.0001;
const строка = '';
let _tmp = 0;
const $foo2 = true;
7.3.2 保留字

保留字不能作为变量名,但可以作为属性名。

所有 JavaScript 关键字都是保留字:

await break case catch class const continue debugger default delete do else export extends finally for function if import in instanceof let new return static super switch this throw try typeof var void while with yield

以下标记也是关键字,但目前在语言中没有使用:

enum implements package protected interface private public

以下文字是保留字:

true false null

从技术上讲,这些词并不是保留字,但你也应该避免使用它们,因为它们实际上是关键字:

Infinity NaN undefined async

你也不应该为你自己的变量和参数使用全局变量的名称(StringMath等)。

7.4 语句 vs. 表达式

在本节中,我们探讨了 JavaScript 如何区分两种语法结构:语句表达式。之后,我们会看到这可能会引起问题,因为相同的语法在不同的上下文中可能意味着不同的东西。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 我们假装只有语句和表达式

为了简单起见,我们假设在 JavaScript 中只有语句和表达式。

7.4.1 语句

语句是一段可以执行并执行某种操作的代码。例如,if是一个语句:

let myStr;
if (myBool) {
 myStr = 'Yes';
} else {
 myStr = 'No';
}

另一个语句的例子:函数声明。

function twice(x) {
 return x + x;
}
7.4.2 表达式

表达式是一段可以评估产生一个值的代码。例如,括号之间的代码就是一个表达式:

let myStr = (myBool ? 'Yes' : 'No');

括号之间使用的运算符_?_:_称为三元运算符。它是if语句的表达式版本。

让我们看更多表达式的例子。我们输入表达式,REPL 为我们评估它们:

> 'ab' + 'cd'
'abcd'
> Number('123')
123
> true || false
true
7.4.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 
}

7.5 歧义语法

JavaScript 有几种编程结构在语法上是模棱两可的:相同的语法根据是在语句上下文还是表达式上下文中使用而有不同的解释。本节探讨了这种现象和它引起的陷阱。

7.5.1 相同的语法:函数声明和函数表达式

函数声明是一个语句:

function id(x) {
 return x;
}

函数表达式是一个表达式(=的右侧):

const id = function me(x) {
 return x;
};
7.5.2 相同的语法:对象字面量和代码块

在下面的代码中,{}是一个对象字面量:一个创建空对象的表达式。

const obj = {};

这是一个空代码块(一个语句):

{
}
7.5.3 消除歧义

歧义只在语句上下文中是一个问题:如果 JavaScript 解析器遇到歧义的语法,它不知道它是一个普通语句还是一个表达式语句。例如:

  • 如果一个语句以function开头:它是函数声明还是函数表达式?

  • 如果一个语句以{开头:它是对象字面量还是代码块?

为了消除歧义,以function{开头的语句永远不会被解释为表达式。如果你想要一个表达式语句以这些标记之一开头,你必须将其包裹在括号中:

(function (x) { console.log(x) })('abc');

// Output:
// 'abc'

在这段代码中:

  1. 我们首先通过函数表达式创建一个函数:

    function (x) { console.log(x) }
    
  2. 然后我们调用那个函数:('abc')

在(1)中显示的代码片段之所以被解释为表达式,是因为我们将其包裹在括号中。如果我们不这样做,我们将得到一个语法错误,因为此时 JavaScript 期望一个函数声明,并抱怨缺少函数名。此外,你不能在函数声明后立即放置一个函数调用。

在本书的后面,我们将看到由语法歧义引起的更多陷阱的例子:

  • 通过对象解构赋值

  • 从箭头函数返回对象文字

7.6 分号

7.6.1 分号的经验法则

每个语句都以分号结束:

const x = 3;
someFunction('abc');
i++;

除了以块结束的语句:

function foo() {
 // ···
}
if (y > 0) {
 // ···
}

以下情况稍微棘手:

const func = () => {}; // semicolon!

整个const声明(一个语句)以分号结束,但在其中有一个箭头函数表达式。也就是说,结束语句并不是语句本身以花括号结束;而是嵌入的箭头函数表达式。这就是为什么最后有一个分号。

7.6.2 分号:控制语句

控制语句的主体本身就是一个语句。例如,这是while循环的语法:

while (condition)
 statement

主体可以是一个单独的语句:

while (a > 0) a--;

但是块也是语句,因此是控制语句的合法主体:

while (a > 0) {
 a--;
}

如果你想让一个循环有一个空主体,你的第一个选择是一个空语句(只是一个分号):

while (processNextItem() > 0);

你的第二个选择是一个空块:

while (processNextItem() > 0) {}

7.7 自动分号插入(ASI)

虽然我建议总是写分号,但在 JavaScript 中大多数分号是可选的。使这种可能的机制称为自动分号插入(ASI)。在某种程度上,它纠正了语法错误。

ASI 的工作方式如下。语句的解析会一直持续,直到出现以下情况之一:

  • 一个分号

  • 行终止符后跟一个非法标记

换句话说,ASI 可以被看作是在换行符处插入分号。接下来的小节将介绍 ASI 的陷阱。

7.7.1 意外触发的 ASI

关于 ASI 的好消息是 - 如果你不依赖它并总是写分号 - 那么你需要注意的只有一个陷阱。那就是 JavaScript 禁止在某些标记后换行。如果你插入一个换行符,分号也会被插入。

这在实际上最相关的标记是return。例如,考虑以下代码:

return
{
 first: 'jane'
};

这段代码被解析为:

return;
{
 first: 'jane';
}
;

也就是说:

  • 没有操作数的返回语句:return;

  • 代码块的开始:{

  • 表达式语句'jane';与标签first:

  • 代码块的结束:}

  • 空语句:;

为什么 JavaScript 会这样做?它防止在return后的行中意外返回一个值。

7.7.2 意外未触发的 ASI

在某些情况下,当你认为应该触发 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);

7.8 分号:最佳实践

我建议你总是写分号:

  • 我喜欢它给代码带来的视觉结构 - 你清楚地看到语句在哪里结束。

  • 要记住的规则较少。

  • 大多数 JavaScript 程序员使用分号。

然而,也有很多人不喜欢分号增加的视觉混乱。如果你是其中之一:不用它们的代码是合法的。我建议你使用工具来帮助你避免错误。以下是两个例子:

  • 自动代码格式化程序Prettier可以配置为不使用分号。它然后会自动修复问题。例如,如果它遇到以方括号开头的行,它会在该行前面加上一个分号。

  • 静态检查器ESLint一个规则,告诉你首选的风格(总是分号或尽可能少的分号),并警告你关于关键问题。

7.9 严格模式与松散模式

从 ECMAScript 5 开始,JavaScript 有两种可以执行的模式

  • 正常的“松散”模式是脚本的默认模式(作为模块的前导片段,并受浏览器支持)。

  • 严格模式是模块和类的默认模式,并且可以在脚本中打开(稍后会解释)。在此模式下,消除了普通模式的几个陷阱,并且会抛出更多的异常。

在现代 JavaScript 代码中,您很少会遇到懈怠模式,它几乎总是位于模块中。在本书中,我假设严格模式总是打开的。

7.9.1 打开严格模式

在脚本文件和 CommonJS 模块中,您可以通过在第一行放置以下代码来为整个文件切换到严格模式:

'use strict';

这个“指令”的好处是,ECMAScript 5 之前的版本简单地忽略它:它是一个什么都不做的表达式语句。

您还可以仅为单个函数切换到严格模式:

function functionInStrictMode() {
 'use strict';
}
7.9.2 严格模式的改进

让我们看看严格模式比懈怠模式做得更好的三件事。在这一部分中,所有代码片段都在懈怠模式下执行。

7.9.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

7.9.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);
7.9.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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 测验:高级

查看测验应用程序。

评论

八、控制台:交互式 JavaScript 命令行

原文:exploringjs.com/impatient-js/ch_console.html

译者:飞龙

协议:CC BY-NC-SA 4.0


  • 8.1 尝试 JavaScript 代码

    • 8.1.1 浏览器控制台

    • 8.1.2 Node.js REPL

    • 8.1.3 其他选项

  • 8.2 console.* API:打印数据和更多

    • 8.2.1 打印值:console.log()(stdout)

    • 8.2.2 打印错误信息:console.error()(stderr)

    • 8.2.3 通过JSON.stringify()打印嵌套对象


8.1 尝试 JavaScript 代码

您有许多选项可以快速运行 JavaScript 代码片段。以下各小节描述了其中的一些。

8.1.1 浏览器控制台

Web 浏览器有所谓的控制台:交互式命令行,您可以通过console.log()打印文本,并在其中运行代码片段。如何打开控制台因浏览器而异。图 3 显示了 Google Chrome 的控制台。

要找出如何在您的 Web 浏览器中打开控制台,您可以通过 Web 搜索“控制台«您的浏览器名称»”来了解。以下是一些常用 Web 浏览器的页面:

图 3:打开 Web 浏览器“Google Chrome”的控制台(在窗口的下半部分),同时访问网页。

图 3:打开 Web 浏览器“Google Chrome”的控制台(在窗口的下半部分),同时访问网页。

8.1.2 Node.js REPL

REPL代表read-eval-print loop,基本上意味着命令行。要使用它,您必须首先从操作系统命令行启动 Node.js,通过命令node。然后,与之交互看起来如图所示 4:>后面的文本是用户输入;其他所有内容都是 Node.js 的输出。

图 4:启动和使用 Node.js REPL(交互式命令行)。

图 4:启动和使用 Node.js REPL(交互式命令行)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 阅读:REPL 交互

我偶尔会通过 REPL 交互演示 JavaScript。然后我也使用大于号符号(>)来标记输入,例如:

> 3 + 5
8
8.1.3 其他选项

其他选项包括:

  • 有许多 Web 应用程序可以让您在 Web 浏览器中尝试 JavaScript,例如Babel 的 REPL

  • 还有原生应用程序和 IDE 插件可用于运行 JavaScript。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 控制台通常在非严格模式下运行

在现代 JavaScript 中,大多数代码(例如模块)都是在严格模式下执行的。然而,控制台通常在非严格模式下运行。因此,当使用控制台执行本书中的代码时,您偶尔可能会得到略有不同的结果。

8.2 console.* API:打印数据和更多

在浏览器中,控制台是一个通常隐藏的东西,您可以打开它。对于 Node.js,控制台是 Node.js 当前运行的终端。

完整的console.* API 在MDN web 文档Node.js 网站上有文档。它不是 JavaScript 语言标准的一部分,但大部分功能都得到了浏览器和 Node.js 的支持。

在本章中,我们只关注以下两种打印数据的方法(“打印”意味着在控制台中显示):

  • console.log()

  • console.error()

8.2.1 打印值:console.log()(stdout)

这个操作有两种变体:

console.log(...values: any[]): void
console.log(pattern: string, ...values: any[]): void
8.2.1.1 打印多个值

第一个变体在控制台上打印(文本表示的)值:

console.log('abc', 123, true);
// Output:
// abc 123 true

最后,console.log()总是打印一个换行符。因此,如果你用零个参数调用它,它就只打印一个换行符。

8.2.1.2 用替换打印字符串

第二个变体执行字符串替换:

console.log('Test: %s %j', 123, 'abc');
// Output:
// Test: 123 "abc"

这些是你可以用于替换的一些指令:

  • %s将相应的值转换为字符串并插入它。

    console.log('%s %s', 'abc', 123);
    // Output:
    // abc 123
    
  • %o插入一个对象的字符串表示。

    console.log('%o', {foo: 123, bar: 'abc'});
    // Output:
    // { foo: 123, bar: 'abc' }
    
  • %j将一个值转换为 JSON 字符串并插入它。

    console.log('%j', {foo: 123, bar: 'abc'});
    // Output:
    // {"foo":123,"bar":"abc"}
    
  • %%插入一个单独的%

    console.log('%s%%', 99);
    // Output:
    // 99%
    
8.2.2 打印错误信息:console.error()(stderr)

console.error()的工作方式与console.log()相同,但它记录的是错误信息。对于 Node.js,这意味着输出会被发送到 stderr 而不是 Unix 上的 stdout。

8.2.3 通过JSON.stringify()打印嵌套对象

JSON.stringify() 有时用于打印嵌套对象:

console.log(JSON.stringify({first: 'Jane', last: 'Doe'}, null, 2));

输出:

{
  "first": "Jane",
  "last": "Doe"
}

评论

九、断言 API

原文:exploringjs.com/impatient-js/ch_assertion-api.html

译者:飞龙

协议:CC BY-NC-SA 4.0


  • 9.1 软件开发中的断言

  • 9.2 本书中如何使用断言

    • 9.2.1 通过断言在代码示例中记录结果

    • 9.2.2 通过断言实现测试驱动的练习

  • 9.3 普通比较 vs. 深度比较

  • 9.4 快速参考:模块assert

    • 9.4.1 普通相等

    • 9.4.2 深度相等

    • 9.4.3 期望异常

    • 9.4.4 另一个工具函数


9.1 软件开发中的断言

在软件开发中,断言陈述关于值或代码片段的事实,这些事实必须为真。如果不是,将抛出异常。Node.js 通过其内置模块assert支持断言-例如:

import * as assert from 'assert/strict';
assert.equal(3 + 5, 8);

此断言陈述了 3 加 5 的预期结果为 8。导入语句使用了assert推荐的strict版本

9.2 本书中如何使用断言

在本书中,断言有两种用法:用于记录代码示例中的结果,以及实现测试驱动的练习。

9.2.1 通过断言在代码示例中记录结果

在代码示例中,断言表达了预期的结果。例如,以下函数:

function id(x) {
 return x;
}

id()返回其参数。我们可以通过断言展示它的作用:

assert.equal(id('abc'), 'abc');

在示例中,我通常省略了导入assert的语句。

使用断言的动机是:

  • 您可以明确指定预期的内容。

  • 代码示例可以自动测试,这确保它们确实有效。

9.2.2 通过断言实现测试驱动的练习

本书的练习是通过测试框架 Mocha 进行测试驱动的。测试中的检查是通过assert的方法进行的。

以下是这样一个测试的示例:

// For the exercise, you must implement the function hello().
// The test checks if you have done it properly.
test('First exercise', () => {
 assert.equal(hello('world'), 'Hello world!');
 assert.equal(hello('Jane'), 'Hello Jane!');
 assert.equal(hello('John'), 'Hello John!');
 assert.equal(hello(''), 'Hello !');
});

有关更多信息,请参阅§10“开始使用测验和练习”。

9.3 普通比较 vs. 深度比较

严格的equal()使用===来比较值。因此,对象只等于自身-即使另一个对象具有相同的内容(因为===不比较对象的内容,只比较它们的标识):

assert.notEqual({foo: 1}, {foo: 1});

deepEqual()是比较对象的更好选择:

assert.deepEqual({foo: 1}, {foo: 1});

这种方法也适用于数组:

assert.notEqual(['a', 'b', 'c'], ['a', 'b', 'c']);
assert.deepEqual(['a', 'b', 'c'], ['a', 'b', 'c']);

9.4 模块assert的快速参考

有关完整文档,请参阅Node.js 文档

9.4.1 普通相等
  • function equal(actual: any, expected: any, message?: string): void

    actual === expected必须为true。如果不是,则抛出AssertionError

    assert.equal(3+3, 6);
    
  • function notEqual(actual: any, expected: any, message?: string): void

    actual !== expected必须为true。如果不是,则抛出AssertionError

    assert.notEqual(3+3, 22);
    

可选的最后一个参数message可用于解释所断言的内容。如果断言失败,将使用消息设置抛出的AssertionError

let e;
try {
 const x = 3;
 assert.equal(x, 8, 'x must be equal to 8')
} catch (err) {
 assert.equal(
 String(err),
 'AssertionError [ERR_ASSERTION]: x must be equal to 8');
}
9.4.2 深度相等
  • function deepEqual(actual: any, expected: any, message?: string): void

    actual必须是深度等于expected。如果不是,则抛出AssertionError

    assert.deepEqual([1,2,3], [1,2,3]);
    assert.deepEqual([], []);
    
    // To .equal(), an object is only equal to itself:
    assert.notEqual([], []);
    
  • function notDeepEqual(actual: any, expected: any, message?: string): void

    actual必须不是深度等于expected。如果是,则抛出AssertionError

    assert.notDeepEqual([1,2,3], [1,2]);
    
9.4.3 期望异常

如果你想要(或期望)收到一个异常,你需要使用throws():这个函数调用它的第一个参数,函数block,只有在它抛出异常时才成功。可以使用额外的参数来指定异常的样子。

  • function throws(block: Function, message?: string): void

    assert.throws(
     () => {
     null.prop;
     }
    );
    
  • function throws(block: Function, error: Function, message?: string): void

    assert.throws(
     () => {
     null.prop;
     },
     TypeError
    );
    
  • function throws(block: Function, error: RegExp, message?: string): void

    assert.throws(
     () => {
     null.prop;
     },
     /^TypeError: Cannot read properties of null \(reading 'prop'\)$/
    );
    
  • function throws(block: Function, error: Object, message?: string): void

    assert.throws(
     () => {
     null.prop;
     },
     {
     name: 'TypeError',
     message: "Cannot read properties of null (reading 'prop')",
     }
    );
    
9.4.4 另一个工具函数
  • function fail(message: string | Error): never

    每次调用时都会抛出一个AssertionError。这在单元测试中偶尔是有用的。

    try {
     functionThatShouldThrow();
     assert.fail();
    } catch (_) {
     // Success
    }
    

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 测验

请参阅测验应用。

评论

十、开始使用测验和练习

原文:exploringjs.com/impatient-js/ch_quizzes-exercises.html

译者:飞龙

协议:CC BY-NC-SA 4.0


  • 10.1 测验

  • 10.2 练习

    • 10.2.1 安装练习

    • 10.2.2 运行练习

  • 10.3 JavaScript 中的单元测试

    • 10.3.1 典型测试

    • 10.3.2 Mocha 中的异步测试


在大多数章节中,都有测验和练习。这是一个付费功能,但提供了全面的预览。本章解释了如何开始使用它们。

10.1 测验

安装:

  • 下载并解压impatient-js-quiz.zip

运行测验应用程序:

  • 在 Web 浏览器中打开impatient-js-quiz/index.html

  • 你将看到所有测验的目录。

10.2 练习

10.2.1 安装练习

要安装练习:

  • 下载并解压impatient-js-code.zip

  • 按照README.txt中的说明操作

10.2.2 运行练习
  • 本书中通过路径引用练习。

    • 例如:exercises/quizzes-exercises/first_module_test.mjs
  • 在每个文件中:

    • 第一行包含运行练习的命令。

    • 以下行描述了你需要做的事情。

10.3 JavaScript 中的单元测试

本书中的所有练习都是通过测试框架Mocha运行的测试。本节简要介绍了这一点。

10.3.1 典型测试

典型的测试代码分为两部分:

  • 第 1 部分:要测试的代码。

  • 第 2 部分:代码的测试。

例如,以下是两个文件:

  • id.mjs(要测试的代码)

  • id_test.mjs(测试)

10.3.1.1 第 1 部分:代码

代码本身位于id.mjs中:

export function id(x) {
 return x;
}

关键是:我们想要测试的所有内容都必须被导出。否则,测试代码无法访问它。

10.3.1.2 第 2 部分:测试

不要担心测试的确切细节

你不需要担心测试的确切细节:它们总是为你实现的。因此,你只需要阅读它们,而不需要编写它们。

代码的测试位于id_test.mjs中:

// npm t demos/quizzes-exercises/id_test.mjs
suite('id_test.mjs');

import * as assert from 'assert/strict'; // (A)
import {id} from './id.mjs'; // (B)

test('My test', () => { // (C)
 assert.equal(id('abc'), 'abc'); // (D)
});

这个测试文件的核心是 D 行 - 一个断言:assert.equal()指定id('abc')的预期结果是'abc'

至于其他行:

  • 开头的注释显示了运行测试的 shell 命令。

  • A 行:我们导入 Node.js 断言库(在严格断言模式下)。

  • B 行:我们导入要测试的函数。

  • C 行:我们定义一个测试。这是通过调用函数test()来完成的:

    • 第一个参数:测试的名称。

    • 第二个参数:通过箭头函数提供的测试代码。参数t使我们能够访问 AVA 的测试 API(断言等)。

要运行测试,我们在命令行中执行以下操作:

npm t demos/quizzes-exercises/id_test.mjs

ttest的缩写。也就是说,这个命令的完整版本是:

npm test demos/quizzes-exercises/id_test.mjs

练习:你的第一个练习

以下练习让你初尝练习的滋味:

  • exercises/quizzes-exercises/first_module_test.mjs
10.3.2 Mocha 中的异步测试

阅读

在学习异步编程的章节之前,你可能想推迟阅读本节。

编写异步代码的测试需要额外的工作:测试稍后接收其结果,并在返回时向 Mocha 发出信号表明它尚未完成。以下小节探讨了三种方法。

10.3.2.1 通过回调实现异步

如果我们传递给 test() 的回调有一个参数(例如 done),Mocha 将切换到基于回调的异步性。当我们完成异步工作时,我们必须调用 done

test('divideCallback', (done) => {
 divideCallback(8, 4, (error, result) => {
 if (error) {
 done(error);
 } else {
 assert.strictEqual(result, 2);
 done();
 }
 });
});

这是 divideCallback() 的样子:

function divideCallback(x, y, callback) {
 if (y === 0) {
 callback(new Error('Division by zero'));
 } else {
 callback(null, x / y);
 }
}
10.3.2.2 通过 Promises 实现异步性

如果一个测试返回一个 Promise,Mocha 将切换到基于 Promise 的异步性。如果 Promise 被实现,测试被认为是成功的,如果 Promise 被拒绝,或者结算时间超过超时时间,测试被认为是失败的。

test('dividePromise 1', () => {
 return dividePromise(8, 4)
 .then(result => {
 assert.strictEqual(result, 2);
 });
});

dividePromise() 的实现如下:

function dividePromise(x, y) {
 return new Promise((resolve, reject) => {
 if (y === 0) {
 reject(new Error('Division by zero'));
 } else {
 resolve(x / y);
 }
 });
}
10.3.2.3 异步函数作为测试“主体”

异步函数总是返回 Promises。因此,异步函数是实现异步测试的一种便捷方式。以下代码等同于之前的示例。

test('dividePromise 2', async () => {
 const result = await dividePromise(8, 4);
 assert.strictEqual(result, 2);
 // No explicit return necessary!
});

我们不需要显式地返回任何东西:隐式返回的 undefined 被用来实现由这个异步函数返回的 Promise。如果测试代码抛出异常,那么异步函数会负责拒绝返回的 Promise。

注释

第三部分:变量和值

原文:exploringjs.com/impatient-js/pt_variables-values.html

译者:飞龙

协议:CC BY-NC-SA 4.0

接下来:11 变量和赋值

十一、变量和赋值

原文:exploringjs.com/impatient-js/ch_variables-assignment.html

译者:飞龙

协议:CC BY-NC-SA 4.0


  • 11.1 let

  • 11.2 const

    • 11.2.1 const和不可变性

    • 11.2.2 const和循环

  • 11.3 在constlet之间做出决定

  • 11.4 变量的作用域

    • 11.4.1 变量屏蔽
  • 11.5 (高级)

  • 11.6 术语:静态 vs. 动态

    • 11.6.1 静态现象:变量的作用域

    • 11.6.2 动态现象:函数调用

  • 11.7 全局变量和全局对象

  • 11.8 声明:作用域和激活

    • 11.8.1 constlet:时间死区

    • 11.8.2 函数声明和早期激活

    • 11.8.3 类声明不会早期激活

    • 11.8.4 var:变量提升(部分早期激活)

  • 11.9 闭包

    • 11.9.1 绑定变量 vs. 自由变量

    • 11.9.2 什么是闭包?

    • 11.9.3 示例:增量器的工厂

    • 11.9.4 闭包的用例


这些是 JavaScript 声明变量的主要方式:

  • let声明可变变量。

  • const声明常量(不可变变量)。

在 ES6 之前,还有var。但它有一些怪癖,所以最好在现代 JavaScript 中避免使用它。您可以在Speaking JavaScript中了解更多信息。

11.1 let

通过let声明的变量是可变的。

let i;
i = 0;
i = i + 1;
assert.equal(i, 1);

您也可以同时声明和赋值:

let i = 0;

11.2 const

通过const声明的变量是不可变的。您必须立即初始化:

const i = 0; // must initialize

assert.throws(
 () => { i = i + 1 },
 {
 name: 'TypeError',
 message: 'Assignment to constant variable.',
 }
);
11.2.1 const和不可变性

在 JavaScript 中,const只表示绑定(变量名和变量值之间的关联)是不可变的。值本身可能是可变的,就像下面的示例中的obj一样。

const obj = { prop: 0 };

// Allowed: changing properties of `obj`
obj.prop = obj.prop + 1;
assert.equal(obj.prop, 1);

// Not allowed: assigning to `obj`
assert.throws(
 () => { obj = {} },
 {
 name: 'TypeError',
 message: 'Assignment to constant variable.',
 }
);
11.2.2 const和循环

您可以在for-of循环中使用const,其中为每次迭代创建一个新的绑定:

const arr = ['hello', 'world'];
for (const elem of arr) {
 console.log(elem);
}
// Output:
// 'hello'
// 'world'

在普通的for循环中,您必须使用let

const arr = ['hello', 'world'];
for (let i=0; i<arr.length; i++) {
 const elem = arr[i];
 console.log(elem);
}

11.3 在constlet之间做出决定

我建议以下规则来决定使用const还是let

  • const表示不可变的绑定,变量永远不会改变其值。最好使用它。

  • let表示变量的值会改变。只有在无法使用const时才使用它。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 练习:const

exercises/variables-assignment/const_exrc.mjs

11.4 变量的作用域

变量的作用域是程序中可以访问它的区域。考虑以下代码。

{ // // Scope A. Accessible: x
 const x = 0;
 assert.equal(x, 0);
 { // Scope B. Accessible: x, y
 const y = 1;
 assert.equal(x, 0);
 assert.equal(y, 1);
 { // Scope C. Accessible: x, y, z
 const z = 2;
 assert.equal(x, 0);
 assert.equal(y, 1);
 assert.equal(z, 2);
 }
 }
}
// Outside. Not accessible: x, y, z
assert.throws(
 () => console.log(x),
 {
 name: 'ReferenceError',
 message: 'x is not defined',
 }
);
  • 作用域 A 是x的*(直接)作用域*。

  • 作用域 B 和 C 是作用域 A 的内部作用域

  • 作用域 A 是作用域 B 和作用域 C 的外部作用域

每个变量在其直接作用域以及所有嵌套在该作用域内的作用域中都是可访问的。

通过constlet声明的变量被称为块作用域,因为它们的作用域始终是最内部的周围块。

11.4.1 遮蔽变量

你不能在同一级别两次声明相同的变量:

assert.throws(
 () => {
 eval('let x = 1; let x = 2;');
 },
 {
 name: 'SyntaxError',
 message: "Identifier 'x' has already been declared",
 });

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 为什么使用eval()?

eval() 延迟解析(因此延迟了SyntaxError),直到执行assert.throws()的回调。如果我们不使用它,当这段代码被解析时,我们已经会得到一个错误,assert.throws()甚至不会被执行。

但是,你可以嵌套一个块并使用与块外部相同的变量名x

const x = 1;
assert.equal(x, 1);
{
 const x = 2;
 assert.equal(x, 2);
}
assert.equal(x, 1);

在块内部,内部的 x 是唯一可访问的具有该名称的变量。内部的 x 被称为遮蔽外部的 x。一旦离开块,你可以再次访问旧值。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 测验:基础

参见测验应用。

11.5(高级)

其余部分都是高级的。

11.6 术语:静态 vs. 动态

这两个形容词描述了编程语言中的现象:

  • 静态意味着某物与源代码相关,并且可以在不执行代码的情况下确定。

  • 动态意味着在运行时。

让我们来看看这两个术语的例子。

11.6.1 静态现象:变量的作用域

变量作用域是一个静态现象。考虑以下代码:

function f() {
 const x = 3;
 // ···
}

x静态(或词法作用域。也就是说,它的作用域是固定的,在运行时不会改变。

变量作用域形成静态树(通过静态嵌套)。

11.6.2 动态现象:函数调用

函数调用是一个动态现象。考虑以下代码:

function g(x) {}
function h(y) {
 if (Math.random()) g(y); // (A)
}

在 A 行的函数调用是否发生,只能在运行时决定。

函数调用形成一个动态树(通过动态调用)。

11.7 全局变量和全局对象

JavaScript 的变量作用域是嵌套的。它们形成一个树:

  • 最外层的作用域是树的根。

  • 直接包含在该作用域中的作用域是根的子级。

  • 等等。

根也被称为全局作用域。在 Web 浏览器中,直接处于该作用域的唯一位置是在脚本的顶层。全局作用域的变量称为全局变量,并且可以在任何地方访问。有两种全局变量:

  • 全局声明变量是普通变量。

    • 它们只能在脚本的顶层通过constlet和类声明创建。
  • 全局对象变量存储在所谓的全局对象的属性中。

    • 它们是在脚本的顶层通过var和函数声明创建的。

    • 全局对象可以通过全局变量globalThis访问。它可以用于创建、读取和删除全局对象变量。

    • 除此之外,全局对象变量的工作方式与普通变量相同。

以下 HTML 片段演示了globalThis和两种全局变量。

<script>
 const declarativeVariable = 'd';
 var objectVariable = 'o';
</script>
<script>
 // All scripts share the same top-level scope:
 console.log(declarativeVariable); // 'd'
 console.log(objectVariable); // 'o'

 // Not all declarations create properties of the global object:
 console.log(globalThis.declarativeVariable); // undefined
 console.log(globalThis.objectVariable); // 'o'
</script>

每个 ECMAScript 模块都有自己的作用域。因此,在模块的顶层存在的变量并不是全局的。图 5 说明了各种作用域之间的关系。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5:全局作用域是 JavaScript 的最外层作用域。它有两种变量:对象变量(通过全局对象管理)和普通的声明变量。每个 ECMAScript 模块都有自己的作用域,它包含在全局作用域中。

11.7.1 globalThis [ES2020]

全局变量globalThis是访问全局对象的新标准方式。它的名称来自于它在全局作用域中与this具有相同的值。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 globalThis并不总是直接指向全局对象

例如,在浏览器中,存在一个间接访问。这种间接通常是不明显的,但它存在并且可以被观察到。

11.7.1.1 globalThis的替代方案

访问全局对象的旧方法取决于平台:

  • 全局变量window:是指全局对象的经典方式。但在 Node.js 和 Web Workers 中不起作用。

  • 全局变量self:在 Web Workers 和通常的浏览器中可用。但 Node.js 不支持。

  • 全局变量global:仅在 Node.js 中可用。

11.7.1.2 globalThis的用例

全局对象现在被认为是 JavaScript 无法摆脱的错误,因为向后兼容性。它对性能产生负面影响,通常令人困惑。

ECMAScript 6 引入了一些功能,使得更容易避免全局对象-例如:

  • constlet和类声明在全局作用域中使用时不会创建全局对象属性。

  • 每个 ECMAScript 模块都有自己的局部作用域。

最好通过变量访问全局对象变量,而不是通过globalThis的属性。前者在所有 JavaScript 平台上始终以相同的方式工作。

网络教程偶尔通过window.globVar访问全局变量globVar。但前缀“window.”是不必要的,我建议省略它:

window.encodeURIComponent(str); // no
encodeURIComponent(str); // yes

因此,globalThis的用例相对较少-例如:

  • Polyfills为旧的 JavaScript 引擎添加新功能。

  • 特性检测,以找出 JavaScript 引擎支持的功能。

11.8 声明:作用域和激活

这些是声明的两个关键方面:

  • 作用域:声明的实体可以在哪里看到?这是一个静态特征。

  • 激活:何时可以访问实体?这是一个动态特征。一些实体可以在我们进入它们的作用域时立即访问。对于其他实体,我们必须等到执行到达它们的声明时才能访问。

Tbl. 1 总结了各种声明如何处理这些方面。

表 1:声明的方面。“重复”描述了声明是否可以在相同的名称下两次使用(每个作用域)。“全局属性”描述了当在脚本的全局作用域中执行时,声明是否向全局对象添加属性。TDZ表示暂时死区(稍后会解释)。(*) 函数声明通常是块作用域的,但在松散模式中是函数作用域的。

作用域激活重复全局属性
const声明(TDZ)
let声明(TDZ)
function块 (*)开始
class声明(TDZ)
import模块与导出相同
var函数开始,部分

import在§27.5“ECMAScript 模块”中有描述。以下各节更详细地描述了其他构造。

11.8.1 constlet:暂时死区

对于 JavaScript,TC39 需要决定在直接作用域中访问常量时会发生什么:

{
 console.log(x); // What happens here?
 const x;
}

一些可能的方法是:

  1. 名称在当前作用域周围的作用域中解析。

  2. 你会得到undefined

  3. 有一个错误。

第一种方法被拒绝了,因为语言中没有这种方法的先例。因此,对 JavaScript 程序员来说不直观。

第二种方法被拒绝了,因为x在其声明之前和之后将不是一个常量-它将具有不同的值。

let 使用与 const 相同的方法 3,因此两者的工作方式类似,很容易在它们之间切换。

进入变量作用域并执行其声明之间的时间称为时间死区(TDZ):

  • 在此期间,该变量被视为未初始化(就好像那是它的特殊值)。

  • 如果访问未初始化的变量,将会得到 ReferenceError

  • 一旦到达变量声明,变量就会被设置为初始化程序的值(通过赋值符号指定),或者 undefined - 如果没有初始化程序。

以下代码说明了时间死区:

if (true) { // entering scope of `tmp`, TDZ starts
 // `tmp` is uninitialized:
 assert.throws(() => (tmp = 'abc'), ReferenceError);
 assert.throws(() => console.log(tmp), ReferenceError);

 let tmp; // TDZ ends
 assert.equal(tmp, undefined);
}

下一个示例显示了时间死区确实是时间相关的:

if (true) { // entering scope of `myVar`, TDZ starts
 const func = () => {
 console.log(myVar); // executed later
 };

 // We are within the TDZ:
 // Accessing `myVar` causes `ReferenceError`

 let myVar = 3; // TDZ ends
 func(); // OK, called outside TDZ
}

即使 func() 位于 myVar 的声明之前并使用该变量,我们也可以调用 func()。但是我们必须等到 myVar 的时间死区结束。

11.8.2 函数声明和提前激活

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 有关函数的更多信息

在本节中,我们正在使用函数 - 在我们有机会正确学习它们之前。希望一切仍然有意义。每当不明白时,请参阅§25“可调用值”。

函数声明总是在进入其作用域时执行,无论它在作用域内的位置如何。这使您可以在声明之前调用函数 foo()

assert.equal(foo(), 123); // OK
function foo() { return 123; }

foo() 的提前激活意味着前面的代码等同于:

function foo() { return 123; }
assert.equal(foo(), 123);

如果您通过 constlet 声明函数,则不会提前激活。在以下示例中,只能在声明后使用 bar()

assert.throws(
 () => bar(), // before declaration
 ReferenceError);

const bar = () => { return 123; };

assert.equal(bar(), 123); // after declaration 
11.8.2.1 提前调用而不提前激活

即使函数 g() 没有提前激活,也可以通过前面的函数 f()(在相同的作用域中)调用它,如果我们遵守以下规则:f() 必须在 g() 的声明之后调用。

const f = () => g();
const g = () => 123;

// We call f() after g() was declared:
assert.equal(f(), 123);

模块的函数通常在其完整体执行后调用。因此,在模块中,您很少需要担心函数的顺序。

最后,请注意,提前激活会自动遵守上述规则:进入作用域时,所有函数声明都会首先执行,然后再进行任何调用。

11.8.2.2 提前激活的一个陷阱

如果您依赖提前激活来调用函数而不是声明它,那么您需要小心,不要访问未提前激活的数据。

funcDecl();

const MY_STR = 'abc';
function funcDecl() {
 assert.throws(
 () => MY_STR,
 ReferenceError);
}

如果您在声明 MY_STR 之后调用 funcDecl(),问题就会消失。

11.8.2.3 提前激活的利弊

我们已经看到提前激活有一个陷阱,并且您可以在不使用它的情况下获得大部分好处。因此,最好避免提前激活。但我对此并不感到强烈,并且如前所述,通常使用函数声明,因为我喜欢它们的语法。

11.8.3 类声明不会提前激活

尽管它们在某些方面类似于函数声明,类声明 不会提前激活:

assert.throws(
 () => new MyClass(),
 ReferenceError);

class MyClass {}

assert.equal(new MyClass() instanceof MyClass, true);

为什么?考虑以下类声明:

class MyClass extends Object {}

extends 的操作数是一个表达式。因此,您可以做这样的事情:

const identity = x => x;
class MyClass extends identity(Object) {}

对这样的表达式进行评估必须在提到它的位置进行。其他任何操作都会令人困惑。这就解释了为什么类声明不会提前激活。

11.8.4 var:提升(部分提前激活)

var 是一种旧的声明变量的方式,早于 constlet(现在更受青睐)。考虑以下 var 声明。

var x = 123;

此声明有两个部分:

  • 声明 var xvar 声明的变量的作用域是最内层的周围函数,而不是最内层的周围块,就像大多数其他声明一样。这样的变量在其作用域开始时已经激活并初始化为 undefined

  • 赋值 x = 123:赋值总是在原地执行。

以下代码演示了var的效果:

function f() {
 // Partial early activation:
 assert.equal(x, undefined);
 if (true) {
 var x = 123;
 // The assignment is executed in place:
 assert.equal(x, 123);
 }
 // Scope is function, not block:
 assert.equal(x, 123);
}

11.9 闭包

在我们探讨闭包之前,我们需要了解绑定变量和自由变量。

11.9.1 绑定变量 vs. 自由变量

每个作用域中都有一组被提及的变量。在这些变量中,我们区分:

  • 绑定变量是在作用域内声明的。它们是参数和局部变量。

  • 自由变量是在外部声明的。它们也被称为非局部变量

考虑以下代码:

function func(x) {
 const y = 123;
 console.log(z);
}

func()的主体中,xy是绑定变量。z是自由变量。

11.9.2 闭包是什么?

那么闭包是什么呢?

闭包是一个函数加上与其“诞生地”存在的变量的连接。

保持这种连接的意义是什么?它为函数的自由变量提供了值-例如:

function funcFactory(value) {
 return () => {
 return value;
 };
}

const func = funcFactory('abc');
assert.equal(func(), 'abc'); // (A)

funcFactory返回一个分配给func的闭包。因为func与其诞生地的变量有连接,所以当它在 A 行被调用时,它仍然可以访问自由变量value(尽管它“逃离”了它的作用域)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 JavaScript 中的所有函数都是闭包

JavaScript 中支持静态作用域。因此,每个函数都是一个闭包。

11.9.3 示例:增量器工厂

以下函数返回增量器(我刚刚编造的一个名字)。增量器是一个内部存储数字的函数。当它被调用时,它通过将参数添加到数字中来更新该数字,并返回新值。

function createInc(startValue) {
 return (step) => { // (A)
 startValue += step;
 return startValue;
 };
}
const inc = createInc(5);
assert.equal(inc(2), 7);

我们可以看到,在 A 行创建的函数保留了其内部数字在自由变量startValue中。这一次,我们不仅仅是从诞生作用域中读取,我们使用它来存储我们改变的数据,并且这些数据在函数调用之间保持不变。

我们可以通过局部变量在诞生作用域中创建更多的存储槽:

function createInc(startValue) {
 let index = -1;
 return (step) => {
 startValue += step;
 index++;
 return [index, startValue];
 };
}
const inc = createInc(5);
assert.deepEqual(inc(2), [0, 7]);
assert.deepEqual(inc(2), [1, 9]);
assert.deepEqual(inc(2), [2, 11]);
11.9.4 闭包的用例

闭包有什么好处?

  • 首先,它们只是静态作用域的一种实现。因此,它们为回调提供上下文数据。

  • 它们也可以被函数用来存储在函数调用之间持续存在的状态。createInc()就是一个例子。

  • 它们可以为对象提供私有数据(通过字面量或类生成)。关于这是如何工作的细节在探索 ES6中有解释。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 测验:高级

查看测验应用。

评论

十二、值

原文:exploringjs.com/impatient-js/ch_values.html

译者:飞龙

协议:CC BY-NC-SA 4.0


  • 12.1 什么是类型?

  • 12.2 JavaScript 的类型层次结构

  • 12.3 语言规范的类型

  • 12.4 原始值 vs. 对象

    • 12.4.1 原始值(简称:原始值)

    • 12.4.2 对象

  • 12.5 typeofinstanceof运算符:值的类型是什么?

    • 12.5.1 typeof

    • 12.5.2 instanceof

  • 12.6 类和构造函数

    • 12.6.1 与原始类型相关的构造函数
  • 12.7 类型转换

    • 12.7.1 类型之间的显式转换

    • 12.7.2 强制转换(类型之间的自动转换)


在本章中,我们将研究 JavaScript 具有哪些类型的值。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 支持工具:===

在本章中,我们偶尔会使用严格相等运算符。a === b的结果为true,如果ab相等。这究竟意味着什么,在§13.4.2 “严格相等(===!==)”中有解释。

12.1 什么是类型?

在本章中,我认为类型是值的集合,例如,boolean类型是集合{ falsetrue }。

12.2 JavaScript 的类型层次结构

图 6:JavaScript 类型的部分层次结构。缺少错误类的类,与原始类型相关的类等。该图表暗示了并非所有对象都是的实例。)

图 6:JavaScript 类型的部分层次结构。缺少错误类的类,与原始类型相关的类等。该图表暗示了并非所有对象都是Object的实例。

图 6 显示了 JavaScript 的类型层次结构。我们从该图中学到了什么?

  • JavaScript 区分两种值:原始值和对象。我们很快就会看到它们之间的区别。

  • 该图表区分了对象和Object类的实例。Object的每个实例也是一个对象,但反之则不然。然而,在实践中,你遇到的几乎所有对象都是Object的实例,例如,通过对象字面量创建的对象。有关此主题的更多详细信息,请参阅§29.7.3 “并非所有对象都是Object的实例”。

12.3 语言规范的类型

ECMAScript 规范只知道总共八种类型。这些类型的名称是(我使用的是 TypeScript 的名称,而不是规范的名称):

  • undefined,唯一元素为undefined

  • null,唯一元素为null

  • boolean,元素为falsetrue

  • number所有数字的类型(例如,-1233.141

  • bigint所有大整数的类型(例如,-123n

  • string所有字符串的类型(例如,'abc'

  • symbol所有符号的类型(例如,Symbol('My Symbol')

  • object所有对象的类型(不同于ObjectObject类及其子类的所有实例的类型)

12.4 原始值 vs. 对象

规范对值进行了重要的区分:

  • 原始值undefinednullbooleannumberbigintstringsymbol类型的元素。

  • 所有其他值都是对象

与 Java(在这里受到启发的 JavaScript)不同,原始值不是二等公民。它们与对象之间的区别更加微妙。简而言之:

  • 原始值:是 JavaScript 中的原子数据构建块。

    • 它们是通过值传递的:当原始值分配给变量或传递给函数时,它们的内容被复制。

    • 它们是通过值比较的:当比较两个原始值时,它们的内容被比较。

  • 对象:是复合数据。

    • 它们是通过标识(我的术语)传递的:当对象被分配给变量或传递给函数时,它们的标识(想象指针)被复制。

    • 它们是通过标识(我的术语)比较的:当比较两个对象时,它们的标识被比较。

除此之外,原始值和对象非常相似:它们都有属性(键值条目)并且可以在相同的位置使用。

接下来,我们将更深入地了解原始值和对象。

12.4.1 原始值(简称:原始值)
12.4.1.1 原始值是不可变的

您无法更改、添加或删除原始值的属性:

const str = 'abc';
assert.equal(str.length, 3);
assert.throws(
 () => { str.length = 1 },
 /^TypeError: Cannot assign to read only property 'length'/
);
12.4.1.2 原始值是通过值传递的

原始值是通过值传递的:变量(包括参数)存储原始值的内容。将原始值分配给变量或将其作为参数传递给函数时,其内容被复制。

const x = 123;
const y = x;
// `y` is the same as any other number 123
assert.equal(y, 123);

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 观察通过值传递和通过引用传递的区别

由于原始值是不可变的并且按值比较(见下一小节),所以无法观察通过值传递和通过标识传递(在 JavaScript 中用于对象)之间的区别。

12.4.1.3 原始值是通过值比较的

原始值是通过值比较的:当比较两个原始值时,我们比较它们的内容。

assert.equal(123 === 123, true);
assert.equal('abc' === 'abc', true);

要了解这种比较方式的特殊之处,请继续阅读并了解对象是如何比较的。

12.4.2 对象

对象在§28“对象”和接下来的章节中有详细介绍。在这里,我们主要关注它们与原始值的区别。

让我们首先探讨创建对象的两种常见方法:

  • 对象文字:

    const obj = {
     first: 'Jane',
     last: 'Doe',
    };
    

    对象文字以大括号{}开始和结束。它创建一个具有两个属性的对象。第一个属性具有键'first'(字符串)和值'Jane'。第二个属性具有键'last'和值'Doe'。有关对象文字的更多信息,请参阅§28.3.1“对象文字:属性”。

  • 数组文字:

    const fruits = ['strawberry', 'apple'];
    

    数组文字以方括号[]开始和结束。它创建一个包含两个元素'strawberry''apple'的数组。有关数组文字的更多信息,请参阅§31.3.1“创建、读取、写入数组”。

12.4.2.1 对象默认是可变的

默认情况下,您可以自由更改、添加和删除对象的属性:

const obj = {};

obj.count = 2; // add a property
assert.equal(obj.count, 2);

obj.count = 3; // change a property
assert.equal(obj.count, 3);
12.4.2.2 对象是通过标识传递的

对象是通过标识(我的术语)传递的:变量(包括参数)存储对象的标识

对象的标识就像是指向对象在堆上实际数据的指针(或透明引用)(想象 JavaScript 引擎的共享主内存)。

将对象分配给变量或将其作为参数传递给函数时,其标识被复制。每个对象文字在堆上创建一个新对象并返回其标识。

const a = {}; // fresh empty object
// Pass the identity in `a` to `b`:
const b = a;

// Now `a` and `b` point to the same object
// (they “share” that object):
assert.equal(a === b, true);

// Changing `a` also changes `b`:
a.name = 'Tessa';
assert.equal(b.name, 'Tessa');

JavaScript 使用垃圾回收来自动管理内存:

let obj = { prop: 'value' };
obj = {};

现在obj的旧值{ prop: 'value' }垃圾(不再使用)。JavaScript 将自动垃圾回收它(从内存中删除),在某个时间点(可能永远不会,如果有足够的空闲内存)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 详细信息:通过标识传递

“通过标识传递”意味着对象的标识(透明引用)是按值传递的。这种方法也称为“通过共享传递”

12.4.2.3 对象通过标识进行比较

对象通过标识进行比较(我的术语):只有当两个变量包含相同的对象标识时,它们才相等。如果它们引用具有相同内容的不同对象,则它们不相等。

const obj = {}; // fresh empty object
assert.equal(obj === obj, true); // same identity
assert.equal({} === {}, false); // different identities, same content

12.5 typeofinstanceof运算符:值的类型是什么?

两个运算符typeofinstanceof让您确定给定值x的类型:

if (typeof x === 'string') ···
if (x instanceof Array) ···

它们有何不同?

  • typeof区分规范中的 7 种类型(减去一个遗漏,加上一个添加)。

  • instanceof测试哪个类创建了给定的值。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 经验法则:typeof用于原始值;instanceof用于对象

12.5.1 typeof

表 2:typeof运算符的结果。

xtypeof x
undefined'undefined'
null'object'
布尔值'boolean'
数字'number'
大整数'bigint'
字符串'string'
符号'symbol'
函数'function'
所有其他对象'object'

Tbl. 2 列出了typeof的所有结果。它们大致对应于语言规范的 7 种类型。遗憾的是,存在两个差异,它们是语言怪癖:

  • typeof null返回'object'而不是'null'。这是一个错误。不幸的是,它无法修复。TC39 试图这样做,但它在网络上破坏了太多代码。

  • 函数的typeof应该是'object'(函数是对象)。为函数引入一个单独的类别是令人困惑的。

以下是使用typeof的几个例子:

> typeof undefined
'undefined'
> typeof 123n
'bigint'
> typeof 'abc'
'string'
> typeof {}
'object'

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 练习:关于typeof的两个练习

  • exercises/values/typeof_exrc.mjs

  • 奖励:exercises/values/is_object_test.mjs

12.5.2 instanceof

此运算符回答问题:值x是否由类C创建?

x instanceof C

例如:

> (function() {}) instanceof Function
true
> ({}) instanceof Object
true
> [] instanceof Array
true

原始值不是任何东西的实例:

> 123 instanceof Number
false
> '' instanceof String
false
> '' instanceof Object
false

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 练习:instanceof

exercises/values/instanceof_exrc.mjs

12.6 类和构造函数

JavaScript 的原始对象工厂是构造函数:普通函数,如果通过new运算符调用它们,则返回自己的“实例”。

ES6 引入了,主要是构造函数的更好语法。

在本书中,我将构造函数这两个术语互换使用。

类可以被视为将规范中的单一类型object分成子类型 - 它们给我们比规范中有限的 7 种类型更多的类型。每个类都是由它创建的对象的类型。

12.6.1 与原始类型相关联的构造函数

每种原始类型(除了规范内部类型undefinednull)都有一个关联的构造函数(考虑类):

  • 构造函数Boolean与布尔值相关联。

  • 构造函数Number与数字相关联。

  • 构造函数String与字符串相关联。

  • 构造函数Symbol与符号相关联。

这些函数各自扮演多个角色 - 例如,Number

  • 您可以将其作为函数使用并将值转换为数字:

    assert.equal(Number('123'), 123);
    
  • Number.prototype提供了数字的属性 - 例如,方法.toString()

    assert.equal((123).toString, Number.prototype.toString);
    
  • Number是用于数字的工具函数的命名空间/容器对象 - 例如:

    assert.equal(Number.isInteger(123), true);
    
  • 最后,您还可以将Number用作类并创建数字对象。这些对象与实际数字不同,应该避免使用。

    assert.notEqual(new Number(123), 123);
    assert.equal(new Number(123).valueOf(), 123);
    
12.6.1.1 包装原始值

与原始类型相关的构造函数也称为包装类型,因为它们提供了将原始值转换为对象的规范方式。在这个过程中,原始值被“包装”在对象中。

const prim = true;
assert.equal(typeof prim, 'boolean');
assert.equal(prim instanceof Boolean, false);

const wrapped = Object(prim);
assert.equal(typeof wrapped, 'object');
assert.equal(wrapped instanceof Boolean, true);

assert.equal(wrapped.valueOf(), prim); // unwrap

包装在实践中很少重要,但在语言规范中内部使用,以赋予原始属性。

12.7 类型之间的转换

在 JavaScript 中,有两种方式将值转换为其他类型:

  • 显式转换:通过诸如String()之类的函数。

  • 强制转换(自动转换):当操作接收到无法处理的操作数/参数时发生。

12.7.1 显式类型转换

与原始类型相关联的函数明确地将值转换为该类型:

> Boolean(0)
false
> Number('123')
123
> String(123)
'123'

您也可以使用Object()将值转换为对象:

> typeof Object(123)
'object'

以下表格更详细地描述了这种转换方式:

xObject(x)
undefined{}
null{}
booleannew Boolean(x)
numbernew Number(x)
bigintBigInt的一个实例(new抛出TypeError
stringnew String(x)
symbolSymbol的一个实例(new抛出TypeError
objectx
12.7.2 强制转换(类型之间的自动转换)

对于许多操作,如果它们的类型不匹配,JavaScript 会自动转换操作数/参数。这种自动转换称为强制转换

例如,乘法运算符将其操作数强制转换为数字:

> '7' * '3'
21

许多内置函数也会进行强制转换。例如,Number.parseInt()在解析之前会将其参数强制转换为字符串。这解释了以下结果:

> Number.parseInt(123.45)
123

数字123.45在解析之前被转换为字符串'123.45'。解析在第一个非数字字符之前停止,这就是为什么结果是123

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 练习:将值转换为原始值

exercises/values/conversion_exrc.mjs

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 测验

参见测验应用程序。

评论

十三、运算符

原文:exploringjs.com/impatient-js/ch_operators.html

译者:飞龙

协议:CC BY-NC-SA 4.0


  • 13.1 理解运算符

    • 13.1.1 运算符将其操作数强制转换为适当的类型

    • 13.1.2 大多数运算符只能用于原始值

  • 13.2 加法运算符 (+)

  • 13.3 赋值运算符

    • 13.3.1 普通赋值运算符

    • 13.3.2 复合赋值运算符

  • 13.4 相等:== vs. ===

    • 13.4.1 宽松相等 (==!=)

    • 13.4.2 严格相等 (===!==)

    • 13.4.3 建议:始终使用严格相等

    • 13.4.4 比 === 更严格:Object.is()

  • 13.5 排序运算符

  • 13.6 其他各种运算符

    • 13.6.1 逗号运算符

    • 13.6.2 void 运算符


13.1 理解运算符

JavaScript 的运算符可能看起来有些古怪。有了以下两条规则,它们就更容易理解:

  • 运算符将其操作数强制转换为适当的类型

  • 大多数运算符只能用于原始值

13.1.1 运算符将其操作数强制转换为适当的类型

如果运算符得到不正确类型的操作数,则很少抛出异常。相反,它会强制(自动转换)操作数,以便可以使用它们。让我们看两个例子。

首先,乘法运算符只能用于数字。因此,在计算结果之前,它将字符串转换为数字。

> '7' * '3'
21

其次,用于访问对象属性的方括号运算符([ ])只能处理字符串和符号。所有其他值都被强制转换为字符串:

const obj = {};
obj['true'] = 123;

// Coerce true to the string 'true'
assert.equal(obj[true], 123);
13.1.2 大多数运算符只能用于原始值

如前所述,大多数运算符只能用于原始值。如果操作数是对象,则通常会被强制转换为原始值,例如:

> [1,2,3] + [4,5,6]
'1,2,34,5,6'

为什么?加法运算符首先将其操作数强制转换为原始值:

> String([1,2,3])
'1,2,3'
> String([4,5,6])
'4,5,6'

接下来,连接两个字符串:

> '1,2,3' + '4,5,6'
'1,2,34,5,6'

13.2 加法运算符 (+)

在 JavaScript 中,加法运算符的工作方式如下:

  • 首先,它将两个操作数转换为原始值。然后切换到两种模式之一:

    • 字符串模式:如果两个原始值中有一个是字符串,则将另一个转换为字符串,连接两个字符串,并返回结果。

    • 数字模式:否则,将两个操作数转换为数字,相加,并返回结果。

字符串模式允许我们使用 + 来组装字符串:

> 'There are ' + 3 + ' items'
'There are 3 items'

数字模式意味着如果没有一个操作数是字符串(或者是一个变成字符串的对象),那么一切都会被强制转换为数字:

> 4 + true
5

Number(true)1

13.3 赋值运算符

13.3.1 普通赋值运算符

普通赋值运算符用于更改存储位置:

x = value; // assign to a previously declared variable
obj.propKey = value; // assign to a property
arr[index] = value; // assign to an Array element

变量声明中的初始化器也可以看作是一种赋值:

const x = value;
let y = value;
13.3.2 复合赋值运算符

JavaScript 支持以下赋值运算符:

  • 算术赋值运算符:+= -= *= /= %= [ES1]

    • += 也可以用于字符串连接

    • 后引入:**= [ES2016]

  • 按位赋值运算符:&= ^= |= [ES1]

  • 按位移位赋值运算符:<<= >>= >>>= [ES1]

  • 逻辑赋值运算符:||= &&= ??= [ES2021]

13.3.2.1 逻辑赋值运算符 [ES2021]

逻辑赋值运算符与其他复合赋值运算符的工作方式不同:

赋值运算符等同于仅在 a 为时赋值
a &#124;&#124;= ba &#124;&#124; (a = b)假值
a &&= ba && (a = b)真值
a ??= ba ?? (a = b)空值

为什么 a ||= b 等同于以下表达式?

a || (a = b)

为什么不使用这个表达式?

a = a || b

前一个表达式的好处在于 短路:只有在 a 评估为 false 时才会评估赋值。因此,只有在必要时才执行赋值。相反,后一个表达式总是执行赋值。

有关 ??= 的更多信息,请参阅 §14.4.5 “空值合并赋值运算符 (??=) [ES2021]”

13.3.2.2 剩余的复合赋值运算符

对于除 || && ?? 之外的操作符 op,以下两种赋值方式是等效的:

myvar op= value
myvar = myvar op value

例如,如果 op+,那么我们得到的运算符 += 的工作方式如下。

let str = '';
str += '<b>';
str += 'Hello!';
str += '</b>';

assert.equal(str, '<b>Hello!</b>');

13.4 相等:== vs. ===

JavaScript 有两种类型的相等运算符:宽松相等 (==) 和严格相等 (===)。建议始终使用后者。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 ===== 的其他名称

  • == 也被称为 双等号。它在语言规范中的官方名称是 抽象相等比较

  • === 也被称为 三等号

13.4.1 宽松相等 (==!=)

宽松相等是 JavaScript 的怪癖之一。它经常强制操作数。其中一些强制是有意义的:

> '123' == 123
true
> false == 0
true

其他的不太一样:

> '' == 0
true

如果(且仅当!)另一个操作数是原始值,对象才会被强制为原始值:

> [1, 2, 3] == '1,2,3'
true
> ['1', '2', '3'] == '1,2,3'
true

如果两个操作数都是对象,则只有当它们是同一个对象时才相等:

> [1, 2, 3] == ['1', '2', '3']
false
> [1, 2, 3] == [1, 2, 3]
false

> const arr = [1, 2, 3];
> arr == arr
true

最后,== 认为 undefinednull 是相等的:

> undefined == null
true
13.4.2 严格相等 (===!==)

严格相等从不强制。只有当它们具有相同的类型时,两个值才相等。让我们重新审视我们与 == 运算符的先前交互,并看看 === 运算符的作用:

> false === 0
false
> '123' === 123
false

只有当对象等于另一个值时,该对象才等于另一个值:

> [1, 2, 3] === '1,2,3'
false
> ['1', '2', '3'] === '1,2,3'
false

> [1, 2, 3] === ['1', '2', '3']
false
> [1, 2, 3] === [1, 2, 3]
false

> const arr = [1, 2, 3];
> arr === arr
true

=== 运算符不认为 undefinednull 是相等的:

> undefined === null
false
13.4.3 建议:始终使用严格相等

我建议始终使用 ===。它使您的代码更易于理解,并避免了对 == 的怪癖进行思考。

让我们看看 == 的两个用例以及我建议做的替代方案。

13.4.3.1 == 的用例:与数字或字符串比较

== 让你检查一个值 x 是否是一个数字或该数字的字符串 - 只需一次比较:

if (x == 123) {
 // x is either 123 or '123'
}

我更喜欢以下两种替代方案之一:

if (x === 123 || x === '123') ···
if (Number(x) === 123) ···

当您首次遇到 x 时,您也可以将其转换为数字。

13.4.3.2 == 的用例:与 undefinednull 比较

== 的另一个用例是检查值 x 是否为 undefinednull

if (x == null) {
 // x is either null or undefined
}

这段代码的问题在于您无法确定某人是否打算以这种方式编写它,还是是否打错了字,打算使用 === null

我更喜欢以下两种替代方案之一:

if (x === undefined || x === null) ···
if (!x) ···

第二种替代方案的缺点是它接受除 undefinednull 之外的值,但这是 JavaScript 中一个成熟的模式(将在 §15.3 “基于真值的存在性检查” 中详细解释)。

以下三个条件也大致等效:

if (x != null) ···
if (x !== undefined && x !== null) ···
if (x) ···
13.4.4 比 === 更严格:Object.is()

Object.is() 方法比较两个值:

> Object.is(123, 123)
true
> Object.is(123, '123')
false

它甚至比 === 更严格。例如,它认为 NaN,涉及数字的计算的错误值,等于它自己:

> Object.is(NaN, NaN)
true
> NaN === NaN
false

这偶尔是有用的。例如,您可以使用它来实现 .indexOf() 的改进版本:

const myIndexOf = (arr, elem) => {
 return arr.findIndex(x => Object.is(x, elem));
};

myIndexOf() 在数组中查找 NaN,而 .indexOf() 不会:

> myIndexOf([0,NaN,2], NaN)
1
> [0,NaN,2].indexOf(NaN)
-1

结果 -1 表示 .indexOf() 在数组中找不到其参数。

13.5 排序运算符

表 3:JavaScript 的排序运算符。

运算符名称
<小于
<=小于或等于
>大于
>=大于或等于

JavaScript 的排序运算符(表 3)适用于数字和字符串:

> 5 >= 2
true
> 'bar' < 'foo'
true

<=>= 基于严格相等。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 排序运算符在人类语言中效果不佳

排序运算符在比较人类语言的文本时效果不佳,例如,当涉及大写或重音时。详细信息请参见§20.6 “比较字符串”。

13.6 其他各种运算符

本书的其他地方涵盖了以下运算符:

  • 布尔值、数字、字符串、对象的运算符

  • 空值合并运算符 (??) 用于默认值

接下来的两个小节讨论了两个很少使用的运算符。

13.6.1 逗号运算符

逗号运算符有两个操作数,评估它们两个并返回第二个:

> 'a', 'b'
'b'

有关此运算符的更多信息,请参见Speaking JavaScript

13.6.2 void 运算符

void 运算符评估其操作数并返回 undefined

> void (3 + 2)
undefined

有关此运算符的更多信息,请参见Speaking JavaScript

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 测验

请参见测验应用。

评论

  • 16
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值