原文:IoT Development for ESP32 and ESP8266 with JavaScript

协议:CC BY-NC-SA 4.0

一、入门指南

本章将带您收集本书所需的所有硬件和软件,并在微控制器上运行您的第一个 JavaScript 应用程序。同时,本章还展示了如何使用 JavaScript 源代码级调试器xsbug的有用特性。

安装所有的软件工具和设置您的开发环境需要一点时间,但是一旦您可以运行一个示例,您就可以运行本书中的任何示例了。您还将拥有开始使用可修改的 SDK 编写自己的应用程序所需的一切。

硬件要求

本书中的大多数示例只需要很少的硬件,但您至少需要以下内容:

  • 带有 USB 端口的电脑(macOS Sierra 版本 10.12 或更高版本、Windows 7 Pro SP1 或更高版本,或者 Linux)

  • 微型 USB 电缆(高速、支持数据同步)

  • ESP32 节点 MCU 模块或 ESP8266 节点 MCU 模块

Note

所有的例子都在 ESP32 或 ESP8266 上运行,除了在第四章中讨论的使用蓝牙低能量(BLE)的例子只在 ESP32 上运行,因为 ESP8266 不支持 BLE。如果你对本书中的 BLE 例子感兴趣,你需要使用 ESP32。

如图 1-1 所示,使用 ESP32 和 ESP8266 模块对示例进行了测试。

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

图 1-1

ESP32(左)和 ESP8266(右)

使用传感器和致动器的示例(第六章和第七章)需要一些额外的组件:

  • 触觉按钮

  • 三色 LED(共阳极)

  • 三个 330 欧姆电阻

  • 微型伺服系统

  • TMP36 温度传感器

  • TMP102 温度传感器

  • 迷你金属扬声器(8 欧姆,0.5W)

  • 跳线

这些硬件组件如图 1-2 所示。在讨论它们的章节中提供了关于在哪里可以买到它们的更多信息。

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

图 1-2

章节 6 和 7 的硬件部件

使用 Poco 渲染器(第九章)或 Piu 用户界面框架(第十章)的示例可以在您计算机上的硬件模拟器上运行,但是强烈建议您使用实际的显示器并在您的 ESP32 或 ESP8266 上运行它们。如果您喜欢在试验板上将元件连接在一起,以下是您需要的:

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

图 1-3

ILI9341 QVGA 触摸显示器

  • ILI9341 QVGA 触摸显示屏(图 1-3 ),可在易贝和其他地方在线购买;搜索“spi display 2.4 touch”,你应该会找到几个不贵的选项。请注意,尽管这种显示效果很好,但还有许多其他选择。可修改的 SDK 包括对其他几种不同成本和质量的显示器的内置支持;有关更多信息,请参见可修改 SDK 的documentation/displays目录。

  • 一块试验板。

  • 公母跳线。

如果你不想自己布线,你可以从可修改的网站上购买一个可修改的或两个可修改的。可修改的是一个 ESP8266 有线电容触摸屏;Moddable Two 是一个连接到同一个触摸屏的 ESP32。两者都是紧凑外形的现成开发套件。图 1-4 显示了一个可修改的。

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

图 1-4

可修改的

可修改的 SDK 还支持基于 ESP32 的开发套件,带有内置屏幕。一个流行的选择是 M5Stack FIRE,如图 1-5 所示。有关支持的开发工具包的更多信息,请参见 GitHub 上的可修改的 SDK 库。

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

图 1-5

M5 栈火灾

软件需求

您需要以下软件:

  • 代码编辑器

  • 示例代码文件

  • 可修改的 SDK

  • ESP32 和/或 ESP8266 的构建工具

您可以选择自己喜欢的代码编辑器。有许多 JavaScript 友好的编辑器,包括 Visual Studio 代码、Sublime Text 3 和 Atom。

接下来的部分将解释如何下载示例代码文件,以及如何为您的设备设置可修改的 SDK 和构建工具。

下载示例代码

所有的例子都可以在 https://github.com/Moddable-OpenSource/iot-product-dev-book 找到。您可以使用git命令行工具下载示例代码。

Note

在本书中,您在命令行上输入的命令前面有一个>符号。该符号不是命令的一部分;包含它只是为了阐明每个单独的命令从哪里开始。

在 macOS/Linux 上,使用终端:

> cd ~/Projects
> git clone https://github.com/Moddable-OpenSource/    iot-product-dev-book

在 Windows 上,使用命令提示符(将<username>改为您的用户名):

> cd C:\Users\<username>\Projects
> git clone https://github.com/Moddable-OpenSource/    iot-product-dev-book

您还需要设置EXAMPLES环境变量来指向示例存储库的本地副本,如下所示:

  • 在 macOS/Linux 上:

    > export EXAMPLES=~/Projects/iot-product-dev-book
    
    
  • 在 Windows 上:

    > set EXAMPLES=C:\Users\<username>\Projects\    iot-product-dev-book
    
    

设置您的构建环境

在构建和运行示例之前,请遵循可修改 SDK 的documentation目录中的“可修改 SDK–入门”文档中的说明。本文档提供了安装、配置和构建适用于 macOS、Linux 和 Windows 的可修改 SDK 的分步说明,以及安装使用 ESP32 和 ESP8266 所需工具的说明。

使用xsbug

xsbug调试器提供运行在 XS JavaScript 引擎上的 JavaScript 代码的源代码级调试。它通过 USB 连接到设备,并具有图形用户界面(如图 1-6 所示)以使其易于使用。

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

图 1-6

xsbug调试器

与其他调试器类似,xsbug支持设置断点和浏览源代码、调用栈和变量。它还提供实时工具来跟踪内存使用情况,并分析应用程序和资源消耗。

当您为微控制器开发时,在目标设备上启动应用程序之前,构建系统会自动打开xsbug

当开发桌面模拟器时,您需要自己打开xsbug,方法是双击它的应用程序图标或从命令行打开它,如下所示:

  • 在 macOS 上:

    > open $MODDABLE/build/bin/mac/release/xsbug.app
    
    
  • 在 Windows/Linux 上:

    > xsbug
    
    

本书中示例的重要特征

这本书不经常参考xsbug,因为例子都已经调试好了。然而,在您创建自己的应用程序时,xsbug是一个非常有价值的工具。本书中使用的最重要的xsbug特征如下:

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

图 1-7

xsbug机器标签和控制按钮

  • 机器选项卡–连接到xsbug的每个 XS 虚拟机在窗口的左上角都有自己的选项卡(如图 1-7 中虚线边框所突出显示的)。单击选项卡会将左侧窗格切换到 machine 选项卡视图,您可以在其中查看仪器、使用控制按钮等。

  • 控制按钮–这些位于机器选项卡视图顶部的图标按钮(在图中用虚线边框突出显示)控制虚拟机。从左到右分别是

  • 控制台–能够在应用程序执行期间查看诊断信息通常很有用。trace函数将消息写入xsbug右下角的调试控制台。

关于xsbug所有特性的完整文档,请参见可修改 SDK 的documentation/xs目录下的xsbug文档。

运行示例

本书存储库中的例子是按章节组织的,每一章都有几个例子。为了更快地构建和启动示例,每个章节都有自己的主机,它包含运行该章节的示例所需的软件环境;主机是 JavaScript 模块、配置变量和其他可供应用程序使用的软件的集合。因为微控制器的空间非常有限,所以不可能有一台主机包含本书示例中使用的所有模块。

您可以将主机视为基本的应用程序。在 web 浏览器中运行 JavaScript 时,web 浏览器是主机;在 web 服务器上运行 JavaScript 时,Node.js 是主机。

单独安装主机,而不是将主机和示例一起安装,通过最大限度地减少需要下载的软件数量,大大加快了开发速度。在您的设备上安装主机通常需要 30 到 90 秒。完成后,您可以在几秒钟内安装大多数示例,因为主机已经包含了示例所需的设备固件和 JavaScript 模块。

接下来的小节将带您完成安装主机的整个过程,然后是一个示例,从helloworld开始。请注意,在本书的上下文中,安装应用程序会导致该应用程序在设备上运行。

安装主机

第一步是刷新设备以安装主机。如果你好奇的话,可以在host目录中找到每一章主机的源代码。要使用主机,您真正需要知道的是,它包含了相应示例所需的所有模块。

您使用mcconfig命令行工具来刷新设备。

mcconfig

mcconfig命令行工具在微控制器或模拟器上构建和安装应用程序。这里提供了用于在每个支持的平台上安装本章主机的命令。

在 ESP32 上,使用以下命令:

  • 在 macOS/Linux 上:

    > cd $EXAMPLES/ch1-gettingstarted/host
    > mcconfig -d -m -p esp32
    
    
  • 在 Windows 上:

    > cd %EXAMPLES%\ch1-gettingstarted\host
    > mcconfig -d -m -p esp32
    
    

在 ESP8266 上,使用以下命令:

  • 在 macOS/Linux 上:

    > cd $EXAMPLES/ch1-gettingstarted/host
    > mcconfig -d -m -p esp
    
    
  • 在 Windows 上:

    > cd %EXAMPLES%\ch1-gettingstarted\host
    > mcconfig -d -m -p esp
    
    

确认主机已安装

一旦主机安装完毕,它会将图 1-8 所示的消息写入调试控制台。

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

图 1-8

xsbug中来自主机的消息

安装helloworld

helloworld示例只包含三行 JavaScript 代码:

debugger;
let message = "Hello, World";
trace(message + "\n");

这个例子使用了两个重要的特性:

  • debugger语句,它停止执行并中断到xsbug

  • trace函数,它将消息写入调试控制台。注意trace不会自动在消息末尾添加一个换行符(\n)。这使您能够使用几个trace语句来生成一行的输出。确保在行尾包含换行符,以便文本在xsbug中正确显示。

您使用mcrun来安装示例。

mcrun

mcrun命令行工具构建并安装额外的 JavaScript 模块和资源,改变微控制器或模拟器上可修改应用的行为或外观。mcconfigmcrun都构建脚本和资源。与mcrun不同,mcconfig也构建本地代码。用 JavaScript 术语来说,mcconfig构建主机。

使用mcrun安装示例后,设备会重新启动。这将重新启动主机,主机将运行您安装的示例。

使用以下命令安装helloworld示例。确保您将<platform>更改为适合您设备的正确平台,无论是esp32还是esp

  • 在 macOS/Linux 上:

    > cd $EXAMPLES/ch1-gettingstarted/helloworld
    > mcrun -d -m -p <platform>
    
    
  • 在 Windows 上:

    > cd %EXAMPLES%\ch1-gettingstarted\helloworld
    
    > mcrun -d -m -p <platform>
    
    

收尾工作

一旦安装了应用程序,您应该立即进入xsbug。点击运行按钮,可以看到写入调试控制台的消息Hello, World,如图 1-9 所示。

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

图 1-9

HelloWorld写入xsbug中的控制台

如果一切顺利,如果您使用的是裸 NodeMCU 板,那么您可以进入本章的“结论”部分。如果您想要添加显示器(强烈建议),请继续“添加显示器”一节。

如果您遇到了问题,请参阅下一节。

解决纷争

当你试图安装一个应用程序时,你可能会遇到错误或警告形式的障碍;本节解释了一些常见问题以及如何解决这些问题。

设备未连接/未识别

错误消息

error: cannot access /dev/cu.SLAB_USBtoUART

意味着该设备未连接到您的电脑,或者电脑无法识别该设备。发生这种情况有几个原因:

  • 您的设备没有插入计算机。运行构建命令时,请确保它已接通电源。

  • 您有一根 USB 电缆,只能通电。确保您使用的是支持数据同步的 USB 电缆。

  • 计算机无法识别您的设备。要解决此问题,请参阅您的操作系统下面的说明。

macOS/Linux

要测试您的计算机是否识别您的设备,请拔下设备并输入以下命令:

> ls /dev/cu*

然后插入设备并重复相同的命令。如果没有新内容出现,则设备未被检测到。确保您安装了正确的 VCP 驱动程序。

如果看到了,您现在就有了设备名称,并且需要编辑UPLOAD_PORT环境变量。输入以下命令,用系统上的设备名称替换/dev/cu.SLAB_USBtoUART:

> export UPLOAD_PORT=/dev/cu.SLAB_USBtoUART

Windows 操作系统

检查设备管理器中的 USB 设备列表。如果您的设备显示为未知设备,请确保您安装了正确的 VCP 驱动程序。

如果您的设备出现在 COM3 以外的 COM 端口上,您需要编辑UPLOAD_PORT环境变量。输入以下命令,将COM3替换为适合您系统的设备 COM 端口:

> set UPLOAD_PORT=COM3

不兼容的波特率

以下警告消息是正常的,不必担心:

warning: serialport_set_baudrate: baud rate 921600 may not work

但是,有时上传开始但没有完成。当跟踪到控制台的进度条达到 100%时,您可以判断上传完成。例如:

...................................................... [ 16% ]
...................................................... [ 33% ]
...................................................... [ 49% ]
...................................................... [ 66% ]
...................................................... [ 82% ]
...................................................... [ 99% ]
..                                                    [ 100% ]

上传可能中途失败有几个原因:

  • 你的 USB 线有问题。

  • 您的 USB 电缆不支持更高的波特率。

  • 您使用的主板要求的波特率低于可修改 SDK 使用的默认波特率。

要解决最后两个问题,您可以更改为较慢的波特率,如下所示:

  1. 如果您正在使用 ESP32,请打开moddable/tools/mcconfig/make.esp32.mk;如果是 ESP8266,打开moddable/tools/mcconfig/make.esp.mk

  2. 找到这一行,它将上传速度设置为 921,600:

UPLOAD_SPEED ?= 921600

  1. 将速度设置为较小的数值。例如:
UPLOAD_SPEED ?= 115200

设备不处于引导加载模式

如果您使用某些基于 ESP32 的主板,这个问题并不罕见。当您尝试刷新设备时,会短暂停止跟踪状态消息,几秒钟后您会收到以下错误消息:

A fatal error occurred: Failed to connect to ESP32: Timed out waiting for packet header

如果设备不处于引导加载程序模式,则不能刷新设备。如果您使用的是 NodeMCU 模块,请在每次刷新时遵循以下步骤:

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

图 1-10

ESP32 上的启动按钮

  1. 拔下设备插头。

  2. 按住开机按钮(图 1-10 中圈出)。

  3. 将设备插入电脑。

  4. 输入mcconfig命令。

  5. 等待几秒钟,然后松开启动按钮。

添加显示器

虽然本书中的大多数示例都不需要显示器,但在 ESP32 或 ESP8266 中添加显示器可以极大地改善用户体验。它使您能够执行以下操作:

  • 显示比几个闪烁的灯更多的信息

  • 创建现代用户界面

  • 添加功能

本书中的示例是为分辨率为 240 x 320 或 320 x 240 的显示器设计的(例如,QVGA)。这些显示器的尺寸通常在 2.2 英寸到 3.5 英寸之间,在早期的智能手机中很常见。其他尺寸的显示器也可以连接到这些微控制器,但这个尺寸与这些微控制器的性能非常匹配。

以下部分说明如何将 ILI9341 QVGA 触摸显示器连接到 ESP32 或 ESP8266。如果您使用的是 Moddable One 或 Moddable Two 这样的开发板,您可以跳到“安装 helloworld-gui”一节。

将显示器连接到 ESP32

表 1-1 和图 1-11 显示了如何将显示器连接到 ESP32。

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

图 1-11

将显示器连接到 ESP32 的接线图

表 1-1

将显示器连接到 ESP32 的接线

|

ILI9341 显示器

|

ESP32

|
| — | — |
| SDO/MISO | GPIO12 |
| 发光二极管 | 3.3V |
| 血清肌酸激酶 | GPIO14 |
| SDI/MOSI | GPIO13 |
| 特许测量员 | GPIO15 |
| 直流电 | GPIO2 |
| 重置 | 3.3V |
| 地线 | 地线 |
| VCC | 3.3V |
| 唐岛 | GPIO12 |
| T_DIn | GPIO13 |
| S7-1200 可编程控制器 | GPIO14 |
| T_IRQ | GPIO23 |
| S7-1200 可编程控制器 | GPIO18 |

将显示器连接到 ESP8266

表 1-2 和图 1-12 显示了如何将显示器连接到 ESP8266。

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

图 1-12

将显示器连接到 ESP8266 的接线图

表 1-2

将显示器连接到 ESP8266 的接线

|

ILI9341 显示器

|

ESP8266

|
| — | — |
| SDO/MISO | GPIO12 |
| 发光二极管 | 3.3V |
| 血清肌酸激酶 | GPIO14 |
| SDI/MOSI | GPIO13 |
| 特许测量员 | GPIO15 |
| 直流电 | GPIO2 |
| 重置 | 3.3V |
| 地线 | 地线 |
| VCC | 3.3V |
| 唐岛 | GPIO12 |
| T_DIn | GPIO13 |
| S7-1200 可编程控制器 | GPIO14 |
| T_IRQ | GPIO16 |
| S7-1200 可编程控制器 | GPIO0 |

安装helloworld-gui

helloworld-gui示例是在屏幕上显示文本的helloworld的一个版本。如果您自己将显示器连接到设备,使用helloworld-gui应用程序刷新设备是测试连接是否正确的好方法。

要使用的命令与用于安装helloworld的命令非常相似。唯一的区别是平台标识符。平台标识符告诉构建系统包括适当的显示和触摸驱动程序。如果您使用的是可修改的,平台标识符是esp/moddable_one;对于一个可修改的二,它是esp32/moddable_two。如果您根据前面章节的说明添加了一个显示器,平台标识符为esp32/moddable_zeroesp/moddable_zero

使用以下命令安装helloworld-gui示例。确保将<platform>更改为适合您设备的正确平台。

  • 在 macOS/Linux 上:

    > cd $EXAMPLES/ch1-gettingstarted/helloworld-gui
    > mcconfig -d -m -p <platform>
    
    
  • 在 Windows 上:

    > cd %EXAMPLES%\ch1-gettingstarted\helloworld-gui
    > mcconfig -d -m -p <platform>
    
    

如果您指定了正确的平台并且您的接线正确,您将看到如图 1-13 所示的屏幕。

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

图 1-13

图形helloworld应用程序

结论

既然您的开发环境已经设置好,并且您已经熟悉了在您的设备上安装本章示例的过程,那么您已经准备好尝试更多的示例了!

此时,你已经掌握了跟随第二章到第十章的所有材料和技能。这些章节彼此独立,所以你可以按任何顺序阅读。当您开始使用某一章中的示例时,一定要安装该章的主机,否则在启动示例时会遇到错误。一旦你对可修改的 SDK 的 API 感到满意,你就可以进入第十一章了,这一章涵盖了更高级的主题。

二、面向嵌入式 C 和 C++程序员的 JavaScript

本章是对已经熟悉 C 或 C++的开发人员的快速、实用的 JavaScript 介绍。它假设您已经知道如何编程,并且可能有一些嵌入式系统的开发经验。这里介绍的 JavaScript 语言与 web 上使用的语言完全相同。但是因为这里的重点是嵌入式系统而不是 web 浏览器,所以这一章讲述了 JavaScript 的一些方面,这些方面很少被从事 web 工作的开发人员使用。例如,考虑到如果不操作二进制数据(比如一个字节数组),编写嵌入式软件几乎是不可能的;JavaScript 通过内置的类型化数组类支持二进制数据,但大多数 web 开发人员从未使用过该特性,因为在构建网页时没有必要。因此,即使你熟悉 web 上的 JavaScript,你也可能想阅读这一章来熟悉嵌入式系统上比 web 上更常见的语言特性。

C 和 C++程序员在入门 JavaScript 时有一个很大的优势,因为这种语言看起来与 C 相当相似,这不是偶然的:JavaScript 编程语言被设计成与 Java 编程语言相似;Java 是由 C++演化而来的;创建 C++是为了将面向对象编程引入 C。许多相似之处将帮助您快速读写 JavaScript 代码。尽管如此,这两种语言在许多方面也是不同的。本章以相似之处为基础,向您介绍一些不同之处。

JavaScript 已经有 20 多年的历史了,而且还在不断发展。本章介绍了现代 JavaScript,包括 2019 版 JavaScript 中的功能以及一些(如私有字段)正在纳入未来版本的功能。这里只描述作为标准语言一部分的 JavaScript 特性。由于 JavaScript 历史悠久,某些特性不再推荐使用;本章指出了其中的一些。特别是,2012 年标准化的 JavaScript 第五版引入了严格模式 ,消除了一些令人困惑和低效的特性。这些原始行为在*松散模式、*中仍然可用,这主要是为了网站的向后兼容性,但这本书专门使用严格模式。

基本语法

本节介绍一些基础知识,比如如何使用 JavaScript 进行函数调用、声明变量,以及使用ifswitchforwhile语句控制执行流程。在 C 和 JavaScript 中,所有这些都非常相似,但是在这个过程中,您将了解到一些重要的区别。

“你好,世界”

学习 C 语言的传统起点是 Kernighan 和 Ritchie 的书《C 编程语言》中的hello, world程序。在 JavaScript 中,只有一行:

trace("hello, world\n");

这里,C printf函数被来自可修改 SDK 的trace函数所取代。(在网络上使用 JavaScript 的开发者使用console.log而不是trace。)与 C #一样,函数的参数在括号内传递,语句以分号结束。传递给该函数的字符串文字也是相同的——一个用双引号括起来的字符串——并且使用 C 语言中常见的反斜杠(\)符号来转义特殊字符,比如这里的换行符。

分号

C 和 C++的一个显著区别是,由于自动分号插入(ASI)特性,语句结尾的分号在 JavaScript 中是可选的。以下代码在 JavaScript 中是允许的,但在 C #中却失败了:

trace("hello, ")
trace("world")
trace("\n")

虽然这很方便,因为它省去了一次击键,并悄悄地纠正了省略分号的常见错误,但在某些模糊的情况下,它会产生歧义,从而导致错误。因此,您应该始终以分号结束语句,而不是依赖 ASI。JavaScript linters,比如 ESLint,包含了对缺少分号的检查。

声明变量和常数

JavaScript 中的变量是用let语句声明的:

let a = 12;
let b = "hello";
let c = false;

与 C 语言不同,变量声明不包含任何类型信息(如intboolchar *)。这是因为任何变量都可以包含任何类型。这种动态类型,将在本章后面进一步解释,是 JavaScript 的一个特性,需要 C 程序员花一点时间来适应。

JavaScript 中的变量名通常遵循 C 惯例:它们区分大小写,所以iI是不同的名称,并且变量名的长度没有限制。JavaScript 变量名也可能包含 Unicode 字符,如下例所示:

let garçon = "boy";
let 東京都 = "Tokyo";
let $ = "dollar";
let under_score = "_";

你用const声明常量值:

const PRODUCT_NAME = "Light Sensor";
const LUMEN_REGISTER = 2;
const USE_OPTIMIZATIONS = true;

任何向常数赋值的尝试都会产生错误;但是,与 C #不同,此错误是在运行时而不是在构建时生成的。

如清单 2-1 所示,用letconst做的声明遵守与 c 中声明相同的作用域规则

let x = 1;
const y = 2;
let z = 3;
if (true) {
    const x = 2;
    let y = 1;
    trace(x);   // output: 2
    trace(y);   // output: 1
    trace(z);   // output: 3
    y = 4;
    z += y;
}
trace(x);       // output: 1
trace(y);       // output: 2
trace(z);       // output: 7

Listing 2-1.

JavaScript 还允许使用var来声明变量,这仍然很常见,因为let是一个相对较新的添加。然而,这本书建议只使用let,因为var不遵守与 c 语言中声明相同的作用域规则

if声明

JavaScript 中的if语句与 C 语言中的结构相同,如清单 2-2 所示。

if (x) {
    trace("x is true\n");
}
else {
    trace("x is false\n");
}

Listing 2-2.

像在 C 中一样,当ifelse块是一个单独的语句时,可以省略分隔该块的大括号:

if (!x)
    trace("x is false\n");
else
    trace("x is true\n");

清单 2-2 中if语句的条件简单来说就是x。在 C 中,这意味着如果x为 0,则条件为假;否则就是真的。在 JavaScript 中,这更复杂,因为变量x可能是任何类型,而不仅仅是数字(或者指针,但是指针在 JavaScript 中是不存在的)。JavaScript 定义了以下规则来评估给定值是真还是假:

  • 对于一个布尔值,这个决定很简单:这个值要么是true要么是false

  • 对于一个数字,JavaScript 遵循 C 的规则,将 0 值视为false,其他所有值视为true

  • 空字符串(长度为 0 的字符串)计算结果为false,所有非空字符串计算结果为true

在 JavaScript 中,在一个条件中评估为true的值称为“真值”,评估为false的值称为“假值”。

一种紧凑形式的if语句,即条件(三元)运算符,在 JavaScript 中可用,并且具有与 C 中相同的结构:

let x = y ? 2 : 3;

switch声明

如清单 2-3 所示,JavaScript 中的switch语句看起来非常像 c 语言中的语句

switch (x) {
    case 0:
        trace("zero\n");
        break;
    case 1:
        trace("one\n");
        break;
    default:
        trace("unexpected!\n");
        break;
}

Listing 2-3.

然而,有一个重要的区别:case关键字后面的值不限于整数值。例如,您可以使用一个浮点数(参见清单 2-4 )。

switch (x) {
    case 0.25:
        trace("one quarter\n");
        break;
    case 0.5:
        trace("one half\n");
        break;
}

Listing 2-4.

也可以使用字符串(列表 2-5 )。

switch (x) {
    case "zero":
    case "Zero":
        trace("0\n");
        break;
    case "one":
    case "One":
        trace("1\n");
        break;
    default:
        trace("unexpected!\n");
        break;
}

Listing 2-5.

此外,JavaScript 允许您在case语句中混合不同类型的值,尽管这很少是必要的。

JavaScript 既有for又有while循环,看起来与 C 语言中的循环相似(参见清单 2-6 )。

for (i = 0; i < 10; i++)
    trace(i);

let j = 12;
while (j--)
    trace(j);

Listing 2-6.

JavaScript 循环同时支持continuebreak(列举 2-7 )。

for (i = 0; i < 10; i++) {
    if (i & 1)
        continue;   // Skip odd numbers
    trace(i);
}

let j = 0;
do {
    let jSquared = j * j;
    if (jSquared > 100)
        break;
    trace(jSquared);
    j++;
} while (true);

Listing 2-7.

类型

JavaScript 只有少数内置类型,所有其他类型都是从这些类型中创建的。这些类型中有许多是 C 和 C++程序员所熟悉的,比如BooleanNumberString,尽管这些类型在 JavaScript 版本中有所不同,但您需要理解它们。其他类型,比如undefined,在 C 或 C++中没有对等的。

请注意,本节没有介绍所有类型。例如,它省略了RegExpBigIntSymbol,因为它们在嵌入式系统的 JavaScript 开发中并不常用;但是,如果您的项目需要它们,它们是可用的。

undefined

在 C 和 C++中,一个操作可以有一个语言没有定义的结果。例如,如果您忘记在下面的代码中初始化x,则y的值是未知的:

int x;
int y = x + 1;      // ??

同样,如果您忘记包含一个return语句,函数的结果是未知的:

int add(int a, int b) {
    int result = a + b;
}
int z = add(1, 2);  // ??

您的 C 或 C++编译器通常会检测到这类错误并发出警告,以便您可以修复问题。尽管如此,在 C 和 C++中仍有许多方法会导致代码出现不可预知的结果。

在 JavaScript 中,永远不会出现结果不可预测的情况。实现这一点的方法之一是使用特殊值undefined,它表示没有赋值。在 C 中,出于类似的目的,0 有时被用作无效值,但在 0 是有效值的情况下,这是不明确的。

当你定义一个新的局部变量时,它的值是undefined,直到你给它赋值。如果你的函数在没有return语句的情况下退出,它的返回值是undefined。在本章中你会看到undefined的其他用法。

严格来说,JavaScript 有一个undefined类型,它总是有值undefined

布尔值

JavaScript 中的布尔值是truefalse。这些和 C 中的 1 和 0 不一样;它们是由语言定义的截然不同的价值观。

let x = 42;
let y = x == 42;    // true
let z = x == "dog"; // false

民数记

JavaScript 中的每个数字值都被定义为双精度(64 位)IEEE 754 浮点值。在您对微控制器的性能影响感到恐惧之前,要知道 Moddable SDK 中使用的 XS JavaScript 引擎在内部将数字存储为整数,并在可能的情况下对它们执行整数数学运算。这确保了在没有 FPU 的微控制器上的实现是高效的,同时保持了与标准 JavaScript 的完全兼容性。

let x = 1;
let y = -2.3;
let z = 5E2;    // 500

将每个数字都定义为 64 位浮点有一些好处。首先,整数溢出的可能性要小得多。如果整数运算的结果溢出 32 位整数,它会自动提升为浮点值。64 位浮点值在失去精度之前可以存储多达 53 位的整数。如果您碰巧执行了生成分数结果的数学运算,例如,将一个奇数除以 2,JavaScript 会以浮点值的形式返回精确的分数结果。

InfinityNaN

JavaScript 对数字有一些特殊的值:

  • 除以 0 不会产生错误,而是返回Infinity

  • 试图执行一个无意义的操作会返回NaN,意思是“不是一个数字”例如,5 / "a string"5 + undefined返回NaN,因为用一个字符串值除一个整数或者给一个整数加上undefined是没有意义的。

基础

JavaScript 对十六进制和二进制常量有特殊的表示法:

  • 一个数字的前缀意味着它是十六进制的,就像在 c 语言中一样。

  • 一个数字的前缀意味着它是二进制的,如 C++14 所支持的。

这些前缀在处理二进制数据时非常有用,如下例所示:

let hexMask = 0x0F;
let bitMask = 0b00001111;

与 C 不同,JavaScript 不支持以 0 开头的八进制数,如0557;如果您尝试使用它,它会在构建时生成错误。形式0o557支持八进制数值。

数字分隔符

JavaScript 允许使用下划线字符(_)作为数字分隔符来分隔数字。分隔符不会改变数字的值,但可以使它更容易阅读。C++14 也有一个数字分隔符,但它使用单引号字符(')代替。

let mask = 0b0101101011110000;
let maskWithSeparators = 0b0101_1010_1111_0000;

按位运算符

JavaScript 为数字提供了按位运算符,包括:

  • ~–按位非

  • &–按位 AND

  • |–按位或

  • ^–按位异或

它还提供了这些用于移位的按位运算符:

  • >>–带符号右移

  • >>>–无符号右移

  • <<–向左移位

没有无符号左移,因为左移一个非零值总是会丢弃符号位。当执行任何按位运算时,JavaScript 总是首先将值转换为 32 位整数;丢弃任何小数部分或附加位。

Math物体

Math对象提供了许多 C 程序员从math.h头文件中使用的函数。除了常见的常量如Math.PIMath.SQRT2Math.E之外,它还包括常见的函数,如清单 2-8 所示。

let x = Math.min(1, 2, 3);  // minimum = 1
let y = Math.max(2, 3);     // maximum = 3
let r = Math.random();      // random number between 0 and 1
let z = Math.abs(-3.2);     // absolute value = 3.2
let a = Math.sqrt(100);     // square root = 10
let b = Math.round(3.9);    // rounded value = 4
let c = Math.trunc(3.9);    // truncated value = 3
let z = Math.cos(Math.PI);  // cosine of pi = -1

Listing 2-8.

关于由Math对象提供的常量值和函数的完整列表,请查阅 JavaScript 参考资料。

将数字转换为字符串

在 C 中,将数字转换成字符串的一种常见方法是使用sprintf将数字打印到字符串缓冲区。在 JavaScript 中,通过调用数字的toString方法将数字转换为字符串(是的,在 JavaScript 中,甚至数字也是一个对象!):

let a = 1;
let b = a.toString();   // "1"

toString的默认基数是 10;要转换成非十进制值,比如十六进制或二进制,将基数作为参数传递给toString:

let a = 240;
let b = a.toString(16);     // "f0"
let c = a.toString(2);      // "11110000"

要转换成浮点记数法,使用toFixed代替toString,并指定小数点后的位数:

let a = 24.328;
let b = a.toFixed(1);   // "24.3"
let c = a.toFixed(2);   // "24.33"
let d = a.toFixed(4);   // "24.3280"

函数toExponentialtoPrecision为将数字转换成字符串提供了额外的格式选项。

将字符串转换为数字

在 C 中,将字符串转换为数字的一种常见方法是使用sscanf。在 JavaScript 中,根据您希望结果是整数还是浮点值,使用parseIntparseFloat:

let a = parseInt("12.3");       // 12
let b = parseFloat("12.30");    // 12.3

parseInt的默认基数是 10,除非字符串以0x开头,在这种情况下,默认值是 16。parseInt函数采用可选的第二个参数来表示基数。下面的示例分析一个十六进制值:

let a = parseInt("F0", 16);     // 240

您也可以通过Number.parseIntNumber.parseFloat访问parseIntparseFloat的功能;然而,这并不常见。

用线串

在 C 语言中,字符串不是一个独特的类型,而只是一个 8 位字符的数组。因为字符串如此普遍,C 标准库提供了许多使用它们的函数。尽管如此,在 C 语言中处理字符串并不容易,很容易导致安全错误,比如缓冲区溢出。C++解决了一些问题,尽管使用字符串仍然不容易或不安全。相比之下,JavaScript 有一个内置的String类型,它被设计成易于程序员使用和安全使用;这反映了 JavaScript 作为网络语言的起源,其中字符串操作在构建网页时很常见。

在 JavaScript 中,字符串在许多方面不同于普通的 C 字符串。字符串是 16 位 Unicode 字符(UTF-16)的序列,而不是 8 位字符的数组。使用 Unicode 表示字符串可以确保所有应用程序都能可靠地处理任何语言的字符串值。虽然这些字符在概念上是 16 位 Unicode,但是 JavaScript 引擎可以在内部以任何表示形式存储它们。XS 引擎将字符串存储在 UTF-8 中,因此对于来自通用 7 位 ASCII 字符集的字符来说没有额外的内存开销。

访问单个字符

JavaScript 字符串不是数组;但是,它们支持 C 语言的数组语法来访问单个字符。但与 C 不同,结果不是字符的 Unicode(数字)值,而是包含该索引处字符的单字符字符串。

let a = "garçon";
let b = a[3];   // "ç"
let c = a[4];   // "o"

在 C #中,访问无效索引(例如,超过字符串末尾)会返回一个未定义的值。对于前面代码中声明的aa[100]将访问字符串开始后 100 字节内存中发生的任何事情。通过访问未映射的内存,这种访问甚至可能导致内存故障。在 JavaScript 中,试图读取字符串有效范围之外的字符会返回值undefined

要获得给定索引处字符的 Unicode 值,使用charCodeAt函数:

let a = "garçon";
let b = a.charCodeAt(3);    // 231
let c = a.charCodeAt(4);    // 111
let d = a.charCodeAt(100);  // NaN

修改字符串

c #允许你读写字符串中的字符。JavaScript 字符串是只读的,也叫不可变;您不能“就地”修改字符串例如,以下代码中对a[0]的赋值在 JavaScript 中不起任何作用:

let a = "a string";
a[0] = "A";

对于来自 C 的人来说,这种限制可能有点难以适应,但是对于使用提供的许多方法来操作字符串的一些经验来说,这就变得很熟悉了。

确定字符串的长度

要确定 C 中字符串的长度,可以使用strlen函数,该函数返回字符串中的字节数。它通过扫描值为 0 的字节来确定长度,因为 C 中的字符串被定义为以 0 字节结束。在 JavaScript 中,字符串是 Unicode 字符序列,没有终止空字符;JavaScript 引擎知道序列中的字符数,可以通过length属性获得。

let a = "hello";
let b = a.length;   // 5

strlen的一个问题是,当字符是 8 位 ASCII 字符时,字符串中的字节数只等于字符串的长度。对于 Unicode 字符,strlen不提供字符数。当然,也有其他函数会这样做,但是 C 程序员经常会错误地对字符串使用strlen来获取字符数,从而导致错误。JavaScript length属性避免了这个问题,因为它总是返回一个字符数。

清单 2-9 中的例子使用length属性来计算字符串中的空格数。

let a = "zéro un deux";
let spaces = 0;
for (let i = 0; i < a.length; i++) {
    if (a[i] == " ")
        spaces += 1;
}
trace(spaces);

Listing 2-9.

嵌入引号和控制字符

到目前为止,本章中的字符串文字值都使用双引号(")来定义字符串的开始和结束。由双引号分隔的字符串可以包含单引号(')。

let a = "Let's eat!";

与 C #中一样,这样的字符串不能包含双引号。与 C #不同,您可以用单引号而不是双引号来分隔字符串,这对于包含双引号的字符串来说很方便。

let a = '"This is a test," she said.';

由单引号或双引号分隔的字符串必须完全包含在一行中。通过使用\n指定换行符,可以在字符串中包含换行符;反斜杠(\)让您可以像在 c 中一样对字符进行转义。

let a = 'line 1\nline 2\nline 3\n';

在 JavaScript 中描述字符串的另一种方法是使用反斜杠字符(```js)。以这种方式定义的字符串被称为模板文字,并且有几个有用的属性,包括它们可以跨越多行(潜在地使你的字符串更可读;将清单 2-10 与之前的示例进行比较。

let a =
`line 1
line 2
line 3
`;

Listing 2-10.

```js

#### 字符串替换

模板文字提供了一种替换机制,对于由几个部分组成一个字符串很有用。这提供的功能非常类似于在 C 中使用带有格式化字符串的`printf`。然而,C 将格式化字符串与要格式化的值分开,而 JavaScript 将它们合并。这一开始可能会让人感到陌生,但是将需要格式化的值直接放入字符串中不容易出错。

let a = “one”;
let b = “two”;
let c = ${a}, ${b}, three; // “one, two, three”


在模板文本中,`${``}`之间的字符被计算为 JavaScript 表达式,这使您能够执行计算并将格式应用于结果:

let a = 2 + 2 = ${2 + 2}; // “2 + 2 = 4”
let b = Pi to three decimals is ${Math.PI.toFixed(3)};
// “Pi to three decimals is 3.142”


一个被称为*标签*的特殊特性使函数能够修改模板文字的默认行为。例如(正如第四章将演示的),您可以使用这个特性将 UUID 的字符串表示转换成二进制数据。带标签的模板文本如何工作的细节超出了本章的范围,但是使用它们很容易:只需将标签放在模板文本之前。

let a = uuid1805;
let b = uuid9CF53570-DDD9-47F3-BA63-09ACEFC60415;


#### 添加字符串

您可以使用加法运算符(`+`)在 JavaScript 中组合字符串:

let a = “one”;
let b = “two”;
let c = a + ", " + b + “, three”; // “one, two, three”


JavaScript 允许您将字符串添加到非字符串值中,如数字。它的工作规则通常会给你预期的结果,但并不总是这样:

let a = “2 + 2 = " + 4; // “2 + 2 = 4”
let b = 2 + 2 + " = 2 + 2”; // “4 = 2 + 2”
let c = "2 + 2 = " + 2 + 2; // “2 + 2 = 22”


因为在字符串添加过程中记住所有关于类型转换的规则可能很困难,所以建议您使用模板文字,这更容易预测,通常也更容易阅读。

#### 转换字符串大小写

在 C 语言中,将字符串转换成大写或小写是很有挑战性的,尤其是当您使用完整的 Unicode 字符集时。JavaScript 有内置的函数来完成这些转换。

let a = “Garçon”;
let b = a.toUpperCase(); // “GARÇON”
let c = a.toLowerCase(); // “garçon”


请注意,`toUpperCase``toLowerCase`函数不会修改存储在前面示例中的变量`a`中的原始字符串,而是返回一个带有修改值的新字符串。所有操作字符串的 JavaScript 函数都是这样的,因为所有的字符串都是不可变的。

#### 提取部分字符串

要将一个字符串的一部分提取到另一个字符串中,使用`slice`函数。它的参数是开始和结束索引,其中结束索引是索引*,在此之前*结束提取。如果省略结束索引,则使用字符串的长度。

let a = “hello, world!”;
let b = a.slice(0, 5); // “hello”
let c = a.slice(7, 12); // “world”
let d = a.slice(7); // “world!”


JavaScript 还有一个`substr`函数,它提供了与`slice`相似的功能,但参数略有不同。然而,`slice``substr`更受青睐,后者主要是为 web 上的遗留代码维护的。

#### 重复字符串

要创建一个特定值重复几次的字符串,使用`repeat`函数:

let a = “-”;
let b = a.repeat(3); // “—”
let c = “.-”;
let d = c.repeat(2); // “.-.-”


#### 修剪琴弦

解析字符串时,您通常希望删除开头或结尾的空白(空格字符、制表符、回车符、换行符等)。修剪功能只需一个步骤即可删除空白:

let a = " JS ";
let b = a.trim(); // “JS”
let c = a.trimStart(); // “JS "
let d = a.trimEnd(); // " JS”


trim 函数可以完全在 JavaScript 中实现(大多数字符串函数也可以),但是将它们构建到语言中意味着它们的实现要快得多,并且它们的行为在所有应用程序中都是一致的。

#### 搜索字符串

C 中的`strstr`函数在一个字符串中找到另一个字符串。JavaScript 中的`indexOf`函数类似于`strstr`。如清单 2-11 所示,`indexOf`的第一个参数是要搜索的子串,可选的第二个参数是开始搜索的字符索引,函数的结果是找到子串的索引,如果没有找到则为-1

let string = “the cat and the dog”;
let a = string.indexOf(“cat”); // 4
let b = string.indexOf(“frog”); // –1
let c = string.indexOf(“the”); // 0
let d = string.indexOf(“the”, 2); // 12

Listing 2-11.


有时您希望找到子字符串的最后一个匹配项。在 C 中,这需要多次调用`strstr`,直到找不到进一步的匹配。JavaScript 为这种情况提供了`lastIndexOf`函数。

let string = “the cat and the dog”;
let a = string.lastIndexOf(“the”); // 12
let b = string.lastIndexOf(“the”, a - 1); // 0


在计算字符串时,检查字符串是以特定的字符串开始还是结束是很有用的。在 c 语言中,你使用`strcmp``strncmp`来做这件事。这种情况很常见,以至于 JavaScript 提供了专用的`startsWith``endsWith`函数。

if (string.startsWith(“And “))
trace(Don't start sentence with "and");
if (string.endsWith(”…”))
trace(Don't end sentence with ellipsis);


### 功能

JavaScript 当然也有函数,c 也一样,两种语言中有些函数非常相似。

function add(a, b) {
return a + b;
}


#### 函数参数

因为 JavaScript 变量可以保存任何类型的值,所以没有给出参数的类型,只给出了它们的名称。与 CC++不同的是,这里没有函数声明;您只需将源代码写入该函数,然后任何可以访问该函数的代码都可以调用它。这种特别的方法允许更快的编码。

在 CC++中,可以使用指针通过引用传递参数值,但在 JavaScript 中,必须始终通过值传递参数。因此,JavaScript 函数永远不会改变传递给它的变量值。例如,清单 2-12 中的`add`函数不会改变`x`的值。

function add(a, b) {
a += b;
return a;
}
let x = 1;
let y = 2;
let z = add(x, y);

Listing 2-12.


当您将对象传递给函数时,函数可以修改对象的属性,但不能修改保存该对象的调用的局部变量。这类似于在 c 中传递一个指向数据结构的指针。在清单 2-13 中,`setName`函数将`name`属性添加到传递给它的对象中。它对一个新的空对象的参数`a`的赋值不会改变`b`的值。

function setName(a, name) {
a.name = name;
a = {};
}

let b = {};
setName(b, “thermostat”);
// b.name is “thermostat”

Listing 2-13.

CC++中,函数的实现可以决定传递给它的参数的数量,并且可以使用`va_start``va_end``va_arg`来访问每个参数。这些是强大的工具,但使用起来可能会很复杂。JavaScript 还提供了处理函数参数的工具。调用者没有传递的任何参数都被设置为`undefined`,因此(如清单 2-14 中对`b`所做的那样),您可以检查一个参数是否没有被传递。

function add(a, b) {
if (b == undefined)
return NaN;
return a + b;
}

add(1);

Listing 2-14.


访问传递给函数的参数的另一种方法是使用特殊的`arguments`变量,它的行为类似于包含参数的数组。这种方法类似于使用`va_arg`,额外的好处是知道参数的数量。在清单 2-15 中,`add`函数接受任意数量的参数并返回它们的总和。

function add() {
let result = 0;
for (let i = 0; i < arguments.length; i++)
result += arguments[i];
return result;
}

let c = add(1, 2);
let d = add(1, 2, 3);

Listing 2-15.


在 JavaScript 中使用`arguments`很常见,但并不是在所有情况下都可用。这里介绍它是因为您可能会在代码中看到它。现代 JavaScript 有一个额外的特性,叫做 *rest 参数*,它提供类似的功能,总是可用的,并且更加灵活(参见清单 2-16 )

function add(…values) {
let result = 0;
for (let i = 0; i < values.length; i++)
result += values[i];
return result;
}

Listing 2-16.


这里的`...values`表示所有剩余的参数(在这个例子中是所有的参数)将被放入一个名为`values`的数组中。清单 2-17 中的代码添加了一个`round`参数来控制值在求和前是否应该四舍五入。

function addR(round, …values) {
let result = 0;
for (let i = 0; i < values.length; i++)
result += round ? Math.round(values[i]) : values[i];
return result;
}

let c = addR(false, 1.1, 2.9, 3.5); // c = 7.5
let d = addR(true, 1.1, 2.9, 3.5); // d = 8

Listing 2-17.


正如 rest 参数将几个参数组合成一个数组一样, *spread 语法*将数组的内容分隔成单独的参数。Spread 语法使用与 rest 参数相同的三点语法。清单 2-18 中的函数对其参数的绝对值求和;它首先获取参数的绝对值,然后使用 spread 语法调用`add`函数来计算总和。

function addAbs(…values) {
for (let i = 0; i < values.length; i++)
values[i] = Math.abs(values[i]);
return add(…values);
}

let c = addAbs(-1, -2, 3); // c = 6

Listing 2-18.


spread 语法还有许多其他用途,例如,克隆一个数组:

let a = [1, 2, 3, 4];
let b = […a]; // b = [1, 2, 3, 4]


还可以使用 spread 语法连接两个数组:

let a = [1, 2];
let b = [3, 4];
let c = […a, …b]; // c = [1, 2, 3, 4]


在某些情况下,为参数提供默认值很有用。这在 C 中是不可能的,但在 C++中可以做到,使用的语法与 JavaScript 中的相同。在 JavaScript 中,由于调用者没有传递的参数被设置为`undefined`,所以您可以为任何具有该值的参数提供默认值。清单 2-19 中的函数接受一个温度值;如果没有指定单位,则使用默认的摄氏温度。

function setCelsiusTemperature(temperature) {
trace(setCelsiusTemperature ${temperature}\n);
}

function setTemperature(temperature, units = “Celsius”) {
switch (units) {
case “Fahrenheit”:
temperature -= 32;
temperature /= 1.8;
break;
case “Kelvin”:
temperature -= 273.15;
break;
case “Celsius”:
// no conversion needed
break;
}
setCelsiusTemperature(temperature);
}

setTemperature(14); // units argument defaults to Celsius
setTemperature(14, “Celsius”);
setTemperature(57, “Fahrenheit”);

Listing 2-19.

C 语言不同,JavaScript 中的每个函数都有返回值;如果没有明确定义的返回值,函数就无法退出。考虑清单 2-20 中显示的三个函数。

function a() {
return undefined;
}
function b() {
return;
}
function c() {
}

Listing 2-20.


函数`a`显式返回值`undefined`。函数`b`没有向`return`语句提供任何值,因此返回`undefined`。函数`c`没有`return`语句,但像函数`b`一样返回`undefined`,因为`undefined`是所有函数返回的默认值。根据代码作者的偏好,您可以在 JavaScript 代码中找到所有这三种形式。函数的调用者无法区分它们。

相比之下,下面的代码在 c 中是允许的。函数`c`的结果是为返回值保留的内存或寄存器中的任何值。

int c(void) {
}
int b = c(); // b is unknown


#### 将函数作为参数传递

在 C 语言中,向一个函数传递指向另一个函数的指针是很常见的,这使您能够自定义被传递函数的行为,例如,提供一个在排序时使用的比较函数。类似地,JavaScript 函数可以作为参数传递给另一个函数,如清单 2-21 所示。

function square(a) {
return a * a;
}
function circleArea® {
return Math.PI * r * r;
}
function sum(filter, …values) {
let result = 0;
for (let i = 0; i < values.length; i++)
result += filter(values[i]);
return result;
}

let a = sum(square, 1, 2, 3); // 14
let b = sum(circleArea, 1); // 3.14…

Listing 2-21.


还可以将内置函数作为参数传递。例如,下面的代码计算其余参数的平方根之和:

let c = sum(Math.sqrt, 1, 4, 9); // 6


通常,当传递一个函数时,该函数只在那个地方使用。在 C 语言中,函数的实现经常不在它被调用的地方,这损害了可读性。与 C 不同,JavaScript 允许匿名(未命名)内联函数。以下示例调用清单 2-21 中定义的`sum`函数,使用匿名内嵌函数作为过滤器来计算等边三角形的面积之和:

let a = sum(function(a) {
return a * (a / 2);
}, 1, 2, 3); // 7


在 JavaScript 代码中,匿名函数被广泛用于各种回调。将函数的源代码视为函数调用的参数有点不寻常,但是您会习惯的。如果您喜欢将函数实现与函数调用分开,可以使用嵌套函数。在清单 2-22 中,函数`triangleArea`只在函数`main`内部可见。使用嵌套函数可以使过滤函数的实现靠近使用它的地方,这通常可以提高代码的可维护性。

function main() {
function triangleArea(a) {
return a * (a / 2);
}

let a = sum(triangleArea, 1, 2, 3);  // 7

}

Listing 2-22.


#### 声明函数

如前所述,与 CC++不同,JavaScript 中没有函数声明:当您在 JavaScript 中声明一个函数时,实际上是在声明一个变量。下面一行代码使用声明函数的通用语法,创建了一个名为`example`的局部变量:

function example() {}


下面的代码行还创建了一个名为`example`的局部变量,并为它分配了一个匿名函数:

let example = function() {};


这两行代码是等价的,两个函数都可以用相同的方式调用。但是因为两种形式都创建一个局部变量,所以不能有同名的函数和局部变量。但是,您可以更改局部变量引用的函数,如清单 2-23 所示。

function log(a) {
trace(a);
}

log(“one”);

// Disable logging
let originalLog = log;
log = function(a) {}

log(“two”);

// Reenable logging
log = originalLog;

log(“three”);

Listing 2-23.


#### 关闭

JavaScript 函数最强大的特性之一是闭包。它们通常用于回调函数。一个*闭包*将一个函数和函数外的一组变量绑定在一起。对外部变量的引用在闭包的整个生命周期中都存在。闭包在 C 中并不存在,只是在 2011 年才作为 lambda 表达式被添加到 C++中;因此,许多使用 CC++的开发人员对它们并不熟悉。尽管名字晦涩难懂,但是闭包非常容易使用,以至于很容易忘记自己正在使用它们。

清单 2-24 使用闭包来实现计数器。`makeCounter`函数返回一个函数。在 C 中,你可以让一个函数返回一个指向另一个函数的指针,但是这里有一个不同:返回的匿名函数引用了一个名为`value`的变量,而这个变量不是匿名函数的本地变量;相反,它是包含匿名函数的`makeCounter`函数中的一个局部变量。

function makeCounter() {
let value = 0;

return function() {
    value += 1;
    return value;
}

}

Listing 2-24.


每次调用由`makeCounter`返回的函数时,它增加`value`并返回该值。它是这样工作的:当一个函数引用它自己的局部范围之外的变量时,它被称为“关闭”这些变量,自动创建一个闭包。在这个例子中,在匿名函数中使用变量`value`创建一个闭包,让它从`makeCounter`访问局部变量`value`。JavaScript 使得使用局部变量变得安全,即使在`makeCounter`返回并且`makeCounter`的栈帧已经被释放之后(参见清单 2-25 )

let counter = makeCounter();

let a = counter(); // 1
let b = counter(); // 2
let c = counter(); // 3

Listing 2-25.


清单 2-25 中的例子做了你期望的事情:`makeCounter`函数返回一个计数器函数;每次调用 counter 函数时,它都会递增计数器并返回新值。但是如果你调用两次`makeCounter`会怎么样呢?第二个调用是返回一个单独的计数器还是对第一个计数器的引用?答案见清单 2-26

let counterOne = makeCounter();
let counterTwo = makeCounter();

let a = counterOne(); // 1
let b = counterOne(); // 2
let c = counterTwo(); // 1
let d = counterTwo(); // 2
let e = counterOne(); // 3
let f = counterTwo(); // 3

Listing 2-26.


如您所见,每次调用`makeCounter`时,它返回的函数都有一个新的闭包,其中有一个单独的`value`副本。

如果现在很难想象如何在自己的代码中使用闭包,不要担心;许多程序员甚至没有意识到自己在使用它们。闭包在使用回调函数的 API 中很常见;安装回调函数时,它通常会关闭调用回调函数时使用的变量。

如果您有面向对象编程的经验,您可能会认为以这种方式使用的闭包类似于对象实例,事实上它们可以用于这种目的。然而,JavaScript 有更好的选择,使用类(在本章后面介绍)。

### 目标

JavaScript 是一种面向对象的编程语言;c 不是。很少有不使用对象而使用 JavaScript 的实用方法。在本章的前几节中,即使是对数字和字符串的普通操作也需要调用 number 和 string 对象的方法。C++是一种面向对象的语言,但是 C++和 JavaScript 对对象采取非常不同的方法。例如,C++有类模板、运算符重载和多重继承——这些都不是 JavaScript 的一部分。如果你来自 C 语言,你需要学习一些关于对象的知识。如果你来自 C++,你需要学习 JavaScript 更紧凑的对象方法。好消息是,数百万开发者已经成功地使用 JavaScript 中的对象来构建网页、web 服务、移动应用和嵌入式固件。

要在 JavaScript 中创建对象,可以像在 C++中一样使用`new`关键字。JavaScript 中的所有对象都源自内置对象`Object`。下面几行创建了一个`Object`的实例:

let a = new Object();
let b = new Object;


`Object`是一种特殊的函数,称为`constructor`。当用`new`调用`Object`构造函数时,会创建一个`Object`的实例,并执行构造函数来初始化该实例。如果没有参数传递给构造函数,参数列表的括号是可选的。因此,前面两行是相同的;使用哪种形式是个人编码风格的问题。

JavaScript 中内置了许多其他对象。清单 2-27 展示了如何为其中一些调用构造函数的例子。关于这些和其他内置对象的详细信息将在本章的后面部分提供。

let a = new Array(10); // array of length 10
let b = new Date(“September 6, 2019”);
let c = new Date; // current date and time
let d = new ArrayBuffer(128); // 128-byte buffer
let e = new Error(“bad value”);

Listing 2-27.


基本对象`Object`本身并没有做太多事情。尽管如此,它在 JavaScript 代码中还是很常见的,因为它可以用作特别记录。在 C 中,你用一个结构(`struct`)来保存一组值;在 C++中,你要么使用结构,要么使用类(`struct``class`)。与 CC++中的结构不同,JavaScript 对象不是一组固定的字段。C 调用的字段在 JavaScript 中被称为*属性*。如清单 2-28 所示,你可以随时给一个对象添加属性;它们不需要事先声明。

let a = new Object;
a.one = 1;
a.two = “two”;
a.object = new Object;
a.add = function(a, b) {
return a + b;
};

Listing 2-28.


因为创建这些特设对象是如此常见,JavaScript 提供了一个快捷方式:您可以使用`{}`代替`new Object`。结果是一样的,但是代码更紧凑。您可以通过枚举大括号内的属性来初始化对象的属性。以下内容等同于前面的示例:

let a = {one: 1, two: “two”, object: {},
add: function(a, b) {return a + b;}};


JavaScript 开发人员倾向于使用大括号风格(这本书几乎专门使用它),因为它更紧凑,可读性更好。

#### 对象速记

通常将几次计算的结果存储在局部变量中,然后将它们放入一个对象中。当局部变量与对象的属性同名时,代码看起来是多余的,如清单 2-29 中的例子所示。

let one = 1;
let two = “two”;
let object = {};
let add = function(a, b) {return a + b;};
let result = {one: one, two: two, object: object, add: add};

Listing 2-29.


因为这种情况经常发生,所以 JavaScript 为它提供了一个捷径。清单 2-30 中的代码等同于前面的例子。

let one = 1;
let two = “two”;
let object = {};
let add = function(a, b) {return a + b;};
let result = {one, two, object, add};

Listing 2-30.


另一种快捷方式可用于定义以函数为值的属性。清单 2-31 展示了简单明了的方法。

let object = {
add: function(a, b) {
return a + b;
},
subtract: function(a, b) {
return a - b;
}
};

Listing 2-31.


清单 2-32 显示了快捷版本,它删除了冒号(`:`)`function`关键字。

let object = {
add(a, b) {
return a + b;
},
subtract(a, b) {
return a - b;
}
};

Listing 2-32.


除了更加简洁和易读之外,您很快就会看到,在 JavaScript 中,同样的语法也用于定义类。

#### 删除属性

您不仅可以随时向 JavaScript 对象添加属性,还可以删除它们。使用`delete`关键字删除属性:

delete a.one;
delete a.missing;


一旦删除了一个属性,从对象中获取它就会得到值`undefined`。您可能还记得,当您试图访问一个超出字符串长度的字符时,返回的是同一个值。在对象没有的属性上使用`delete`不是错误。例如,如前所示删除(具有属性`one``two``object` ) `a.missing`的给定对象`a`时,不会产生错误。

C++程序员熟悉用`delete`来销毁对象,因此可能认为删除一个属性会销毁该属性所引用的对象;然而,JavaScript 中的关键字`delete`是不同的,这将在“内存管理”一节中讨论。

#### 检查属性

因为属性可以随时出现和消失,所以有时您需要检查某个对象上是否存在某个特定的属性。有两种方法可以做到这一点。因为任何缺失的属性都有值`undefined`,所以您可以检查获取属性是否会给出值`undefined`

if (a.missing == undefined)
trace(“a does not have property ‘missing’”);


但是不要那么做!可能会出现几个微妙的问题。例如,考虑以下代码:

let a = {missing: undefined};
if (a.missing == undefined)
trace(“a does not have property ‘missing’”);


这里,对象有一个值为`undefined``missing`属性。还有其他方法可以使这个检查失败,但是现在这个例子足以证明需要一个更好的解决方案。使用关键字`in`是检查一处房产是否存在的更好方法。以下示例适用于所有情况:

if (!(“missing” in a))
trace(“a does not have property ‘missing’”);


#### 向函数添加属性

JavaScript 中的函数是对象,这意味着您可以像处理任何其他对象一样添加和删除函数的属性。清单 2-33 定义了一个名为`calculate`的函数,它支持三种运算,每种运算对应于一个被赋予常数的函数的属性:`add`1`subtract`2`multiply`3。这里定义的操作类似于 CC++中的枚举。然而,在 CC++中,操作值不是作为`enum``calculate`函数分开定义的,而是直接附加到使用它们的函数中。这种为常量提供名称的方式在可修改的 SDK 的某些部分中使用。

function calculate(operation, a, b) {
if (calculate.add == operation)
return a + b;
if (calculate.subtract == operation)
return a - b;
if (calculate.multiply == operation)
return a * b;
}
calculate.add = 1;
calculate.subtract = 2;
calculate.multiply = 3;

let a = calculate(calculate.add, 1, 2); // 3
let b = calculate(calculate.subtract, 1, 2); // -1

Listing 2-33.


#### 冷冻物体

有些情况下,您希望确保对象的属性不能被更改。您可能会尝试使用`const`来实现这一点:

const a = {
b: 1
};


然而,那是行不通的。使用`const`不会使常量声明中`=`右侧的对象成为只读的;在本例中,它只将`a`设为只读。考虑这些后续任务:

a = 3; // generates an error
a.b = 2; // OK - can change existing property
a.c = 3; // OK - can add new property


为了防止修改作为常量值的对象,您可以使用`Object.freeze`,这是一个内置函数,它使对象的所有现有属性变为只读,并防止添加新属性。正如您在清单 2-34 中看到的,试图改变冻结对象中的属性值或向对象添加新属性会产生错误。

const a = Object.freeze({
b: 1
});

a = 3; // generates an error
a.b = 2; // error - can’t change existing property
a.c = 3; // error - can’t add new property

Listing 2-34.


注意,`Object.freeze`返回传递给它的对象,这在本例中很方便,因为它避免了添加一行代码。`Object.freeze`现在很少在 web 的 JavaScript 中使用,但可修改的 SDK 广泛使用它,因为它使对象能够有效地存储在嵌入式设备的 ROM 或闪存中,从而节省有限的 RAM`Object.freeze`是一个浅层操作,这意味着它不会冻结嵌套的对象。例如,在清单 2-35 中,分配给属性`c`的嵌套对象没有被冻结。

const a = Object.freeze({
b: 1,
c: {
d: 2
}
});
a.c.d = 3; // OK
a.c.e = 4; // OK
a.b = 2; // error - can’t change existing property
a.e = 3; // error - can’t add new property

Listing 2-35.


您可以显式冻结`c`,但是这样会变得冗长且容易出错,如清单 2-36 所示。

const a = Object.freeze({
b: 1,
c: Object.freeze({
d: 2
})
});

Listing 2-36.


因为冻结对象有助于优化嵌入式设备上的内存使用,所以在可修改的 SDK 中使用的 XS JavaScript 引擎使用可选的第二个参数扩展了`Object.freeze`,从而实现了深度冻结——也就是说,递归地冻结所有嵌套的对象(参见清单 2-37 )

const a = Object.freeze({
b: 1,
c: {
d: 2
}
}, true);
a.c.d = 3; // error - can’t change existing property
a.c.e = 4; // error - can’t add new property

Listing 2-37.


注意,`Object.freeze`的这个扩展不是 JavaScript 语言标准的一部分,所以它在大多数环境中不工作。然而,它确实解决了嵌入式开发中的一个常见需求。也许 JavaScript 语言的未来版本将支持这一功能。

如果你的代码需要知道一个对象是否被冻结,你可以使用`Object.isFrozen`。和`Object.freeze`一样,这是一个浅层操作,所以它不会告诉你是否有任何嵌套的对象被冻结。

if (!Object.isFrozen(a)) {
a.b = 2;
a.c = 3;
}


冻结对象是单向操作:没有`Object.unfreeze`。这是因为`Object.freeze`有时被用作一种安全措施来防止不受信任的客户端代码篡改对象。如果不受信任的代码可以解冻对象,它将使安全措施被破坏。

### `null`CC++一样,JavaScript 代码使用值`null`。在 CC++中,这被写成`NULL`,表示它是用宏定义的;在 JavaScript 中,`null`是一个内置值。

c 使用`NULL`作为当前不引用任何东西的指针的值。JavaScript 没有指针,所以这个意思没有意义。在 JavaScript 中,`null`是一个值,表示没有对对象的引用。值`null`被认为是一个特殊的空对象,因此具有类型`Object`。

很容易把`null``undefined`搞混。它们很相似,但不完全相同:`undefined`表示没有给出任何值;`null`明确声明没有对象引用,这意味着变量或属性将在执行过程中的某个时刻保存一个对象。通常,当一个局部变量或对象属性打算引用一个对象时,在没有对象时赋值`null`。

## 比较

在 C #中比较两个值在很大程度上是很简单的,因为你通常是在比较两个相同类型的值。在少数情况下,C 语言会在比较之前应用类型转换。例如,这使您能够将一个`uint8_t`值与一个`uint32_t`值进行比较,而不必显式转换任一值的类型。C++通过提供运算符重载使比较变得更加强大,使程序员能够为他们定义的类型提供自己的比较运算符实现。在这方面,JavaScript 更像 C 而不是 c++;它不支持操作符重载,所以比较的行为完全由 JavaScript 语言定义。

与 C 类似,JavaScript 在使用等号运算符(`==`)进行比较时会隐式转换某些类型。清单 2-38 显示了几个例子。

let a = 1 == “1”; // true
let b = 0 == “”; // true
let c = 0 == false; // true
let d = “0” == false; // true
let e = 1 == true; // true
let f = 2 == true; // false
let g = Infinity == “Infinity”; // true

Listing 2-38.


正如您所看到的,在比较中如何转换类型的规则并不总是您所期望的那样。出于这个原因,JavaScript 程序员通常通过使用*严格相等运算符* ( `===`)来避免隐式转换,如清单 2-39 所示。严格相等运算符从不执行类型转换;如果这两个值是不同类型的,它们总是不相等的。

let a = 1 === “1”; // false
let b = 0 === “”; // false
let c = 0 === false; // false
let d = “0” === false; // false
let e = 1 === true; // false
let f = 2 === true; // false
let g = Infinity === “Infinity”; // false

Listing 2-39.


JavaScript 还提供了一个*严格不等式运算符* ( `!==`),可以用来代替不等式运算符(`!=`),避免类型转换:

let a = 1 !== “1”; // true
let b = 0 !== “”; // true
let c = 0 !== false; // true


在许多情况下,使用`==``!=`而不是严格版本没有坏处。然而,行为不同的边缘情况会引入难以追踪的错误。因此,JavaScript 编程中当前的最佳实践是始终使用严格版本的操作符。

本章中介绍严格比较运算符之前的一些例子使用了`==``!=`。既然您已经了解了这些操作符的严格版本以及为什么它们是首选,本书剩余部分中的示例将只使用严格操作符。

### 比较对象

在 JavaScript 中比较两个对象时,只有当它们引用同一个实例时才相等。这通常是您所期望的,尽管有时开发人员错误地期望如果两个不同实例的所有属性都相等,则相等比较的结果是`true`。JavaScript 没有直接提供这种深度比较,但是如果需要的话,可以在应用程序中实现。

let a = {b: 1};
let b = a === {b: 1}; // false
let c = a;
let d = a === c; // true

C++中,比较对象的默认行为与 JavaScript 中的相同。使用运算符重载,如果类实现了支持,C++程序员可以执行深度比较。

## 错误和异常

JavaScript 包含一个内置的`Error`类型,用于报告执行过程中出现的问题。错误几乎只与 JavaScript 的异常机制一起使用,这在许多方面与 C++异常类似。C 语言不包含异常,尽管类似的功能经常使用 C 标准库中的`setjmp``longjmp`来构建。

要创建一个错误,调用`Error`构造函数。为了帮助调试,您可以提供一个可选的错误消息。

let a = new Error;
let b = new Error(“invalid value”);


还有其他种类的错误,用于指示特定的问题。这些包括`RangeError``TypeError``ReferenceError`。你使用它们的方式和`Error`一样。最常见的是简单地使用`Error`,但是如果其他的适合你的情况,你也可以使用。

一旦你有一个错误,你使用一个`throw`语句报告它(清单 2-40 )

function setTemperature(value) {
if (value < 0)
throw new RangeError(“too cold”);

}

Listing 2-40.


您可以在`throw`语句后指定任何值,尽管按照惯例,该值通常是一个错误的实例。

当抛出异常时,当前执行路径结束。在栈上的第一个`catch`块处继续执行。如果栈上没有`catch`块,该异常被认为是未处理的异常。未处理的异常被忽略,这意味着宿主不会尝试处理异常。为了捕捉异常,你可以像在 C++中一样编写`try``catch`块;清单 2-41 来自前面的例子来说明这一点。

try {
setTemperature(-1); // throws an exception
// Execution never reaches here
displayMessage(“Temperature set to -1\n”);
}
catch (e) {
trace(setTemperature failed: ${e}\n);
}

Listing 2-41.


在本例中,当`setTemperature`生成异常时,执行跳转到`catch`块,跳过对`displayMessage`的调用。清单 2-40 中所示的`throw`语句的参数——由`setTemperature`函数创建的`RangeError`实例——在这里提供在名为`e`的局部变量中,该变量在`catch`关键字后面的括号中指定。如果您的`catch`块不使用那个值,您可以省略`catch`后面的括号,如清单 2-42 所示。

try {
setTemperature(-1); // throws an exception
// Execution never reaches here
displayMessage(“Temperature set to -1\n”);
}
catch {
trace(“setTemperature failed\n”);
}

Listing 2-42.


在捕获错误之后,您可以选择传播它,就像它没有被捕获一样。如果您希望在错误发生时执行清理,并且错误还需要由调用栈中更高层的代码来处理,这将非常有用。为了传播异常,使用`catch`块中的`throw`语句(列出了 2-43 )

try {
setTemperature(-1); // throws an exception
// Execution never reaches here
displayMessage(“Temperature set to -1”);
}
catch (e) {
trace(setTemperature failed: ${e}\n);
throw e;
}

Listing 2-43.


您的异常处理可能还包括一个`finally`块,如清单 2-44 所示。(标准 C++不提供`finally`,但它是微软 C++方言的一部分。)无论异常是如何处理的,或者即使它没有被`catch`块捕获,总是会调用`finally`块。

try {
setTemperature(-1);
}
catch (e) {
trace(setTemperature failed: ${e}\n);
throw e;
}
finally {
displayMessage(Temperature set to ${getTemperature()}\n); // always executes
}

Listing 2-44.


在清单 2-44 中,不管`setTemperature`是否抛出异常,对`displayMessage`的调用都会发生。当使用`finally`时,可以省略`catch`(清单 2-45 ,在这种情况下,在`finally`块执行后,异常将继续在栈中向上传播。

try {
setTemperature(-1);
}
finally {
displayMessage(Temperature set to ${getTemperature()});
}

Listing 2-45.


当一个异常没有被处理时——例如,当`setTemperature`在清单 2-442-45 中抛出异常时——一个警告被追踪到调试控制台。让一个异常被捕获并不一定是错误的,但是它可能是一个问题的标志。警告可以包括检测到未捕获异常的函数的名称;这是一个本地函数,通常是可修改的 SDK 运行时的一部分,所以这个名字可能不熟悉。

虽然这些例子在`try`块中只有几行代码,但真实世界的代码通常在单个`try`块中有大量代码。这使您能够将处理错误的代码保持在较小且独立的范围内,而不是像 c #中那样,让它成为每个函数调用的一部分。

`try``catch``finally`块的组合为您的代码如何响应或不响应异常提供了很大的灵活性。当你开始使用它们时,不要太担心。编写没有异常处理的代码,然后在处理失败案例时添加它,这是很常见的。

## 班级

像 C++一样,JavaScript 允许你通过定义类来创建你自己的对象。在 JavaScript 中,使用`class`关键字来定义和实现类。JavaScript 中的类比 C++中的要简单得多。即使您不期望创建自己的类,您也应该熟悉 JavaScript 类,这样您就能够理解他人编写的代码。

JavaScript 的早期版本没有`class`关键字,这使得创建类更加困难。2015 年第六版语言标准(通常称为“ES6)中引入了该关键字。在此之前,JavaScript 开发人员使用底层方法创建类,包括`Object.create`,或者直接操纵对象的`prototype`属性。虽然这些技术仍然有效,并且在 web 中的遗留代码中很常见,但本节将重点介绍现代 JavaScript,其中`class`使代码更具可读性,并且对运行时性能没有影响。

### 类构造函数和方法

清单 2-46 显示了一个简单的类`Bulb`,代表一个可以开也可以关的灯泡。

class Bulb {
constructor(name) {
this.name = name;
this.on = false;
}
turnOn() {
this.on = true;
}
turnOff() {
this.on = false;
}
toString() {
return "${this.name}" is ${this.on ? "on" : "off"};
}
}

Listing 2-46.


不像在 C++中,没有类的声明;只有一个实现。用于定义类中函数的语法与您已经看到的类外函数的语法相同(在“对象速记”一节中)。然而,与函数在普通对象中被定义为属性不同,在类中函数之间没有逗号。

正如你在清单 2-46 中看到的,`Bulb`类是一个函数集合。类中名为`constructor`的函数是特殊的;创建对象时会自动调用它。构造函数在将新实例返回给创建者之前执行任何必要的初始化。下面的代码创建了一个`Bulb`的实例:

let wallLight = new Bulb(“wall light”);
wallLight.turnOn();


JavaScript 类中的另一个特殊函数是`toString`。当 JavaScript 需要对象的字符串表示时,会自动调用这个函数。`Bulb``toString`方法提供了当前状态的摘要,这对调试很有用。

let wallLight = new Bulb(“wall light”);
wallLight.turnOn();
trace(wallLight);
// output: “wall light” is on


因为`trace`函数输出字符串,所以它将其参数转换为字符串,这将调用`toString`方法。你也可以直接给`toString`打电话,比如`wallLight.toString()``toString`方法是 JavaScript 中的一个特例;没有其他转换函数,比如`toNumber`。

Note

类构造函数的调用必须发生在定义类之后。这意味着你只能在定义了清单 2-46 中的类之后调用`new Bulb`,而不是之前。在此之前调用它会抛出一个运行时异常,并显示消息`get Bulb: not initialized yet!`。

### 静态方法

与 C++一样,JavaScript 类可能包含静态方法,这意味着通过类而不是实例来访问函数。静态方法的一个简单例子是返回实现的版本(清单 2-47 )

class Bulb {
… // as earlier
static getVersion() {
return 1.2;
}
}

Listing 2-47.


静态方法附加到该类,因此甚至可以在创建实例之前调用。

if (Bulb.getVersion() < 1.5)
throw new Error(“incompatible version”);


### 子类

类的大部分功能来自于创建子类的能力。在 JavaScript 中,使用`extends`关键字创建一个子类。清单 2-48 中的代码将`DimmableBulb`实现为清单 2-46 中定义的`Bulb`类的子类。

class DimmableBulb extends Bulb {
constructor(name) {
super(name);
this.dimming = 100;
}
setDimming(value) {
if ((value < 0) || (value > 100))
throw new RangeError(“bad dimming value”);
this.dimming = value;
}
}

Listing 2-48.


正如你对子类的期望,`DimmableBulb`类从`Bulb`继承了`turnOff``turnOn`方法。`constructor`函数需要一些解释。它立即用传递给它的相同参数调用`super`。在 JavaScript 类中,`super`是对超类的构造函数的引用——这里是`Bulb`的构造函数。因此,`DimmableBulb`构造函数执行的第一个任务是构造它的超类`Bulb`。

虽然子类的构造函数可以在调用其超类的构造函数之前执行计算,但它最终必须调用它。在此之前,`this`是未定义的,因此任何获取或设置实例属性的尝试都将失败。例如,修改清单 2-49 所示的`DimmableBulb`构造函数,当它试图设置`dimming`属性时会产生一个异常,因为`this`还不可用。

class DimmableBulb extends Bulb {
constructor(name) {
this.dimming = 100; // throws an exception
super(name);
}

}

Listing 2-49.


`DimmableBulb`实现也从`Bulb`继承了`toString`方法。`toString`的实现为`Bulb`不打印调光等级;`DimmableBulb`(清单 2-50 )`toString`的实现通过首先调用`Bulb`(`super`指定)中的`toString`方法,然后将调光级别附加到结果中,从而添加调光级别。

class DimmableBulb extends Bulb {

toString() {
return super.toString() +
with dimming ${this.dimming};
}
}

Listing 2-50.


内置的`Object`类是所有 JavaScript 类的最终超类。`Bulb`类直接继承自`Object`。这是由它的实现中缺少一个`extends`子句所暗示的,但是它也可以被明确地声明,如清单 2-51 所示。

class Bulb extends Object {
constructor(name) {
super();
this.name = name;
this.on = false;
}

}

Listing 2-51.


注意,因为`Bulb`现在显式扩展了`Object`,`Bulb`构造函数必须通过调用`super`来调用它所扩展的类的构造函数。如果省略对`super`的调用,访问`this`会抛出一个异常,并显示消息`Bulb: this is not initialized yet!`。

为了保持源代码简洁,直接从`Object`继承的类通常不这样写。但是这个例子暗示了 JavaScript 类的另一个特性:继承内置对象的能力。清单 2-52 中的例子子类化了内置的`Array`(你将很快了解到更多)来添加寻找数组中值的总数和平均值的方法。

class MyArray extends Array {
sum() {
let total = 0;
for (let i = 0; i < this.length; i++)
total += this[i];
return total;
}
average() {
return this.sum() / this.length;
}
}

let a = new MyArray;
a[0] = 1;
a[1] = 2;
let b = a.sum(); // 3
let c = a.average(); // 1.5

Listing 2-52.


在构建产品时,您可能有不止一个`Bulb`实例。例如,您可能正在制作一个控制几个灯泡的灯开关,并且您可能将灯的列表保存在一个数组中。为此,您可以创建一个`Array`的子类,该子类(清单 2-53 中的例子中的`Bulbs`)提供对灯泡的批处理操作。

class Bulbs extends Array {
allOn() {
for (let i = 0; i < this.length; i++)
this[i].turnOn();
}
allOff() {
for (let i = 0; i < this.length; i++)
this[i].turnOff();
}
}

let bulbs = new Bulbs;
bulbs[0] = new Bulb(“hall light”);
bulbs[1] = new DimmableBulb(“wall light”);
bulbs[2] = new DimmableBulb(“floor light”);
bulbs.allOn();

Listing 2-53.

`Bulbs`中有一个`dimAll`方法当然很好,但是这只适用于`DimmableBulb`的实例;在`Bulb`的实例上调用`setDimming`会抛出异常,因为该方法不存在。JavaScript `instanceof`操作符在这里很有帮助,它使您能够确定一个实例是否对应于一个特定的类(清单 2-54 )

let a = new Bulb(“hall light”);
let b = new DimmableBulb(“wall light”);
let c = a instanceof Bulb; // true
let d = b instanceof Bulb; // true
let e = a instanceof DimmableBulb; // false
let f = b instanceof DimmableBulb; // true

Listing 2-54.


如您所见,`instanceof`检查指定的类,包括它的超类。在清单 2-54 的例子中,这意味着`b``DimmableBulb``Bulb`的实例,因为`Bulb``DimmableBulb`的超类。有了这些知识,现在就可以实现`dimAll`(清单 2-55 )

class Bulbs extends Array {

dimAll(value) {
for (let i = 0; i < this.length; i++) {
if (this[i] instanceof DimmableBulb)
this[i].setDimming(value);
}
}
}

Listing 2-55.


`Bulb`实例的属性是普通的 JavaScript 属性,使它们对类实现和使用该类的代码都可用:

let wallLight = new Bulb(“wall light”);
wallLight.turnOn();
trace(Light on: ${wallLight.on}\n);


这很有用,但有时您希望在实现中使用与 API 中不同的值表示。例如,`setDimming`方法接受从 0100 的值,因为百分比是描述调光水平的自然方式;然而,实现可能更喜欢存储从 01.0 的值,因为这样对其内部计算更有效。JavaScript 类支持对这类转换有用的 getters 和 setters。清单 2-56 中的实现用`dimming`属性的 getter 和 setter 替换了`setDimming`方法。

class DimmableBulb extends Bulb {
constructor(name) {
super(name);
this._dimming = 1.0;
}
set dimming(value) {
if ((value < 0) || (value > 100))
throw new RangeError(“bad dimming value”);
this._dimming = value / 100;
}
get dimming() {
return this._dimming * 100;
}
}

let a = new DimmableBulb(“hall light”);
a.dimming = 50;
a.dimming = a.dimming / 2;

Listing 2-56.


该类的用户将`dimming`属性作为普通的 JavaScript 属性来访问。然而,当设置属性时,调用该类的`set dimming` setter 方法,当读取属性时,调用`get dimming` getter 方法。

### 私有字段

清单 2-56 中的 getter 和 setter 将值存储在名为`_dimming`的属性中。JavaScript 代码长期以来一直在属性名的开头使用下划线(`_`)来表示它们仅供内部使用。与 C++不同,JavaScript 没有在类中提供私有字段。将私有字段添加到 JavaScript 标准的工作即将完成;本节介绍私有字段,因为它们被认为是 JavaScript 标准的一部分。XS JavaScript 引擎支持私有字段,以便在嵌入式开发中使用。

JavaScript 中的私有字段通过在字段名前加一个散列字符(`#`)来表示。私有字段必须在类体中声明。清单 2-57 显示了清单 2-56`DimmableBulb`的版本,它被重写为使用一个名为`#dimming`的私有字段来代替`_dimming`

class DimmableBulb extends Bulb {
#dimming = 1.0;

set dimming(value) {
    if ((value < 0) || (value > 100))
        throw new RangeError("bad dimming value");
    this.#dimming = value / 100;
}
get dimming() {
    return this.#dimming * 100;
}

}

let a = new DimmableBulb(“hall light”);
a.dimming = 50;
a.dimming = a.dimming / 2;
a.#dimming = 100; // error

Listing 2-57.


注意私有字段`#dimming`在类体的声明中被初始化为 1.0。这是可选的;相反,它可以在构造函数中初始化。在初始化之前,它的值为`undefined`。

还要注意清单 2-57 中的例子完全删除了构造函数。这在这里是可能的,因为`#dimming`已经被初始化了。由于`DimmableBulb`继承自`Bulb`,当`DimmableBulb`上没有构造函数时,创建实例时会自动调用`Bulb`的构造函数。正如你对 C++的期望,类外的代码不能访问私有字段;因此,试图给`#dimming`赋值的示例的最后一行产生了一个错误。

JavaScript 不支持 C++ `friends``protected`类特性。类的私有属性只能由类体内的代码直接访问。私有字段是真正私有的,即使对子类和超类也是不可见的。

### 私有方法

除了私有字段,JavaScript 语言标准还增加了*私有方法*——只能从类的实现中调用的函数。例如,清单 2-58 中的`DimmableBulb`类有一个私有的`#log`方法。

class DimmableBulb extends Bulb {
#dimming = 1.0;

set dimming(value) {
    if ((value < 0) || (value > 100))
        throw new RangeError("bad dimming value");
    this.#dimming = value / 100;

    this.#log(`set dimming ${this.#dimming}`);
}
get dimming() {
    this.#log("get dimming");
    return this.#dimming * 100;
}
#log(msg) {
    trace(msg);
}

}

let a = new DimmableBulb(“hall light”);
a.#log(“test”); // error

Listing 2-58.


### 在类中使用回调函数

有时,类实现会将函数作为回调传递给 API。一个常见的例子是当一个 API 使用一个定时器来延迟一个动作到未来。JavaScript web 开发人员通常使用`setTimeout`来实现这个目的;在嵌入式 JavaScript 中,对应的是`Timer.set`。清单 2-59 中的例子向`Bulb`类添加了一个方法,在指定的时间间隔过后打开或关闭灯。

class Bulb {

setOnAfter(value, delayInMS) {
let bulb = this;
Timer.set(function() {
if (value)
bulb.turnOn();
else
bulb.turnOff();
}, delayInMS);
}
}

Listing 2-59.


`setOnAfter`方法用两个参数调用`Timer.set`:一个在定时器到期后执行的匿名函数和以毫秒为单位的等待时间。回调函数使用闭包来访问`bulb`;这是必要的,因为回调中的`this`的值不是调用`setOnAfter`的灯泡的实例,而是全局对象(`globalThis`)。这段代码可以工作,但是 JavaScript 有更好的工具来实现这个功能。

像现代 C++一样,现代 JavaScript 也有 *lambda 函数*——通常称为 *arrow 函数*,因为用于声明它们的`=>`语法。像闭包一样,箭头函数有点难理解,但是很容易使用。当一个箭头函数被调用时,它的`this`值与定义该箭头函数的函数的`this`值相同。箭头函数的这一特性被称为*词法* `this`,因为箭头函数内的`this`的值取自封闭函数。

箭头函数很受欢迎,因为它们保持了`this`的值,并且在源代码中更简洁。清单 2-60 中的例子展示了使用`function`关键字和箭头函数语法的相同函数。

function randomTo100() {
return Math.random() * 100;
}
let randomTo100 = () => Math.random() * 100;

function cube(a) {
return a * a * a;
}
let cube = a => a * a * a;

function add(a, b) {
return a + b;
}
let add = (a, b) => a + b;

function upperFirst(str) {
let first = str[0].toUpperCase();
return first + str.slice(1);
}
let upperFirst = str => {
let first = str[0].toUpperCase();
return first + str.slice(1);
};

Listing 2-60.


清单 2-60 中的所有示例对在功能上都是等价的,除了函数中`this`的值;但是,示例中没有使用`this`。清单 2-61 中的代码使用了一个箭头函数,通过利用词法`this`消除局部变量`bulb`来改进`setOnAfter`(在清单 2-59)的实现。使用这种方法,回调的代码能够像类方法一样使用`this`

class Bulb {

setOnAfter(value, delayInMS) {
Timer.set(
() => value ? this.turnOn() : this.turnOff(),
delayInMS
);
}
}

Listing 2-61.


熟悉箭头函数很重要,因为它们在 JavaScript 中非常常见。你会在本书的一些例子中遇到它们。请记住,箭头函数不仅仅是编写函数源代码的一种替代方式;它们还会改变函数中`this`的值。

## 模块

模块是 JavaScript 中打包代码库的机制。CC++中的 JavaScript 模块和共享或动态库有一些相似之处:两者都指定了共享有限数量的类、函数和值的导出;两者都可以从其他库中导入类、函数和值。像 C 中的动态库一样,JavaScript 模块是在运行时加载的。还有许多不同之处,包括没有与静态链接 C 库等价的 JavaScript。

### 从模块导入

要使用模块提供的功能,必须首先导入相应的类、函数或值。从一个模块导入有许多不同的方法,这种方法非常灵活,可以让您控制导入的内容以及如何命名这些导入。

上一节中的例子使用了`Timer`类,但没有显示它的来源。`Timer`类包含在`timer`模块中。要从一个模块导入,可以使用`import`语句。

import Timer from “timer”;

Timer.set(() => trace(“done”), 1000);


在 JavaScript 中,`import`语句的特殊之处在于它在所有其他代码之前执行。习惯上把`import`语句放在源代码的顶部,就像 C 语言中的`include`语句一样,但是即使它们不是第一个,它们仍然会首先执行。

前面形式的`import`语句由两部分组成:

*   存储导入的变量的名称。这里是`Timer`,但是你可以用任何你喜欢的名字。选择名称的能力有助于避免名称冲突,尤其是在处理许多模块时。

*`from`关键字之后,是模块说明符。这里是`"timer"`。

像`"timer"`一样不是路径的模块说明符被称为*裸模块说明符* **对于嵌入式 JavaScript,这些比较常见;事实上,这本书只使用了裸模块说明符。原因之一是嵌入式设备中通常没有文件系统来解析路径。相比之下,web 上的 JavaScript 目前只使用模块说明符的路径,所以你会看到带有`from`子句的`import`语句,比如`from "./modules/timer.js"`。

前面展示的`import`语句的形式导入了`timer`模块的默认导出。每个模块都有一个默认的导出。一些模块有额外的出口;例如,在第三章中使用的`http`模块导出了一个`Request`类和一个`Server`类。清单 2-62 展示了从`http`导入这些非默认导出的不同方式。

import {Server} from “http”; // server only
new Server;

import {Request} from “http”; // client only
new Request;

import {Server, Request} from “http”;
new Server;
new Request;

Listing 2-62.


您可以使用`as`关键字来重命名您导入的非默认导出。清单 2-63`Server`更名为`HTTPServer``Request`更名为`HTTPClient`

import {Server as HTTPServer, Request as HTTPClient} from “http”;
new HTTPServer;
new HTTPClient;
new Request; // fails, Request is undefined
new Server; // fails, Server is undefined

Listing 2-63.


如果您喜欢可读性,您可以在多个`import`语句中使用相同的模块说明符:

import {Server as HTTPServer} from “http”;
import {Request as HTTPClient} from “http”;


您也可以从模块导入所有导出。执行此操作时,您将导入分配给一个对象。通过避免名称冲突,JavaScript 的这一特性起到了与 C++中的名称空间相似的作用。

import * as HTTP from “http”;
new HTTP.Server;
new HTTP.Request;


一旦从模块中导入了一个类,就可以像在同一个源文件中声明的类或 JavaScript 内置类一样使用它。如您所见,您可以用`new`操作符实例化该类。您也可以创建导入类的子类:

import {Request} from “http”;
class MyRequest extends Request {

}


### 从模块导出

当你开始编写自己的类时,你会想把它们打包到你自己的模块中;这些模块需要导出它们的类,以便它们可以被其他代码使用(并且函数和值同样可以被导出)。下面一行使用`export`语句提供`Bulb`类作为模块的默认导出:

export default Bulb;


您可以选择将`export`语句放在类的声明之前,这意味着您可以将`export default``Bulb`类的定义结合起来,如下所示:

export default class Bulb {

}


这种方法是有效的,但不太常见。当前的 JavaScript 最佳实践建议将所有的`import`语句放在源文件的开头,将所有的`export`语句放在末尾,这样代码更容易阅读和维护。

以下示例显示了提供`Bulb``DimmableBulb`的非默认导出的两种方式:

export {Bulb};
export {DimmableBulb};

export {Bulb, DimmableBulb};

`import`语句一样,`export`语句可以使用`as`来执行重命名。当您希望导出与实现中使用的名称不同的名称时,这很有用。

export {Bulb as BULB, DimmableBulb as DIMMABLEBULB};


一个模块访问另一个模块内容的唯一方式是通过它的导出。不能直接访问未导出的类、函数和值。它们相当于 CC++中使用`static`关键字定义的类、函数和值,但是有一个重要的区别:CC++中,默认情况下,除非声明了`static`,否则所有内容都会被导出,而在 JavaScript 中,除了由`export`语句指示的内容,其他内容都不会被导出。JavaScript 方法——一个白名单,而不是 C 语言中的黑名单——通过避免意外的导出,有助于安全性和可维护性。

### ECMAScript 模块与 CommonJS 模块

本书中使用的模块是 JavaScript 语言规范的一部分。它们有时被称为 ECMAScript 模块,或 ESM。在模块被添加到官方规范之前,一个名为 CommonJS 的模块系统在一些环境中使用,特别是在 Node.js 中。然而,它们不能在本书使用的主机中工作,大多数环境(包括 Node.js)都在向标准 JavaScript 模块迁移。

## 全球

像 CC++一样,JavaScript 也有全局变量。其中一些你已经用过了,比如`Object``Array``ArrayBuffer`。这些内置类被分配给与类同名的全局变量。您只需使用它们的名称就可以访问这些全局变量。如果该名称不在当前范围内,则使用全局变量。如果没有该名称的全局变量,将会生成一个错误。这类似于在 c #中访问不存在的全局变量时产生的链接错误。

function example() {
let a = Date; // OK, Date is built in
let b = DateTime; // error, DateTime is not defined
}

CC++中,通过在源代码文件的顶级范围声明变量来创建全局变量。除非标记为静态,否则该变量对静态链接到该文件的所有代码都是可见的。在 JavaScript 中,你必须明确地创建一个全局变量。要在 JavaScript 中定义一个新的全局变量,需要将它添加到一个名为`globalThis`的对象中。下面的代码行创建了一个名为`AppName`的全局变量,并设置了它的初始值:

globalThis.AppName = “light bulb”;


一旦定义了全局变量,您可以通过只声明它的名称来隐式地访问它,或者通过从`globalThis`读取属性来显式地访问它:

AppName = “Light Bulb”;
globalThis.AppName += " App";


如果你想知道一个特定的全局变量是否已经被定义,使用本章“对象”一节中介绍的`in`关键字。

if (“AppName” in globalThis)
trace(AppName is ${AppName}\n);
else
trace(“AppName not available”);


与使用`delete`操作符从对象中删除属性的方式相同,您可以删除全局变量:

delete globalThis.AppName;


注意,`globalThis`对象的原名是`global`,更容易记忆和键入;出于兼容性原因,对其进行了更改。一些环境支持将`global`作为`globalThis`的别名。

当你使用模块时,它们似乎有全局变量。考虑清单 2-64 中的模块示例。

let counter = 0;

function count() {
return ++counter;
}

export default count;

Listing 2-64.


在顶级作用域声明`counter`变量的方式,看起来像是 CC++中的全局变量声明,但事实并非如此。`counter`变量是模块私有的,因为它没有被显式导出。这些变量是模块的局部变量。在 CC++中,通过在变量声明前加上`static`来限制其对当前源代码文件的可见性,可以获得相同的结果。

## 数组

在 CC++中,任何指向类型(`char *`)或结构(`struct DataRecord *`)的指针都可以访问单个元素(`*ptr`)或数组(`ptr[0]``ptr[1]`)。指针的这些使用可能会导致错误,例如,写入超出为数组保留的内存末尾的索引。为了帮助避免使用数组的一些危险,C++提供了一个`std:array`类模板,它还提供了迭代器和其他常见的帮助函数。JavaScript 的内置`Array`对象更像 C++中的`std:array`,因为它被设计为安全的,并且它提供了许多帮助函数来执行常见的操作。

与 CC++不同,JavaScript 不会在数组实例化时永久设置元素的数量;数组可能包含数量可变的元素。调用构造函数时,可以选择指示元素的数量。

let a = new Array; // empty array
let b = new Array(10); // 10-element array


正如您可能已经猜到的,所有的数组条目都被初始化为`undefined`。请注意,创建数组时,没有指明它将保存的数据类型。这是因为每个数组元素可能包含任何值;这些值不需要属于同一类型。

### 数组速记

因为创建数组非常常见,所以 JavaScript 提供了一种快捷方式:

let a = []; // empty array


使用此快捷语法,您可以提供数组的初始值:

let a = [0, 1, 2];
let b = [undefined, 1, “two”, {three: 3}, [4]];


### 访问数组的元素

访问数组元素使用与 CC++中相同的语法。元素从 0 开始编号。

let a = [0, 1, 2];
a[0] += 1;
trace(a[0]);
a[1] = a[2];


读取数组末尾以外的值会返回`undefined`。在数组末尾之外写入一个值会创建该值,从而扩展数组的长度。

let sparse = [0, 1, 2];
sparse[3] = 3;
sparse[1_000_000] = “big array”;


您可能会认为,在内存有限的微控制器上,对数组的第 100 万个元素的赋值将会失败:ESP8266 只有大约 64 KBRAM,因此它如何保存包含 100 万个元素的数组呢?然而赋值成功了,访问`sparse[1_000_000]`返回`"big array"`。这是怎么回事?

JavaScript 中的数组可能是*稀疏的,*意味着不是所有的元素都必须存在。任何不存在的元素的值都是`undefined`。在这里的数组`sparse`中,只有五个元素,它们恰好位于索引 01231000000 处。

数组有一个`length`属性,它指示数组中元素的数量。例如,长度用于迭代数组中的元素。对于稀疏数组,`length`不是具有赋值的元素的数量,而是比具有赋值的最高索引多 1。在这里的数组`sparse`的例子中,`length`1000001,尽管只有五个元素被赋值。

设置`length`属性会改变数组。将其设置为较小的值会截断数组。以下代码将前面的`sparse`数组截断为四个元素:

sparse.length = 4; // [0, 1, 2, 3]


将数组的`length`属性设置为更大的值不会改变数组的内容。

### 遍历数组

如清单 2-65 所示,您可以使用`length`属性通过`for`循环迭代数组中的元素,就像在 CC++中一样。

let a = [0, 1, 2, 3, 4, 5];
let total = 0;
for (let i = 0; i < a.length; i++)
total += a[i];

Listing 2-65.


不使用 C 风格的`for`循环,可以使用 JavaScript `for` - `of`循环。

for (let value of a)
total += value;


`for` - `of`循环方法更加紧凑,消除了管理值`i`和在数组`a[i]`中查找值的代码。C 风格的`for`循环和`for` - `in`循环都遍历从索引 0 到数组长度的所有值,即使对于有未赋值的值的稀疏数组也是如此。因为未赋值的值有一个值`undefined`,所以在清单 2-66 的代码末尾`total`有一个值`NaN`

let a = [0, 1, 2, 3, 4, 5];
a[1_000_000] = 6;
let total = 0;
for (let i in a)
total += a[i];

Listing 2-66.


您可以修改此代码以忽略值为`undefined`的数组元素,如下所示:

for (let i in a)
total += (undefined === a[i]) ? 0 : a[i];


另一个解决方案是使用一个`for` - `of`循环来迭代数组中的值,这个循环只包含有赋值的数组元素,如清单 2-67 所示;这段代码末尾的`total`的值是 21,而不是清单 2-66 中的`NaN`

let a = [0, 1, 2, 3, 4, 5];
a[1_000_000] = 6;
let total = 0;
for (let value of a)
total += value;

Listing 2-67.


`Array`对象也有以多种不同方式迭代数组的方法,每种方法都使用一个回调函数。`forEach`方法类似于`for` - `in`循环(见清单 2-68 )。像`for` - `of`循环一样,这个方法跳过没有赋值的数组元素。

let a = [0, 1, 2, 3, 4, 5];
let total = 0;
a.forEach(function(value) {
total += value;
});

Listing 2-68.


使用箭头函数将迭代代码减少到一行:

let a = [0, 1, 2, 3, 4, 5];
let total = 0;
a.forEach(value => total += value);


正如您可能猜到的,在 JavaScript 中,并不是所有迭代数组的方法都同样有效。例如,`forEach`方法是最紧凑的代码,但是需要对每个元素进行函数调用,这会增加开销。对于小型数组,使用最方便的方法;对于大型阵列,测量不同方法的性能以找到最快的方法是值得的。

当您需要对数组的每个元素执行操作时,`map`方法非常有用。它对每个元素调用回调,并返回包含结果的新数组。下面的示例创建一个包含原始数组中值的平方的数组。为每个元素调用的 arrow 函数使用求幂运算符(`**`)来计算平方。

let a = [-2, -1, 0, 1, 2];
let b = a.map(value => value ** 2); // [4, 1, 0, 1, 4]


### 添加和移除数组元素

因为 JavaScript 数组不是固定长度的,所以它们的用途不仅仅是简单的有序列表。`push``pop`函数使你能够使用一个数组作为栈(后进先出),如清单 2-69 所示。

let stack = [];
stack.push(“a”);
stack.push(“b”);
stack.push(“c”);
let c = stack.pop(); // “c”
stack.push(“d”);
let d = stack.pop(); // “d”
let b = stack.pop(); // “b”

Listing 2-69.


使用`unshift``pop`函数,您可以将数组用作队列(先进先出)。函数的作用是:将数值添加到一个数组的开头;见清单 2-70 (还有`shift`,它从数组中移除第一项。)

let queue = [];
queue.unshift(“first”);
queue.unshift(“second”);
let a = queue.pop(); // “first”
queue.unshift(“third”);
let b = queue.pop(); // “second”

Listing 2-70.


使用`unshift``pop`来添加和删除队列的元素是有用的,但并不完全直观。如果这些函数的名字对队列来说更有意义,那么它们会更容易使用;你可以通过创建`Array`的子类来实现,如清单 2-71 所示。

class Queue extends Array {
add(element) {
this.unshift(element);
}
remove(element) {
return this.pop();
}
}

let queue = new Queue;
queue.add(“first”);
queue.add(“second”);
let a = queue.remove(); // “first”
queue.add(“third”);
let b = queue.remove(); // “second”

Listing 2-71.


要将一个数组的一部分提取到另一个数组中,使用`slice`函数。当使用`slice`提取字符串的一部分时,需要两个参数:起始和结束索引(其中结束索引是结束提取之前的索引)。如果省略结束索引,则使用字符串的长度。`slice`函数从不改变它所操作的数组的内容。

let a = [0, 1, 2, 3, 4, 5];
let b = a.slice(0, 2); // [0, 1]
let c = a.slice(2, 4); // [2, 3]


要删除数组的一部分,使用`splice`。名字`splice``slice`非常相似,两者的操作也很相似:它们采用相同的参数,并且两个函数都返回一个数组,该数组包含由参数标识的数组部分。然而,`splice`也会从原始数组中删除元素。

let a = [0, 1, 2, 3, 4, 5];
let b = a.splice(0, 2); // [0, 1]
let c = a.splice(0, 2); // [2, 3]
// a = [4, 5] here


### 搜索数组

在数组中搜索特定的值是很常见的,有几个函数可以帮助实现这一点。如清单 2-72 所示,您可以使用`indexOf`从数组的开头开始搜索,或者使用`lastIndexOf`从末尾开始搜索。第一个参数是要搜索的值;可选的第二个参数指示在数组中开始搜索的索引。如果没有找到值,两个函数都返回–1

let a = [0, 1, 2, 3, 2, 1, 0];
let b = a.indexOf(1); // 1
let c = a.lastIndexOf(1); // 5
let d = a.indexOf(1, 3); // 5
let e = a.lastIndexOf(1, 3); // 1
let f = a.indexOf(“one”); // –1

Listing 2-72.


`indexOf``lastIndexOf`函数使用严格的等式运算符来测试是否找到匹配。如果您想应用不同的测试,使用`findIndex`函数,它调用一个回调函数来测试匹配。以下示例执行不区分大小写的匹配:

let a = [“Zero”, “One”, “Two”];
let search = “one”;
let b = a.findIndex(value =>
value.toLowerCase() === search); // 1


### 排序数组

排序是对数组的另一种常见操作。数组上的`sort`函数类似于 CC++中的`qsort`函数,尽管它可能使用不同的排序算法来实现。像`qsort`一样,JavaScript 的`sort`就地运行,所以没有创建新的数组。内置的`sort`函数的默认行为是将数组值作为字符串进行比较。

let a = [“Zero”, “One”, “Two”];
a.sort();
// [“One”, “Two”, “Zero”]


要实现其他行为,您需要提供一个回调函数来执行比较。比较类似于 CC++ `qsort`函数中回调函数的比较,接收两个值进行比较,根据比较结果返回负数、0 或正数。例如,以下代码对数字数组进行排序:

let a = [0, 1, 2, 3, 2, 1, 0];
a.sort((x, y) => x - y);
// [0, 0, 1, 1, 2, 2, 3]


清单 2-73 中的例子使用了一个更复杂的比较函数来对字符串进行不区分大小写的排序。

let a = [“Zero”, “zero”, “two”, “Two”];
a.sort();
// [“Two”, “Zero”, “two”, “zero”]
a.sort((x, y) => {
x = x.toLowerCase();
y = y.toLowerCase();
if (x > y)
return +1;
if (x < y)
return -1;
return 0;
});
// [“Two”, “two”, “Zero”, “zero”]

Listing 2-73.


## 二进制数据

JavaScript 并不总是支持二进制数据,不像 CC 从一开始就支持包含原生整数类型的内存缓冲区。在 C 语言中,你首先要学习的事情之一是如何用`malloc`分配内存,以及如何用数组和其他数据结构填充内存。直接操作内存缓冲区的能力对于许多类型的嵌入式开发来说是必不可少的,例如,在各种网络和硬件协议中处理二进制消息时。JavaScript 支持与您在用 CC++编写代码时所习惯的相同类型的操作,尽管您执行这些操作的方式非常不同。

在 JavaScript 中使用二进制数据的另一个好处是可以减少项目的内存使用。JavaScript 的一个基本特性是任何值都可以包含任何类型,但是这个强大的特性是有代价的:每个值都需要额外的内存来存储值的类型。C 语言中的布尔值只有一个字节(或一个位,使用位字段),而 JavaScript 中的布尔值可以更多,例如 816 个字节。使用 JavaScript 中的二进制数据,只需做一点工作,就可以在一个字节(甚至一个位)中存储一个布尔值。如果您的项目在内存中维护大量数据,可以考虑使用 JavaScript 的二进制数据特性,因为这样可以节省大量内存。使用标准的`Array`对象创建一个包含 1000 个元素的 JavaScript 布尔值数组可能需要 16 KBRAM,比 ESP8266 上可用的内存还多,但是使用`Uint8Array`对象创建它只需要 1 KBRAM——与 c 语言中完全一样。

### `ArrayBuffer``calloc`对应的 JavaScript 是`ArrayBuffer`类。一个`ArrayBuffer`是一个固定字节数的内存块。内存最初设置为 0,以避免未初始化内存带来的任何意外。

let a = new ArrayBuffer(10); // 10 bytes


如果因为没有足够的空闲内存而无法分配缓冲区,`ArrayBuffer`构造函数会抛出一个异常。

要检索一个`ArrayBuffer`中的字节数,获取`byteLength`属性。包含在一个`ArrayBuffer`实例中的字节数在创建时是固定的。没有相当于`realloc`的;您不能设置`ArrayBuffer``byteLength`属性。

let a = new ArrayBuffer(16);
let b = a.byteLength; // 16
a.byteLength = 20; // exception thrown


与数组一样,使用`slice`方法将缓冲区的一部分提取到一个新的`ArrayBuffer`实例中:

let a = new ArrayBuffer(16);
let b = a.slice(0, 8); // copy first half
let c = a.slice(8, 16); // copy second half
let d = a.slice(0); // clone entire buffer


您可能希望能够使用数组语法(例如,`a[0]`)来访问`ArrayBuffer`的内容,但事实并非如此。一个`ArrayBuffer`只是一个字节的缓冲区。因为没有与数据相关的类型,JavaScript 不知道如何解释字节——例如,字节值是有符号的还是无符号的。要访问`ArrayBuffer`中的数据,您需要将它包装在一个视图中。下面几节介绍两种视图:类型化数组和数据视图。

### 类型化数组

JavaScript *类型化数组*是一个类的集合,它允许你使用存储在`ArrayBuffer`中的整数数组和浮点值数组。您不直接使用`TypedArray`类,而是使用其特定类型的子类,例如`Int8Array``Uint16Array``Float32Array`。使用类型化数组类似于用`calloc`C 中创建一个内存缓冲区,并将结果赋给一个整型或浮点型的指针。

您可以创建一个包装现有`ArrayBuffer`的类型化数组。以下示例将一个`ArrayBuffer`包装到一个`Uint8Array`:

let a = new ArrayBuffer(16);
let b = new Uint8Array(a);


现在您已经有了缓冲区的视图,您可以像预期的那样使用数组括号语法来访问内容:

b[0] = 12;
b[1] += b[0];


类型化数组,比如前面例子中的`Uint8Array`,有一个`byteLength`属性,和`ArrayBuffer`一样,但是它们也有一个`length`属性,指示数组中元素的数量。当元素是字节时,这两个值是相等的,但是对于更大的类型,它们是不同的(参见清单 2-74 )

let a = new ArrayBuffer(24);
let b = new Uint8Array(a);
let c = new Uint16Array(a);
let d = new Uint32Array(a);
let e = b.length; // 24
let f = c.length; // 12
let g = d.length; // 6

Listing 2-74.


这里一个单独的`ArrayBuffer`被几个视图所包装;这是允许的。在 C 语言中,这被称为“别名”,这是很危险的,因为它会干扰某些编译器优化。在 JavaScript 中,这是安全的,尽管您应该小心使用它,以避免在读取和写入重叠视图时出现意外。

您可以创建引用缓冲区子集的类型化数组视图,方法是将偏移量(以字节为单位)包含到视图的开头,并包含视图中元素的数量。这就像在内存缓冲区中间给一个整数指针赋值一样。然而,在 JavaScript 中,当读取超过缓冲区末尾时,没有不可预测的结果;它总是返回`undefined`(参见清单 2-75 )

let a = new ArrayBuffer(18)
let b = new Int16Array(a);
b[0] = 0;
b[1] = 1;
b[2] = 2;
b[3] = 3;
let c = new Int16Array(a, 6, 1);
// c begins 6 bytes into a and has one element
let d = c[0]; // 3
let e = c[1]; // undefined (read past end of view)

Listing 2-75.


在清单 2-75 中为变量`c`创建的`Int16Array`视图从偏移量 6 开始,但是它可以从任何偏移量开始,包括奇数。访问该数组中的 16 位值需要错位读取。不是所有的微控制器都支持错位读写;ESP8266 是一种不支持错位内存访问的微控制器。当 C 代码执行未对齐的读或写操作时,会产生硬件异常,导致微控制器复位。JavaScript 代码没有这个问题,因为这种语言保证未对齐的操作与对齐的操作产生相同的结果——这是 JavaScript 使嵌入式产品上的编码更容易的另一种方式。

#### 类型化数组速记

创建小整数数组是很常见的。在 CC++中,可以很容易地在栈上声明静态数组。

static uint16_t values[] = {0, 1, 2, 3};


在 JavaScript 中,您可以在类型化数组上使用 static `of`方法获得相同的结果:

let a = Uint16Array.of(0, 1, 2, 3);
let b = a.byteLength; // 8
let c = a.length; // 4


`of`函数自动创建一个存储值所需大小的`ArrayBuffer`。您可以通过获取类型化数组的`buffer`属性来访问由`of`创建的`ArrayBuffer`。该缓冲区可用于其他视图,如数据视图。

let a = Uint16Array.of(0, 1, 2, 3);
let b = a.buffer;
let c = b.byteLength; // 8


#### 复制类型化数组

在 CC++中,使用`memcpy``memmove`在单个缓冲区内或两个缓冲区之间复制数据值。您已经看到了如何使用 JavaScript 中的`ArrayBuffer`上的`slice`将部分或全部缓冲区复制到一个新的缓冲区;您可以使用`copyWithin`在单个缓冲区内复制值,使用`set`将值从一个缓冲区复制到另一个缓冲区。在 C 中,当源和目标重叠时,在单个缓冲区内复制时需要特别小心,而 JavaScript 的`copyWithin`方法保证了结果的可预测性和正确性。`copyWithin`的第一个参数是目标索引,第二个和第三个参数是要复制的开始和结束源索引(其中结束索引是结束之前的索引)

let a = Uint16Array.of(0, 1, 2, 3, 4, 5, 6);
a.copyWithin(4, 1, 3);
// [0, 1, 2, 3, 1, 2, 6]


方法将一个类型化数组写入另一个类型化数组。第一个参数是要写入的源数据,第二个参数是开始写入数据的索引。

let a = Int16Array.of(0, 1, 2, 3, 4, 5, 6);
let b = Int16Array.of(-2, -3);
a.set(b, 2);
// [0, 1, -2, -3, 4, 5, 6]


要仅写入源数据的子集,您需要创建另一个视图。`subarray`方法对此很方便,如清单 2-76 所示。给定一个类型化数组的起始和结束索引,`subarray`返回一个新的类型化数组,该数组只引用那些索引。注意`subarray`没有分配新的`ArrayBuffer`;它引用了同一个`ArrayBuffer`

let a = Int16Array.of(0, 1, 2, 3, 4, 5, 6);
let b = Int16Array.of(0, -1, -2, -3, -4, -5, -6);
let c = b.subarray(2, 4);
a.set(c, 2);
// [0, 1, -2, -3, 4, 5, 6]

Listing 2-76.


您可以使用`slice`代替`subarray`来复制新`Int16Array`中的子集,但是这会临时使用额外的内存,所以在这种情况下最好使用`subarray``TypedArray`类不是`Array`的子类;它们是完全独立的类,但是它们被设计成共享公共 API。例如,您学习的用于类型化数组的`copyWithin`方法在`Array`中可用。类似地,许多`Array`方法,包括`map``forEach``indexOf``lastIndexOf``findIndex``sort`,也可用于类型化数组。

#### 填充类型化数组

另一个对`Array``TypedArray`都有用的方法是`fill`,类似于 CC++中的`memset`。但是`memset`只对字节值进行操作,`fill`对类型化数组的类型值进行操作。如清单 2-77 所示,`fill`的第一个参数是要赋值的值,可选的第二个和第三个参数是要填充的开始和结束索引(其中结束索引是结束填充之前的索引)。如果没有提供可选参数,则填充整个数组。

let a = new Uint16Array(4);
a.fill(0x1234);
// [0x1234, 0x1234, 0x1234, 0x1234]

a.fill(0, 1, 3);
// [0x1234, 0, 0, 0x1234]

let b = new Uint32Array(2);
b.fill(0x12345678);
// [0x12345678, 0x12345678]

Listing 2-77.


#### 写入类型化数组值

将值写入类型化数组通常像在 c 中一样。例如,如果将 16 位值写入 8 位类型化数组,则使用最低有效的 8 (参见清单 2-78 )

let a = new Uint32Array(1);
a[0] = 0x12345678; // 0x12345678

let b = new Uint16Array(1);
b[0] = 0x12345678; // 0x5678

let c = new Uint8Array(1);
c[0] = 0x12345678; // 0x78

Listing 2-78.


JavaScript 还有一个`Uint8ClampedArray`,它实现了一种不同的行为:它不是获取最低有效位,而是将输入值固定在 0 和类型化数组实例可以存储的最大值之间的一个值。

let a = new Uint8ClampedArray(1);
a[0] = 5; // 5
a[0] = 256; // 255
a[0] = -1; // 0


#### 浮点类型数组

浮点型数组有两种:`Float32Array``Float64Array`。由于 JavaScript 中的数字值是 64IEEE 754 浮点型的,`Float64Array`能够存储这些值而不损失精度。`Float32Array`降低可能存储的值的精度和范围,但在某些情况下已经足够。

Note

类型化数组类不保证存储值时的字节顺序(即,是大端还是小端)。JavaScript 引擎实现可以自由地以它选择的任何方式存储值,只要保持值的准确性。它通常按照与主机微控制器相同的顺序存储它们,以获得最高效率。要控制值的字节顺序,请使用数据视图(接下来将讨论)。

### 数据视图

`DataView`类为`ArrayBuffer`提供了另一种视图。与所有值都是同一类型的类型化数组不同,*数据视图*用于在缓冲区中读写不同大小的整数和浮点值。您可以使用`DataView`来访问对应于包含不同类型值的 CC++ `struct`的二进制数据。

您可以通过向`DataView`构造函数传递一个`ArrayBuffer`来实例化数据视图,以便视图进行包装,就像您可以向类型化数组构造函数传递一个`ArrayBuffer`一样:

let a = new ArrayBuffer(16);
let b = new DataView(a);


与类型化数组一样,您可以将偏移量和大小传递给`DataView`构造函数,以将视图限制为总缓冲区的一个子集。这种能力对于访问嵌入在较大内存缓冲区中的数据结构很有用。

let a = new ArrayBuffer(16);
let b = new DataView(a, 4, 12);
// b may only access bytes 4 through 12 of a


#### 访问数据视图的值

一个`DataView`实例能够获取和设置与类型化数组相同的所有类型,如清单 2-79 所示。getter 和 setter 方法都将视图中的偏移量作为第一个参数。setter 方法的第二个参数指定要设置的值。

let a = new DataView(new ArrayBuffer(8));
a.setUint8(0, 0);
a.setUint8(1, 1);
a.setUint16(2, 0x1234);
a.setUint32(4, 0x01020304);

Listing 2-79.


因为默认情况下`DataView`方法以大端字节顺序写入多字节值,所以在清单 2-79 中的示例执行后,缓冲区`a`包含以下十六进制字节:

00 01 12 34 01 02 03 04


使用相应的 getter 方法读回这些值。以下示例假设前面显示的`DataView`实例`a`:

let b = a.getUint8(0); // 0
let c = a.getUint8(1); // 1
let d = a.getUint16(2); // 0x1234
let e = a.getUint32(4); // 0x01020304


`DataView`方法有一个可选的最终参数来控制字节顺序。如果省略参数或`false`,字节顺序为 big-endian;如果是`true`,那就是小端(见清单 2-80 )

let a = new DataView(new ArrayBuffer(8));
a.setUint8(0, 0);
a.setUint8(1, 1);
a.setUint16(2, 0x1234, true);
a.setUint32(4, 0x01020304, true);

Listing 2-80.


因为`setUint8`写的是单字节值,没有字节顺序,所以第三个参数是不必要的。对清单 2-80 中的`setUint16``setUint32`的调用将字节顺序参数设置为`true`,因此输出是 little-endian。

00 01 34 12 04 03 02 01


要读取以小端顺序存储的值,将`true`作为最终参数传递给 getter 方法:

let b = a.getUint16(2, true); // 0x1234 (little-endian get)
let c = a.getUint16(2); // 0x3412 (big-endian get)


`DataView`类包括对应于`TypedArray`中所有可用类型的 getter 和 setter 方法:`Int8``Int16``Uint8``Uint16``Uint32``Float32``Float64``DataView`类是操纵二进制数据结构的一种非常灵活的方式,但是代码不是特别可读。不要像在 C 中那样写`a.value`来访问字段,你必须写类似`a.getUint16(6, true)`的东西。提高可读性和减少错误可能性的一种方法是为数据结构创建一个子类`DataView`。假设您有清单 2-81 中所示的 C 数据结构,用于您想在 JavaScript 中使用的网络数据包报头。为简单起见,假设字段之间没有填充。

typedef struct Header {
uint8_t kind;
uint8_t priority;
uint16_t sequenceNumber;
uint32_t value;
}

Listing 2-81.


清单 2-82 中的 JavaScript `Header`类继承了`DataView`来实现对 C `Header`结构的简单访问。因为网络数据包通常使用大端字节排序,所以多字节值以大端顺序写入。

class Header extends DataView {
constructor(buffer = new ArrayBuffer(8)) {
super(buffer);
}
get kind() {return this.getUint8(0);}
set kind(value) {this.setUint8(0, value);}
get priority() {return this.getUint8(1);}
set priority(value) {this.setUint8(1, value);}
get sequenceNumber() {return this.getUint16(2);}
set sequenceNumber(value) {this.setUint16(2, value);}
get value() {return this.getUint32(4);}
set value(value) {this.setUint32(4, value);}
}

Listing 2-82.


因为该类使用 getters 和 setters,所以该类用户的结果代码类似于 c。清单 2-83 中的示例使用`Header`类从变量`p`中接收的数据包中读取值。

let a = new Header§;
let b = a.kind;
let c = a.priority;
let d = a.sequenceNumber;
let e = a.value;

Listing 2-83.


清单 2-84 创建一个新的包,初始化值,并调用一个`send`函数来传输`Header`实例使用的`ArrayBuffer a.buffer`进行存储。

let a = new Header;
a.kind = 1;
a.priority = 2;
a.sequenceNumber = 3;
a.value = 4;
send(a.buffer);

Listing 2-84.


正如您所看到的,定义一个表示二进制数据结构的类使得处理该数据结构的代码更加清晰。处理二进制数据是 C 在代码紧凑性方面具有优势的一个领域;尽管如此,用 JavaScript 的可读代码也可以达到同样的效果。JavaScript 在这里也有好处:考虑到从网络上接收的数据中读取的代码通常是脆弱的。在本例中,如果收到的数据包只有四个字节,而不是所需的八个字节,那么读取`value`字段会产生一个未定义的结果,这可能会泄漏私有数据,甚至导致崩溃。如果在 JavaScript 中出现这种情况,使用`getUint32`读取`value`的尝试会失败,并出现异常,因为读取超出了范围。

## 内存管理

内存管理是 JavaScript 与 CC++显著不同的地方。在 CC++中,你用`malloc``calloc``realloc`显式分配内存,用`free`释放内存。这些内存分配和释放函数不在语言本身中,而是在标准库中。在 C++中,当你使用`new`实例化一个类时,你也分配内存,当你使用`delete`调用类的析构函数时,你释放内存。

JavaScript 在语言中内置了内存管理。当您创建一个对象、字符串、`ArrayBuffer`或任何其他需要内存的内置对象时,JavaScript 引擎会透明地分配内存。如您所料,该语言还会释放内存;然而,JavaScript 并不要求您的代码进行类似于`free`的调用或者使用 C++ `delete`操作符,而是在确定这样做是安全的时候自动释放内存。这种内存管理方法是使用垃圾收集器实现的。在特定时间点,JavaScript 引擎运行垃圾收集器,该收集器扫描引擎分配的所有内存,识别不再被引用的任何分配,并释放任何未被引用的内存块。

考虑以下代码:

let a = “this is a test”;
a = {};
a = new ArrayBuffer(16);


此示例执行以下操作:

1.  第一行分配一个字符串,并将其赋给`a`。因为字符串被`a`引用,所以不能被垃圾回收。

2.  第二行将一个空对象分配给`a`,删除对字符串的引用。因为没有其他变量或属性引用该字符串,所以它有资格被垃圾回收。

3.  在第三行的`ArrayBuffer`赋值之后,空对象就可以进行垃圾收集了。

JavaScript 语言没有定义垃圾收集器何时运行。可修改的 SDK 中使用的 XS 引擎中的垃圾收集器会在内存不足时运行;这可能永远不会发生,一小时一次,或者一秒钟多次,取决于运行的代码。

垃圾收集器非常适合管理内存。它减少了您需要编写的代码量,因为分配和释放都是自动发生的。它消除了忘记释放内存导致内存泄漏的 bug 这是嵌入式系统中的一个主要问题,许多嵌入式系统必须一次运行数月或数年,因为定期发生的少量内存泄漏最终会导致系统故障。垃圾收集器还消除了读取已被释放的内存的错误,因为如果代码仍然能够引用,内存就不会被释放。

尽管垃圾收集器有很多好处,但它并不是资源管理的通用解决方案。考虑清单 2-85 ,它打开一个文件两次,第一次是写模式,第二次是只读模式。

let f = new File(“/foo.txt”, 1); // 1 for write
f.write(“this is a test”);
f = undefined;

let g = new File(“/foo.txt”); // read-only

Listing 2-85.


在本例中,当`undefined`被分配给`f`时,对应于为写访问而打开的文件的`File`类的实例有资格进行垃圾收集。在大多数文件系统中,当文件被打开进行写访问时,该访问是独占的,这意味着该文件不能被第二次打开。因为垃圾收集器可能随时运行,所以以只读模式打开文件的调用可能成功,也可能失败,这取决于是否已经收集了写访问文件对象。因此,用于表示非内存资源的对象(如打开的文件)通常提供了一种显式释放资源的方法。在可修改的 SDK 中,`close`方法用于释放资源,类似于在 C++中使用`delete`操作符。

let f = new File(“/foo.txt”, 1); // 1 for write
f.write(“this is a test”);
f.close();

`close`的调用会立即关闭文件。任何向`f`中的实例写入的进一步尝试都将失败。该文件现在可以以读或写模式再次打开。

## `Date`C 标准库提供了`gettimeofday``localtime`函数来确定当前日期、时间、时区和夏令时偏移量。同一个库中的`strftime`函数使用格式字符串将日期和时间转换为文本格式。JavaScript 在内置的`Date`类中提供了等效的功能。

下面的代码创建了一个`Date`类的实例。该实例包含一个时间值,当不带参数调用`Date`构造函数时,该值被初始化为当前时间。

let now = new Date;
trace(now.toString());
// Tue Sep 24 2019 11:18:26 GMT-0700 (PDT)


`Date`构造函数接受参数将值初始化为当前时间以外的值。您可以从字符串中初始化它,但是不建议这样做,因为字符串格式很容易出错。

let d = new Date(“Tue Sep 24 2019 11:18:26 GMT-0700 (PDT)”);


相反,您可以将时间的组成部分(小时、分钟、年等)作为参数传递给构造函数:

let d = new Date(2019, 8, 24);
// September 24 2019 midnight
let e = new Date(2019, 8, 24, 11, 18, 26);
// September 24 2019 11:18:26


请注意,9 月份的值是 8,而不是您预期的 9。这是因为 JavaScript `Date` API 中的月份数字从 0 开始,而不是从 1 开始;这是在 JavaScript 开发的早期决定的,以匹配 Java 语言的`java.util.Date`对象。还要注意,第二个声明中指定的时间是本地时间,而不是 UTC(协调世界时)。要指定 UTC 时间,使用`Date.UTC`函数和`Date`构造函数。

let d = new Date(Date.UTC(2019, 8, 24));
// September 24 2019 midnight UTC


一个`Date`实例存储一个以毫秒为单位的时间值,并且总是以 UTC 时间为单位。要检索该值,调用`getTime`方法。

let now = new Date;
let utcTimeInMS = now.getTime();


如果您的代码需要经常检索时间,前面的例子是低效的,因为它在每次需要当前时间时创建一个新的`Date`实例。对于这种情况,静态方法`now`以毫秒为单位返回当前的 UTC 时间。

let utcTimeInMS = Date.now();


`Date`类提供了对组成日期和时间的所有部分的访问(参见清单 2-86 )

let now = new Date;
let ms = now.getMilliseconds(); // 0 to 999
let seconds = now.getSeconds(); // 0 to 59
let minutes = now.getMinutes(); // 0 to 59
let hours = now.getHours(); // 0 to 23
let day = now.getDay(); // 0 (Sunday) to 6 (Saturday)
let date = now.getDate(); // 1 to 31
let month = now.getMonth(); // 0 (January) to 11 (December)
let year = now.getFullYear();

Listing 2-86.


清单 2-86 中返回的值是当地时间,应用了时区和夏令时偏移。用于 UTC 值的相同函数的版本也是可用的;它们以`getUTC`开头,如`getUTCMilliseconds``getUTCSeconds`等等。

还有对应于所有 getter 方法的 setter 方法。清单 2-87 创建一个日期对象,并将其修改为下一个元旦的午夜。

let d = new Date;
d.setMilliseconds(0);
d.setSeconds(0);
d.setMinutes(0);
d.setHours(0);
d.setDate(1);
d.setMonth(0);
d.setFullYear(d.getFullYear() + 1);

Listing 2-87.


`setHours``setFullYear`方法支持额外的参数,使得清单 2-87 中的例子写得更简洁:

let d = new Date;
d.setHours(0, 0, 0, 0);
d.setFullYear(d.getFullYear() + 1, 0, 1);


要从 UTC 时间检索当前时区偏移量,请调用`getTimezoneOffset`方法。返回值以分钟为单位,并应用了当前夏令时偏移量。

let timeZoneOffset = d.getTimezoneOffset();
// timeZoneOffset = 420 (offset in minutes from UTC)


如本节前面所示,`Date`对象的`toString`方法提供了一个字符串,表示应用了时区和夏令时偏移量的本地时间。对于某些情况,例如网络,用 UTC 时间表示字符串是很有帮助的。使用`toUTCString`方法创建一个表示 UTC 时间的字符串。

let d = new Date;
trace(d.toUTCString());
// “Tue, 24 Sep 2019 18:18:26 GMT”


许多标准使用的另一种时间和日期格式是 ISO 8601`toISOString`方法以字符串形式提供日期的 ISO 8601 兼容版本。

let d = new Date;
trace(d.toISOString());
// “2019-09-24T18:18:26.000Z”


虽然`toUTCString``toISOString`很方便,但是您可以使用 JavaScript 日期和字符串知识来生成项目所需的任何格式的字符串。

## 事件驱动编程

嵌入式程序,尤其是那些运行在功能较弱的设备上的程序,通常是围绕一个连续执行的循环来组织的。清单 2-88 显示了一个简单的例子。

while (true) {
if (readButton())
lightOn();
else
lightOff();
}

Listing 2-88.


这种编程风格适用于非常简单的嵌入式设备。然而,对于具有许多不同输入和输出的大型系统来说,这种方法并不适用;对于这样的系统,事件驱动编程是首选。事件驱动程序等待事件的发生,比如按钮的按下。当事件发生时,调用回调来响应它。JavaScript 是为事件驱动程序设计的,因为这是 web 浏览器的工作方式。

清单 2-89 是上例中无限循环的事件驱动版本。这里,当按钮改变时调用`onRead`回调,这样代码就不需要不断地轮询按钮状态。

let button = new Button;
button.onRead = function(value) {
if (value)
lightOn();
else
lightOff();
}

Listing 2-89.


通常,只有当微控制器空闲时,才会调用传递事件的回调。当 JavaScript 代码执行时,回调被延迟,直到代码完成。在清单 2-88 中,因为循环是无限的,所以不能调用回调。因此,通常不可能使用单个循环作为 JavaScript 应用程序的基础;您必须采用事件驱动的编程风格。

如果你以前没有做过很多事件驱动的编程,不要担心。本书中的例子都是为了向您展示如何在事件驱动的编程风格中使用嵌入式 JavaScript APIs。稍加练习,它应该成为第二天性。

## 结论

有了对 JavaScript 的介绍,您就可以继续阅读本书了。剩下的章节是关于如何在嵌入式系统上使用 JavaScript,使用可修改的 SDK 提供的特性来创建物联网产品。

JavaScript 语言规范非常庞大,超过 750 页。这本书不可能解释这种语言的每一个特性和细微差别,但是许多优秀的资源可以帮助你学习更多。Mozilla 的 MDN Web Docs ( [`developer.mozilla.org`](http://developer.mozilla.org) )是 JavaScript 语言事实上的参考。这是最新的标准,提供了大量的例子,非常详细。对于嵌入式开发人员来说,这是一个很好的资源,因为即使您不是 web 开发人员,也可以理解它提供的许多示例。
Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐