JavaScript-从新手到专家-全-
JavaScript 从新手到专家(全)
原文:
zh.annas-archive.org/md5/f167a53930583b701401fcd470be2025
译者:飞龙
前言
JavaScript 是一种令人惊叹的多功能语言,在网页开发(以及其他方面)中被广泛使用。网页上发生的任何交互都是 JavaScript 在起作用。实际上,所有现代浏览器都支持 JavaScript——很快你也会理解它。
本书涵盖了创建 JavaScript 应用程序和使用 JavaScript 在网页上所需了解的一切。当你完成这本书时,你将能够创建交互式网页、动态应用程序,以及在你专业 JavaScript 之旅中更多内容!
本书面向对象
要开始学习这本书,你不需要任何 JavaScript 经验。然而,如果你有一些编码经验,你可能会更容易地阅读这本书和练习。对 HTML 和 CSS 的基本了解将有所帮助。如果你是第一次编程,我们荣幸地欢迎你加入本书中的编程世界。一开始可能看起来很难,但我们将引导你顺利通过。
本书涵盖内容
第一章,JavaScript 入门,涵盖了理解本书其余部分所需了解的 JavaScript 语言的基本知识。
第二章,JavaScript 基础知识,处理诸如变量、数据类型和运算符等基础知识。
第三章,JavaScript 多值,介绍了如何使用数组和对象在一个变量中存储多个值。
第四章,逻辑语句,是真正的乐趣开始的地方:我们将使用逻辑语句为我们做出决策!
第五章,循环,讨论了需要重复代码块的情况,这就是我们使用循环的原因。我们使用不同类型的循环,如 for
循环和 while
循环。
第六章,函数,介绍了一个非常有用的代码片段重复块:函数!这使我们能够在脚本中的任何时间调用指定的代码块来为我们做事。这将帮助你避免重复,这是编写干净代码的基本原则之一。
第七章,类,继续介绍 JavaScript 的构建块,帮助我们更好地构建应用程序。我们已经看到了如何创建对象,而通过类,我们学习如何创建可以随时重用的对象模板。
第八章,内置 JavaScript 方法,处理一些内置功能。函数是我们自己可以编写的,但当我们需要执行常见任务时,如检查某物是否为数字,我们经常会使用内置的 JavaScript 函数。
第九章,文档对象模型,深入探讨了浏览器对象模型和文档对象模型(DOM)。这将极大地丰富我们使用 JavaScript 的方式。我们将学习 DOM 是什么,以及我们如何通过 JavaScript 影响它并改变我们的网站。
第十章,使用 DOM 动态操作元素,展示了如何动态地操作 DOM 元素,这将使您能够创建现代的用户体验。我们可以根据用户的行为,如点击按钮,来改变我们的网站。
第十一章,交互式内容和事件监听器,将我们对用户的响应提升到新的水平。例如,我们将学习如何响应用户事件,如光标离开输入框和用户鼠标移动。
第十二章,中级 JavaScript,涉及您需要编写中级 JavaScript 代码的主题,例如正则表达式、递归和调试,以提升代码的性能。
第十三章,并发,介绍了并发和异步编程的主题,这将使我们的代码能够同时做很多事情,并真正灵活。
第十四章,HTML5、Canvas 和 JavaScript,专注于 HTML5 和 JavaScript。在前面的章节中,我们已经看到了很多 HTML 和 JavaScript 的内容,但在这里我们将专注于 HTML5 特有的功能,例如 canvas 元素。
第十五章,下一步,探讨了在掌握了 JavaScript 的所有基本功能并能够编写使用 JavaScript 的巧妙程序之后,您可以采取的下一步。我们将探讨一些著名的 JavaScript 库和开发框架,如 Angular、React 和 Vue,并查看 Node.js 以了解后端如何用 JavaScript 编写。
为了充分利用本书
之前的编码经验会有帮助,但绝对不是必需的。如果您有一台装有文本编辑器(如记事本或 TextEdit,而不是 Word!)和浏览器的电脑,您就可以开始阅读本书。我们鼓励您参与练习和项目,并在阅读章节时不断实验,以确保在继续之前对每个概念都感到舒适。
下载示例代码文件
书籍的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/JavaScript-from-Beginner-to-Professional
。我们还有其他丰富的图书和视频的代码包,可在github.com/PacktPublishing/
找到。请查看它们!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表的彩色图像的 PDF 文件。您可以从这里下载:static.packt-cdn.com/downloads/9781800562523_ColorImages.pdf
。
使用的约定
本书使用了多种文本约定。
CodeInText
: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。例如;“我们还需要通过<!DOCTYPE>
声明让浏览器知道我们正在处理什么类型的文档。”
一段代码按照以下方式设置:
<html>
<script type="text/javascript">
alert("Hi there!");
</script>
</html>
任何命令行输入或输出都按照以下方式编写:
console.log("Hello world!")
粗体: 表示新术语、重要单词或您在屏幕上看到的单词,例如在菜单或对话框中,也以这种方式出现在文本中。例如:“如果您在 macOS 系统上右键单击并选择检查,您将看到一个屏幕出现,类似于以下截图。”
警告或重要注意事项看起来像这样。
小技巧和技巧看起来像这样。
联系我们
我们始终欢迎读者的反馈。
一般反馈: 请发送电子邮件至 feedback@packtpub.com
,并在邮件主题中提及书籍标题。如果您对本书的任何方面有疑问,请通过 questions@packtpub.com
发送电子邮件给我们。
勘误: 尽管我们已经尽最大努力确保内容的准确性,错误仍然可能发生。如果您在这本书中发现了错误,我们非常感谢您能向我们报告。请访问www.packtpub.com/submit-errata
,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
盗版: 如果您在互联网上以任何形式发现我们作品的非法副本,我们非常感谢您能提供位置地址或网站名称。请通过 copyright@packtpub.com
联系我们,并提供材料的链接。
如果您有兴趣成为作者: 如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com
。
分享您的想法
一旦您阅读了《从入门到精通 JavaScript》,我们非常乐意听到您的想法!请点击此处直接访问此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区都至关重要,并将帮助我们确保我们提供高质量的内容。
第一章:开始学习 JavaScript
看起来你已经决定开始学习 JavaScript。这是一个非常好的选择!JavaScript 是一种可以在应用程序的客户端和服务器端使用的编程语言。应用程序的服务器端是通常在数据中心运行的计算机上的后端逻辑,并与数据库交互,而客户端是运行在用户设备上的部分,通常是 JavaScript 的浏览器。
你可能已经使用过用 JavaScript 编写的功能。如果你使用过像 Chrome、Firefox、Safari 或 Edge 这样的网络浏览器,那么你肯定已经使用过了。JavaScript 遍布整个网络。如果你进入一个网页,它要求你接受饼干,并且你点击了确定,那么弹出窗口就会消失。这就是 JavaScript 在起作用。如果你想要导航到一个网站,并且一个子菜单展开,这意味着有更多的 JavaScript。通常,当你在一个网店中过滤产品时,这涉及到 JavaScript。那么,当你在一个网站上停留了一段时间后,开始与你交谈的这些聊天是什么?好吧,你已经猜到了——JavaScript!
几乎我们与网页的所有交互都是因为 JavaScript;你点击的按钮、你创建的生日卡片,以及你进行的计算。任何需要比静态网页更复杂的功能的东西都需要 JavaScript。
在本章中,我们将涵盖以下主题:
-
你为什么应该学习 JavaScript?
-
设置你的环境
-
浏览器是如何理解 JavaScript 的?
-
使用浏览器控制台
-
将 JavaScript 添加到网页中
-
编写 JavaScript 代码
注意:练习、项目和自我检查测验的答案可以在附录中找到。
你为什么应该学习 JavaScript?
有很多原因让你想要学习 JavaScript。JavaScript 诞生于 1995 年,通常被认为是使用最广泛的编程语言。这是因为 JavaScript 是网络浏览器支持并理解的语言。如果你有网络浏览器和文本编辑器,你电脑上已经安装了所有你需要来解释它的东西。然而,有更好的设置,我们将在本章后面讨论这些。
它是一种非常适合初学者的编程语言,大多数高级软件开发者至少会知道一些 JavaScript,因为他们会在某个时候遇到它。JavaScript 是初学者的一个很好的选择,原因有很多。第一个原因是你可以比想象中更快地开始使用 JavaScript 构建真正酷的应用程序。当你到达第五章,循环时,你将能够编写相当复杂的脚本,与用户交互。到本书结束时,你将能够编写动态网页,做各种事情。
JavaScript 可以用来编写许多不同类型的应用程序和脚本。它可以用于网页浏览器的编程,也可以用于我们看不到的应用程序逻辑层代码(例如与数据库的通信),这些代码可以用 JavaScript 编写,包括游戏、自动化脚本以及众多其他用途。JavaScript 还可以用于不同的编程风格,我们这里指的是结构化和编写代码的方式。你如何进行取决于你的脚本目的。如果你之前从未编码过,你可能不太理解这些概念,在这个阶段也不是完全必要的,但 JavaScript 可以用于(半)面向对象、函数式和过程式编程,这些都是不同的编程范式。
一旦你掌握了 JavaScript 的基础知识,你就可以使用大量的库和框架。这些库和框架将真正提升你的软件开发生活,使你在更短的时间内完成更多的工作。这些优秀的库和框架包括 React、Vue.js、jQuery、Angular 和 Node.js。现在不必担心这些;只需把它们看作是未来的额外奖励。我们将在本书的最后简要介绍其中的一些。
最后,我们将提到 JavaScript 社区。JavaScript 是一种非常流行的编程语言,很多人在使用它。特别是对于初学者来说,你几乎可以在互联网上找到所有问题的解决方案。
JavaScript 的社区非常庞大。流行的 Stack Overflow 论坛包含大量各种编码问题的帮助,并且有一个关于 JavaScript 的巨大部分。你会发现自己在搜索问题和技巧时经常会遇到这个网页。
如果 JavaScript 是你的第一门编程语言,你对整个软件社区都是新手,那么你将有一个美好的体验。软件开发者,无论使用哪种语言,都喜欢互相帮助。网上有论坛和教程,你可以找到几乎所有问题的答案。然而,作为一个初学者,理解所有答案可能很困难。坚持下去,继续尝试和学习,你很快就会理解。
设置你的开发环境
你可以通过许多方式设置 JavaScript 编码环境。首先,你的电脑可能已经拥有了你编写 JavaScript 所需要的所有最小组件。我们建议你让生活变得更轻松一些,并使用集成开发环境(IDE)。
集成开发环境
集成开发环境(IDE)是一种特殊的应用程序,用于编写、运行和调试代码。你可以像打开任何程序一样打开它。例如,要编写一个文本文档,你需要打开程序,选择正确的文件,然后开始编写。编码也是如此。你打开 IDE 并编写代码。如果你想执行代码,IDE 通常有一个特殊的按钮用于此目的。按下此按钮将在 IDE 内部运行代码。对于 JavaScript,你可能会在某些情况下手动打开浏览器。
然而,IDE 的功能远不止于此;它通常具有语法高亮功能。这意味着你的代码中的某些元素将具有特定的颜色,你可以轻松地看到哪里出了问题。另一个出色的功能是自动建议功能,编辑器会帮助你完成你正在编码的地方的选项。这通常被称为代码补全。许多 IDE 都有特殊的插件,你可以使与其他工具的协作更加直观,并为其添加功能,例如浏览器中的热重载。
目前市面上有许多集成开发环境(IDE),它们提供的功能各不相同。我们在本书中使用了 Visual Studio Code,但这只是个人偏好。在撰写本书时,其他流行的 IDE 包括 Atom、Sublime Text 和 WebStorm。IDE 种类繁多,而且还在不断出现,所以当你阅读本书时,最流行的 IDE 可能不在本列表中。还有很多其他选择。你可以在网上快速搜索 JavaScript IDE。在选择 IDE 时,有几个需要注意的事项。确保它支持 JavaScript 的语法高亮、调试和代码补全功能。
网络浏览器
你还需要一个网络浏览器。大多数浏览器都适合这个用途,但最好不要使用不支持最新 JavaScript 功能的 Internet Explorer。两个不错的选择是 Chrome 和 Firefox。它们支持最新的 JavaScript 功能,并且有可用的有用插件。
额外工具
在编码过程中,你可以使用许多额外的东西,例如可以帮助你调试或使事物更容易查看的浏览器插件。在这个阶段,你实际上并不需要任何这些工具,但当你遇到其他人非常兴奋的工具时,请保持开放的心态。
在线编辑器
你可能没有电脑可以使用,也许只有平板电脑,或者你无法在你的笔记本电脑上安装任何东西。在这些情况下,也有出色的在线编辑器。我们不会列出任何名称,因为它们正在快速发展,可能在你阅读本书时就已经过时了。但如果你在网上搜索“在线 JavaScript IDE”,你会找到许多在线选项,你可以在那里开始编写 JavaScript 代码,并点击按钮来运行它。
浏览器是如何理解 JavaScript 的?
JavaScript 是一种解释型语言,这意味着计算机在运行时理解它。有些语言在运行前会先进行处理,这称为编译,但 JavaScript 不是。计算机可以直接解释 JavaScript。理解 JavaScript 的“引擎”在这里被称为解释器。
一个网页不仅仅是 JavaScript。网页是用三种语言编写的:HTML、CSS 和 JavaScript。
HTML 决定了页面上有什么;页面的内容就在那里。如果页面上有一个段落,页面的 HTML 包含一个段落。如果有标题,HTML 被用来添加标题,等等。HTML 由元素组成,也称为标签。它们指定页面上有什么。以下是一个小示例,它将创建一个包含文本Hello world
的网页:
<html>
<body>
Hello world!
</body>
</html>
在第九章,文档对象模型中,我们有一个关于 HTML 的简要课程,所以如果你从未见过它,请不要担心。
CSS 是网页的布局。例如,如果文本颜色是蓝色,这是由 CSS 完成的。字体大小、字体家族和页面上的位置都是由 CSS 决定的。JavaScript 是拼图中最后一块,它定义了网页能做什么以及如何与用户或后端交互。
当处理 JavaScript 时,你迟早会遇到ECMAScript这个术语。这是 JavaScript 语言的规范或标准化。当前的标准是ECMAScript 6(也称为ES6)。浏览器使用这个规范来支持 JavaScript(除了我们后面会看到的文档对象模型(DOM)等其他一些主题)。JavaScript 有许多实现,可能略有不同,但 ECMAScript 可以被认为是 JavaScript 实现一定会包含的基本规范。
使用浏览器控制台
你可能已经看到了,或者没有看到,但网络浏览器有一个内置选项可以查看使你所在的网页成为可能的代码。如果你在 Windows 计算机上的网络浏览器中按F12
,或者在 macOS 系统上右键单击并选择Inspect,你将看到一个出现的屏幕,类似于下面的截图。
它可能在你的机器上的浏览器中略有不同,但通常右键单击并选择Inspect就能解决问题:
图 1.1:Packt 网站上的浏览器控制台
这个截图包含顶部多个标签页。我们现在正在查看元素标签页,它包含所有的 HTML 和 CSS(还记得吗?)。如果你点击控制台标签页,你将在面板底部找到一个可以直接插入代码的地方。你可能在这个标签页中看到一些警告或错误消息。这是很常见的,如果页面正在工作,请不要担心。
开发者使用控制台来记录正在发生的事情并进行任何调试。调试是在应用程序未显示预期行为时寻找问题的过程。如果你记录了有意义的消息,控制台会提供一些关于正在发生什么的见解。这实际上是我们将要学习的第一个命令:
console.log("Hello world!");
如果你点击这个控制台标签,输入上面的第一段 JavaScript 代码,然后按 Enter
,这将显示你代码中的输出。它看起来像以下截图:
图 1.2:浏览器控制台中的 JavaScript
在本书中,你将大量使用 console.log()
语句来测试你的代码片段并查看结果。还有其他控制台方法,例如 console.table()
,当输入的数据可以表示为表格时,它会创建一个表格。另一个控制台方法是 console.error()
,它将记录输入的数据,但以一种吸引注意力的样式显示,表明它是一个错误。
练习 1.1
使用控制台:
-
打开浏览器控制台,输入
4 + 10
并按Enter
。你看到了什么响应? -
使用
console.log()
语法,在圆括号内放置一个值。尝试输入你的名字,并用引号括起来(这是为了表明它是一个文本字符串——我们将在下一章中讨论这一点)。
在网页中添加 JavaScript
有两种方法可以将 JavaScript 链接到网页。第一种方法是在 HTML 中直接在两个 <script>
标签之间输入 JavaScript。在 HTML 中,第一个标签 <script>
是用来声明下面的脚本将被执行。然后我们有应该放在这个元素内的内容。接下来,我们用相同的标签关闭脚本,但前面有一个向前斜杠,</script>
。或者,你可以使用 HTML 页面顶部的脚本标签将 JavaScript 文件链接到 HTML 文件。
直接在 HTML 中
这里是一个如何编写一个非常简单的网页的示例,该网页将弹出一个显示 Hi there!
的对话框:
<html>
<script type="text/javascript">
alert("Hi there!");
</script>
</html>
如果你将其保存为 .html
文件,并在浏览器中打开该文件,你将得到以下截图类似的内容。我们将将其保存为 Hi.html
:
图 1.3:JavaScript 使文本 "Hi there!" 的弹出窗口出现
alert
命令将创建一个弹出窗口,显示一条消息。这条消息位于 alert
后面的括号内。
目前,我们的内容直接位于 <html>
标签内。这不是最佳实践。我们需要在 <html>
内创建两个元素——<head>
和 <body>
。在 <head>
元素中,我们写入元数据,并且我们稍后会使用这部分来将外部文件连接到我们的 HTML 文件。在 <body>
中,我们有网页的内容。
我们还需要通过 <!DOCTYPE>
声明让浏览器知道我们正在处理哪种类型的文档。由于我们在 HTML 文件中编写 JavaScript,我们需要使用 <!DOCTYPE html>
。以下是一个示例:
<!DOCTYPE html>
<html>
<head>
<title>This goes in the tab of your browser</title>
</head>
<body>
The content of the webpage
<script>
console.log("Hi there!");
</script>
</body>
</html>
这个示例网页将显示以下内容:网页内容
。如果你查看浏览器控制台,你会发现一个惊喜!它已经执行了 JavaScript,并在控制台中记录了 Hi there!
。
练习 1.2
HTML 页面中的 JavaScript:
-
打开你的代码编辑器并创建一个 HTML 文件。
-
在你的 HTML 文件中,设置 HTML 标签、文档类型、HTML、head 和 body,然后继续添加脚本标签。
-
在脚本标签内放置一些 JavaScript 代码。你可以使用
console.log("hello world!")
。
将外部文件链接到我们的网页
你也可以将外部文件链接到 HTML 文件。这被认为是一种更好的做法,因为它更好地组织代码,并且你可以避免由于 JavaScript 而导致的非常长的 HTML 页面。除了这些好处之外,你可以在你的网站的其他网页上重用 JavaScript,而无需复制和粘贴。比如说,你有 10 页相同的 JavaScript,你需要对这个脚本进行修改。如果你按照我们在这个示例中展示的方式来做,你只需要更改一个文件。
首先,我们将创建一个单独的 JavaScript 文件。这些文件有 .js
后缀。我将称之为 ch1_alert.js
。这将是我们文件的内容:
alert("Saying hi from a different file!");
然后,我们将创建一个单独的 HTML 文件(再次使用 .html
后缀)。我们将给它以下内容:
<html>
<script type="text/javascript" src="img/ch1_alert.js"></script>
</html>
确保将文件放在同一位置,或者在你的 HTML 中指定 JavaScript 文件的路径。文件名区分大小写,应该完全匹配。
你有两种选择。你可以使用相对路径和绝对路径。我们先来谈谈后者,因为它最容易解释。你的计算机有一个根目录。对于 Linux 和 macOS,它是 /
,对于 Windows,通常是 C:/
。从根目录开始到文件的路径是绝对路径。这是最容易添加的,因为它在你的机器上会起作用。但是有一个问题:在你的机器上,如果这个网站文件夹后来被移动到服务器,绝对路径将不再有效。
第二种更安全的选择是相对路径。你指定从你当前所在的文件如何到达那里。所以如果它在同一个文件夹中,你只需要插入名称。如果它在名为 "example" 的文件夹中,而这个文件夹又位于你的文件所在的文件夹内,你将需要指定 example/文件名.js
。如果是上一级文件夹,你将需要指定 ../文件名.js
。
如果你打开 HTML 文件,你应该得到以下内容:
图 1.4:JavaScript 在不同文件中创建的弹出窗口
练习 1.3
链接到 JS JavaScript 文件:
-
创建一个扩展名为
.js
的名为app
的单独文件。 -
在
.js
文件中添加一些 JavaScript 代码。 -
在你创建的练习 1.2中的 HTML 文件内链接到单独的
.js
文件。 -
在你的浏览器中打开 HTML 文件,并检查 JavaScript 代码是否正确运行。
编写 JavaScript 代码
因此,我们现在有很多上下文,但你是如何实际编写 JavaScript 代码的呢?有一些重要的事情需要记住,比如如何格式化代码,使用正确的缩进级别,使用分号,以及添加注释。让我们从格式化代码开始。
代码格式化
代码需要很好地格式化。如果你有一个很长的文件,有很多行代码,而你又没有坚持一些基本的格式化规则,那么理解你所写的内容将会很困难。所以,基本格式化规则是什么?目前最重要的两个是缩进和分号。还有命名约定,但这些问题将在未来的每个主题中讨论。
缩进和空白
当你编写代码时,通常一行代码属于某个特定的代码块(两个大括号{}
之间的代码)或父语句。如果是这样,你给该块中的代码一个缩进,以确保你可以轻松地看到哪些是块的一部分,以及何时开始新的块。你不需要理解下面的代码片段,但它将展示有缩进和无缩进时的可读性。
不使用新行:
let status = "new"; let scared = true; if (status === "new") { console.log("Welcome to JavaScript!"); if (scared) { console.log("Don't worry you will be fine!"); } else { console.log("You're brave! You are going to do great!"); } } else { console.log("Welcome back, I knew you'd like it!"); }
使用新行但不缩进:
let status = "new";
let scared = true;
if (status === "new") {
console.log("Welcome to JavaScript!");
if (scared) {
console.log("Don't worry you will be fine!");
} else {
console.log("You're brave! You are going to do great!");
}
} else {
console.log("Welcome back, I knew you'd like it!");
}
使用新行和缩进:
let status = "new";
let scared = true;
if (status === "new") {
console.log("Welcome to JavaScript!");
if (scared) {
console.log("Don't worry you will be fine!");
} else {
console.log("You're brave! You are going to do great!");
}
} else {
console.log("Welcome back, I knew you'd like it!");
}
如你所见,你现在可以很容易地看到代码块何时结束。这就是if
在相同缩进级别有相应的}
的地方。在未缩进的例子中,你将不得不数括号来确定if
块何时结束。即使对于工作代码来说这不是必要的,但请确保正确使用缩进。你会在以后感谢自己的。
分号
在每个语句之后,你应该插入一个分号。JavaScript 非常宽容,会理解许多你忘记添加分号的情况,但最好养成在每行代码后都添加分号的习惯。当你声明一个代码块,如if
语句或循环时,你不应该以分号结束。这只是为了单独的语句。
代码注释
使用注释,你可以告诉解释器忽略文件中的某些行。如果它们是注释,则不会执行。能够避免执行文件的一部分通常很有用。这可能是以下原因之一:
-
你不希望在运行脚本时执行一段代码,所以你将其注释掉,这样它就会被解释器忽略。
-
元数据。为代码添加一些上下文,例如作者,以及文件涵盖的描述。
-
在代码的特定部分添加注释来解释正在发生的事情或为什么做出了某个选择。
写注释有两种方式。你可以写单行注释或者多行注释。以下是一个例子:
// I'm a single line comment
// console.log("single line comment, not logged");
/* I'm a multi-line comment. Whatever is between the slash asterisk and the asterisk slash will not get executed.
console.log("I'm not logged, because I'm a comment");
*/
// on the line will get ignored. The second one is multiline; it is written by starting with /* and ending with */.
练习 1.4
添加注释:
-
通过设置变量值向你的 JavaScript 代码中添加一个新语句。由于我们将在下一章中介绍这个内容,你可以使用以下行:
let a = 10;
-
在语句末尾添加注释,说明你设置了
10
的值。 -
使用
console.log()
打印值。添加注释说明这将做什么。 -
在你的 JavaScript 代码末尾使用多行注释。在实际的生产脚本中,你可能可以使用这个空间来添加文件目的的简要概述。
提示
我们还想在这里向你展示另一个命令提示符。它的工作方式与警报非常相似,但它会从用户那里获取输入。我们将很快学习如何存储变量,一旦你知道这一点,你就可以存储这个提示函数的结果并对其进行处理。例如,将 alert()
改为 prompt()
在 Hi.html
文件中,如下所示:
prompt("Hi! How are you?");
然后,刷新 HTML。你将看到一个带有输入框的弹出窗口,你可以在其中输入文本,如下所示:
图 1.5:页面提示用户输入
你(或任何其他用户)输入的值将被返回到脚本中,并可以在你的代码中使用!这对于获取用户输入以塑造代码的工作方式非常有用。
随机数
为了在这本书的前几章进行有趣的练习,我们希望你知道如何在 JavaScript 中生成一个随机数。即使你现在还不完全理解发生了什么,也没有关系;只需知道这是创建随机数的命令:
Math.random();
我们可以在控制台中执行它,并查看结果是否出现:
console.log(Math.random());
这个数字将在 0 和 1 之间的小数。如果我们想要一个介于 0 和 100 之间的数字,我们可以将其乘以 100,如下所示:
console.log(Math.random() * 100);
不要担心,我们将在 第二章,JavaScript 基础 中介绍数学运算符。
如果我们不希望得到小数结果,我们可以使用 Math.floor
函数,它将向下取整到最接近的整数:
console.log(Math.floor(Math.random() * 100));
不要担心现在还没有掌握这个。这本书后面会更详细地解释。在 第八章,内置 JavaScript 方法 中,我们将更详细地讨论内置方法。在此之前,请相信我们这确实会在 0 到 100 之间生成一个随机数。
章节项目
创建一个 HTML 文件和一个链接的 JavaScript 文件
创建一个 HTML 文件,创建一个单独的 JavaScript 文件。然后,从 HTML 文件连接到 JavaScript 文件。
-
在 JavaScript 文件中,将你的名字输出到控制台,并在你的代码中添加一个多行注释。
-
尝试在 JavaScript 文件中注释掉控制台消息,这样控制台就不会显示任何内容。
自我检查测验
-
添加外部 JavaScript 文件的 HTML 语法是什么?
-
你能在浏览器中运行扩展名为 JS 的文件中的 JavaScript 代码吗?
-
你如何在 JavaScript 中编写多行注释?
-
最好的方法是什么,可以从正在运行的代码中移除一行代码,同时你希望保留它以供调试?
摘要
干得漂亮!你已经开始了 JavaScript 的学习!在本章中,我们讨论了许多背景知识,这些知识在你开始编写 JavaScript 代码之前需要了解。我们了解到 JavaScript 可以用于许多目的,其中最受欢迎的使用场景之一是网页。浏览器可以与 JavaScript 协同工作,因为它们有一个特殊的部分,称为解释器,可以处理 JavaScript。我们看到了在电脑上编写 JavaScript 的多种选择。我们需要一个 IDE,这是一个我们可以用来编写和运行代码的程序。
将 JavaScript 添加到网页中可以通过几种方式完成。我们看到了如何在脚本元素中包含它,以及如何将单独的 JavaScript 文件添加到页面中。我们以一些关于如何编写结构良好、易于阅读和维护的代码的重要一般性说明结束本章,这些代码通过注释进行了良好的文档记录。我们还看到,我们可以使用console.log()
方法向控制台写入,并使用prompt()
方法请求用户输入。最后,我们还看到,我们可以使用Math.random()
函数生成随机数。
接下来,我们将探讨 JavaScript 的基本数据类型以及可以用来操作它们的运算符!
第二章:JavaScript 基础
在本章中,我们将处理 JavaScript 的一些基本构建块:变量和操作符。我们将从变量开始,了解它们是什么,以及存在哪些不同的变量数据类型。我们需要这些基本构建块来存储和处理脚本中的变量值,使它们变得动态。
一旦我们了解了变量,我们就可以处理操作符了。在这个阶段,我们将讨论算术、赋值、条件逻辑操作符。我们需要操作符来修改我们的变量或告诉我们关于这些变量的信息。这样我们就可以根据用户输入等因素进行基本计算。
在这个过程中,我们将涵盖以下主题:
-
变量
-
原始数据类型
-
分析和修改数据类型
-
操作符
注意:练习、项目和自我检查测验的答案可以在附录中找到。
变量
变量是学习大多数语言时你将首先接触到的构建块。变量是代码中的值,每次代码运行时可以代表不同的值。以下是一个脚本中两个变量的示例:
firstname = "Maaike";
x = 2;
并且它们可以在代码运行时分配新值:
firstname = "Edward";
x = 7;
没有变量,一段代码在每次运行时都会做完全相同的事情。尽管在某些情况下这仍然可能是有帮助的,但通过使用变量可以使代码更强大,允许我们的代码在每次运行时做不同的事情。
声明变量
第一次创建变量时,你需要声明它。为此,你需要一个特殊的词:let
、var
或 const
。我们很快就会讨论这三个参数的使用。第二次调用变量时,你只需使用现有变量的名称来为其分配新值:
let firstname = "Maria";
firstname = "Jacky";
在我们的示例中,我们将在代码中将值分配给我们的变量。这被称为“硬编码”,因为你的变量值是在你的脚本中定义的,而不是从某些外部输入动态获取。在实际代码中,你不会经常这样做,因为更常见的情况是值来自外部源,例如用户在网站上填写过的输入框、数据库或调用你的代码的其他代码。从外部源获取变量而不是将它们硬编码到脚本中,实际上是脚本能够适应新信息而不必重写代码的原因。
我们刚刚确立了变量构建块在代码中的强大功能。现在,我们将硬编码变量到我们的脚本中,因此它们将不会变化,直到程序员更改程序。然而,我们很快就会学习如何使我们的变量接受来自外部源的价值。
let, var 和 const
变量定义由三部分组成:变量定义关键字(let
、var
或 const
)、名称和值。让我们从 let
、var
或 const
之间的区别开始。这里你可以看到使用不同关键字的一些变量示例:
let nr1 = 12;
var nr2 = 8;
const PI = 3.14159;
let
和 var
都用于可能在程序中的某个地方被赋予新值的变量。let
和 var
之间的区别很复杂,它与作用域有关。
如果你理解以下关于作用域的句子,那很好,但如果你现在不理解,也完全没关系。随着你继续阅读本书,你很快就会理解它。
var
有 全局作用域,而 let
有 块级作用域。var
的全局作用域意味着你可以在整个脚本中使用用 var
定义的变量。另一方面,let
的块级作用域意味着你只能在定义它们的特定代码块中使用用 let
定义的变量。记住,代码块总是以 {
开始,以 }
结束,这就是你识别它们的方式。
另一方面,const
用于只被赋予一次值的变量——例如,π 的值,它不会改变。如果你尝试重新分配用 const
声明的值的值,你会得到一个错误:
const someConstant = 3;
someConstant = 4;
这将导致以下输出:
Uncaught TypeError: Assignment to constant variable.
我们将在大多数示例中使用 let
—— 目前,请相信我们,在大多数情况下你应该使用 let
。
变量命名
当谈到变量命名时,有一些约定:
-
变量以小写字母开头,并且应该是描述性的。如果某个东西包含年龄,不要称其为
x
,而应该称其为age
。这样,当你以后阅读你的脚本时,只需阅读你的代码就可以轻松理解你所做的一切。 -
变量不能包含空格,但可以使用下划线。如果你使用空格,JavaScript 不会将其识别为单个变量。
我们在这里将使用驼峰式命名法。这意味着当我们想要用多个单词来描述一个变量时,我们将以小写字母开头,然后在第一个单词之后的每个新单词首字母大写——例如:ageOfBuyer
。
无论你所在的地方有什么惯例,关键是保持一致性。如果所有的命名都以类似格式完成,代码将看起来更干净、更易读,这使得稍后进行小修改变得容易得多。
你的变量的值可以是任何东西。让我们从变量可以是最简单的事情开始:原始数据类型。
原始数据类型
现在你已经知道了什么是变量以及为什么我们需要在代码中使用它们,现在是时候看看我们可以存储在变量中的不同类型的值了。变量会分配一个值。而且这些值可以是不同类型的。JavaScript 是一种弱类型语言。这意味着 JavaScript 会根据值来确定类型。类型不需要显式命名。例如,如果你声明了一个值为 5,JavaScript 会自动将其定义为数字类型。
原始数据类型和其他更复杂的数据类型之间存在区别。在本章中,我们将介绍原始类型,它是一种相对简单的数据结构。现在让我们假设它们只包含一个值并且有一个类型。JavaScript 有七个原始类型:String、Number、BigInt、Boolean、Symbol、undefined 和 null。我们将在下面更详细地讨论每个类型。
字符串
字符串用于存储文本值。它是一系列字符。声明字符串有不同的方式:
-
双引号
-
单引号
-
反引号:可以直接使用变量的特殊模板字符串
单引号和双引号都可以这样使用:
let singleString = 'Hi there!';
let doubleString = "How are you?";
您可以使用您喜欢的选项,除非您正在处理已经选择了这些选项之一的代码。再次强调,一致性是关键。
单引号和双引号之间的主要区别是,您可以在双引号字符串中使用单引号作为字面字符,反之亦然。如果您用单引号声明字符串,那么字符串将在检测到第二个引号时结束,即使它位于单词的中间。所以例如,以下将导致错误,因为字符串将在 let's
中的第二个单引号处结束:
let funActivity = 'Let's learn JavaScript';
Let
将被识别为字符串,但在此之后,跟随的一串字符无法被 JavaScript 解释。然而,如果您使用双引号声明字符串,它不会在遇到单引号时立即结束字符串,因为它正在寻找另一个双引号。因此,这个替代方案可以正常工作:
let funActivity = "Let's learn JavaScript";
以同样的方式,使用双引号,以下将不会工作:
let question = "Do you want to learn JavaScript? "Yes!"";
再次强调,编译器不会区分不同上下文中使用的双引号,并将输出错误。
在使用反引号的字符串中,您可以指向变量,变量的值将被替换到行中。您可以在以下代码片段中看到这一点:
let language = "JavaScript";
let message = `Let's learn ${language}`;
console.log(message);
如您所见,您将不得不使用相当古怪的语法来指定这些变量——不要感到害怕!在这些模板字符串中,变量是在 ${nameOfVariable}
之间指定的。之所以语法如此复杂,是因为它们希望避免将其用作您通常会用到的,这会使得这样做变得不必要地困难。在我们的例子中,控制台输出将如下所示:
Let's learn JavaScript
如您所见,language
变量会被其值替换:JavaScript
。
转义字符
假设我们想在字符串中包含双引号、单引号和反引号。我们会遇到问题,因为我们现在没有足够的工具来解决这个问题。有一个优雅的解决方案。有一个特殊字符可以用来告诉 JavaScript,“不要像平常那样处理下一个字符。”这就是转义字符,一个反斜杠。
在这个例子中,反斜杠可以用来确保您的解释器不会看到单引号或双引号标记,并且不会过早地结束字符串:
let str = "Hello, what's your name? Is it \"Mike\"?";
console.log(str);
let str2 = 'Hello, what\'s your name? Is it "Mike"?';
console.log(str2);
这会在控制台输出以下内容:
Hello, what's your name? Is it "Mike"?
Hello, what's your name? Is it "Mike"?
如您所见,字符串中的两种引号类型都已记录下来而没有引发错误。这是因为引号字符前面的反斜杠给引号字符赋予了不同的意义。在这种情况下,意义是它应该是一个字面字符,而不是表示字符串结束的指示符。
转义字符有更多用途。你可以用它来创建换行符\n
,或者用\\
在文本中包含反斜杠字符:
let str3 = "New \nline.";
let str4 = "I'm containing a backslash: \\!";
console.log(str3);
console.log(str4);
这些行的输出如下:
New
line.
I'm containing a backslash: \!
还有更多选项,但现在我们先放一放。让我们通过查看数字类型来回到原始数据类型。
Number
数字数据类型用于表示,嗯,数字。在许多编程语言中,不同类型的数字之间有一个非常明显的区别。JavaScript 的开发者决定为所有这些数字使用一个数据类型:number。更准确地说,他们决定使用 64 位浮点数。这意味着它可以存储相当大的数字,包括有符号和无符号的数字,小数数字等等。
然而,它可以表示不同种类的数字。首先,整数,例如:4 或 89。但数字数据类型也可以用来表示小数、指数、八进制、十六进制和二进制数字。以下代码示例应该可以说明一切:
let intNr = 1;
let decNr = 1.5;
let expNr = 1.4e15;
let octNr = 0o10; //decimal version would be 8
let hexNr = 0x3E8; //decimal version would be 1000
let binNr = 0b101; //decimal version would be 5
如果你对这些不熟悉,你不需要担心。这些只是你在计算机科学更广泛的领域中可能会遇到的不同表示数字的方式。这里的要点是上述所有数字都是数字数据类型。所以整数是数字,就像这些:
let intNr2 = 3434;
let intNr3 = -111;
浮点数也是数字,就像这个:
let decNr2 = 45.78;
二进制数字也是数字数据类型的一部分,例如,这个:
let binNr2 = 0b100; //decimal version would be 4
我们刚刚看到了非常常用的数字数据类型。但在某些特殊情况下,你需要一个更大的数字。
BigInt
数字数据类型的限制在 2⁵³-1 和-(2⁵³-1)之间。如果你需要更大的(或更小的)数字,BigInt 就派上用场了。BigInt 数据类型可以通过后缀n
来识别:
let bigNr = 90071992547409920n;
让我们看看当我们开始在我们之前创建的整数 Number,intNr
,和 BigInt,bigNr
之间进行一些计算时会发生什么:
let result = bigNr + intNr;
输出结果如下:
Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions
哎呀,一个TypeError
!它非常清楚地说明了出了什么问题。我们不能将 BigInt 与 Number 数据类型混合进行操作。这是在以后实际使用 BigInt 时需要注意的事情——你只能用其他 BigInt 进行操作。
布尔值
布尔数据类型可以存储两个值:true
和false
。中间没有其他值。这种布尔值在代码中用得很多,尤其是在评估为布尔值的表达式:
let bool1 = false;
let bool2 = true;
在前面的例子中,你可以看到布尔数据类型所提供的选项。它用于需要存储true
或false
值(可以表示开/关或是/否)的情况。例如,一个元素是否被删除:
let objectIsDeleted = false;
或者,灯光是开启还是关闭:
let lightIsOn = true;
这些变量分别表明指定的对象没有被删除,以及特定的灯光是开启的。
符号
Symbol 是 ES6(我们在第一章,JavaScript 入门中提到的 ECMA Script 6,或 ES6)中引入的一种全新的数据类型。当变量即使值和类型相同,也需要确保它们不相等时,可以使用 Symbol。比较以下字符串声明与符号声明,它们的值都是相同的:
let str1 = "JavaScript is fun!";
let str2 = "JavaScript is fun!";
console.log("These two strings are the same:", str1 === str2);
let sym1 = Symbol("JavaScript is fun!");
let sym2 = Symbol("JavaScript is fun!");
console.log("These two Symbols are the same:", sym1 === sym2);
以及输出:
These two strings are the same: true
These two Symbols are the same: false
在前半部分,JavaScript 认为字符串是相同的。它们具有相同的值和类型。然而,在第二部分,每个符号都是唯一的。因此,尽管它们包含相同的字符串,但它们并不相同,并且在比较时输出false
。这些符号数据类型可以作为对象的属性非常有用,我们将在第三章,JavaScript 多重值中看到。
未定义
JavaScript 是一种非常特殊的语言。它为未赋值的变量提供了一个特殊的数据类型。这个数据类型是未定义的:
let unassigned;
console.log(unassigned);
此处的输出将是:
Undefined
我们也可以故意赋一个undefined
值。重要的是要知道这是可能的,但更重要的是要知道手动赋值 undefined 是一种不良做法:
let terribleThingToDo = undefined;
好吧,这是可以做到的,但建议不要这样做。这有多个原因——例如,检查两个变量是否相同。如果一个变量是未定义的,而你的变量被手动设置为未定义,它们将被视为相等。这是一个问题,因为如果你正在检查相等性,你想要知道两个值是否实际上相等,而不仅仅是它们都是未定义的。这样,某人的宠物和他们的姓氏可能会被认为是相同的,而实际上它们只是两个空值。
null
在最后一个例子中,我们看到了一个可以用基本类型 null 解决的问题。null 是一个表示变量为空或具有未知值的特殊值。这是大小写敏感的。你应该使用小写 null:
let empty = null;
为了解决我们遇到的将变量设置为未定义的问题,请注意,如果你将其设置为 null,你将不会遇到相同的问题。这就是为什么当你想要表示变量最初是空和未知时,将 null 赋给变量更好的原因之一:
let terribleThingToDo = undefined;
let lastName;
console.log("Same undefined:", lastName === terribleThingToDo);
let betterOption = null;
console.log("Same null:", lastName === betterOption);
这将输出以下内容:
Same undefined: true
Same null: false
这表明一个自动未定义的变量lastName
和一个故意未定义的变量terribleThingToDo
被认为是相等的,这是有问题的。另一方面,lastName
和显式声明为 null 值的betterOption
不相等。
分析和修改数据类型
我们已经看到了原始数据类型。有一些内置的 JavaScript 方法可以帮助我们处理与原始数据类型相关的常见问题。内置方法是一些逻辑片段,可以在不自己编写 JavaScript 逻辑的情况下使用。
我们已经看到了一个内置方法:console.log()
。
有很多这样的内置方法,而您在本章中将要遇到的方法只是您将要遇到的前几个。
确定变量的类型
尤其是对于 null 和 undefined,确定你正在处理的数据类型可能很困难。让我们看看 typeof
。这个函数返回变量的类型。你可以通过输入 typeof
,然后是空格后跟相关的变量,或者将变量放在括号中来检查变量的类型:
testVariable = 1;
variableTypeTest1 = typeof testVariable;
variableTypeTest2 = typeof(testVariable);
console.log(variableTypeTest1);
console.log(variableTypeTest2);
如您所料,这两种方法都会输出 number
。不需要括号,因为技术上 typeof
是一个运算符,而不是一个方法,与 console.log
不同。但有时您可能会发现使用括号可以使您的代码更容易阅读。在这里您可以看到它的实际应用:
let str = "Hello";
let nr = 7;
let bigNr = 12345678901234n;
let bool = true;
let sym = Symbol("unique");
let undef = undefined;
let unknown = null;
console.log("str", typeof str);
console.log("nr", typeof nr);
console.log("bigNr", typeof bigNr);
console.log("bool", typeof bool);
console.log("sym", typeof sym);
console.log("undef", typeof undef);
console.log("unknown", typeof unknown);
在这里,在同一个 console.log()
打印命令中,我们正在打印每个变量的名称(作为字符串,用双引号声明),然后是其类型(使用 typeof
)。这将产生以下输出:
str string
nr number
bigNr bigint
bool boolean
sym symbol
undef undefined
unknown object
其中有一个例外,那就是 null 类型。在输出中,您可以看到 typeof null
返回 object
,而实际上,null 真的是一个原始类型,而不是一个对象。这是一个从很久以前就存在的错误,现在由于向后兼容性问题无法移除。不用担心这个错误,因为它不会影响我们的程序——只需意识到它,因为它不会很快消失,并且有可能破坏应用程序。
转换数据类型
JavaScript 中的变量可以改变类型。有时 JavaScript 会自动这样做。你认为运行以下代码片段的结果会是什么?
let nr1 = 2;
let nr2 = "2";
console.log(nr1 * nr2);
我们尝试将一个 Number 类型的变量与一个 String 类型的变量相乘。JavaScript 不会像许多语言那样直接抛出错误,而是首先尝试将字符串值转换为数字。如果可以这样做,它就可以无任何问题地执行,就像声明了两个数字一样。在这种情况下,console.log()
将 4
写入控制台。
但这是危险的!猜猜这个代码片段做了什么:
let nr1 = 2;
let nr2 = "2";
console.log(nr1 + nr2);
这个会输出 22
。加号可以用来连接字符串。因此,在这个例子中,不是将字符串转换为数字,而是将数字转换为字符串,然后将两个字符串合并在一起——“2
”和“2
”合并成“22
”。幸运的是,我们不需要依赖于 JavaScript 在转换数据类型时的行为。我们可以使用一些内置函数来转换我们变量的数据类型。
有三种转换方法:String()
、Number()
和Boolean()
。第一个方法将变量转换为 String 类型。它几乎可以接受任何值,包括 undefined 和 null,并在其周围加上引号。
第二个尝试将一个变量转换为数字。如果无法逻辑上完成转换,它将值更改为 NaN(不是一个数字)。Boolean()
函数将变量转换为布尔值。除了 null、undefined、0(数字)、空字符串和 NaN 之外的所有内容都将返回 true。让我们看看它们在实际操作中的表现:
let nrToStr = 6;
nrToStr = String(nrToStr);
console.log(nrToStr, typeof nrToStr);
let strToNr = "12";
strToNr = Number(strToNr);
console.log(strToNr, typeof strToNr);
let strToBool = "any string will return true";
strToBool = Boolean(strToBool);
console.log(strToBool, typeof strToBool);
这将记录以下内容:
6 string
12 number
true boolean
这可能看起来很简单,但并非所有选项都同样明显。例如,这些可能不是您所想的:
let nullToNr = null;
nullToNr = Number(nullToNr);
console.log("null", nullToNr, typeof nullToNr);
let strToNr = "";
strToNr = Number(strToNr);
console.log("empty string", strToNr, typeof strToNr);
上述代码片段将在控制台记录以下内容:
null 0 number
empty string 0 number
如您所见,空字符串和 null 都会导致数字 0。这是 JavaScript 制作者做出的一个选择,您必须知道——在您想要将空字符串或 null 转换为 0 时,这有时会很有用。
接下来,输入以下代码片段:
let strToNr2 = "hello";
strToNr2 = Number(strToNr2);
console.log(strToNr2, typeof strToNr2);
将被记录到控制台的结果是:
NaN number
在这里,我们可以看到任何不能通过简单地去除引号来解释为数字的东西都会评估为NaN
(不是一个数字)。
让我们继续以下代码:
let strToBool2 = "false";
strToBool2 = Boolean(strToBool2);
console.log(strToBool2, typeof strToBool2);
let strToBool = "";
strToBool = Boolean(strToBool);
console.log(strToBool, typeof strToBool);
最后,这个会记录以下内容:
true boolean
false boolean
这个输出显示,任何转换为布尔值时都会返回 true 的字符串,即使是字符串"false"
!只有空字符串、null 和 undefined 才会导致布尔值为 false。
让我们再稍微刺激一下你的大脑。你认为这个会记录什么?
let nr1 = 2;
let nr2 = "2";
console.log(nr1 + Number(nr2));
这一个会记录4
!在执行加法操作之前,字符串被转换为数字,因此它是一个数学运算,而不是字符串连接。在本章的下一节中,我们将更深入地讨论运算符。
练习 2.1
下面列出的变量类型是什么?使用typeof
进行验证,并将结果输出到控制台:
let str1 = 'Laurence';
let str2 = "Svekis";
let val1 = undefined;
let val2 = null;
let myNum = 1000;
运算符
在看到许多数据类型和一些转换方法之后,现在是时候介绍下一个主要构建块:运算符。当我们想要与变量一起工作、修改它们、对它们进行计算或比较它们时,这些运算符就派上用场了。它们被称为运算符,因为我们使用它们来对变量进行操作。
算术运算符
算术运算符可以用来对数字执行操作。大多数这些操作对您来说都非常自然,因为它们是您在早年生活中已经接触过的基础数学。
加法
JavaScript 中的加法非常简单,我们之前已经见过。我们使用+
进行这个操作:
let nr1 = 12;
let nr2 = 14;
let result1 = nr1 + nr2;
然而,这个运算符也可以在连接字符串时非常有用。注意在"Hello"
之后添加的空格,以确保最终结果包含空格字符:
let str1 = "Hello ";
let str2 = "addition";
let result2 = str1 + str2;
打印result1
和result2
的输出将如下所示:
26
Hello addition
如您所见,添加数字和字符串会导致不同的结果。如果我们添加两个不同的字符串,它们将被连接成一个单一的字符串。
练习 2.2
为你的名字创建一个变量,另一个用于你的年龄,还有一个用于你是否能编写 JavaScript。
在控制台输出以下句子,其中name
、age
和true
/false
是变量:
Hello, my name is Maaike, I am 29 years old and I can code JavaScript: true.
减法
减法与我们的预期一样工作。我们使用-
来进行这个操作。你认为在这个第二个例子中变量中存储了什么?
let nr1 = 20;
let nr2 = 4;
let str1 = "Hi";
let nr3 = 3;
let result1 = nr1 - nr2;
let result2 = str1 - nr3;
console.log(result1, result2);
输出如下:
16 NaN
第一个结果是16
。第二个结果更有趣。它给出NaN
,不是一个错误,而只是简单地得出结论:一个单词和一个数字相减的结果不是数字。感谢 JavaScript 没有崩溃!
乘法
我们可以使用*
字符来乘以两个数值。与某些其他语言不同,在 JavaScript 中我们不能成功地将一个数字和一个字符串相乘。
乘以一个数值和一个非数值的结果是 NaN:
let nr1 = 15;
let nr2 = 10;
let str1 = "Hi";
let nr3 = 3;
let result1 = nr1 * nr2;
let result2 = str1 * nr3;
console.log(result1, result2);
输出:
150 NaN
除法
另一个直接的运算符是除法。我们可以使用/
字符来除以两个数字:
let nr1 = 30;
let nr2 = 5;
let result1 = nr1 / nr2;
console.log(result1);
输出如下:
6
幂运算
幂运算意味着将某个基数数提升到指数的幂,例如,x^y。这可以读作x的y次幂。这意味着我们将x乘以自身y次。以下是在 JavaScript 中如何进行此操作的示例——我们使用**
来进行这个运算符:
let nr1 = 2;
let nr2 = 3;
let result1 = nr1 ** nr2;
console.log(result1);
我们得到以下输出:
8
这个操作的结果是 2 的 3 次方(2 * 2 * 2),即8
。我们不会在这里深入讲解数学课程,但我们可以通过使用分数指数来找到数字的根:例如,一个值的平方根等同于将其提升到 0.5 的幂。
取模
这通常需要一点解释。取模是在将一个数字除以另一个数字的整个过程中确定剩余多少的操作。这个数字可以在另一个数字中容纳的次数在这里并不重要。结果将是余数,或者剩下的部分。我们用于这个操作的字符是%
字符。以下是一些示例:
let nr1 = 10;
let nr2 = 3;
let result1 = nr1 % nr2;
console.log(`${nr1} % ${nr2} = ${result1}`);
let nr3 = 8;
let nr4 = 2;
let result2 = nr3 % nr4;
console.log(`${nr3} % ${nr4} = ${result2}`);
let nr5 = 15;
let nr6 = 4;
let result3 = nr5 % nr6;
console.log(`${nr5} % ${nr6} = ${result3}`);
并且输出如下:
10 % 3 = 1
8 % 2 = 0
15 % 4 = 3
第一个示例是10 % 3
,其中 3 可以 3 次进入 10,然后剩下 1。第二个示例是8 % 2
。结果是 0,因为 2 可以 4 次进入 8 而没有剩余。最后一个示例是15 % 4
,其中 4 可以 3 次进入 15。然后我们得到 3 作为结果。
这是我让你在当前时间上加 125 分钟时,你头脑中会自动发生的事情。你可能做两件事:整数除法来确定 125 分钟中有多少个完整的小时,然后 125 模 60(用 JavaScript 术语,125 % 60
)来确定你需要将 5 分钟加到当前时间上。假设我们的当前时间是 09:59,你可能会先加 2 个小时,变成 11:59,然后加 5 分钟,然后你将执行另一个模运算,用 59 和 5,再增加 1 个小时到总时间,剩下 4 分钟:12:04。
一元运算符:递增和递减
如果你是编程新手(或者只熟悉另一种编程语言),那么我们算术运算符部分中的最后两个运算符可能对你来说是新的。这些是递增和递减运算符。我们在这里使用的一个术语是操作数。操作数受运算符的影响。所以,如果我们说 x + y
,x 和 y 是操作数。
这些运算符只需要一个操作数,因此我们也将它们称为一元运算符。如果我们看到 x++
,我们可以将其读作 x = x + 1。对于递减运算符也是一样:x--
可以读作 x = x – 1:
let nr1 = 4;
nr1++;
console.log(nr1);
let nr2 = 4;
nr2--;
console.log(nr2);
输出如下:
5
3
前缀和后缀运算符
我们可以在操作数之后有递增运算符(x++
),在这种情况下,我们称之为后缀一元运算符。我们也可以在它之前(++x
),这被称为前缀一元运算符。但这做的是不同的事情——接下来的几行可能比较复杂,所以如果你需要多次阅读并仔细查看这里的示例,请不要担心。
后缀运算是在将变量传递之后执行的,然后是执行操作。在下面的例子中,nr
在记录后增加 1。所以第一个记录语句仍然记录的是旧值,因为它还没有被更新。它已经在第二个记录语句中更新了:
let nr = 2;
console.log(nr++);
console.log(nr);
输出如下:
2
3
前缀运算会在将变量传递之前执行,通常这也是你需要的一个。看看下面的例子:
let nr = 2;
console.log(++nr);
我们得到以下输出:
3
好的,如果你能弄清楚下一个代码片段将输出什么到控制台,你应该真的掌握了它:
let nr1 = 4;
let nr2 = 5;
let nr3 = 2;
console.log(nr1++ + ++nr2 * nr3++);
它输出 16
。根据基本的数学运算顺序,它首先会进行乘法运算。对于乘法,它使用 6(前缀,所以 5 在乘法之前增加)和 2(后缀,所以 2 只在执行后增加,这意味着它不会影响我们当前的运算)。这等于 12。然后 nr1
是后缀运算符,所以这个运算会在加法之后执行。因此,它会将 12 加到 4 上,变成 16。
结合运算符
这些运算符可以组合,并且它们的工作方式与数学中的方式相同。它们的执行顺序是特定的,并不一定是从左到右。这是由于一个称为运算符优先级的现象。
这里还有一点需要注意,那就是分组。你可以使用 (
和 )
进行分组。括号内的运算具有最高优先级。之后,运算的顺序根据运算类型(优先级最高先进行)进行,如果它们的优先级相同,则从左到右进行:
名称 | 符号 | 示例 |
---|---|---|
分组 | (...) |
(x + y) |
指数运算 | ** |
x ** y |
前缀递增和递减 | -- , ++ |
--x , ++y |
乘法、除法、取模 | * , / , % |
x * y , x / y , x % y |
加法和减法 | + , - |
x + y , x - y |
练习 2.3
编写一些代码,使用勾股定理计算三角形的斜边长度,当给定其他两边的值时。定理指定了直角三角形边之间的关系是 a² + b² = c²。
勾股定理仅适用于直角三角形。与 90 度角相连的边称为相邻边和对边,在公式中用 a 和 b 表示。最长的边,不与 90 度角相连,称为斜边,用 c 表示。
你可以使用 prompt()
获取 a 和 b 的值。编写代码从用户那里获取 a
和 b
的值。然后对 a
和 b
的值进行平方,将它们相加,并找到平方根。将你的答案打印到控制台。
赋值运算符
当我们给变量赋值时,我们已经见过一个赋值运算符了。这个基本赋值操作的字符是 =
。还有一些其他的赋值运算符可用。每个二元算术运算符都有一个对应的赋值运算符,以便编写更短的代码。例如,x += 5 表示 x = x + 5,而 x **= 3 表示 x = x ** 3(x 的 3 次方)。
在这个第一个例子中,我们声明了一个变量 x
,并将其初始值设置为 2
:
let x = 2;
x += 2;
在这个赋值操作之后,x
的值变为 4,因为 x += 2 与 x = x + 2 是相同的:
在下一个赋值操作中,我们将减去 2
:
x -= 2;
因此,经过这次操作后,x
的值再次变为 2
(x = x – 2)。在下一个操作中,我们将值乘以 6:
x *= 6;
当这一行代码执行完毕后,x
的值不再是 2,而是变成了 12(x = x * 6)。在下一行,我们将使用赋值运算符来进行除法操作:
x /= 3;
将 x
除以 3
后,新的值变为 4。接下来我们将使用的是指数运算符:
x **= 2;
x
的值变为 16,因为旧值是 4,4 的平方等于 16(4 * 4)。我们将讨论的最后一个是取模赋值运算符:
x %= 3;
在这个赋值操作之后,x
的值是 1,因为 3 可以被 16 整除 5 次,然后剩下 1。
练习 2.4
为三个数字创建变量:a、b 和 c。使用赋值运算符更新这些变量,执行以下操作:
-
将 b 加到 a 上
-
将 a 除以 c
-
将 c 的值替换为 c 和 b 的模
-
将所有三个数字打印到控制台
比较运算符
比较运算符与我们迄今为止看到的运算符不同。比较运算符的结果始终是布尔值,即 true 或 false。
相等
有几个相等运算符可以确定两个值是否相等。它们有两种类型:仅值相等,或值和数据类型相等。第一种在值相等时返回 true,即使类型不同,而第二种仅在值和类型都相同时返回 true:
let x = 5;
let y = "5";
console.log(x == y);
双等号运算符,两个等号,表示它将只检查值是否相等,而不检查数据类型。它们都有值 5
,所以它将在控制台输出 true
。这种相等性有时被称为松散相等。
三等号运算符,写作三个等号,表示它将评估值和数据类型,以确定两边是否相等。为了使这个语句为真,它们都必须相等,但它们不相等,因此下面的语句输出 false
:
console.log(x === y);
这有时也称为严格相等。当你需要检查相等性时,你应该最常使用这个三等号运算符,因为只有使用这个运算符,你才能确保两个变量确实是相等的。
不等于
不等于与等于非常相似,除了它做的是相反的操作——当两个变量不相等时返回 true,当它们相等时返回 false。我们使用感叹号表示不等于:
let x = 5;
let y = "5";
console.log(x != y);
这将在控制台输出 false
。如果你想知道这里发生了什么,请再次查看双等号和三等号运算符,因为这里也是同样的情况。然而,当不等于运算符中只有一个等号时,它是在进行松散的非相等比较。因此,它得出结论它们是相等的,因此不相等应该导致 false。带有两个等号的那个是在检查严格非相等:
console.log(x !== y);
这将得出结论,由于 x
和 y
具有不同的数据类型,它们不是相同的,并且将在控制台输出 true
。
大于和小于
大于运算符如果操作符的左侧大于右侧,则返回 true。我们使用 >
字符表示这一点。我们还有一个大于等于运算符,>=
,如果左侧大于或等于右侧,则返回 true
。
let x = 5;
let y = 6;
console.log(y > x);
这一个将输出 true
,因为 y
大于 x
。
console.log(x > y);
由于 x
不大于 y
,这将输出 false
。
console.log(y > y);
y
不大于 y
,因此这将输出 false
。
console.log(y >= y);
最后一个是在查看 y
是否大于或等于 y
,由于它等于自身,它将输出 true
。
可能不会让你感到惊讶,我们还有小于(<
)和小于等于运算符(<=
)。让我们看看小于运算符,因为它与前面的运算符非常相似。
console.log(y < x);
这个第一个将是 false
,因为 y
不小于 x
。
console.log(x < y);
因此,第二个将记录 true
,因为 x
小于 y
。
console.log(y < y);
y
不小于 y
,所以这将记录 false
。
console.log(y <= y);
最后一个检查的是 y
是否小于等于 y
。它等于 y
,所以它将记录 true
。
逻辑运算符
每当你想要检查一个条件,或者你需要否定一个条件时,逻辑运算符就派上用场了。你可以使用“与”、“或”和“非”。
与
我们首先将查看的是“与”运算符。如果你想检查 x
是否大于 y
且 y
是否大于 z
,你需要能够组合两个表达式。这可以通过 &&
运算符来完成。只有当两个表达式都为真时,它才会返回 true
:
let x = 1;
let y = 2;
let z = 3;
考虑到这些变量,我们将查看逻辑运算符:
console.log(x < y && y < z);
这将记录 true
,你可以这样读:如果 x
小于 y
且 y
小于 z
,它将记录 true
。这是这种情况,所以它将记录 true
。下一个示例将记录 false
:
console.log(x > y && y < z);
由于 x
不大于 y
,表达式的一部分不是真的,因此它将导致 false
。
或
如果你想要在任一表达式为真时得到 true
,你将使用“或”。这个运算符是 ||
。这些管道用于检查这两个中的任何一个是否为真,在这种情况下,整个表达式评估为 true
。让我们看看“或”运算符的实际应用:
console.log(x > y || y < z);
这将导致 true
,而之前使用 &&
时是 false
。这是因为只需要两个部分中的任何一个为真,整个表达式才能评估为 true
。这是因为 y
小于 z
。
当双方都为假时,它将记录false
,这在下一个示例中就是这样:
console.log(x > y || y > z);
非
在某些情况下,你可能需要否定一个布尔值。这将使其变为相反的值。可以使用感叹号来完成,它读作“不是”:
let x = false;
console.log(!x);
这将记录 true
,因为它只是简单地翻转布尔值的值。你也可以否定一个评估为布尔值的表达式,但你需要确保表达式首先通过分组进行评估。
let x = 1;
let y = 2;
console.log(!(x < y));
x
小于 y
,所以表达式评估为 true
。但是,由于感叹号的存在,它打印 false
到控制台。
章节项目
英里到公里的转换器
创建一个包含英里值的变量,将其转换为公里,并按以下格式记录公里值:
The distance of 130 kms is equal to 209.2142 miles
为了参考,1 英里等于 1.60934 公里。
BMI 计算器
设置身高为英寸和体重为磅的值,然后将这些值转换为厘米和公斤,并将结果输出到控制台:
-
1 英寸等于 2.54 厘米
-
2.2046 磅等于 1 公斤
输出结果。然后,计算并记录 BMI:这是体重(以千克为单位)除以身高的平方(以米为单位)。将结果输出到控制台。
自我检查测验
-
以下变量的数据类型是什么?
const c = "5";
-
以下变量的数据类型是什么?
const c = 91;
-
哪一行通常更好,第 1 行还是第 2 行?
let empty1 = undefined; //line 1 let empty2 = null; //line 2
-
以下代码的控制台输出是什么?
let a = "Hello"; a = "world"; console.log(a);
-
控制台将记录什么?
let a = "world"; let b = `Hello ${a}!`; console.log(b);
-
a
的值是多少?let a = "Hello"; a = prompt("world"); console.log(a);
-
输出到控制台的
b
的值是多少?let a = 5; let b = 70; let c = "5"; b++; console.log(b);
-
result
的值是多少?let result = 3 + 4 * 2 / 8;
-
total
和total2
的值是多少?let firstNum = 5; let secondNum = 10; firstNum++; secondNum--; let total = ++firstNum + secondNum; console.log(total); let total2 = 500 + 100 / 5 + total--; console.log(total2);
-
这里记录了什么到控制台?
const a = 5; const b = 10; console.log(a > 0 && b > 0); console.log(a == 5 && b == 4); console.log(true ||false); console.log(a == 3 || b == 10); console.log(a == 3 || b == 7);
概述
在本章中,我们处理了前两个编程构建块:变量和运算符。变量是具有名称并包含值的特殊字段。我们通过使用以下特殊变量定义词之一来声明变量:let
、var
或const
。变量使我们能够使我们的脚本动态化,存储值,稍后访问它们,并稍后更改它们。我们讨论了一些原始数据类型,包括字符串、数字、布尔值和符号,以及更抽象的类型,如未定义和 null。你学习了如何使用typeof
这个词来确定变量的类型。你还看到了如何使用内置的 JavaScript 方法Number()
、String()
和Boolean()
来转换数据类型。
然后,我们继续讨论并讨论了运算符。运算符使我们能够处理我们的变量。它们可以用来执行计算、比较变量以及更多操作。我们讨论了包括算术运算符、赋值运算符、比较运算符和逻辑运算符在内的运算符。
在本章之后,你将准备好处理更复杂的数据类型,例如数组和对象。我们将在下一章介绍这些内容。
第三章:JavaScript 多值
基本数据类型在前一章中已经处理过了。现在是时候看看一个稍微复杂一点的主题:数组和对象。在前一章中,你看到了只包含单个值的变量。为了允许更复杂的编程,对象和数组可以包含多个值。
你可以将对象视为属性和方法的集合。属性可以被视为变量。它们可以是简单的数据结构,如数字和字符串,也可以是其他对象。方法执行操作;它们包含一定数量的代码行,当方法被调用时将执行这些代码行。我们将在本书的后面部分更详细地解释方法,并专注于属性。一个对象的例子可以是现实生活中的对象,例如,一只狗。它有属性,如名字、重量、颜色和品种。
我们还将讨论数组。数组是一种对象类型,它允许你存储多个值。它们有点像列表。所以,你可以有一个包含以下值的购物清单数组:苹果、鸡蛋和面包。这个列表将以单个变量的形式呈现,包含多个值。
在此过程中,我们将涵盖以下主题:
-
数组和它们的属性
-
数组方法
-
多维数组
-
JavaScript 中的对象
-
与对象和数组一起工作
让我们从数组开始。
注意:练习、项目和自我检查测验的答案可以在附录中找到。
数组和它们的属性
数组是值的列表。这些值可以是所有数据类型,一个数组甚至可以包含不同的数据类型。在单个变量中存储多个值通常非常有用;例如,学生名单、购物清单或测试分数。一旦你开始编写脚本,你会发现你需要经常编写数组;例如,当你想要跟踪所有用户输入时,或者当你想要向用户展示选项列表时。
创建数组
到现在为止,你可能已经相信数组很棒了,那么让我们看看我们如何创建它们。实际上,有正确和错误的方法来做这件事。这里都有。你认为哪个是正确的?
arr1 = new Array("purple", "green", "yellow");
arr2 = ["black", "orange", "pink"];
如果你猜对了第二个选项,使用方括号,你是正确的。这是创建新数组的最佳和最可读的方式。另一方面,第一个选项可能会做意想不到的事情。看看这两行代码。你认为它们会做什么?
arr3 = new Array(10);
arr4 = [10];
很可能,你在这里感觉到有些不对劲。它们并不都创建一个包含一个值10
的数组。第二个,arr4
,确实创建了。第一个选项创建了一个包含 10 个未定义值的数组。如果我们这样记录值:
console.log(arr3);
console.log(arr4);
这里是它记录的内容:
[ <10 empty items> ]
[ 10 ]
谢谢,JavaScript!这非常有帮助。所以,除非你需要这样做,请使用方括号!
如我之前提到的,我们可以有混合数组,数组可以存储任何类型的变量。数组的值不会被转换为单一的数据类型或类似的东西。JavaScript 简单地将所有变量及其数据类型和值存储在数组中:
let arr = ["hi there", 5, true];
console.log(typeof arr[0]);
console.log(typeof arr[1]);
console.log(typeof arr[2]);
这将在控制台输出:
string
number
boolean
我们在这里要讨论的最后一个数组有趣的事实是,如果你使用 const
定义数组会发生什么。你可以改变常量数组的值,但不能改变数组本身。以下是一段演示代码:
const arr = ["hi there"];
arr[0] = "new value";
console.log(arr[0]);
arr = ["nope, now you are overwriting the array"];
数组第一个元素的新的值一切正常,但你不能为整个数组分配一个新的值。以下是它将输出的内容:
new value
TypeError: Assignment to constant variable.
访问元素
我们刚刚创建的这个漂亮的数组,如果我们能够访问其元素,将会变得更有力量。我们可以通过引用数组的索引来实现这一点。这是我们在创建数组时没有指定的事情,我们也不需要指定。JavaScript 会为数组的每个值分配一个索引。第一个值被分配为位置 0,第二个为 1,第三个为 2,以此类推。如果我们想根据其在数组中的位置调用一个特定的值,我们可以使用我们的数组名,在末尾添加方括号,并在方括号中放置我们想要访问的索引,如下所示:
cars = ["Toyota", "Renault", "Volkswagen"];
console.log(cars[0]);
这条日志语句将把 Toyota
写入控制台,因为我们调用了数组的第 0 个位置,它输出了列表中的第一个值。
console.log(cars[1]);
调用索引位置 1 给我们的是数组中的第二个元素,它是 Renault
。这将记录到控制台。
console.log(cars[2]);
我们数组中的第三个元素索引为 2,所以这将记录 Volkswagen
。你认为如果我们使用负索引或高于我们得到的值的索引会发生什么?
console.log(cars[3]);
console.log(cars[-1]);
我们没有为负数或不存在的索引分配值,所以当我们请求它时,其值是未定义的。因此,日志输出将是未定义的。JavaScript 由于这个原因不会抛出错误。
覆盖元素
数组中的元素可以被覆盖。这可以通过使用索引访问某个元素并分配新值来完成:
cars[0] = "Tesla";
console.log(cars[0]);
这条日志的输出是 Tesla
,因为它覆盖了旧值 Toyota
。如果我们输出整个数组:
console.log(cars);
它将输出以下内容:
[ 'Tesla', 'Renault', 'Volkswagen' ]
如果你尝试覆盖一个不存在的元素会发生什么?
cars[3] = "Kia";
或者甚至是负索引?
cars[-1] = "Fiat";
让我们看看当我们尝试将值写入控制台时会发生什么:
console.log(cars[3]);
console.log(cars[-1]);
然后,输出如下:
Kia
Fiat
哈哈!它们突然出现了。你可能想知道这是怎么回事?我们将在下一节中讨论这个问题。现在,只需记住,这不是向数组添加值的正确方法。当我们解释 Array methods 部分时,我们将讨论正确的方法。
内置长度属性
数组有一个非常有用的内置属性:长度。这将返回数组中的值的数量:
colors = ["black", "orange", "pink"]
booleans = [true, false, false, true];
emptyArray = [];
console.log("Length of colors:", colors.length);
console.log("Length of booleans:", booleans.length);
console.log("Length of empty array:", emptyArray.length);
第一次调用console.log
返回3
,表示颜色数组包含 3 个值。第二次调用返回4
,最后一次调用返回一个长度为0
的空数组:
Length of colors: 3
Length of booleans: 4
Length of empty array: 0
因此,请注意,长度比最大索引多 1,因为数组的索引从 0 开始,但在确定长度时,我们查看元素的数量,这里有四个独立的元素。这就是为什么长度为 4 时,最大索引是 3。因此,数组中最后一个元素的位置值将比元素总数少 1。
花点时间想想你如何使用长度来访问数组的最后一个元素:
lastElement = colors[colors.length - 1];
你可以通过从长度中减去 1 来得到最高索引,因为你知道,数组是零索引的。所以,数组中最后一个元素的位置值将比元素总数少 1。
所以,这看起来可能很简单。记得我们在上一节中调用的不存在的索引位置吗?让我们看看这个例子会发生什么:
numbers = [12, 24, 36];
numbers[5] = 48;
console.log(numbers.length);
数组的长度只计算从 0 开始的整数,直到最高填充索引。如果序列中间有未设置值的元素,它们仍然会被计算。在这种情况下,长度变为 6。如果我们记录数组,我们可以看到原因:
console.log("numbers", numbers);
输出将如下所示:
numbers [ 12, 24, 36, <2 empty items>, 48 ]
因为我们已经在索引 5 处添加了一个元素,48,它也在索引位置 3 和 4 创建了包含空值的 2 个元素。现在,让我们看看数组方法,并找出正确添加到数组中的方法。
练习 3.1
-
创建一个包含 3 个项目的数组作为你的购物清单:“牛奶”、“面包”和“苹果。”
-
在控制台中检查你的列表长度。
-
将“面包”更新为“香蕉。”
-
将整个列表输出到控制台。
数组方法
我们刚刚看到了内置的 length 属性。我们还有一些内置的方法。方法是在某个对象上的函数。与属性不同,它们执行操作。我们将在第六章,函数中深入讨论函数。现在,你需要知道的是,你可以调用方法和函数,当你这样做时,函数内部指定的某些代码会被执行。
我们只是意外地看到我们可以使用新的索引添加元素。这不是正确的做法,因为它很容易出错,意外地覆盖某个值或跳过某个索引。正确的方法是使用特殊的方法来做这件事。同样,我们也可以删除数组中的元素并对数组中的元素进行排序。
添加和替换元素
我们可以使用push()
方法添加元素:
favoriteFruits = ["grapefruit", "orange", "lemon"];
favoriteFruits.push("tangerine");
值被添加到数组的末尾。push
方法返回数组的新的长度,在这个例子中是 4。你可以将这个长度存储在一个变量中,如下所示:
let lengthOfFavoriteFruits = favoriteFruits.push("lime");
值 5 被存储在lengthOfFavoriteFruits
变量中。如果我们像这样记录我们的数组favoriteFruits
:
console.log(favoriteFruits);
这是新的数组:
[ 'grapefruit', 'orange', 'lemon', 'tangerine', 'lime' ]
这很简单,对吧?但如果你想在某个索引处添加元素呢?你可以使用 splice()
方法。这个稍微有点难:
let arrOfShapes = ["circle", "triangle", "rectangle", "pentagon"];
arrOfShapes.splice(2, 0, "square", "trapezoid");
console.log(arrOfShapes);
之后,包含数组的输出如下:
[
'circle',
'triangle',
'square',
'trapezoid',
'rectangle',
'pentagon'
]
首先,让我们指出这个输出的不同布局。这可能会取决于你使用的解释器,但最终它将决定单行显示太长,并自动应用格式化使数组更易读。这不会改变数组的值;这只是相同值的另一种表示形式,如果它们在单行上显示的话。
如你所见,正方形和梯形被插入到索引 2 处。数组的其余部分向右移动。splice()
方法接受多个参数。第一个参数,在我们的例子中是 2,是我们想要开始插入的数组索引。第二个参数,在我们的例子中是 0,是从先前定义的起始索引开始删除的元素数量。这些第一个参数之后的参数,在我们的例子中是 square
和 trapezoid
,是应该从起始索引开始插入的内容。
所以,如果我们说的是这个:
arrOfShapes.splice(2, 2, "square", "trapezoid");
console.log(arrOfShapes);
因此,如果我们替换了元素 rectangle
和 pentagon
并用 square
和 trapezoid
取代它们,结果如下:
[ 'circle', 'triangle', 'square', 'trapezoid' ]
如果你将第二个参数增加到比我们的数组更大的数字,它不会影响结果,因为 JavaScript 会一遇到没有值可以删除就停止。尝试以下代码:
arrOfShapes.splice(2, 12, "square", "trapezoid");
console.log(arrOfShapes);
这也会得到以下输出:
[ 'circle', 'triangle', 'square', 'trapezoid' ]
你也可以将另一个数组添加到你的数组中。这可以通过 concat()
方法完成。这样,你可以创建一个新的数组,它由两个数组的连接组成。第一个数组的元素将首先出现,然后是 concat()
参数的元素,它们将连接到末尾:
let arr5 = [1, 2, 3];
let arr6 = [4, 5, 6];
let arr7 = arr5.concat(arr6);
console.log(arr7);
这是输出:
[ 1, 2, 3, 4, 5, 6 ]
这个 concat()
方法还能做更多!我们可以用它来添加值。我们可以添加单个值,或者我们可以用逗号分隔多个值:
let arr8 = arr7.concat(7, 8, 9);
console.log(arr8);
数组的新值如下:
[ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
删除元素
删除数组中的元素有几种方法。移除最后一个元素使用 pop()
:
arr8.pop();
执行 pop()
后记录数组的结果如下:
[ 1, 2, 3, 4, 5, 6, 7, 8 ]
删除第一个元素可以使用 shift()
。这会导致所有其他索引减少 1:
arr8.shift();
新数组将是:
[ 2, 3, 4, 5, 6, 7, 8 ]
记得 splice()
吗?这是一个非常特殊的方法,因为我们也可以用它来删除值。我们指定从哪里开始删除的索引,然后是我们想要删除的元素数量。
arr8.splice(1, 3);
之后,数组的值如下:
[ 2, 6, 7, 8 ]
如你所见,从第二个位置索引开始的 3 个元素已被删除。值 3、4 和 5 都不见了。如果你不想改变后面的任何索引,你也可以使用 delete
操作符。这不是一个方法,但你可以用它来将数组中某个位置上的值改为 undefined
:
delete arr8[0];
数组随后变为:
[ <1 empty item>, 6, 7, 8 ]
这在需要依赖索引或长度的某些情况下很有用。例如,如果你正在保存用户输入,并且你想根据用户推送到数组的数组来确定用户输入的数量,删除会减少输入的数量,而这可能不是你想要的。
查找元素
如果你想检查一个值是否存在于数组中,你可以使用find()
方法。find()
方法中要放入的内容略有不同。实际上,它是一个函数。这个函数将在数组的每个元素上执行,直到找到匹配项,如果没有找到,则返回undefined
。
不要担心现在这太难了;很快就会变得清晰。在下面的代码片段中,我们以两种不同的方式编写函数。它们实际上是在做同样的事情,只是第一个是检查元素是否等于 6,而第二个是检查元素是否等于 10:
arr8 = [ 2, 6, 7, 8 ];
let findValue = arr8.find(function(e) { return e === 6});
let findValue2 = arr8.find(e => e === 10);
console.log(findValue, findValue2);
日志语句将记录6
和undefined
,因为它可以找到匹配6
的元素,但不能找到匹配10
的元素。
函数可以接受某些输入。在这种情况下,它接受数组的元素作为输入。当数组的元素等于 6(findValue
)或 10(findValue2
)时,它返回该元素。在第六章,函数中,我们将更详细地介绍函数。这对于初学者来说是一大挑战,所以如果你现在还不清楚,可以稍后复习。
通常,你不仅想要找到元素,还想知道它在什么位置。这可以通过indexOf()
方法实现。此方法返回找到值的索引。如果值在数组中多次出现,它将返回第一次出现的位置。如果找不到值,它将返回-1
:
arr8 = [ 2, 6, 7, 8 ];
let findIndex = arr8.indexOf(6);
let findIndex2 = arr8.indexOf(10);
console.log(findIndex, findIndex2);
因此,第一个将返回 1,因为这是数组中 6 的索引位置。第二个将返回-1,因为数组中不包含 10。
如果你想要找到指定数字的下一个出现位置,你可以向indexOf()
添加一个第二个参数,指定从哪个位置开始搜索:
arr8 = [ 2, 6, 7, 8 ];
let findIndex3 = arr8.indexOf(6, 2);
在这种情况下,findIndex3
的值将是-1,因为从索引 2 开始找不到 6。
最后一次出现的位置也可以找到。这是通过lastIndexOf()
方法实现的:
let animals = ["dog", "horse", "cat", "platypus", "dog"];
let lastDog = animals.lastIndexOf("dog");
lastDog
的值将是 4,因为这是数组中dog
的最后一个出现位置。
排序
此外,还有一个用于排序数组的内置方法。它将数字从小到大排序,并将字符串按字母顺序排序。你可以在数组上调用sort()
方法,数组的值顺序将变为排序顺序:
let names = ["James", "Alicia", "Fatiha", "Maria", "Bert"];
names.sort();
排序后的names
值如下:
[ 'Alicia', 'Bert', 'Fatiha', 'James', 'Maria' ]
如你所见,数组现在已按字母顺序排序。对于数字,它按升序排序,如下面的代码片段所示:
let ages = [18, 72, 33, 56, 40];
ages.sort();
执行此sort()
方法后,ages
的值如下:
[ 18, 33, 40, 56, 72 ]
反转
可以通过在数组上调用内置方法reverse()
来反转数组的元素。它将最后一个元素放在最前面,第一个元素放在最后。不管数组是否排序,它只是反转顺序。
反转之前names
的值如下:
[ 'Alicia', 'Bert', 'Fatiha', 'James', 'Maria' ]
现在我们将调用reverse()
方法:
names.reverse();
新的顺序将是:
[ 'Maria', 'James', 'Fatiha', 'Bert', 'Alicia' ]
练习题 3.2
-
创建一个空数组作为购物清单。
-
将
Milk
、Bread
和Apples
添加到你的列表中。 -
将
Bread
更新为Bananas
和Eggs
。 -
从数组中移除最后一个元素并将其输出到控制台。
-
按字母顺序排序列表。
-
查找并输出
Milk
的索引值。 -
在
Bananas
之后添加Carrots
和Lettuce
。 -
创建一个包含
Juice
和Pop
的新列表。 -
将两个列表合并,将新列表两次添加到第一个列表的末尾。
-
获取
Pop
的最后一个索引值并将其输出到控制台。 -
你的最终列表应该看起来像这样:
["Bananas", "Carrots", "Lettuce", "Eggs", "Milk", "Juice", "Pop", "Juice", "Pop"]
多维数组
之前我们已经确定数组可以包含任何数据类型。这意味着数组也可以包含其他数组(反过来,也可以包含...其他数组!)。这被称为多维数组。听起来很复杂,但它只是数组的数组:列表的列表:
let someValues1 = [1, 2, 3];
let someValues2 = [4, 5, 6];
let someValues3 = [7, 8, 9];
let arrOfArrays = [someValues1, someValues2, someValues3];
因此,我们可以创建一个包含已存在数组的数组。这被称为二维数组。我们可以这样写:
let arrOfArrays2 = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
如果你想访问内部数组的元素,你必须指定两次索引:
let value1 = arrOfArrays[0][1];
这个语句将获取第一个数组,因为它有一个索引位置0
。从这个第一个数组中,它将获取第二个值,因为它有一个索引位置1
。然后它将这个值存储在value1
中。这意味着value1
的值将是 2。你能猜出下一个值是多少吗?
let value2 = arrOfArrays[2][2];
它获取第三个数组,并从这个第三个数组中获取第三个值。因此,9 将被存储在value2
中。而且它不会停止在这里;它可以深入很多层级。让我们通过创建一个包含我们的数组数组的数组来展示这一点。我们只是简单地将这个数组存储在另一个数组中三次:
arrOfArraysOfArrays = [arrOfArrays, arrOfArrays, arrOfArrays];
这是数组在值方面的样子:
[
[ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7, 8, 9 ] ],
[ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7, 8, 9 ] ],
[ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7, 8, 9 ] ]
]
让我们获取这个数组的中间元素,即属于第二个数组数组的值 5。这样做:
let middleValue = arrOfArraysOfArrays[1][1][1];
第一步是获取数组的第二维数组,因此索引为 1。然后我们需要获取这个数组的第二维数组,这同样也是索引 1。现在我们达到了值的层级,我们需要第二个值,所以再次使用索引 1。这在很多情况下都很有用,例如,当你想处理矩阵时。
练习题 3.3
-
创建一个包含三个值:1、2 和 3 的数组。
-
将原始数组嵌套到新数组中三次。
-
将数组中的一个值 2 输出到控制台。
JavaScript 中的对象
现在是时候看看另一种可以包含多个值的复杂数据结构了:对象!对象非常有用,可以用来描述现实生活中的对象,以及更复杂的抽象概念,这可以让你的代码更加灵活。
暗中,你已经接触到了对象,因为数组是一种非常特殊类型的对象。数组是具有索引属性的对象。所有其他对象,以及我们在这里将要看到的对象,都是具有命名属性的对象。这意味着我们不会提供一个自动生成的索引号,而是会提供一个自定义的描述性名称。
从以下代码中我们可以看出,JavaScript 将数组定义为对象类型:
let arr = [0, 1, 2];
console.log(typeof arr);
上述代码的输出如下:
Object
对象与现实世界中的对象并不太相似。它们 有 属性,它们 可以 执行动作,方法。在这里,我们只处理属性。我们将在 第七章,类 中介绍方法,在看过函数之后。对象是将多个变量组合成一个的机会。这是通过大括号 {
和 }
来完成的。让我们看看这个狗的对象:
let dog = { dogName: "JavaScript",
weight: 2.4,
color: "brown",
breed: "chihuahua",
age: 3,
burglarBiter: true
};
我们创建了一个变量 dog
,并给它赋了一个对象作为值。我们可以通过看到 { 和 } 来识别这是一个对象。在大括号之间,我们看到一堆属性及其值。
如果你曾经想过某个东西是否应该是一个属性,只需在你的脑海中尝试以下模板句子:
对象名 有一个 属性名
例如,一只狗有一个名字,一只狗有一个颜色,一只狗有一个重量。对于布尔属性来说,情况略有不同,你可以使用 "is" 或 "is not" 而不是 "has"。
我们可以用非常类似的方式访问这个对象的属性,就像访问数组一样。这次,我们不是使用索引号,而是使用属性名来获取值:
let dogColor1 = dog["color"];
还有另一种方法来做这件事。而不是使用方括号,属性名也可以通过中间的点添加到对象名中:
let dogColor2 = dog.color;
这可能看起来很熟悉。你还记得我们是如何使用内置属性 length 获取数组长度的吗?是的——同样的方式!属性和方法之间的区别是属性没有括号。
更新对象
我们可以更改对象的属性值。同样,这和数组类似,因为数组也是一个对象,但对于属性,我们有两种选择:
dog["color"] = "blue";
dog.weight = 2.3;
这已经改变了我们的吉娃娃 JavaScript 的属性。颜色更新为蓝色,并且由于新重量降低了 0.1,它失去了一点点重量。所以如果我们记录我们的狗:
console.log(dog);
我们将得到以下:
{
dogName: 'JavaScript',
weight: 2.3,
color: 'blue',
breed: 'chihuahua',
age: 3,
burglarBiter: true
}
有必要注意的是,如果我们更改我们属性之一的数据类型,例如:
dog["age"] = "three";
这不是问题。JavaScript 只会将整个值和数据类型更改为新的情况。
另一个需要注意的元素是,我们现在使用字面字符串值来引用对象的属性,但我们也可以使用变量来实现这一点。所以,例如:
let variable = "age";
console.log(dog[variable]);
这仍然会输出three
,因为我们刚刚将年龄的值更改为三。如果我们将变量的值更改为另一个狗属性,我们将访问另一个属性,如下所示:
variable = "breed";
console.log(dog[variable]);
这将打印chihuahua
。当我们以这种方式更新值时,它与使用字面字符串访问的方式完全相同:
dog[variable] = "dachshund";
console.log(dog["breed"]);
因此,这将在控制台记录dachshund
。
练习 3.4
-
为一辆车创建一个新的
myCar
对象。添加一些属性,包括但不限于make
和model
,以及典型汽车或您汽车的值。您可以自由使用布尔值、字符串或数字。 -
创建一个可以存储字符串值
color
的变量。这个包含字符串值color
的变量现在可以用来引用myCar
中的属性名称。然后,使用方括号符号中的变量来为myCar
中的颜色属性分配新值。 -
使用相同的变量并为其分配一个新的属性字符串值,例如
forSale
。再次使用方括号符号来为forSale
属性分配新值,以表示汽车是否可供购买。 -
将
make
和model
的值输出到控制台。 -
将
forSale
的值输出到控制台。
与对象和数组一起工作
当与对象和数组一起工作时,您会经常看到它们结合使用。在本章的最后部分,我们将处理对象和数组的组合,以及对象中的对象。
对象中的对象
假设我们想要有一个公司对象。这个公司将有一个地址。而地址本身也是一个对象。如果我们给公司一个地址,我们就是在使用对象中的对象:
let company = { companyName: "Healthy Candy",
activity: "food manufacturing",
address: {
street: "2nd street",
number: "123",
zipcode: "33116",
city: "Miami",
state: "Florida"
},
yearOfEstablishment: 2021
};
如您所见,我们的公司对象有一个包含值的地址对象。如果需要,它可以非常深入地分层。
要访问或修改地址中的某个属性,我们可以使用两种方法:
company.address.zipcode = "33117";
company["address"]["number"] = "100";
如您所见,这与数组非常相似。我们首先需要选择地址,然后对想要更改的属性执行相同操作以访问它。
对象中的数组
我们的公司可能有一系列活动而不是一个。我们可以简单地用数组替换之前的示例中的活动:
company = { companyName: "Healthy Candy",
activities: ["food manufacturing",
"improving kids' health", "manufacturing toys"],
address: {
street: "2nd street",
number: "123",
zipcode: "33116",
city: "Miami",
state: "Florida"
},
yearOfEstablishment: 2021
};
我们现在已经在公司对象中使用了数组。您可以直接在属性后面使用方括号来使用数组。检索单个值的方式非常相似。可以使用以下语句获取活动数组的第二个值:
let activity = company.activities[1];
在这里,我们称我们感兴趣的为company
,然后是相关的数组activities
,以及我们正在数组中寻找的变量的索引位置,即1
。
数组中的对象
很可能,我们的公司不是只有一个地址,而是一系列地址。我们可以通过创建地址对象数组来实现这一点。在这种情况下,我们将创建一个包含两个对象的数组:
let addresses = [{
street: "2nd street",
number: "123",
zipcode: "33116",
city: "Miami",
state: "Florida"
},
{
street: "1st West avenue",
number: "5",
zipcode: "75001",
city: "Addison",
state: "Texas"
}];
因此,数组可以通过方括号识别,对象可以通过花括号识别。第一个对象的街道名称可以通过以下语句获取:
let streetName = addresses[0].street;
在这里,我们用 addresses
来调用我们感兴趣的数组,根据我们在数组中寻找的对象的索引位置 0
,然后是对象内的所需变量,即 street
。这可能看起来很复杂,但你可能会注意到,这实际上只是反转了从上一节中获取对象内部数组变量的语法。练习从嵌套的数组和对象中调用变量直到你感到舒适是值得的!
在对象中的数组对象
只为了展示这可以像我们需要的那么多层次,我们将给我们的公司对象添加一个地址对象数组。所以,让我们将这个地址对象数组添加到我们的公司对象中。这样,我们的公司就有了一个地址数组:
company = { companyName: "Healthy Candy",
activities: [ "food manufacturing",
"improving kids' health",
"manufacturing toys"],
address: [{
street: "2nd street",
number: "123",
zipcode: "33116",
city: "Miami",
state: "Florida"
},
{
street: "1st West avenue",
number: "5",
zipcode: "75001",
city: "Addison",
state: "Texas"
}],
yearOfEstablishment: 2021
};
要访问越来越嵌套的对象和数组中的元素,我们只需扩展你在前几节中看到的相同逻辑。要访问 Healthy Candy 的第一个地址的街道名称,我们可以使用以下代码:
let streetName = company.address[0].street;
如你所见,我们可以无限地堆叠对象和数组元素请求。
目前我们不会让它比这更复杂。每次你需要一个列表时,你都会使用数组。每次你想用具有描述性名称的属性表示某物时,最好使用对象。只需记住,对象的属性可以是任何类型。
练习题 3.5
-
创建一个名为
people
的对象,其中包含一个名为friends
的空数组。 -
创建三个变量,每个变量包含一个对象,其中包含你一个朋友的姓氏、名字和 ID 值。
-
将三个朋友添加到
friend
数组中。 -
将其输出到控制台。
章节项目
操作数组
取以下数组:
const theList = ['Laurence', 'Svekis', true, 35, null, undefined, {test: 'one', score: 55}, ['one', 'two']];
使用各种方法操作数组,例如 pop()
、push()
、shift()
和 unshift()
,将其转换为以下形式:
["FIRST", "Svekis", "MIDDLE", "hello World", "LAST"]
你可以采取以下步骤,或者采用你自己的方法:
-
移除第一个和最后一个项目。
-
将
FIRST
添加到数组的开头。 -
将
hello World
分配给第四个项目值。 -
将
MIDDLE
分配给第三个索引值。 -
将
LAST
添加到数组的最后一个位置。 -
将其输出到控制台。
公司产品目录
在这个项目中,你将实现一个产品目录的数据结构并创建查询以检索数据。
-
创建一个数组来存储商店商品的库存。
-
创建三个项目,每个项目具有名称、型号、成本和数量属性。
-
使用数组方法将所有三个对象添加到主数组中,然后将库存数组输出到控制台。
-
访问您第三个项目的数量元素,并将其记录到控制台。通过在您的数据结构中添加和访问更多元素进行实验。
自我检查测验
-
您可以使用
const
并在数组中更新值吗? -
数组中的哪个属性给出了数组中包含的项目数量?
-
控制台中的输出是什么?
const myArr1 = [1,3,5,6,8,9,15]; console.log(myArr1.indexOf(0)); console.log(myArr1.indexOf(3));
-
您如何将数组
myArr = [1,3,5,6,8,9,15]
中的第二个元素替换为值 4? -
控制台中的输出是什么?
const myArr2 = []; myArr2[10] = 'test' console.log(myArr2); console.log(myArr2[2]);
-
控制台中的输出是什么?
const myArr3 = [3,6,8,9,3,55,553,434]; myArr3.sort(); myArr3.length = 0; console.log(myArr3[0]);
摘要
因此,在本章中,我们看到了数组和对象。数组是一系列值。这些可以是相同类型的值,也可以是不同类型的值。数组的每个元素都有一个索引。第一个元素的索引是 0。我们可以使用这个索引来访问数组的元素。我们也可以使用这个索引来更改和删除元素。
然后,我们了解到也可以有包含其他数组的数组;这些是多维数组。要访问多维数组的元素,您需要使用与嵌套数组数量相同的索引。
然后,我们介绍了对象,并了解到数组是一种特殊类型的对象。对象包含属性和方法。我们研究了对象的属性,并看到这些属性被赋予一个名称,并且可以使用这个名称来访问和修改。
我们通过查看数组如何包含对象以及对象如何包含数组和更多内容来结束本模块。这使得我们能够创建复杂的对象结构,这在设计现实生活中的应用程序中将非常有用。
第四章:逻辑语句
到目前为止,我们的代码相当静态。每次执行它都会做同样的事情。在本章中,这一切都将改变。我们将处理逻辑语句。逻辑语句允许我们在代码中创建多个路径。根据某个表达式的结果,我们将遵循一个代码路径或另一个。
有不同的逻辑语句,我们将在本章中介绍它们。我们将从if
和if else
语句开始。然后我们将处理三元运算符,最后我们将处理的是switch
语句。
在旅途中,我们将涵盖以下主题:
-
if
和if else
语句 -
else if
语句 -
条件三元运算符
-
switch 语句
注意:练习、项目和自我检查测验的答案可以在附录中找到。
if
和if else
语句
我们可以使用if
和if else
语句在代码中做出决定。这非常类似于以下模板:
如果某个条件为真,则将发生某个动作,否则*将发生另一个动作**
例如,如果下雨,我会带上我的伞,否则我会把伞留在家里。在代码中并没有太大的不同:
let rain = true;
if(rain){
console.log("** Taking my umbrella when I need to go outside **");
} else {
console.log("** I can leave my umbrella at home **");
}
在这种情况下,rain
的值为true
。因此,它将在控制台记录:
** Taking my umbrella when I need to go outside **
但让我们先退一步,看看语法。我们以单词"if"开始。之后,我们得到括号内的某个东西。括号之间的一切都将被转换为布尔值。如果这个布尔值的值为true
,它将执行与if
关联的代码块。你可以通过大括号来识别这个块。
下一个块是可选的;它是一个else
块。它以单词"else"开始,并且仅在布尔值为false
时执行。如果没有else
块,并且条件评估为false
,则程序将跳到if
下面的代码。
这两个块中只有一个将被执行;当表达式为真时执行if
块,当表达式为假时执行else
块:
if(expression) {
// code associated with the if block
// will only be executed if the expression is true
} else {
// code associated with the else block
// we don't need an else block, it is optional
// this code will only be executed if the expression is false
}
这里有一个另一个例子。如果年龄低于 18 岁,则在控制台记录访问被拒绝,否则在控制台记录该人获准进入:
if(age < 18) {
console.log("We're very sorry, but you can't get in under 18");
} else {
console.log("Welcome!");
}
与if
语句相关的一个常见的编码错误。我在下面的代码片段中犯了这个错误。你能看出这段代码做了什么吗?
let hobby = "dancing";
if(hobby == "coding"){
console.log("** I love coding too! **");
} else {
console.log("** Can you teach me that? **");
}
它将记录以下内容:
** I love coding too! **
这可能会让你感到惊讶。这里的问题是if
语句中的单个等号。它不是评估条件,而是将coding
赋值给hobby
。然后它将coding
转换为布尔值,由于它不是一个空字符串,所以它将是true
,因此if
块将被执行。所以,请始终记住在这种情况下使用双等号。
让我们通过一个练习来测试我们的知识。
练习 4.1
-
创建一个具有布尔值的变量。
-
将变量的值输出到控制台。
-
检查变量是否为真,如果是,则使用以下语法在控制台输出一条消息:
if(myVariable){ //action }
-
添加另一个带有变量前缀
!
的if
语句来检查条件是否不成立,并创建一个在这种情况下将被打印到控制台的消息。你应该有两个if
语句,一个带有!
,另一个不带。你也可以使用一个if
和一个else
语句来代替——尝试一下! -
将变量改为相反的值,看看结果如何变化。
else if
语句
if
语句的一个变体是带有多个 else if
块的 if
语句。在某些情况下,这可能会更有效,因为你总是只会执行一个或零个块。如果你有很多堆叠在一起的 if else
语句,它们将被评估并可能执行,即使上面的某个语句已经有一个条件评估为真并继续执行相关的代码块。
这里是书面模板:
如果 一个值落在某个类别中,则 将发生某些操作,否则如果 值落在与上一个语句不同的类别中,则 将发生某些操作,否则如果 值落在与上述任一括号不同的类别中,则 将发生某些操作
例如,考虑以下语句,以确定票价应该是多少。如果一个人年龄小于 3 岁,则免费入场,否则如果一个人年龄大于 3 岁且小于 12 岁,则入场费为 5 美元,否则如果一个人年龄大于 12 岁且小于 65 岁,则入场费为 10 美元,否则如果一个人年龄为 65 岁或以上,则入场费为 7 美元:
let age = 10;
let cost = 0;
let message;
if (age < 3) {
cost = 0;
message = "Access is free under three.";
} else if (age >= 3 && age < 12) {
cost = 5;
message ="With the child discount, the fee is 5 dollars";
} else if (age >= 12 && age < 65) {
cost = 10;
message ="A regular ticket costs 10 dollars.";
} else {
cost = 7;
message ="A ticket is 7 dollars.";
}
console.log(message);
console.log("Your Total cost "+cost);
很可能你会认为代码比书面模板更容易阅读。在这种情况下,做得很好!你已经开始像 JavaScript 开发者一样思考了。
代码从上到下执行,并且只有一个块会被执行。一旦遇到一个为真的表达式,其他块将被忽略。这就是为什么我们也可以像这样编写我们的示例:
if(age < 3){
console.log("Access is free under three.");
} else if(age < 12) {
console.log("With the child discount, the fee is 5 dollars");
} else if(age < 65) {
console.log("A regular ticket costs 10 dollars.");
} else if(age >= 65) {
console.log("A ticket is 7 dollars.");
}
练习 4.2
-
创建一个提示来询问用户的年龄
-
将提示响应转换为数字
-
声明一个
message
变量,你将使用它来保存用户的控制台消息 -
如果输入的年龄等于或大于 21 岁,将
message
变量设置为确认入场并允许购买酒精 -
如果输入的年龄等于或大于 19 岁,将
message
变量设置为确认入场但拒绝购买酒精 -
提供一个默认的
else
语句,如果没有任何条件成立,则将message
变量设置为拒绝入场 -
将
message
响应变量输出到控制台
条件三元运算符
我们实际上没有在我们的 第二章 JavaScript Essentials 中的运算符部分讨论这个非常重要的运算符。这是因为它有助于首先理解 if else 语句。记住,我们有一个被称为一元运算符的一元运算符,因为它只有一个操作数?这就是为什么我们的三元运算符有这个名字;它有三个操作数。以下是它的模板:
operand1 ? operand2 : operand3;
operand1
是要评估的表达式。如果表达式的值为 true
,则执行 operand2
。如果表达式的值为 false
,则执行 operand3
。在这里,你可以将问号读作“then”,将冒号读作“else”:
expression ? statement for true : statement associated with false;
在心里说它的模板应该是这样的:
if operand1, then operand2, else operand3*
让我们看看几个例子:
let access = age < 18 ? "denied" : "allowed";
If age is lower than 18, *then* it will assign the value denied, *else* it will assign the value allowed. You can also specify an action in a ternary statement, like this:
age < 18 ? console.log("denied") : console.log("allowed");
这种语法一开始可能会让人困惑。在阅读时,心里想说什么的模板真的可以在这里救命。你只能将这些三元运算符用于非常短的操作,因此在这些情况下最好使用三元运算符,因为它使代码更容易阅读。然而,如果逻辑包含多个比较参数,你必须使用常规的 if-else。
练习 4.3
-
为 ID 变量创建一个布尔值
-
使用三元运算符创建一个消息变量,该变量将检查他们的 ID 是否有效,并允许或不允许一个人进入场所
-
将响应输出到控制台
switch 语句
If else 语句非常适合评估布尔条件。你可以用它们做很多事情,但在某些情况下,最好用 switch 语句来替换它们。这尤其适用于评估四个或五个以上的值。
我们将看到 switch 语句如何帮助我们以及它们的外观。首先,看看这个 if else 语句:
if(activity === "Get up") {
console.log("It is 6:30AM");
} else if(activity === "Breakfast") {
console.log("It is 7:00AM");
} else if(activity === "Drive to work") {
console.log("It is 8:00AM");
} else if(activity === "Lunch") {
console.log("It is 12.00PM");
} else if(activity === "Drive home") {
console.log("It is 5:00PM")
} else if(activity === "Dinner") {
console.log("It is 6:30PM");
}
它是根据我们在做什么来确定时间的。最好使用 switch 语句来实现这一点。switch 语句的语法如下所示:
switch(expression) {
case value1:
// code to be executed
break;
case value2:
// code to be executed
break;
case value-n:
// code to be executed
break;
}
你可以在心里这样读:如果表达式等于 value1
,则执行该 case 指定的任何代码。如果表达式等于 value2
,则执行该 case 指定的任何代码,依此类推。
这是我们可以使用 switch 语句以更干净的方式重写我们的长 if else 语句的方法:
switch(activity) {
case "Get up":
console.log("It is 6:30AM");
break;
case "Breakfast":
console.log("It is 7:00AM");
break;
case "Drive to work":
console.log("It is 8:00AM");
break;
case "Lunch":
console.log("It is 12:00PM");
break;
case "Drive home":
console.log("It is 5:00PM");
break;
case "Dinner":
console.log("It is 6:30PM");
break;
}
如果我们的活动值为 Lunch
,它将向控制台输出以下内容:
It is 12:00PM
你可能想知道所有这些 breaks 是怎么回事?如果你在 case 的末尾不使用 break
命令,它将执行下一个 case。这将从匹配的 case 开始执行,直到 switch 语句的末尾或我们遇到一个 break
语句。这是没有 breaks 的 Lunch
活动的 switch 语句的输出:
It is 12:00PM
It is 5:00PM
It is 6:30PM
最后一个注意事项。switch
使用严格类型检查(三等号策略)来确定相等性,这会检查值和数据类型。
默认情况
我们还没有处理开关的一部分,那就是一个特殊的标签,即default
。这和 if else 语句的 else 部分非常相似。如果它没有与任何 case 匹配,并且存在默认 case,那么它将执行与默认 case 关联的代码。下面是一个带有默认 case 的 switch 语句的模板:
switch(expression) {
case value1:
// code to be executed
break;
case value2:
// code to be executed
break;
case value-n:
// code to be executed
break;
default:
// code to be executed when no cases match
break;
}
习惯上,将默认 case 放在 switch 语句的最后一个 case,但代码即使放在中间或第一个 case 也能正常工作。然而,我们建议你坚持这些约定,并将其放在最后一个 case,因为这是其他开发者(以及可能未来的你)在以后处理你的代码时预期的。
假设我们的长if
语句有一个看起来像这样的else
语句相关联:
if(…) {
// omitted to avoid making this unnecessarily long
} else {
console.log("I cannot determine the current time.");
}
switch 语句将看起来像这样:
switch(activity) {
case "Get up":
console.log("It is 6:30AM");
break;
case "Breakfast":
console.log("It is 7:00AM");
break;
case "Drive to work":
console.log("It is 8:00AM");
break;
case "Lunch":
console.log("It is 12:00PM");
break;
case "Drive home":
console.log("It is 5:00PM");
break;
case "Dinner":
console.log("It is 6:30PM");
break;
default:
console.log("I cannot determine the current time.");
break;
}
如果活动的值要是不指定为 case 的内容,例如,“看 Netflix”,它将在控制台记录以下内容:
I cannot determine the current time.
练习 4.4
如第一章中所述,JavaScript 入门,JavaScript 函数Math.random()
将返回一个介于 0 到小于 1 之间的随机数,包括 0 但不包括 1。然后你可以通过乘以结果并将其使用Math.floor()
向下舍入到最接近的整数来将其缩放到所需的范围;例如,要生成一个介于 0 到 9 之间的随机数:
// random number between 0 and 1
let randomNumber = Math.random();
// multiply by 10 to obtain a number between 0 and 10
randomNumber = randomNumber * 10;
// removes digits past decimal place to provide a whole number
RandomNumber = Math.floor(randomNumber);
在这个练习中,我们将创建一个魔法 8 球随机答案生成器:
-
首先设置一个变量,将其随机值分配给它。这个值是通过生成 0-5 之间的随机数来分配的,有 6 种可能的结果。你可以随着添加更多结果来增加这个数字。
-
创建一个提示,可以获取用户输入的字符串值,你可以在最终输出中重复它。
-
使用 switch 语句创建 6 个响应,每个响应分配给随机数生成器的不同值。
-
创建一个变量来保存最终响应,这应该是一个打印给用户的句子。你可以为每个 case 分配不同的字符串值,根据随机值的返回结果分配新值。
-
用户输入问题后,将用户的原问题以及随机选择的 case 响应输出到控制台。
混合 case
有时,你可能想要为多个 case 执行完全相同的事情。在 if 语句中,你必须指定所有不同的或(||
)子句。在 switch 语句中,你可以通过将它们堆叠在一起来简单地合并它们,如下所示:
switch(grade){
case "F":
case "D":
console.log("You've failed!");
break;
case "C":
case "B":
console.log("You've passed!");
break;
case "A":
console.log("Nice!");
break;
default:
console.log("I don't know this grade.");
}
对于F
和D
的值,发生的情况相同。对于C
和B
也是如此。当grade
的值为C
或B
时,它将在控制台记录以下内容:
You've passed!
这比替代的 if-else 语句更易读:
if(grade === "F" || grade === "D") {
console.log("You've failed!");
} else if(grade === "C" || grade === "B") {
console.log("You've passed!");
} else if(grade === "A") {
console.log("Nice!");
} else {
console.log("I don't know this grade.");
}
练习 4.5
-
创建一个名为
prize
的变量,并使用提示让用户通过选择 0 到 10 之间的数字来设置值。 -
将提示响应转换为数字数据类型
-
创建一个用于输出消息的变量,包含值“我的选择:”
-
使用 switch 语句(和创造力),根据所选数字提供有关奖励的响应
-
使用 switch break 添加奖励的合并结果
-
通过连接你的奖励变量字符串和输出消息字符串将消息返回给用户
章节项目
评估数字游戏
请求用户输入一个数字,并检查它是否大于、等于或小于代码中的动态数值。将结果输出给用户。
朋友检查游戏
请求用户输入一个名字,使用 switch 语句返回一个确认,如果所选的名字在 case 语句中已知,则用户是朋友。如果你不知道这个名字,可以添加一个默认响应。将结果输出到控制台。
石头、布、剪刀游戏
这是一个玩家和电脑之间的游戏,两者将随机选择石头、布或剪刀(或者你也可以创建一个使用真实玩家输入的版本!)。石头会打败剪刀,布会打败石头,剪刀会打败布。你可以使用 JavaScript 创建你自己的游戏版本,使用 if 语句应用逻辑。由于这个项目有点难,这里有一些建议的步骤:
-
创建一个包含变量“石头”、“布”和“剪刀”的数组。
-
设置一个变量,生成一个 0-2 之间的随机数用于玩家,然后为电脑的选择也做同样的事情。这个数字代表数组中三个项目的索引值。
-
创建一个用于保存响应消息的变量。这可以显示玩家的随机结果,然后也是从数组中匹配项目的电脑结果。
-
创建一个条件来处理玩家和电脑的选择。如果两者相同,则结果是平局。
-
使用条件来应用游戏逻辑并返回正确的结果。有几种方法可以使用条件语句来完成,但你可以检查哪个玩家的索引值更大,并据此分配胜利,除了石头打剪刀的情况。
-
添加一个新的输出消息,显示玩家选择与电脑选择以及游戏结果。
自我检查测验
-
在这种情况下,控制台将输出什么?
const q = '1'; switch (q) { case '1': answer = "one"; break; case 1: answer = 1; break; case 2: answer = "this is the one"; break; default: answer = "not working"; } console.log(answer);
-
在这种情况下,控制台将输出什么?
const q = 1; switch (q) { case '1': answer = "one"; case 1: answer = 1; case 2: answer = "this is the one"; break; default: answer = "not working"; } console.log(answer);
-
在这种情况下,控制台将输出什么?
let login = false; let outputHolder = ""; let userOkay = login ? outputHolder = "logout" : outputHolder = "login"; console.log(userOkay);
-
在这种情况下,控制台将输出什么?
const userNames = ["Mike", "John", "Larry"]; const userInput = "John"; let htmlOutput = ""; if (userNames.indexOf(userInput) > -1) { htmlOutput = "Welcome, that is a user"; } else { htmlOutput = "Denied, not a user "; } console.log(htmlOutput + ": " + userInput);
-
在这种情况下,控制台将输出什么?
let myTime = 9; let output; if (myTime >= 8 && myTime < 12) { output = "Wake up, it's morning"; } else if (myTime >= 12 && myTime < 13) { output = "Go to lunch"; } else if (myTime >= 13 && myTime <= 16) { output = "Go to work"; } else if (myTime > 16 && myTime < 20) { output = "Dinner time"; } else if (myTime >= 22) { output = "Time to go to sleep"; } else { output = "You should be sleeping"; } console.log(output);
-
在这种情况下,控制台将输出什么?
let a = 5; let b = 10; let c = 20; let d = 30; console.log(a > b || b > a); console.log(a > b && b > a); console.log(d > b || b > a); console.log(d > b && b > a);
-
在这种情况下,控制台将输出什么?
let val = 100; let message = (val > 100) ? `${val} was greater than 100` : `${val} was LESS or Equal to 100`; console.log(message); let check = (val % 2) ? `Odd` : `Even`; check = `${val} is ${check}`; console.log(check);
摘要
现在,让我们来总结一下。在本章中,我们学习了条件语句。我们从 if else 语句开始。每当与 if 关联的条件为真时,if 块就会被执行。如果条件为假且存在 else 块,那么 else 块将被执行。我们还看到了三元运算符以及它们带来的奇特语法。如果你只需要在每个块中写一个语句,这是一种写 if-else 语句的简短方式。
最后,我们看到了 switch 语句以及它们如何被用来优化我们的条件代码。使用 switch 语句,我们可以将一个条件与许多不同的案例进行比较。当它们相等(值和类型)时,与 case 关联的代码将被执行。
在下一章中,我们将向其中添加循环!这将帮助我们编写更高效的代码和算法。
第五章:循环
我们开始对 JavaScript 有一个很好的基本掌握。本章将重点介绍一个非常重要的控制流概念:循环。循环执行代码块一定次数。我们可以使用循环做很多事情,例如重复操作多次以及遍历数据集、数组和对象。每次当你觉得需要复制一小段代码并将其放置在复制的下方时,你可能应该使用循环。
我们将首先讨论循环的基础知识,然后继续讨论嵌套循环,这基本上是在循环内部使用循环。此外,我们还将解释如何遍历我们已看到的两个复杂结构,数组和对象。最后,我们将介绍与循环相关的两个关键字,break
和continue
,以进一步控制循环的流程。
有一个与循环密切相关的话题,但不在本章中。这是内置的foreach
方法。当我们可以使用箭头函数时,我们可以使用此方法遍历数组。由于我们将在下一章讨论这些内容,因此foreach
不包括在内。
本章我们将讨论以下不同的循环:
-
while
循环 -
do
while
循环 -
for
循环 -
for
in
-
for of
循环
注意:练习、项目和自我检查测验的答案可以在附录中找到。
while
循环
while loop:
while (condition) {
// code that gets executed as long as the condition is true
}
while
循环只有在条件为true
时才会执行,所以如果一开始条件就是false
,则代码内部将被跳过。
这里有一个非常简单的while
循环示例,将数字 0 到 10(不包括 10)打印到控制台:
let i = 0;
while (i < 10) {
console.log(i);
i++;
}
输出将如下所示:
1
2
3
4
5
6
7
8
9
这里发生以下步骤:
-
创建一个变量
i
并将其值设置为 0 -
开始
while
循环并检查i
的值是否小于 10 的条件 -
由于条件为真,代码记录了
i
并增加i
的值 1 -
条件再次被评估;1 仍然小于 10
-
由于条件为真,代码记录了
i
并增加i
的值 1 -
记录和增加会持续到
i
变为 10 -
10 不小于 10,所以循环结束
我们可以有一个while
循环来在数组中查找值,如下所示:
let someArray = ["Mike", "Antal", "Marc", "Emir", "Louiza", "Jacky"];
let notFound = true;
while (notFound && someArray.length > 0) {
if (someArray[0] === "Louiza") {
console.log("Found her!");
notFound = false;
} else {
someArray.shift();
}
}
它检查数组的第一个值是否是某个特定的值,如果不是,它将使用shift
方法从数组中删除该值。你还记得这个方法吗?它移除数组的第一个元素。所以,在下一次迭代中,第一个值已经改变并再次进行检查。如果它偶然发现该值,它将记录到控制台并将布尔值notFound
更改为false
,因为它已经找到了它。那将是最后一次迭代,循环结束。它将输出:
Found her!
false
你为什么认为在while
条件中添加了&& someArray.length > 0
?如果我们省略它,并且我们正在寻找的值不在数组中,我们就会陷入无限循环。这就是为什么我们确保如果我们的值不存在,我们也会结束,这样我们的代码就可以继续。
但我们也可以很容易地使用循环做更复杂的事情。让我们看看使用循环填充斐波那契数列到数组有多容易:
let nr1 = 0;
let nr2 = 1;
let temp;
fibonacciArray = [];
while (fibonacciArray.length < 25) {
fibonacciArray.push(nr1);
temp = nr1 + nr2;
nr1 = nr2;
nr2 = temp;
}
在斐波那契数列中,每个值都是前两个值的和,从 0 和 1 开始。我们可以像上面所述的那样使用 while 循环来做这件事。我们创建两个数字,它们在每次迭代中都会改变。我们限制了迭代的次数,使其等于fibonacciArray
的长度,因为我们不希望出现无限循环。在这种情况下,循环将在数组的长度不再小于 25 时结束。
我们需要一个临时变量来存储nr2
的下一个值。在每次迭代中,我们将第一个数字的值推送到数组中。如果你记录数组,你会看到数字会非常快地变得相当高。想象一下,你需要在代码中逐个生成这些值!
[
0, 1, 1, 2, 3,
5, 8, 13, 21, 34,
55, 89, 144, 233, 377,
610, 987, 1597, 2584, 4181,
6765, 10946, 17711, 28657, 46368
]
练习 5.1
在这个练习中,我们将创建一个数字猜谜游戏,该游戏根据用户的猜测准确性进行回复。
-
创建一个变量作为数字猜谜游戏的最大值。
-
使用
Math.random()
和Math.floor()
生成一个随机数作为解。你还需要添加 1,以便返回的值是 1-[设置的任何最大值]。你可以将此值记录到控制台以供开发查看,当游戏完成时,你可以取消注释此控制台输出。 -
创建一个变量用于跟踪答案是否正确,并将其设置为默认的布尔值
false
。如果用户的猜测匹配,我们可以将其更新为true
。 -
使用 while 循环迭代一个提示,要求用户输入一个介于 1 和 5 之间的数字,并将响应转换为数字,以匹配随机数的数据类型。
-
在 while 循环内部,使用条件检查提示值是否等于解的数字。应用逻辑,如果数字正确,将状态设置为
true
并退出循环。向玩家提供一些反馈,说明猜测是高还是低,并启动另一个提示,直到用户猜正确为止。这样我们使用循环不断提问,直到解正确,然后我们可以停止代码块的迭代。
do while 循环
在某些情况下,你确实需要至少执行一次代码块。例如,如果你需要有效的用户输入,你需要至少询问一次。同样,尝试连接数据库或其他外部源也是如此:你必须至少尝试一次才能成功。而且你可能需要一直这样做,直到你得到所需的结果。在这些情况下,你可以使用do while 循环。
以下是语法示例:
do {
// code to be executed if the condition is true
} while (condition);
它执行do
块内的内容,然后执行while
。如果条件为true
,它将再次执行do
块内的内容。它将继续这样做,直到while
中的条件变为false
。
我们可以使用prompt()
方法获取用户输入。让我们使用do while
循环来要求用户输入一个介于 0 和 100 之间的数字。
let number;
do {
number = prompt("Please enter a number between 0 and 100: ");
} while (!(number >= 0 && number < 100));
这是输出;您将需要在这里自己输入控制台中的数字。
Please enter a number between 0 and 100: > -50
Please enter a number between 0 and 100: > 150
Please enter a number between 0 and 100: > 34
>
后面的所有内容都是用户输入。>
是代码的一部分;它是控制台添加的,以使控制台输出(请输入一个介于 0 和 100 之间的数字
)和控制台输入(-50
、150
和34
)之间的区别更清晰。
它询问三次,因为前两次数字不在 0 到 100 之间,while
块中的条件为真。当输入34
时,while
块中的条件变为假,循环结束。
练习 5.2
在这个练习中,我们将创建一个基本的计数器,它将通过一个一致的步长值增加动态变量,直到上限。
-
将起始计数器设置为 0
-
创建一个变量,
step
,用于增加计数器 -
添加一个
do while
循环,将计数器打印到控制台,并在每次循环中增加step
的值 -
继续循环,直到计数器等于 100 或超过 100
for
循环
for
循环是特殊的循环。语法可能一开始有点令人困惑,但您很快就会发现自己会使用它们,因为它们非常有用。
这是语法看起来像什么:
for (initialize variable; condition; statement) {
// code to be executed
}
在for
语句后面的括号中,有三个部分,由分号分隔。第一个部分初始化可以在for
循环中使用的变量。第二个部分是一个条件:只要这个条件为真,循环就会继续迭代。这个条件在第一次迭代之前检查变量初始化(这只会发生在条件评估为真时)。最后一个部分是一个语句。这个语句在每次迭代后执行。以下是for
循环的流程:
-
初始化变量。
-
检查条件。
-
如果条件为真,则执行代码块。如果条件为假,循环将在这里结束。
-
执行语句(例如,第三部分
i++
)。 -
返回到步骤 2。
这是一个简单的示例,将数字 0 到 10(不包括 10)记录到控制台:
for (let i = 0; i < 10; i++) {
console.log(i);
}
它首先创建一个变量i
,并将其设置为0
。然后它检查i
是否小于 10。如果是,它将执行日志语句。之后,它将执行i++
并增加i
的值。
如果我们不增加i
,我们将陷入无限循环,因为i
的值将不会改变,并且将永远小于 10。这是在所有循环中需要注意的事情!
条件再次被检查。然后继续,直到i
达到 10 的值。10 不小于 10,所以循环完成执行,数字 0 到 9 已经记录到控制台。
我们也可以使用for
循环创建一个序列并将值添加到数组中,如下所示:
let arr = [];
for (let i = 0; i < 100; i++) {
arr.push(i);
}
这是循环之后的数组看起来像什么:
[
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,
36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71,
72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83,
84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95,
96, 97, 98, 99
]
由于循环运行了 100 次代码块,从i
的初始值 0 开始,代码块将在数组的末尾添加递增的值。这导致一个计数为 0–99 且长度为 100 项的数组。由于数组从索引值 0 开始,数组中的值实际上与数组中项的索引值相匹配。
或者我们可以创建一个只包含偶数值的数组:
let arr = [];
for (let i = 0; i < 100; i = i + 2) {
arr.push(i);
}
结果是这个数组:
[
0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20,
22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42,
44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64,
66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86,
88, 90, 92, 94, 96, 98
]
最常见的是,你会看到i++
作为for
循环的第三部分,但请注意,你可以在那里写任何语句。在这种情况下,我们使用i = i + 2
来每次将 2 加到前一个值上,创建一个只包含偶数的数组。
练习 5.3
在这个练习中,我们将使用for
循环创建一个包含对象的数组。从创建一个空数组开始,循环内的代码块将创建一个被插入到数组中的对象。
-
设置一个空数组,
myWork
。 -
使用
for
循环创建一个包含 10 个对象的列表,每个对象都是一个编号的课程(例如,课程 1、课程 2、课程 3……),每个其他项目交替true
/false
状态以指示该课程是否将在今年运行。例如:name: 'Lesson 1', status: true
-
你可以通过使用三元运算符来指定状态,该运算符检查给定课程值的模是否等于零,并设置一个布尔值以在每个迭代中交替值。
-
使用一个临时对象变量创建一个课程,包含名称(
lesson
带有数值)和预定义的状态(我们在上一步中设置)。 -
将对象推送到
myWork
数组。 -
将数组输出到控制台。
嵌套循环
有时候在循环内部使用循环可能是必要的。循环内部的循环被称为嵌套循环。通常,这并不是解决问题的最佳方案。它甚至可能是代码编写不佳的迹象(在程序员中有时被称为“代码异味”),但偶尔它是一个完美的解决方案。
这里是while
循环的示例:
while (condition 1) {
// code that gets executed as long as condition 1 is true
// this loop depends on condition 1 being true
while (condition 2) {
// code that gets executed as long as condition 2 is true
}
}
嵌套也可以与for
循环一起使用,或者与for
和while
的组合一起使用,甚至可以与所有类型的循环一起使用;它们可以深入几个层级。
我们可能使用嵌套循环的一个例子是当我们想要创建一个数组数组。在外部循环中,我们创建顶层数组,在内部循环中,我们向数组添加值。
let arrOfArrays = [];
for (let i = 0; i < 3; i++){
arrOfArrays.push([]);
for (let j = 0; j < 7; j++) {
arrOfArrays[i].push(j);
}
}
当我们这样记录这个数组时:
console.log(arrOfArrays);
我们可以看到输出是一个包含从0
到6
的值的数组。
[
[
0, 1, 2, 3, 4, 5, 6
],
[
0, 1, 2, 3, 4, 5, 6
],
[
0, 1, 2, 3, 4, 5, 6
]
]
我们使用了嵌套循环来创建一个数组中的数组,这意味着在创建了这个循环之后,我们可以处理行和列。这意味着嵌套循环可以用来创建表格数据。我们可以使用console.table()
方法将此输出显示为表格,如下所示:
console.table(arrOfArrays);
这将输出:
┌─────────┬───┬───┬───┬───┬───┬───┬───┐
│ (index) │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │
├─────────┼───┼───┼───┼───┼───┼───┼───┤
│ 0 │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │
│ 1 │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │
│ 2 │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │
└─────────┴───┴───┴───┴───┴───┴───┴───┘
让我们在下一个练习中将这个应用到实践中。
练习 5.4
在这个练习中,我们将生成一个值表格。我们将使用循环生成行和列,列将嵌套在行内。嵌套数组可以用来表示表格中的行。这是电子表格中常见的结构,其中每行都是表格内的嵌套数组,这些行的内容是表格中的单元格。列将按照我们在每行中创建相等数量的单元格来对齐。
-
要创建一个表格生成器,首先创建一个空的数组
myTable
来存储你的表格数据。 -
设置行和列的变量值。这将使我们能够动态地控制表格内想要的行和列的数量。将值从主代码中分离出来有助于更容易地更新维度。
-
设置一个初始值为
0
的counter
变量。计数器将用于设置单元格的内容并计算表格内单元格的值。 -
创建一个带有条件的
for
循环来设置迭代次数,并构建表格的每一行。在其中,设置一个新的临时数组tempTable
来存储每行数据。列将嵌套在行内,生成所需的每个列单元格。 -
在第一个循环内嵌套第二个循环来计数列。列在行循环内运行,以便在表格内有均匀数量的列。
-
在内循环的每次迭代中增加主计数器,以便我们跟踪每个单元格的总体计数以及创建了多少个单元格。
-
将计数器值推送到临时数组
tempTable
。由于数组是一个表示表格的嵌套数组,计数器的值也可以用来在表格中相邻显示单元格值。尽管这些是表示新行的单独数组,但计数器的值将有助于说明最终表格中单元格的整体顺序。 -
将临时数组推送到主表格中。随着每次迭代构建一个新行数组项,这将继续在数组中构建主表格。
-
使用
console.table(myTable)
将输出到控制台。这将显示表格结构的可视化表示。
循环和数组
如果你现在还没有完全确信循环的极端有用性,请看看循环和数组。循环让数组的生活变得更加舒适。
我们可以将length
属性和for
循环或while
循环的条件部分结合起来,在数组上循环。在for
循环的情况下,它看起来是这样的:
let arr = [some array];
for (initialize variable; variable smaller than arr.length; statement) {
// code to be executed
}
让我们从以下简单示例开始,该示例将记录数组的每个值:
let names = ["Chantal", "John", "Maxime", "Bobbi", "Jair"];
for (let i = 0; i < names.length; i ++){
console.log(names[i]);
}
这将输出:
Chantal
John
Maxime
Bobbi
Jair
我们使用 length
属性来确定索引的最大值。索引从 0 开始计数,但长度不是。索引总是比长度小 1。因此,我们通过增加长度来遍历数组的值。
在这个例子中,我们还没有做非常有趣的事情;我们只是在打印值。但我们可以在一个循环中改变数组的值,例如,像这样:
let names = ["Chantal", "John", "Maxime", "Bobbi", "Jair"];
for (let i = 0; i < names.length; i ++){
names[i] = "hello " + names[i];
}
我们将 hello
与我们名字的开头连接起来。数组在循环中被更改,循环执行后数组将包含以下内容:
[
'hello Chantal',
'hello John',
'hello Maxime',
'hello Bobbi',
'hello Jair'
]
可能性是无限的。当数组在应用程序的某个地方出现时,可以按值将数据发送到数据库。数据可以按值修改,甚至可以像这样进行过滤:
let names = ["Chantal", "John", "Maxime", "Bobbi", "Jair"];
for (let i = 0; i < names.length; i ++){
if(names[i].startsWith("M")){
delete names[i];
continue;
}
names[i] = "hello " + names[i];
}
console.log(names);
startsWith()
方法只是检查字符串是否以某个字符开头。在这种情况下,它检查名称是否以字符串 M
开头。
别担心,我们将在 第八章,内置 JavaScript 方法 中详细讲解这个函数以及许多其他函数。
输出如下:
[
'hello Chantal',
'hello John',
<1 empty item>,
'hello Bobbi',
'hello Jair'
]
但是在这里你必须小心。如果我们删除项而不是删除并留下空值,我们会意外地跳过下一个值,因为那个值会得到最近删除的那个值的索引,而 i
会增加并移动到下一个索引。
你认为这个函数是做什么的:
let names = ["Chantal", "John", "Maxime", "Bobbi", "Jair"];
for (let i = 0; i < names.length; i++){
names.push("...")
}
你的程序会在这里陷入无限循环。由于每次迭代都会添加一个值,循环的长度会随着每次迭代而增长,i
将永远不会大于或等于 length
。
练习 5.5
探索如何创建一个包含嵌套数组作为表格行的表格网格。每一行将包含设置在变量中的列数所需的单元格数量。这个网格表格将根据变量的值动态调整。
-
创建一个网格数组变量。
-
将单元格的数量设置为
64
。 -
将计数器设置为
0
。 -
创建一个全局变量用于
row
数组。 -
创建一个循环,该循环将迭代到数组中你想要的单元格数量,再加一以包含零值。在我们的例子中,我们会使用 64+1。
-
添加一个外部的
if
语句,该语句使用取模运算来检查主计数器是否能被 8 或你想要的任意列数整除。 -
在前面的
if
语句内部,添加另一个if
语句来检查行是否未定义,这表示是否是第一次运行或者行是否已完成。如果行已经被定义,那么将行添加到主网格数组。 -
为了完成外部的
if
语句,如果计数器能被 8 整除,则清空row
数组——它已经被内部if
语句添加到网格中。 -
在
for
循环结束时,将主计数器增加 1。 -
设置一个临时变量来保存计数器的值并将其推送到
row
数组。 -
在循环迭代中,检查计数器的值是否等于你想要的列总数;如果是,则将当前行添加到网格中。
-
请注意,由于没有足够的单元格在添加行的条件下创建新行,额外的单元格将不会添加到网格中。另一种解决方案是移除循环条件中的+1,并在循环完成后添加
grid.push(row)
,这两种方法都将提供相同的解决方案输出。 -
将网格输出到控制台。
for of 循环
我们还可以使用另一种循环来遍历数组的元素:for of 循环。它不能用来改变与索引关联的值,就像我们可以在常规循环中做的那样,但对于处理值来说,它是一个非常优雅且易于阅读的循环。
以下是语法示例:
let arr = [some array];
for (let variableName of arr) {
// code to be executed
// value of variableName gets updated every iteration
// all values of the array will be variableName once
}
所以你可以这样读:“对于数组中的每个值,称之为variableName
并执行以下操作。”我们可以使用这个循环记录names
数组:
let names = ["Chantal", "John", "Maxime", "Bobbi", "Jair"];
for (let name of names){
console.log(name);
}
我们需要指定一个临时变量;在这个例子中我们称之为name
。这个变量用于放置当前迭代的值,迭代完成后,它会被next
值替换。这段代码的结果如下:
Chantal
John
Maxime
Bobbi
Jair
这里有一些限制;我们无法修改数组,但我们可以将所有元素写入数据库或文件,或者发送到其他地方。这种做法的优势在于我们不会意外陷入无限循环或跳过值。
练习 5.6
这个练习将在遍历 x 的递增值时构建一个数组。一旦数组完成,这个练习还将展示几种输出数组内容的方法。
-
创建一个空数组
-
运行循环 10 次,向数组添加一个新的递增值
-
将数组记录到控制台
-
使用
for
循环遍历数组(调整迭代次数以匹配数组中的值数)并将输出到控制台 -
使用
for of
循环将值输出到控制台
循环和对象
我们刚刚看到了如何遍历数组中的值,但我们也可以遍历对象属性。当我们需要遍历所有属性但不知道正在迭代的对象的确切属性时,这可能会很有帮助。
以几种方式遍历对象。我们可以使用for in
循环直接遍历对象,或者将对象转换为数组并遍历数组。我们将在以下章节中考虑这两种方法。
for in 循环
使用循环操作对象也可以通过for
循环的另一种变体来实现,即for in 循环。for in
循环与for of
循环有些相似。在这里,我们同样需要指定一个临时名称,也称为键,来存储每个属性名称。我们可以在以下示例中看到它的实际应用:
let car = {
model: "Golf",
make: "Volkswagen",
year: 1999,
color: "black",
};
for (let prop in car){
console.log(car[prop]);
}
我们需要使用每次循环的prop
来从car
对象中获取值。输出结果如下:
Golf
Volkswagen
1999
black
如果我们只是像这样记录了 prop:
for (let prop in car){
console.log(prop);
}
这是我们输出的样子:
model
make
year
color
如您所见,所有属性的名称都打印出来了,而不是值。这是因为for in
循环获取的是属性名(键),而不是值。for of
做的是相反的;它获取的是值,而不是键。
这个for in
循环也可以用于数组,但它实际上并不实用。它只会返回索引,因为这些是数组值的“键”。还应注意的是,执行顺序不能保证,尽管这对于数组通常很重要。因此,最好使用在循环和数组部分提到的那些方法。
练习 5.7
在这个练习中,我们将实验如何遍历对象和内部数组。
-
创建一个包含三个项目的简单对象。
-
使用
for in
循环,从对象中获取属性名和值,并将它们输出到控制台。 -
创建一个包含相同三个项目的数组。使用
for
循环或for in
循环,将数组中的值输出到控制台。
通过转换为数组来遍历对象
你可以在将对象转换为数组后使用任何循环。这可以通过三种方式完成:
-
将对象的键转换为数组
-
将对象的值转换为数组
-
将键值对转换为数组(包含包含两个元素的数组:对象键和对象值)
让我们使用这个例子:
let car = {
model: "Golf",
make: "Volkswagen",
year: 1999,
color: "black",
};
如果我们想遍历对象的键,我们可以使用for in
循环,就像我们在上一节中看到的那样,但如果我们首先将其转换为数组,我们也可以使用for of
循环。我们这样做是通过使用Object.keys(nameOfObject)
内置函数。它接受一个对象,获取该对象的所有属性并将它们转换为数组。
为了演示它是如何工作的:
let arrKeys = Object.keys(car);
console.log(arrKeys);
这将输出:
[ 'model', 'make', 'year', 'color' ]
我们可以使用for of
循环像这样遍历数组的属性:
for(let key of Object.keys(car)) {
console.log(key);
}
这就是它输出的内容:
model
make
year
color
类似地,我们可以使用for of
循环通过将值转换为数组来遍历对象的值。这里的主要区别在于我们使用Object.values(nameOfObject)
:
for(let key of Object.values(car)) {
console.log(key);
}
你可以用和遍历任何数组一样的方式遍历这些数组。你可以在常规的for
循环中使用长度和索引策略,如下所示:
let arrKeys = Object.keys(car);
for(let i = 0; i < arrKeys.length; i++) {
console.log(arrKeys[i] + ": " + car[arrKeys[i]]);
}
这将输出:
model: Golf
make: Volkswagen
year: 1999
color: black
更有趣的是如何同时使用for of
循环遍历两个数组。为了做到这一点,我们必须使用Object.entries()
。让我们演示它是如何工作的:
let arrEntries = Object.entries(car);
console.log(arrEntries);
这将输出:
[
[ 'model', 'Golf' ],
[ 'make', 'Volkswagen' ],
[ 'year', 1999 ],
[ 'color', 'black' ]
]
如您所见,它返回一个二维数组,包含键值对。我们可以像这样遍历它:
for (const [key, value] of Object.entries(car)) {
console.log(key, ":", value);
}
这将输出:
model : Golf
make : Volkswagen
year : 1999
color : black
好的,你现在已经看到了许多遍历对象的方法。大多数方法都归结为将对象转换为数组。我们可以想象,到这一点,你可能想使用 break。或者,也许你只是想继续?
break 和 continue
break和continue是两个我们可以用来控制循环执行流程的关键字。break
将停止循环并继续执行循环下面的代码。continue
将停止当前迭代并返回到循环的顶部,检查条件(或者在for
循环的情况下,执行语句然后检查条件)。
我们将使用这个car
对象数组来演示break
和continue
:
let cars = [
{
model: "Golf",
make: "Volkswagen",
year: 1999,
color: "black",
},
{
model: "Picanto",
make: "Kia",
year: 2020,
color: "red",
},
{
model: "Peugeot",
make: "208",
year: 2021,
color: "black",
},
{
model: "Fiat",
make: "Punto",
year: 2020,
color: "black",
}
];
我们将首先更详细地看看break
。
break
我们已经在switch
语句中看到了break。当执行break
时,switch
语句结束。在循环方面,这并没有太大的不同:当执行break
语句时,循环将结束,即使条件仍然为真。
这里有一个愚蠢的例子来演示break
是如何工作的:
for (let i = 0; i < 10; i++) {
console.log(i);
if (i === 4) {
break;
}
}
这看起来像是一个将数字 0 到 10(再次排除 10)记录到控制台的循环。然而,这里有一个陷阱:一旦i
等于 4,我们就执行break
命令。break
会立即结束循环,因此之后不再执行更多的循环代码。
我们还可以使用break
来停止遍历汽车数组,当我们找到符合我们要求的汽车时。
for (let i = 0; i < cars.length; i++) {
if (cars[i].year >= 2020) {
if (cars[i].color === "black") {
console.log("I have found my new car:", cars[i]);
break;
}
}
}
一旦我们遇到一辆 2020 年或之后的黑色汽车,我们就会停止寻找其他汽车,直接购买那辆。数组中的最后一辆车也是一个选择,但我们甚至没有考虑它,因为我们已经找到了一辆。代码片段将输出如下:
I have found my new car: { model: 'Peugeot', make: '208', year: 2021, color: 'black' }
然而,通常使用break
并不是一个好的实践。如果你能够通过管理循环的条件来跳出循环,这会是一个更好的实践。它防止你陷入无限循环,并且代码更容易阅读。
如果循环的条件不是一个真正的条件,而几乎是一个永远运行的语句,代码就很难阅读。
考虑以下代码片段:
while (true) {
if (superLongArray[0] != 42 && superLongArray.length > 0) {
superLongArray.shift();
} else {
console.log("Found 42!");
break;
}
}
这最好不使用break
和不使用像while(true)
这样可怕的东西来写;你可以这样做:
while (superLongArray.length > 0 && notFound) {
if (superLongArray[0] != 42) {
superLongArray.shift();
} else {
console.log("Found 42!");
notFound = false;
}
}
在第二个例子中,我们可以很容易地看到循环的条件,即数组的长度和一个notFound
标志。然而,使用while(true)
我们有点误用了 while 的概念。你想要指定条件,并且它应该评估为true
或false
;这样你的代码就很好阅读。如果你说while(true)
,你实际上是在说永远,你的代码读者将不得不逐行解释以了解发生了什么以及循环是如何通过一个工作break
语句结束的。
continue
break
可以用来退出循环,而continue可以用来继续循环的下一个迭代。它将退出当前迭代并返回到检查条件并开始新迭代的位置。
这里你可以看到一个continue
的例子:
for (let car of cars){
if(car.color !== "black"){
continue;
}
if (car.year >= 2020) {
console.log("we could get this one:", car);
}
}
这里的方法是不考虑所有不是黑色的车,只考虑所有不是在 2020 年或之后生产的其他车。代码将输出如下:
we could get this one: { model: 'Peugeot', make: '208', year: 2021, color: 'black' }
we could get this one: { model: 'Fiat', make: 'Punto', year: 2020, color: 'black' }
在while
循环中注意continue
。在不运行它的情况下,你认为下一个代码片段会做什么?
// let's only log the odd numbers to the console
let i = 1;
while (i < 50) {
if (i % 2 === 0){
continue;
}
console.log(i);
i++;
}
它记录1
,然后它会让你陷入无限循环,因为continue
在i
的值改变之前被触发,所以它会再次遇到continue
,然后又是,然后又是,如此等等。这可以通过将i++
向上移动并从i
中减去 1 来修复,如下所示:
let i = 1;
while (i < 50) {
i++;
if ((i-1) % 2 === 0){
continue;
}
console.log(i-1);
}
但再次强调,这里有一种更好的方法,不需要使用continue
。错误的机会要小得多:
for (let i = 1; i < 50; i = i + 2) {
console.log(i);
}
如您所见,它甚至更短、更易读。break
和continue
的价值通常在您遍历大型数据集时出现,这些数据集可能来自您的应用程序之外。在这里,您将更少地影响应用其他类型的控制。在简单的示例中使用break
和continue
不是最佳实践,但它是一个了解这些概念的好方法。
练习 5.8
这个练习将演示如何创建一个字符串,其中包含循环遍历的所有数字。我们还可以通过添加一个使用continue
跳过匹配条件的条件来设置一个要跳过的值。第二种选择是做同样的练习并使用break
关键字。
-
设置一个字符串变量作为输出。
-
选择一个要跳过的数字,并将其设置为变量。
-
创建一个计数到 10 的
for
循环。 -
添加一个条件来检查循环变量的值是否等于要跳过的数字。
-
如果该数字在条件中要被跳过,则
continue
到下一个数字。 -
当你遍历值时,将新的计数值追加到主输出变量的末尾。
-
循环完成后,输出主变量。
-
重新使用代码,但将
continue
改为break
,看看区别。现在它应该会在跳过的值处停止。
break, continue, and nested loops
break
和continue
也可以用在嵌套循环中,但重要的是要知道,当在嵌套循环中使用break
或continue
时,外层循环不会中断。
我们将使用这个数组的数组来讨论嵌套循环中的break
和continue
:
let groups = [
["Martin", "Daniel", "Keith"],
["Margot", "Marina", "Ali"],
["Helen", "Jonah", "Sambikos"],
];
让我们分解这个例子。我们正在寻找所有有两个以 M 开头的名字的组。如果我们找到这样的组,我们将记录它。
for (let i = 0; i < groups.length; i++) {
let matches = 0;
for (let j = 0; j < groups[i].length; j++) {
if(groups[i][j].startsWith("M")){
matches++;
} else {
continue;
}
if (matches === 2){
console.log("Found a group with two names starting with an M:");
console.log(groups[i]);
break;
}
}
}
我们首先遍历顶层数组并设置一个计数器matches
,起始值为0
,对于这些顶层数组中的每一个,我们将遍历其值。当一个值以 M 开头时,我们将matches
增加 1 并检查是否已经找到了两个匹配项。如果我们找到两个 M,我们将跳出内层循环并继续外层循环。由于内层循环之后没有其他操作,这个循环将移动到下一个顶层数组。
如果名字不以 M 开头,我们不需要检查matches
是否为2
,我们可以继续到内数组的下一个值。
看看这个例子:你认为它会记录什么?
for (let group of groups){
for (let member of group){
if (member.startsWith("M")){
console.log("found one starting with M:", member);
break;
}
}
}
它将遍历数组,并且对于每个数组,它将检查值是否以 M 开头。如果是,内部循环将中断。所以,如果数组中的某个数组包含多个以 M 开头的值,只有第一个会被找到,因为遍历该数组的迭代会中断,然后我们继续到下一个数组。
这个会输出:
found one starting with M: Martin
found one starting with M: Margot
我们可以看到它找到了 Margot,第二个数组中的第一个,但它跳过了 Marina,因为它在该数组中是第二个。找到一组后,它会中断循环,因此不会遍历内部数组的其他元素。它将继续到下一个数组,该数组不包含以 M 开头的名字。
data set contains at least one of something. Because of the nature of the for of loop, it won't give the index or place where it found it. It will simply break, and you have the value of the element of the array to use. If you need to know more, you can work with counters, which are updated every iteration.
如果我们想查看数组数组中的所有名字中是否只有一个以 M 开头,我们就必须跳出外部循环。这是我们可以通过标签循环来做到的。
break 和 continue 以及标签块
我们可以从内部循环中跳出外部循环,但前提是我们给我们的循环一个标签。这可以通过以下方式完成:
outer:
for (let group of groups) {
inner:
for (let member of group) {
if (member.startsWith("M")) {
console.log("found one starting with M:", member);
break outer;
}
}
}
我们通过在代码块前放置一个单词和冒号来给我们的代码块一个标签。这些单词可以是几乎任何东西(在我们的例子中是“outer”和“inner”),但不能是 JavaScript 的保留词,如 for
、if
、break
、else
等。
这只会记录以 M
开头的第一个名字:
found one starting with M: Martin
它只会记录一个,因为它会跳出外部循环,并且所有循环都会在找到第一个匹配项时结束。以类似的方式,你也可以继续外部循环。
每当你想要在找到第一个匹配项后立即完成时,这就是你要使用的选项。例如,如果你想检查错误并在没有错误的情况下退出,这就是你要采取的方式。
章节项目
数学乘法表
在这个项目中,你将使用循环创建一个数学乘法表。你可以通过自己的创意或遵循以下建议步骤之一来完成此操作:
-
设置一个空数组来包含最终的乘法表。
-
设置一个
value
变量来指定你想要相乘的值的数量,并显示结果。 -
创建一个外部
for
循环来遍历每一行,并创建一个temp
数组来存储行值。每一行将是一个单元格数组,它将被嵌套到最终表中。 -
为列值添加一个内部
for
循环,它将乘积的行和列值推送到temp
数组中。 -
将包含计算结果的临时行数据添加到最终表的主体数组中。最终结果将为计算添加一行值。
自我检查测验
-
以下代码的预期输出是什么?
let step = 3; for (let i = 0; i < 1000; i += step) { if (i > 10) { break; } console.log(i); }
-
myArray
的最终值是什么,控制台中的预期输出是什么?const myArray = [1,5,7]; for(el in myArray){ console.log(Number(el)); el = Number(el) + 5; console.log(el); } console.log(myArray);
概述
在本章中,我们介绍了循环的概念。循环使我们能够重复执行特定的代码块。当我们循环时,我们需要某种条件,并且只要该条件为真,我们就会继续循环。一旦它变为假,我们就结束循环。
我们已经看到了 while
循环,其中我们只需插入一个条件,只要该条件为真,我们就继续循环。如果条件永远不为真,我们甚至不会执行循环代码一次。
对于 do while
循环来说,情况就不同了。我们总是先执行代码一次,然后开始检查条件。如果这个条件为真,我们会再次执行代码,并一直这样做,直到条件变为假。当处理来自外部(如用户输入)的输入时,这可能很有用。我们需要请求一次,然后我们可以继续请求,直到它有效。
然后我们看到了 for
循环,它的语法略有不同。我们必须指定一个变量,检查一个条件(最好使用那个变量,但这不是强制性的),然后指定在每次迭代后要执行的操作。同样,最好让操作包括 for
循环第一部分中的变量。这给了我们一段只要条件为真就会执行的代码。
我们还看到了两种遍历数组和对象的方法,for in
和 for of
。for in
循环遍历键,而 for of
循环遍历值。它们遍历集合中的每个元素。这些循环的优势在于 JavaScript 控制执行:你不会错过任何元素,也不会陷入无限循环。
最后,我们看到了 break
和 continue
。我们可以使用 break
关键字立即结束循环,使用 continue
关键字结束当前迭代并回到顶部开始下一次迭代,如果条件仍然为真,那就是了。
在下一章中,我们将向我们的 JavaScript 工具箱添加一个真正强大的工具:函数!它们允许我们将我们的编码技能提升到下一个层次,并更好地组织我们的代码。
第六章:函数
你已经看到了很多 JavaScript,现在你准备好学习函数了。很快你就会发现你已经在使用函数了,但现在是你学习如何开始编写自己的函数的时候了。函数是一个很好的构建块,它将减少你应用中所需的代码量。你需要函数时就可以调用它,你可以将其编写为一种带有变量的模板。所以,根据你如何编写它,你可以在许多情况下重用它。
它确实要求你以不同的方式思考代码的结构,这可能会很困难,尤其是在开始的时候。一旦你习惯了这种思维方式,函数将真正帮助你编写结构良好、可重用和易于维护的代码。让我们深入这个新的抽象层!
在此过程中,我们将涵盖以下主题:
-
基本函数
-
函数参数
-
返回
-
函数中的变量作用域
-
递归函数
-
嵌套函数
-
匿名函数
-
函数回调
注意:练习、项目和自我检查测验的答案可以在附录中找到。
基本函数
我们已经调用函数有一段时间了。还记得prompt()
、console.log()
、push()
和sort()
数组函数吗?这些都是函数。函数是一组语句、变量声明、循环等捆绑在一起的内容。调用函数意味着整个语句组将被执行。
首先,我们将看看如何调用函数,然后我们将看看如何编写我们自己的函数。
调用函数
我们可以通过末尾的括号来识别函数。我们可以这样调用函数:
nameOfTheFunction();
functionThatTakesInput("the input", 5, true);
这是在没有参数的情况下调用一个名为nameOfTheFunction
的函数,以及一个名为functionThatTakesInput
的函数,它需要三个必需的参数。让我们看看当我们开始编写函数时,函数可以看起来像什么。
编写函数
使用function
关键字可以编写函数。以下是编写函数的模板语法:
function nameOfTheFunction() {
//content of the function
}
上面的函数可以这样调用:
nameOfTheFunction();
让我们编写一个函数,询问你的名字,然后问候你:
function sayHello() {
let you = prompt("What's your name? ");
console.log("Hello", you + "!");
}
我们在问号后添加一个空格,以确保用户在问号后一个空格开始输入答案,而不是直接在其后。我们这样调用这个函数:
sayHello();
它将提示:
What's your name? >
让我们继续输入我们的名字。输出将是:
Hello Maaike!
花点时间考虑函数和变量之间的关系。正如你所看到的,函数可以包含变量,这些变量决定了它们的操作方式。相反的情况也是正确的:变量可以包含函数。你还在吗?这里你可以看到一个包含函数的变量(varContainingFunction
)和一个函数内部的变量(varInFunction
)的例子:
let varContainingFunction = function() {
let varInFunction = "I'm in a function.";
console.log("hi there!", varInFunction);
};
varContainingFunction();
变量包含一定的值并且是某物;它们不做任何事情。函数是动作。它们是一组可以在被调用时执行的语句。JavaScript 不会在函数未被调用时运行这些语句。我们将在匿名函数部分回到将函数存储在变量中的想法,并考虑一些好处,但现在让我们继续看看命名函数的最佳方式。
函数命名
给你的函数命名可能看起来是一个微不足道的事情,但这里有一些最佳实践需要记住。为了保持简洁:
-
使用驼峰式命名法为你的函数命名:这意味着第一个单词以小写字母开头,新单词以大写字母开头。这使得阅读更容易,并保持你的代码一致性。
-
确保名称描述了函数正在做什么:将数字加法函数命名为
addNumbers
比myFunc
更好。 -
使用一个动词来描述函数正在做什么:使其成为一个动作。所以,而不是
hiThere
,可以将其命名为sayHi
。
练习第 6.1 节习题
看看你能否为自己编写一个函数。我们希望编写一个可以添加两个数字的函数。
-
创建一个函数,该函数接受两个参数,将参数相加,并返回结果。
-
设置两个不同的变量,并赋予它们不同的值。
-
使用你的函数对两个变量进行操作,并使用
console.log
输出结果。 -
使用两个更多的数字作为参数调用该函数的第二个调用。
练习第 6.2 节习题
我们将创建一个程序,该程序将随机描述输入的名字。
-
创建一个描述性单词的数组。
-
创建一个包含提示用户输入名字的函数。
-
使用
Math.random
从数组中选择一个随机值。 -
在控制台输出提示值和随机选择的数组值。
-
调用该函数。
参数和参数
你可能已经注意到我们在谈论参数和参数。这两个术语通常用来表示传递给函数的信息:
function tester(para1, para2){
return para1 + " " + para2;
}
const arg1 = "argument 1";
const arg2 = "argument 2";
tester(arg1, arg2);
参数定义为函数定义中括号内的变量,它定义了函数的作用域。它们是这样声明的:
function myFunc(param1, param2) {
// code of the function;
}
一个实际例子可以是以下内容,它将x
和y
作为参数:
function addTwoNumbers(x, y) {
console.log(x + y);
}
当被调用时,这个函数将简单地添加参数并记录结果。然而,为了做到这一点,我们可以用参数调用函数:
myFunc("arg1", "arg2");
我们已经看到了各种参数的例子;例如:
console.log("this is an argument");
prompt("argument here too");
let arr = [];
arr.push("argument");
根据你调用函数时使用的参数,函数的结果可以改变,这使得函数成为一个非常强大和灵活的构建块。使用我们的addTwoNumbers()
函数的一个实际例子如下所示:
addTwoNumbers(3, 4);
addTwoNumbers(12,-90);
这将输出:
7
-78
如你所见,函数对两次调用都有不同的结果。这是因为我们用不同的参数调用它,这些参数取代了发送到函数中并在函数作用域内使用的x
和y
。
练习第 6.3 题
创建一个基本的计算器,它接受两个数字和一个表示操作的字符串值。如果操作等于加法,则应将两个数字相加。如果操作等于减法,则应从其中一个数字中减去另一个数字。如果没有指定选项,则选项的值应为add
。
此函数的结果需要被记录。通过使用不同的运算符和未指定运算符来调用该函数来测试你的函数。
-
设置两个包含数字值的变量。
-
设置一个变量来保存一个运算符,可以是加号(+)或减号(-)。
-
创建一个函数,它在其参数中检索两个值和运算符字符串值。使用这些值通过条件检查运算符是加号(+)还是减号(-),并相应地添加或减去值(记住如果没有提供有效的运算符,函数应默认为加法)。
-
在
console.log()
中,使用你的变量调用该函数,并将响应输出到控制台。 -
将运算符值更新为另一种运算符类型——加号或减号——然后再次使用新更新的参数调用该函数。
默认或不合适的参数
如果我们不传递任何参数就调用我们的addTwoNumbers()
函数,会发生什么?花点时间决定你认为它应该做什么:
addTwoNumbers();
一些语言可能会崩溃并哭泣,但 JavaScript 不会。JavaScript 只是给变量赋予一个默认类型,即未定义。而undefined
+ undefined
等于:
NaN
相反,我们可以告诉 JavaScript 使用不同的默认参数。这可以这样做:
function addTwoNumbers(x = 2, y = 3) {
console.log(x + y);
}
如果你现在调用该函数而不传递任何参数,它将自动将2
赋值给x
,将3
赋值给y
,除非你通过传递参数来覆盖它们。用于调用的值优先于硬编码的参数。因此,给定上述函数,以下函数调用的输出将是什么?
addTwoNumbers();
addTwoNumbers(6, 6);
addTwoNumbers(10);
输出将是:
5
12
13
第一个具有默认值,因此x
是2
,y
是3
。第二个将6
赋值给x
和y
。最后一个有点不明显。我们只提供了一个参数,所以哪个参数会被赋予这个值?嗯,JavaScript 不喜欢过于复杂化。它只是将值赋给第一个参数,x
。因此,x
变为10
,y
得到其默认值3
,两者相加为13
。
如果你调用一个函数的参数多于参数,则不会发生任何事情。JavaScript 将仅使用可以映射到参数的第一个参数来执行函数。就像这样:
addTwoNumbers(1,2,3,4);
这将输出:
3
这只是将 1 和 2 相加,并忽略最后两个参数(3
和4
)。
特殊函数和运算符
函数的编写方式有一些特殊的方法,以及一些有用的特殊运算符。我们在这里讨论的是箭头函数和扩展运算符以及剩余运算符。箭头函数非常适合作为参数传递函数和使用更短的表示法。扩展运算符和剩余运算符使我们的工作更加容易,并且在传递参数和处理数组时更加灵活。
箭头函数
箭头函数是一种特殊的函数编写方式,一开始可能会让人感到困惑。它们的用法如下:
(param1, param2) => body of the function;
或者对于没有参数的情况:
() => body of the function;
或者对于单个参数(这里不需要括号):
param => body of the function;
或者对于具有两个参数的多行函数:
(param1, param2) => {
// line 1;
// any number of lines;
};
箭头函数在你想要即时编写实现的地方非常有用,比如在另一个函数作为参数时。这是因为它们是编写函数的简写表示法。它们最常用于只包含一个语句的函数。让我们从一个简单的函数开始,我们将将其重写为箭头函数:
function doingStuff(x) {
console.log(x);
}
要将这段代码重写为箭头函数,你必须将其存储在变量中,或者作为参数传入,以便能够使用它。我们使用变量的名称来执行箭头函数。在这种情况下,我们只有一个参数,所以没有必要将其括在括号中。我们可以这样写:
let doingArrowStuff = x => console.log(x);
然后这样调用它:
doingArrowStuff("Great!");
这将在控制台记录Great!
。如果有多个参数,我们必须使用括号,如下所示:
let addTwoNumbers = (x, y) => console.log(x + y);
我们可以这样称呼它:
addTwoNumbers(5, 3);
然后它会在控制台记录8
。如果没有参数,你必须使用括号,如下所示:
let sayHi = () => console.log("hi");
如果我们调用sayHi()
,它将在控制台记录hi
。
作为最后的例子,我们可以将箭头函数与某些内置方法结合起来。例如,我们可以在数组上使用forEach()
方法。这个方法对数组中的每个元素执行一个特定的函数。看看这个例子:
const arr = ["squirrel", "alpaca", "buddy"];
arr.forEach(e => console.log(e));
它会输出:
squirrel
alpaca
buddy
对于数组中的每个元素,它都会将其作为输入,并对其执行箭头函数。在这种情况下,函数是记录元素。因此,输出是数组中的每个单独元素。
将箭头函数与内置函数结合使用非常强大。我们可以对数组中的每个元素执行某些操作,而无需计数或编写复杂的循环。我们将在稍后看到箭头函数的更多有用示例。
扩展运算符
扩展运算符是一个特殊运算符。它由在引用表达式或字符串之前使用的三个点组成,它将参数或数组的元素展开。
这可能听起来非常复杂,所以让我们看看一个简单的例子:
let spread = ["so", "much", "fun"];
let message = ["JavaScript", "is", ...spread, "and", "very","powerful"];
这个数组的值变为:
['JavaScript', 'is', 'so', 'much', 'fun', 'and', 'very', 'powerful']
如您所见,扩展运算符的元素成为数组中的单独元素。扩展运算符将数组扩展为新数组中的单独元素。它也可以用来向函数传递多个参数,如下所示:
function addTwoNumbers(x, y) {
console.log(x + y);
}
let arr = [5, 9];
addTwoNumbers(...arr);
这将在控制台输出14
,因为它等同于调用函数的方式:
addTwoNumbers(5, 9);
这个运算符避免了需要将长数组或字符串复制到函数中,这节省了时间并减少了代码复杂性。你可以用多个展开运算符调用一个函数。它将使用数组的所有元素作为输入。这里有一个例子:
function addFourNumbers(x, y, z, a) {
console.log(x + y + z + a);
}
let arr = [5, 9];
let arr2 = [6, 7];
addFourNumbers(...arr, ...arr2);
这将在控制台输出27
,调用函数的方式如下:
addFourNumbers(5, 9, 6, 7);
剩余参数
与展开运算符类似,我们还有剩余参数。它具有与展开运算符相同的符号,但它用于函数参数列表中。记住如果我们像这里一样发送过多的参数会发生什么:
function someFunction(param1, param2) {
console.log(param1, param2);
}
someFunction("hi", "there!", "How are you?");
没错。实际上什么也没有:它只是假装我们只传入了两个参数并记录了hi there!
。如果我们使用剩余参数,它允许我们传入任何数量的参数并将它们转换为参数数组。这里有一个例子:
function someFunction(param1, ...param2) {
console.log(param1, param2);
}
someFunction("hi", "there!", "How are you?");
这将记录:
hi [ 'there!', 'How are you?' ]
如您所见,第二个参数已变为一个数组,包含我们的第二个和第三个参数。这在你不确定将获得多少参数时非常有用。使用剩余参数允许你处理这个可变数量的参数,例如,使用循环。
返回函数值
我们还缺少一个非常重要的部分,使函数像它们一样有用:返回值。当我们指定返回值时,函数可以返回一个结果。返回值可以存储在变量中。我们已经这样做过了——还记得prompt()
吗?
let favoriteSubject = prompt("What is your favorite subject?");
我们将prompt()
函数的结果存储在变量favoriteSubject
中,在这个例子中,它将是用户指定的任何内容。让我们看看如果我们存储addTwoNumbers()
函数的结果并将其记录下来会发生什么:
let result = addTwoNumbers(4, 5);
console.log(result);
你可能已经猜到了——这将记录以下内容:
9
undefined
值9
被写入控制台,因为addTwoNumbers()
包含一个console.log()
语句。console.log(result)
行输出undefined
,因为没有将任何内容插入函数以存储结果,这意味着我们的函数addTwoNumbers()
没有返回任何内容。由于 JavaScript 不喜欢引起麻烦和崩溃,它会分配undefined
。为了解决这个问题,我们可以重新编写我们的addTwoNumbers()
函数,使其真正返回值而不是记录它。这要强大得多,因为我们可以在代码的其余部分存储这个函数的结果并继续使用它:
function addTwoNumbers(x, y) {
return x + y;
}
return
结束函数并返回return
之后的所有值。如果它是一个表达式,就像上面的一样,它将计算表达式的结果并将其返回到调用它的地方(在这个例子中是result
变量):
let result = addTwoNumbers(4, 5);
console.log(result);
9 to the terminal.
你认为这段代码做什么?
let resultsArr = [];
for(let i = 0; i < 10; i ++){
let result = addTwoNumbers(i, 2*i);
resultsArr.push(result);
}
console.log(resultsArr);
它将所有结果记录到屏幕上。该函数正在循环中被调用。第一次迭代,i
等于0
。因此,结果是0
。最后一次迭代,i
等于9
,因此数组的最后一个值等于27
。以下是结果:
[
0, 3, 6, 9, 12,
15, 18, 21, 24, 27
]
练习题 6.4
将你在练习题 6.2中制作的计算器修改为返回加法值而不是打印它们。然后在循环中调用该函数 10 次或更多次,并将结果存储在数组中。一旦循环结束,将最终数组输出到控制台。
-
设置一个空数组来存储循环中要计算的所有值。
-
创建一个循环,运行 10 次,每次递增 1,每次迭代创建两个值。对于第一个值,将循环计数值乘以 5。对于第二个值,将循环计数器的值自乘。
-
创建一个函数,当调用该函数时返回传入的两个参数的值。将值相加,返回结果。
-
在循环中调用计算函数,将两个值作为参数传递给函数,并将返回的结果存储在响应变量中。
-
仍然在循环中,随着循环的迭代,将结果值推入数组。
-
循环完成后,将数组的值输出到控制台。
-
你应该在控制台看到数组
[0, 6, 14, 24, 36, 50, 66, 84, 104, 126]
的值。
使用箭头函数返回
如果我们有一个单行箭头函数,我们可以不使用关键字return
来返回。所以如果我们想重写这个函数,我们可以这样写,将其转换为箭头函数:
let addTwoNumbers = (x, y) => x + y;
我们可以这样调用并存储结果:
let result = addTwoNumbers(12, 15);
console.log(result);
这将在控制台输出27
。如果是一个多行函数,你将不得不使用关键字return
,就像在上一节中演示的那样。例如:
let addTwoNumbers = (x, y) => {
console.log("Adding...");
return x + y;
}
函数中的变量作用域
在本节中,我们将讨论一个经常被认为具有挑战性的主题。我们将讨论作用域。作用域定义了你可以访问某个变量的地方。当变量在作用域内时,你可以访问它。当变量不在作用域内时,你不能访问该变量。我们将对局部变量和全局变量进行讨论。
函数中的局部变量
局部变量仅在其定义的函数中有效。这对于let
变量和var
变量都适用。它们之间有一个区别,我们也会在这里简要提及。函数参数(它们不使用let
或var
)也是局部变量。这听起来可能很模糊,但下一个代码片段将演示这是什么意思:
function testAvailability(x) {
console.log("Available here:", x);
}
testAvailability("Hi!");
console.log("Not available here:", x);
这将输出:
Available here: Hi!
ReferenceError: x is not defined
当在函数内部调用时,x
将被记录。函数外的语句失败,因为x
是testAvailability()
函数的局部变量。这表明函数参数在函数外部是不可访问的。
它们在函数外部不可用,在函数内部可用。让我们看看函数内部定义的变量:
function testAvailability() {
let y = "Local variable!";
console.log("Available here:", y);
}
testAvailability();
console.log("Not available here:", y);
控制台显示了以下内容:
Available here: Local variable!
ReferenceError: y is not defined
在函数内部定义的变量在函数外部也是不可用的。
对于初学者来说,将局部变量和 return
结合起来可能会令人困惑。现在,我们告诉你函数内部声明的局部变量在函数外部不可用,但通过 return
,你可以使它们的值在函数外部可用。所以如果你需要在函数外部使用它们的值,你可以返回这些值。这里的关键词是 值!你不能返回变量本身。相反,一个值可以被捕获并存储在不同的变量中,如下所示:
function testAvailability() {
let y = "I'll return";
console.log("Available here:", y);
return y;
}
let z = testAvailability();
console.log("Outside the function:", z);
console.log("Not available here:", y);
因此,分配给局部变量 y
的返回值 I'll return
被返回并存储在变量 z
中。
这个变量 z
实际上也可以被称为 y
,但那样会让人困惑,因为它仍然是一个不同的变量。
这段代码的输出如下:
Available here: I'll return
Outside the function: I'll return
ReferenceError: y is not defined
let
与 var
变量
let
和 var
的区别在于 var
是函数作用域,这是我们上面描述的概念。实际上 let
不是函数作用域,而是块作用域。一个块由两个大括号 {}
定义。那些大括号内的代码是 let
仍然可用的地方。
让我们看看这个区别在实际中的应用:
function doingStuff() {
if (true) {
var x = "local";
}
console.log(x);
}
doingStuff();
这段代码的输出将是:
local
如果我们使用 var
,变量将成为函数作用域,并在函数块中的任何地方(甚至在定义之前值为 undefined
的情况下)可用。因此,在 if
块结束后,x
仍然可以被访问。
下面是 let
发生的情况:
function doingStuff() {
if (true) {
let x = "local";
}
console.log(x);
}
doingStuff();
这将产生以下输出:
ReferenceError: x is not defined
这里我们得到一个错误,提示 x
未定义。由于 let
只能是块作用域,当 if
块结束时 x
就会超出作用域,并且之后不能再访问。
let
和 var
之间的最后一个区别与脚本中声明的顺序有关。尝试在定义 let
之前使用 x
的值:
function doingStuff() {
if (true) {
console.log(x);
let x = "local";
}
}
doingStuff();
这将给出一个 ReferenceError
,提示 x
未初始化。这是因为使用 let
声明的变量在定义之前不能被访问,即使在同一个块内部。你认为这样的 var
声明会发生什么?
function doingStuff() {
if (true) {
console.log(x);
var x = "local";
}
}
doingStuff();
这次,我们不会得到错误。当我们使用 var
变量在 define
语句之前时,我们只是得到 undefined
。这是由于一个称为提升的现象,这意味着在使用 var
变量之前声明它会导致变量变为 undefined
而不是抛出 ReferenceError
。
提升和如何在不必要的情况下消除其影响是更复杂的话题,我们将在 第十二章,中级 JavaScript 中介绍。
const
作用域
常量是块作用域的,就像 let
一样。这就是为什么这里的范围规则与 let
的规则相似。以下是一个示例:
function doingStuff() {
if (true) {
const X = "local";
}
console.log(X);
}
doingStuff();
这将产生以下输出:
ReferenceError: X is not defined
在定义它之前使用一个const
变量也会引发一个ReferenceError
,就像对let
变量所做的那样。
全局变量
如你所猜,全局变量是在函数外部声明的,不在其他代码块中。变量在其定义的作用域(函数或代码块)内是可访问的,以及任何“较低”的作用域。因此,在函数外部定义的变量在函数内部以及该函数内部任何其他函数或其他代码块中都是可用的。因此,在程序顶层定义的变量在程序中的任何地方都是可用的。这个概念被称为全局变量。你可以在下面看到示例:
let globalVar = "Accessible everywhere!";
console.log("Outside function:", globalVar);
function creatingNewScope(x) {
console.log("Access to global vars inside function." , globalVar);
}
creatingNewScope("some parameter");
console.log("Still available:", globalVar);
这将输出:
Outside function: Accessible everywhere!
Access to global vars inside function. Accessible everywhere!
Still available: Accessible everywhere!
如你所见,全局变量因为不在代码块中声明,所以可以从任何地方访问。它们在定义后总是处于作用域内——无论你在哪里使用它们。然而,你可以在该作用域内通过指定一个具有相同名称的新变量来隐藏它们的可访问性;这可以用于let
、var
和const
。(这不会改变const
变量的值;你正在创建一个新的const
变量,它将在内部作用域中覆盖第一个。)在同一个作用域中,你不能指定两个具有相同名称的let
或两个const
变量。你可以为var
这样做,但你不应该这样做,以避免混淆。
如果你在一个函数内部创建了一个具有相同名称的变量,那么在特定函数的作用域内引用该变量名称时,将使用该变量的值。这里有一个示例:
let x = "global";
function doingStuff() {
let x = "local";
console.log(x);
}
doingStuff();
console.log(x);
这将输出:
local
global
如你所见,doingStuff()
函数内部的x
的值是局部
的。然而,在函数外部,值仍然是全局
的。这意味着你必须非常小心,不要在局部和全局作用域中混淆名称。通常最好避免这样做。
参数名称也是如此。如果你有一个与全局变量相同的参数名称,将使用参数的值:
let x = "global";
function doingStuff(x) {
console.log(x);
}
doingStuff("param");
这将记录param
。
过度依赖全局变量存在风险。当你的应用程序增长时,你很快就会遇到这个问题。正如我们刚才看到的,局部变量会覆盖全局变量的值。最好在函数中使用局部变量;这样,你就能更好地控制你所处理的内容。现在这可能会有些模糊,但当你在实际编码中遇到更大、更多行和文件代码时,它将变得清晰。
目前关于作用域还有一个非常重要的点需要说明。让我们从一个例子开始,看看你是否能弄清楚应该记录什么:
function confuseReader() {
x = "Guess my scope...";
console.log("Inside the function:", x);
}
confuseReader();
console.log("Outside of function:", x);
准备好了吗?以下是输出结果:
Inside the function: Guess my scope...
Outside of function: Guess my scope...
不要关闭这本书——我们将解释正在发生的事情。如果你仔细观察,函数中的 x
在没有 let
或 var
关键字的情况下被定义。在代码上方没有 x
的声明;这是整个程序的代码。JavaScript 不看到 let
或 var
,然后决定,“这必须是一个全局变量。”即使它是在函数内部定义的,函数内部 x
的声明具有全局作用域,并且仍然可以在函数外部访问。
我们真的想强调,这是一个糟糕的做法。如果你需要一个全局变量,请在文件顶部声明它。
立即调用的函数表达式
立即调用的函数表达式(IIFE)是一种表达函数的方式,使其立即被调用。它是匿名的,没有名字,并且是自我执行的。
当你想使用此函数初始化某些内容时,这很有用。它也被用于许多设计模式中,例如,创建私有和公共变量和函数。
这与函数和变量可访问的位置有关。如果你在顶层作用域中有一个 IIFE,即使它位于顶层,其中的内容也无法从外部访问。
这就是如何定义它的:
(function () {
console.log("IIFE!");
})();
函数本身被括号包围,这使得它创建一个函数实例。如果没有这些括号,它会抛出一个错误,因为我们的函数没有名字(尽管可以通过将函数赋给一个变量来解决这个问题,这样输出就可以返回到变量中)。
();
执行一个未命名的函数——这必须在函数声明之后立即完成。如果你的函数需要参数,你可以在这些最后的括号内传递它。
你还可以将 IIFE 与其他函数模式结合使用。例如,你可以在这里使用箭头函数来使函数更加简洁:
(()=>{
console.log("run right away");
})();
再次,我们使用 ();
来调用你创建的函数。
练习 6.5
使用 IIFE 创建几个立即调用的函数,并观察作用域是如何受到影响的。
-
使用
let
创建一个变量值,并将其赋值为字符串 1000。 -
创建一个 IIFE 函数,并在该函数作用域内将新值赋给同名变量。在函数内部,将局部值打印到控制台。
-
创建一个 IIFE 表达式,将其赋给新的
result
变量,并在该作用域内将新值赋给同名变量。将此局部值返回到result
变量,并调用该函数。打印result
变量,以及你一直在使用的变量名:现在它包含什么值? -
最后,创建一个带有参数的匿名函数。添加逻辑,将传递的值赋给与其他步骤相同的变量名,并将其作为字符串句子的一部分打印出来。调用该函数,并在圆括号内传递你希望的价值。
递归函数
在某些情况下,你可能需要在函数内部调用同一个函数。这可能是解决相对复杂问题的美丽解决方案。但是也有一些需要注意的事情。你认为这会做什么?
function getRecursive(nr) {
console.log(nr);
getRecursive(--nr);
}
getRecursive(3);
它打印3
然后开始倒计时并且永远不会停止。为什么它不会停止呢?好吧,我们并没有说它应该在什么时候停止。看看我们改进的版本:
function getRecursive(nr) {
console.log(nr);
if (nr > 0) {
getRecursive(--nr);
}
}
getRecursive(3);
这个函数将会一直调用自己,直到参数的值不再大于0
。然后它才会停止。
当我们递归地调用一个函数时会发生什么?每次都会进入一个函数的更深层次。第一次函数调用是最后完成的。对于这个函数,它的过程是这样的:
-
getRecursive(3)
-
getRecursive(2)
-
getRecursive(1)
-
getRecursive(0)
-
getRecursive(0)
执行完成
-
-
getRecursive(1)
执行完成
-
-
getRecursive(2)
执行完成
-
-
getRecursive(3)
执行完成
下一个递归函数将演示:
function logRecursive(nr) {
console.log("Started function:", nr);
if (nr > 0) {
logRecursive(nr - 1);
} else {
console.log("done with recursion");
}
console.log("Ended function:", nr);
}
logRecursive(3);
它将输出:
Started function: 3
Started function: 2
Started function: 1
Started function: 0
done with recursion
Ended function: 0
Ended function: 1
Ended function: 2
Ended function: 3
在某些情况下,递归函数可以非常出色。当你觉得需要在循环中反复调用同一个函数时,你可能应该考虑使用递归。一个例子也可以是搜索某物。你不需要在同一个函数内部循环遍历所有内容,你可以在函数内部分割,并从内部反复调用函数。
然而,必须记住的是,通常情况下,递归的性能略低于使用循环的常规迭代的性能。所以如果这会导致瓶颈情况,真正地减慢你的应用程序,那么你可能需要考虑另一种方法。
看看以下练习中如何使用递归函数计算阶乘。
练习 6.6
我们可以用递归解决的一个常见问题是计算阶乘。
关于阶乘的快速数学复习:
一个数的阶乘是所有大于 0 的正整数的乘积,直到这个数本身。例如,7 的阶乘是 7 * 6 * 5 * 4 * 3 * 2 * 1。你可以写成 7!。
递归函数如何帮助我们计算阶乘?我们将用较小的数字调用函数,直到达到 0。在这个练习中,我们将使用递归来计算函数参数设置的数值的阶乘结果。
-
创建一个函数,在其中包含一个条件,检查参数值是否为
0
。 -
如果参数等于
0
,它应该返回1
的值。否则,它应该返回参数值乘以函数自身返回的值,并从提供的参数值中减去1
。这将导致代码块运行,直到值达到0
。 -
调用函数,提供一个你想要找到阶乘的数字作为参数。代码应该运行最初传递到函数中的任何数字,一直减少到
0
,并将计算结果输出到控制台。它也可以包含一个console.log()
调用,以打印函数被调用时的当前参数值。 -
更改并更新数字,看看它如何影响结果。
嵌套函数
就像循环、if
语句以及实际上所有其他构建块一样,我们可以在函数内部有函数。这种现象称为嵌套函数:
function doOuterFunctionStuff(nr) {
console.log("Outer function");
doInnerFunctionStuff(nr);
function doInnerFunctionStuff(x) {
console.log(x + 7);
console.log("I can access outer variables:", nr);
}
}
doOuterFunctionStuff(2);
这将输出:
Outer function
9
I can access outer variables: 2
如您所见,外部函数正在调用其嵌套函数。这个嵌套函数可以访问父函数的变量。反过来,情况并非如此。在内部函数内部定义的变量具有函数作用域。这意味着它们可以在定义它们的函数内部访问,在这种情况下是内部函数。因此,这将抛出一个ReferenceError
:
function doOuterFunctionStuff(nr) {
doInnerFunctionStuff(nr);
function doInnerFunctionStuff(x) {
let z = 10;
}
console.log("Not accessible:", z);
}
doOuterFunctionStuff(2);
你认为这会做什么?
function doOuterFunctionStuff(nr) {
doInnerFunctionStuff(nr);
function doInnerFunctionStuff(x) {
let z = 10;
}
}
doInnerFunctionStuff(3);
这也会抛出一个ReferenceError
。现在,doInnerFunctionStuff()
是在外部函数中定义的,这意味着它只在外部函数doOuterFunctionStuff()
的作用域内有效。在这个函数外部,它就不再有效。
练习题 6.7
从一个动态值10
开始创建一个倒计时循环。
-
将
start
变量设置为10
,这将是循环的起始值。 -
创建一个函数,它接受一个参数,即倒计时值。
-
在函数内部,将倒计时的当前值输出到控制台。
-
添加一个条件来检查值是否小于 1;如果是,则返回该函数。
-
添加一个条件来检查倒计时的值是否不小于 1,然后通过在函数内部调用函数来继续循环。
-
确保你在倒计时上添加一个递减运算符,这样前面的条件最终会变为真以结束循环。每次循环时,值都会减少,直到达到
0
。 -
如果值大于 0,则使用条件更新和创建第二个倒计时。如果是,则将倒计时的值减 1。
-
使用
return
返回函数,然后再次调用它,直到条件不再为真。 -
确保当你将新的倒计时值作为参数发送到函数中时,有一个方法通过使用
return
关键字和一个条件来退出循环,如果满足条件则继续循环。
匿名函数
到目前为止,我们一直在给我们的函数命名。如果我们将它们存储在变量中,我们也可以创建没有名称的函数。我们称这些函数为匿名函数。以下是一个非匿名函数:
function doingStuffAnonymously() {
console.log("Not so secret though.");
}
这是如何将前面的函数转换为匿名函数的方法:
function () {
console.log("Not so secret though.");
};
如您所见,我们的函数没有名称。它是匿名的。所以你可能想知道你如何调用这个函数。实际上,你不能这样调用!
我们必须将其存储在变量中才能调用匿名函数;我们可以这样存储:
let functionVariable = function () {
console.log("Not so secret though.");
};
匿名函数可以通过变量名来调用,如下所示:
functionVariable();
它将简单地输出 Not so secret though.
。
这可能看起来有点无用,但它是一个非常强大的 JavaScript 构造。将函数存储在变量中使我们能够做非常酷的事情,比如将函数作为参数传递。这个概念为编程添加了另一个抽象层。这个概念被称为回调,我们将在下一节中讨论它。
练习 6.8
-
设置一个变量名并将一个函数分配给它。创建一个带有一个参数的函数表达式,该参数将提供的参数输出到控制台。
-
将参数传递给函数。
-
将相同的函数创建为一个正常的函数声明。
函数回调
这里是一个将函数作为参数传递给另一个函数的例子:
function doFlexibleStuff(executeStuff) {
executeStuff();
console.log("Inside doFlexibleStuffFunction.");
}
如果我们用之前创建的匿名函数 functionVariable
调用这个新函数,如下所示:
doFlexibleStuff(functionVariable);
它将输出:
Not so secret though.
Inside doFlexibleStuffFunction.
但我们也可以用另一个函数来调用它,然后我们的 doFlexibleStuff
函数将执行这个其他函数。这有多酷?
let anotherFunctionVariable = function() {
console.log("Another anonymous function implementation.");
}
doFlexibleStuff(anotherFunctionVariable);
这将产生以下输出:
Another anonymous function implementation.
Inside doFlexibleStuffFunction.
那么,发生了什么?我们创建了一个函数,并将其存储在 anotherFunctionVariable
变量中。然后我们将它作为函数参数传递给我们的 doFlexibleStuff()
函数。这个函数只是简单地执行被传递进来的任何函数。
在这一点上,你可能想知道为什么作者对这种回调概念如此兴奋。在你迄今为止看到的例子中,这可能看起来相当平淡无奇。一旦我们稍后讨论异步函数,这个概念将会非常有帮助。为了满足你对更具体例子的需求,我们将给你一个例子。
如你所知,JavaScript 中有很多内置函数。其中之一是 setTimeout()
函数。这是一个非常特殊的函数,它会在等待一段指定的时间后执行某个函数。它似乎也负责很多性能极差的网页,但这绝对不是这个可怜的、被误解和误用的函数的错。
这段代码真的是你应该尝试理解的东西:
let youGotThis = function () {
console.log("You're doing really well, keep coding!");
};
setTimeout(youGotThis, 1000);
它将等待 1000
ms(一秒)然后打印:
You're doing really well, keep coding!
如果你需要更多的鼓励,你可以使用 setInterval()
函数。它的工作方式非常相似,但与只执行一次指定的函数不同,它会在指定的间隔内持续执行:
setInterval(youGotThis, 1000);
在这种情况下,它将每秒打印一次鼓励信息,直到你终止程序。
这种在函数被调用后执行函数的概念对于管理异步程序执行非常有用。
章节项目
创建一个递归函数
创建一个递归函数,该函数从 10 开始计数。用不同的起始数字作为传递给函数的参数来调用该函数。函数应运行到值大于 10 为止。
设置超时顺序
使用箭头格式创建函数,将值one
和two
输出到控制台。创建一个第三个函数,将值three
输出到控制台,然后调用前两个函数。
创建一个第四个函数,该函数将单词four
输出到控制台,并使用setTimeout()
立即调用第一个函数,然后调用第三个函数。
你的输出在控制台看起来是什么样子?尝试让控制台输出:
Four
Three
One
Two
One
自我检查测验
-
输出到控制台的是什么值?
let val = 10; function tester(val){ val += 10; if(val < 100){ return tester(val); } return val; } tester(val); console.log(val);
-
以下代码将输出到控制台的内容是什么?
let testFunction = function(){ console.log("Hello"); }();
-
控制台将输出什么?
(function () { console.log("Welcome"); })(); (function () { let firstName = "Laurence"; })(); let result = (function () { let firstName = "Laurence"; return firstName; })(); console.log(result); (function (firstName) { console.log("My Name is " + firstName); })("Laurence");
-
控制台将输出什么?
let test2 = (num) => num + 5; console.log(test2(14));
-
控制台将输出什么?
var addFive1 = function addFive1(num) { return num + 2; }; let addFive2 = (num) => num + 2; console.log(addFive1(14));
概述
在本章中,我们介绍了函数。函数是 JavaScript 的一个强大构建块,我们可以用它来重用代码行。我们可以给函数传递参数,这样我们就可以根据函数被调用的参数来改变代码。函数可以返回一个结果;我们使用return
关键字来这样做。我们还可以在调用函数的地方使用return
。我们可以将结果存储在变量中或在另一个函数中使用,例如。
然后,我们遇到了变量作用域。作用域包括变量可访问的地方。默认的let
和const
变量可以在定义它们的块(及其内部块)内访问,而var
则仅可以从定义它的行访问。
我们还可以使用递归函数优雅地解决那些本质上可以递归解决的问题,例如计算阶乘。嵌套函数是我们接下来研究的下一个主题。它们并不是什么大问题,只是函数内部的函数。函数内部的简单函数并不被认为很漂亮,但匿名函数和箭头函数并不少见。匿名函数是没有名称的函数,箭头函数是匿名函数的一种特殊情况,其中我们使用箭头分隔参数和主体。
在下一章中,我们将考虑类,这是另一种强大的编程结构!
第七章:类
在本章中,我们将讨论 JavaScript 类。我们已经看到了 JavaScript 对象,而类是对象创建的蓝图或模板。因此,这里讨论的许多内容不应该听起来太陌生或革命性。
类使面向对象编程成为可能,这是软件开发中最重要的设计进步之一。这种发展大大降低了应用程序的复杂性,并极大地提高了可维护性。
因此,面向对象编程和类对于计算机科学来说非常重要。然而,当我们将其应用于 JavaScript 时,情况并不一定如此。JavaScript 类与其他编程语言相比有一些特殊之处。在表面之下,类被某种特殊函数所包装。这意味着它们实际上是一种使用构造函数定义对象的替代语法。在本章中,我们将学习类是什么,以及我们如何创建和使用它们。在这个过程中,我们将涵盖以下主题:
-
面向对象编程
-
类和对象
-
类
-
继承
-
原型
注意:练习、项目和自我检查测验的答案可以在附录中找到。
面向对象编程
在我们深入探讨类的乐趣之前,让我们简要地谈谈面向对象编程(OOP)。OOP 是一种非常重要的编程范式,其中代码以对象的形式组织,从而产生更易于维护和重用的代码。使用 OOP 可以教会你真正尝试以对象的形式思考各种主题,通过将属性捆绑在一起,使它们可以包含在一个称为类的蓝图内。这反过来又可能从父类继承属性。
例如,如果我们考虑一个动物,我们可以想出一些属性:名称、重量、高度、最大速度、颜色等等。然后如果我们考虑一种特定的鱼类,我们可以重用“动物”的所有属性,并在其中添加一些特定的鱼类属性。对狗也是如此;如果我们考虑狗,我们可以重用“动物”的所有属性,并添加一些特定的狗属性。这样我们就有了可重用的动物类代码。当我们意识到我们忘记了一个非常重要的属性,这个属性对于我们的应用程序中的许多动物都很重要时,我们只需要将其添加到动物类中。
这对于 Java、.NET 和其他经典面向对象编程方式来说非常重要。JavaScript 并不一定是围绕对象构建的。我们需要它们并会使用它们,但它们并不是我们代码的明星,换句话说。
类和对象
作为快速回顾,对象是一系列属性和方法集合。我们在第三章,JavaScript 多值中看到了它们。对象的属性应该有合理的名称。例如,如果我们有一个person
对象,这个对象可以拥有名为age
和lastName
的属性,它们包含值。以下是一个对象的示例:
let dog = { dogName: "JavaScript",
weight: 2.4,
color: "brown",
breed: "chihuahua"
};
JavaScript 中的类封装了属于该类的数据和函数。如果你创建了一个类,你可以稍后使用以下语法使用该类创建对象:
class ClassName {
constructor(prop1, prop2) {
this.prop1 = prop1;
this.prop2 = prop2;
}
}
let obj = new ClassName("arg1", "arg2");
这段代码定义了一个名为ClassName
的类,声明了一个obj
变量,并用这个对象的新实例初始化它。提供了两个参数。这些参数将由构造函数用于初始化属性。正如你所看到的,构造函数的参数和类的属性(prop1
和prop2
)具有相同的名称。类的属性可以通过它们前面的this
关键字来识别。this
关键字指的是它所属的对象,因此它是ClassName
实例的第一个属性。
记得我们说过,类只是表面下的一些特殊函数。我们可以使用如下特殊函数来创建对象:
function Dog(dogName, weight, color, breed) {
this.dogName = dogName;
this.weight = weight;
this.color = color;
this.breed = breed;
}
let dog = new Dog("Jacky", 30, "brown", "labrador");
狗的例子也可以使用类语法来创建。它看起来像这样:
class Dog {
constructor(dogName, weight, color, breed) {
this.dogName = dogName;
this.weight = weight;
this.color = color;
this.breed = breed;
}
}
let dog = new Dog("JavaScript", 2.4, "brown", "chihuahua");
这将产生具有相同属性的对象。如果我们像下面这样进行日志记录,我们就能看到它:
console.log(dog.dogName, "is a", dog.breed, "and weighs", dog.weight, "kg.");
这将输出:
JavaScript is a chihuahua and weighs 2.4 kg.
在下一节中,我们将深入探讨类的所有部分。
类
你可能会想,如果类和简单地定义一个对象做的是完全相同的事情,我们为什么还需要类呢?答案是,类本质上是为对象创建的蓝图。这意味着如果我们有一个dog
类,需要创建 20 只狗时,我们就不需要输入很多代码。如果我们必须创建对象,我们每次都必须指定所有属性的名称。而且很容易出错,拼错属性名。在这些情况下,类非常有用。
如前节所示,我们使用class
关键字告诉 JavaScript 我们想要创建一个类。接下来,我们给类起一个名字。按照惯例,类名应该以大写字母开头。
让我们来看看类的所有不同元素。
构造函数
构造函数是一个特殊的方法,我们用它来使用我们的类蓝图初始化对象。一个类中只能有一个构造函数。这个构造函数包含在初始化类时将被设置的属性。
这里你可以看到一个Person
类中构造函数的例子:
class Person {
constructor(firstname, lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
}
在表面之下,JavaScript 基于这个构造函数创建了一个特殊函数。这个函数获取类名,并使用给定的属性创建一个对象。通过这个特殊函数,你可以创建类的实例(对象)。
这就是如何从Person
类创建一个新对象的方法:
let p = new Person("Maaike", "van Putten");
new
这个词告诉 JavaScript 在Person
类中查找特殊的构造函数并创建一个新对象。构造函数被调用并返回一个具有指定属性的 person 对象实例。这个对象被存储在p
变量中。
如果我们在日志语句中使用我们的新p
变量,你可以看到属性确实被设置了:
console.log("Hi", p.firstname);
这将输出:
Hi Maaike
你认为当我们创建一个没有所有属性的类时会发生什么?让我们找出答案:
let p = new Person("Maaike");
许多语言会崩溃,但 JavaScript 不会。它只是将剩余的属性设置为undefined
。你可以通过记录它来看到会发生什么:
console.log("Hi", p.firstname, p.lastname);
这会导致:
Hi Maaike undefined
你可以在constructor
中指定默认值。你可以这样做:
constructor(firstname, lastname = "Doe") {
this.firstname = firstname;
this.lastname = lastname;
}
这样,它就不会打印Hi Maaike undefined
,而是Hi Maaike Doe
。
练习 7.1
按以下步骤创建一个Person
类,并打印朋友的姓名实例:
-
为
Person
创建一个类,包括firstname
和lastname
的构造函数。 -
创建一个变量,并使用你朋友的第一个和最后一个名字将新
Person
对象赋值给它。 -
现在添加第二个变量,使用另一个朋友的第一个和最后一个名字。
-
将两个朋友的姓名输出到控制台,问候语为
hello
。
方法
在类中,我们可以指定函数。这意味着我们的对象可以使用对象的自身属性开始做一些事情——例如,打印一个名字。类上的函数被称为方法。在定义这些方法时,我们不使用function
关键字。我们直接从名称开始:
class Person {
constructor(firstname, lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
greet() {
console.log("Hi there! I'm", this.firstname);
}
}
我们可以这样在Person
对象上调用greet
方法:
let p = new Person("Maaike", "van Putten");
p.greet();
它将输出:
Hi there! I'm Maaike
你可以在类上指定任意多的方法。在这个例子中,我们使用了firstname
属性。我们这样做是通过说this.property
。如果我们有一个firstname
属性值不同的个人,例如Rob
,它将打印:
Hi there! I'm Rob
就像函数一样,方法也可以接受参数并返回结果:
class Person {
constructor(firstname, lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
greet() {
console.log("Hi there!");
}
compliment(name, object) {
return "That's a wonderful " + object + ", " + name;
}
}
compliment
函数本身不输出任何内容,所以我们记录它
let compliment = p.compliment("Harry", "hat");
console.log(compliment);
输出将是:
That's a wonderful hat, Harry
在这种情况下,我们向我们的方法发送参数,因为你通常不会赞美你自己的属性(这是一个好句子,Maaike!)。然而,每当方法不需要外部输入而只需要对象的属性时,就不需要任何参数,方法可以使用其对象的属性。让我们做一个练习,然后继续使用类外部的属性。
练习 7.2
获取你朋友的完整姓名:
-
在练习 7.1中使用的
Person
类中添加一个名为fullname
的方法,当调用时返回firstname
和lastname
的连接值。 -
使用两个朋友的第一个和最后一个名字为
person1
和person2
创建值。 -
在类中使用
fullname
方法,返回一个人的或两个人的全名。
属性
属性,有时也称为字段,存储类的数据。我们已经在构造函数中创建它们时看到了一种属性:
class Person {
constructor(firstname, lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
}
在这里,Person
类从构造函数中获取两个属性:firstname
和lastname
。属性可以像我们对对象所做的那样添加或删除。这些属性可以从类外部访问,就像我们在类外部通过访问实例来记录它们时看到的那样:
let p = new Person("Maaike", "van Putten");
console.log("Hi", p.firstname);
通常,我们不希望直接提供对属性的访问。我们希望我们的类控制属性的值,有多个原因——也许我们想在属性上执行验证以确保它具有某个值。例如,想象一下想要验证年龄不低于 18 岁。我们可以通过使从类外部直接访问属性变得不可能来实现这一点。
这是如何添加外部不可访问的属性的示例。我们用#
符号作为前缀:
class Person {
#firstname;
#lastname;
constructor(firstname, lastname) {
this.#firstname = firstname;
this.#lastname = lastname;
}
}
目前,firstname
和lastname
属性不能从类外部访问。这是通过在属性前添加#
符号来实现的。如果我们尝试这样做:
let p = new Person("Maria", "Saga");
console.log(p.firstname);
我们将得到:
undefined
如果我们想要确保只能创建以"M"开头的名字的对象,我们可以稍微修改一下构造函数:
constructor(firstname, lastname) {
if(firstname.startsWith("M")){
this.#firstname = firstname;
} else {
this.#firstname = "M" + firstname;
}
this.#lastname = lastname;
}
现在当你尝试创建一个firstname
值不以"M"开头的个人时,它会在前面添加一个"M"。例如,以下名字的值是Mkay
:
let p = new Person("kay", "Moon");
这是一个非常愚蠢的验证示例。在这个阶段,在构造函数之后,我们根本不能从类外部访问它。我们只能从类内部访问它。这就是获取器和设置器发挥作用的地方。
获取器和设置器
获取器和设置器是特殊的属性,我们可以使用它们从类中获取数据,并在类上设置数据字段。获取器和设置器是计算属性。因此,它们更像属性而不是函数。我们称它们为访问器。它们看起来有点像函数,因为它们后面有()
,但它们不是!
这些访问器以get
和set
关键字开头。尽可能将字段设置为私有,并使用获取器和设置器来访问它们被认为是良好的实践。这样,属性就不能从外部设置,除非对象本身控制。这个原则被称为封装。类封装了数据,对象控制自己的字段。
下面是如何做到这一点的示例:
class Person {
#firstname;
#lastname;
constructor(firstname, lastname) {
this.#firstname = firstname;
this.#lastname = lastname;
}
get firstname() {
return this.#firstname;
}
set firstname(firstname) {
this.#firstname = firstname;
}
get lastname() {
return this.#lastname;
}
set lastname(lastname) {
this.#lastname = lastname;
}
}
获取器用于获取属性。因此,它不需要任何参数,只需返回属性即可。设置器则相反:它接受一个参数,将这个新值赋给属性,然后不返回任何内容。设置器可以包含更多的逻辑,例如,一些验证,正如我们下面将要看到的。获取器可以在对象外部使用,就像它是一个属性一样。属性不再可以从外部直接访问,但可以通过获取器获取值,通过设置器更新值。以下是如何在类实例外部使用它的示例:
let p = new Person("Maria", "Saga");
console.log(p.firstname);
这将输出:
Maria
我们创建了一个新的Person
对象,名字为Maria
,姓氏为Saga
。输出显示了名字,这仅因为我们可以使用获取器访问器。我们也可以将值设置为其他内容,因为存在设置器。以下是更新名字的示例,这样名字就不再是Maria
,而是Adnane
。
p.firstname = "Adnane";
在这个阶段,setter 中并没有发生任何特殊的事情。我们可以像在构造函数中那样进行类似的验证,如下所示:
set firstname(firstname) {
if(firstname.startsWith("M")){
this.#firstname = firstname;
} else {
this.#firstname = "M" + firstname;
}
}
这将检查firstname
是否以M
开头,如果是,它将更新值为firstname
参数的值。如果不是,它将在参数前面连接一个M
。
请注意,我们不是像函数一样访问firstname
。如果你在它后面放两个括号()
,实际上你会得到一个错误,告诉你它不是一个函数。
继承
继承是面向对象编程中的一个关键概念。它是这样的概念:类可以有子类,这些子类继承父类的属性和方法。例如,如果你在应用程序中需要各种车辆对象,你可以在一个名为Vehicle
的类中指定一些车辆的共享属性和方法。然后,基于这个Vehicle
类,你可以继续创建具体的子类,例如boat
、car
、bicycle
和motorcycle
。
这可能是Vehicle
类的一个非常简单的版本:
class Vehicle {
constructor(color, currentSpeed, maxSpeed) {
this.color = color;
this.currentSpeed = currentSpeed;
this.maxSpeed = maxSpeed;
}
move() {
console.log("moving at", this.currentSpeed);
}
accelerate(amount) {
this.currentSpeed += amount;
}
}
在我们的Vehicle
类中,有两种方法:move
和accelerate
。这可以是一个使用extends
关键字从该类继承的Motorcyle
类:
class Motorcycle extends Vehicle {
constructor(color, currentSpeed, maxSpeed, fuel) {
super(color, currentSpeed, maxSpeed);
this.fuel = fuel;
}
doWheelie() {
console.log("Driving on one wheel!");
}
}
使用extends
关键字,我们指定一个类是另一个类的子类。在这种情况下,Motorcycle
是Vehicle
的子类。这意味着我们将在我们的Motorcycle
类中访问Vehicle
的属性和方法。我们添加了一个特殊的doWheelie()
方法。这不是可以添加到Vehicle
类中的东西,因为这是一种特定于某些车辆的行为。
构造函数中的super
关键字是在调用父类的构造函数,在这个例子中是Vehicle
构造函数。这确保了父类的字段也被设置,并且方法可用,而无需做任何其他事情:它们是自动继承的。调用super()
是必须的,当你在一个继承自另一个类的类中时,否则你会得到一个ReferenceError
。
因为我们在Motorcycle
中可以访问Vehicle
的字段,所以这会起作用:
let motor = new Motorcycle("Black", 0, 250, "gasoline");
console.log(motor.color);
motor.accelerate(50);
motor.move();
这就是它将输出的内容:
Black
moving at 50
我们不能在我们的Vehicle
类中访问任何Motorcycle
特定的属性或方法。这是因为并不是所有的车辆都是摩托车,所以我们不能确定我们会有来自子类的属性或方法。
目前,我们没有在这里使用任何 getter 和 setter,但我们当然可以。如果父类中有 getter 和 setter,它们也会被子类继承。这样我们就可以影响哪些属性可以被获取和修改(以及如何修改)在我们的类外部。这通常是一个好的实践。
原型
原型是 JavaScript 中使对象成为可能的一种机制。在创建类时没有指定任何内容时,对象会从 Object.prototype
原型继承。这是一个相当复杂的内置 JavaScript 类,我们可以使用它。我们不需要查看它在 JavaScript 中的实现方式,因为我们把它看作是始终位于继承树顶部的基对象,因此始终存在于我们的对象中。
所有类都有一个名为 "prototype" 的 prototype
属性,我们可以这样访问它:
ClassName.prototype
让我们通过 prototype
属性向一个类添加函数的例子。为了做到这一点,我们将使用这个 Person
类:
class Person {
constructor(firstname, lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
greet() {
console.log("Hi there!");
}
}
如此,这是如何使用 prototype
向此类添加函数的方法:
Person.prototype.introduce = function () {
console.log("Hi, I'm", this.firstname);
};
prototype
是一个包含对象所有属性和方法属性的属性。因此,向 prototype
添加函数就是向类添加函数。你可以使用 prototype
向对象添加属性或方法,就像我们在上面的例子中用 introduce
函数所做的那样。你也可以为属性做同样的事情:
Person.prototype.favoriteColor = "green";
然后你可以从 Person
的实例中调用它们:
let p = new Person("Maria", "Saga");
console.log(p.favoriteColor);
p.introduce();
它将输出:
green
Hi, I'm Maria
就好像你定义了一个类,其中有一个喜欢的颜色并持有默认值,还有一个函数 introduce
。它们已经被添加到类中,并且对所有实例和未来的实例都是可用的。
因此,通过 prototype
定义的属性和方法实际上就像它们是在类中定义的一样。这意味着对于特定实例的重写不会影响所有实例。例如,如果我们有一个第二个 Person
对象,这个人可以重写 favoriteColor
值,这不会改变 firstname
为 Maria
的对象中的值。
当你控制类代码并希望永久更改它时,你不应该使用这种方法。在这种情况下,只需更改类即可。然而,你可以像这样扩展现有对象,甚至有条件地扩展现有对象。重要的是要知道,JavaScript 内置对象有原型,并从 Object.prototype
继承。但是,请确保不要修改这个原型,因为它会影响我们的 JavaScript 的工作方式。
练习 7.3
创建一个包含不同动物物种及其叫声的属性的类,并创建两个(或更多)动物:
-
创建一个打印给定动物及其叫声的方法。
-
为动物添加一个具有另一个动作的原型。
-
将整个动物对象输出到控制台。
章节项目
员工跟踪应用
创建一个类来跟踪公司的员工:
-
在构造函数中使用名字、姓氏和工作年数作为值。
-
创建两个或更多具有他们的名字、姓氏和在公司工作年数的人。将这些人添加到数组中。
-
设置一个原型来返回人员的姓名细节以及他们在公司工作的时间。
-
遍历数组的元素,将结果输出到控制台,并添加一些文本使输出成为一个完整的句子。
菜单项价格计算器
创建一个类,它将允许你计算出多个项目的组合价格,并与它交互以计算出不同订单的总成本。
-
创建一个包含两个菜单项价格的私有字段声明。
-
在类中使用构造函数来获取参数值(购买每种物品的数量)。
-
创建一个方法来根据用户选择的每种物品的数量计算并返回总成本。
-
使用 getter 属性来获取计算方法输出的值。
-
创建两个或三个具有不同菜单选择组合的对象,并在控制台输出总成本。
自我检查测验
-
用于创建类的关键字是什么?
-
你会如何设置一个包含
first
和last
作为初始属性的类来存储人的姓名? -
将一种事物获得另一种事物的属性和行为的概念称为什么?
-
以下关于
constructor
方法的哪些说法是正确的?-
当创建新对象时自动执行。
-
应该只在之后添加。
-
它必须包含
constructor
关键字。 -
它用于初始化对象属性。
-
当你有多重值时可以使用它。
-
-
修复以下代码,以便原型将
Person
的姓名输出到控制台。Person
原型的正确语法是什么?function Person(first,last) { this.first = first; this.last = last; } // What should go here: A, B, or C? const friend1 = new Person("Laurence", "Svekis"); console.log(friend1.getName());
A)
Person.prototype.getName = (first,last) { return this.first + " " + this.last; };
B)
Person.prototype.getName = function getName() { return this.first + " " + this.last; };
C)
Person.prototype = function getName() { return this.first + " " + this.last; };
概述
在本章中,我们向您介绍了面向对象编程(OOP)的概念。这意味着我们以这种方式组织代码,使得对象是逻辑中的核心角色。类是对象的蓝图。我们可以为对象制作一个模板,并通过使用new
关键字轻松创建实例。
我们看到类可以通过使用extends
关键字相互继承。从另一个类继承的类必须使用super()
调用此类的构造函数,然后自动访问所有父类的属性和方法。这对于可重用和高度可维护的代码来说是非常好的。
最后,我们遇到了原型。这是 JavaScript 的内置概念,使得类成为可能。通过使用prototype
向类添加属性和方法,我们可以修改该类的蓝图。
在下一章中,我们将考虑一些 JavaScript 的内置方法,这些方法可以用来操纵和增加代码的复杂性!
第八章:内置 JavaScript 方法
我们刚刚覆盖了 JavaScript 中的大多数基本构建块。现在,是时候看看一些将使您的生活更轻松的强大内置方法了,这些方法我们还没有看到。内置方法是我们在使用 JavaScript 时直接获得的功能。我们可以使用这些方法而无需先编写代码。这是我们已经做了很多的事情,例如console.log()
和prompt()
。
许多内置方法也属于内置类。这些类及其方法可以随时使用,因为 JavaScript 已经定义了它们。这些类是为了我们的方便而存在的,因为它们是非常常见的需求,例如Date
、Array
和Object
类。
利用已经内置到 JavaScript 中的功能可以提高代码的有效性,节省时间,并符合开发解决方案的各种最佳实践。我们将讨论一些此类函数的常见用途,例如操作文本、数学计算、处理日期和时间值、交互以及支持健壮的代码。以下是本章涵盖的主题:
-
全局 JavaScript 方法
-
字符串方法
-
数学方法
-
日期方法
-
数组方法
-
数字方法
注意:练习、项目和自我检查测验的答案可以在附录中找到。
内置 JavaScript 方法简介
我们已经看到了许多内置的 JavaScript 方法。任何我们没有自己定义的方法都是内置方法。一些例子包括console.log()
、Math.random()
、prompt()
等等——例如,考虑数组上的方法。方法和函数之间的区别在于,函数可以在脚本中的任何地方定义,而方法是在类内部定义的。因此,方法基本上是类和实例上的函数。
方法通常也可以链式调用;这仅适用于返回结果的方法。下一个方法将在结果上执行。例如:
let s = "Hello";
console.log(
s.concat(" there!")
.toUpperCase()
.replace("THERE", "you")
.concat(" You're amazing!")
);
我们创建了一个变量s
,并在第一行将其中的Hello
存储进去。然后我们想要记录一些内容。这段代码被分成不同的行以提高可读性,但实际上它是一个语句。我们首先在我们的s
变量上执行一个concat()
方法,该方法将字符串附加到我们的字符串上。因此,在第一次操作之后,值变为Hello there!
。然后我们使用下一个方法将其转换为大写。此时,值变为HELLO THERE!
。然后我们继续将THERE
替换为you
。之后,值变为HELLO you!
。然后我们再次向其附加一个字符串,最终将值记录下来:
HELLO you! You're amazing!
在这个例子中,我们需要记录或存储输出,因为仅调用字符串上的方法并不会更新原始字符串值。
全局方法
全局 JavaScript 方法可以在不引用它们所属的内置对象的情况下使用。这意味着我们可以直接使用方法名,就像它是一个在我们当前作用域内定义的函数一样,前面不需要“对象”。例如,而不是写:
let x = 7;
console.log(Number.isNaN(x));
你也可以这样写:
console.log(isNaN(x));
因此,Number
可以省略,因为isNaN
在不需要引用它所属的类(在这种情况下是Number
类)的情况下被全局提供。在这种情况下,这两个console.log
语句都将记录false
(它们正在做完全相同的事情),因为当它不是数字时,isNaN
返回true
。而7
是一个数字,所以它将记录false
。
JavaScript 被构建为可以直接使用这些方法,因此为了实现这一点,在表面之下进行了一些魔法操作。JavaScript 的创造者选择了他们认为最常用的方法。因此,为什么一些方法是作为全局方法提供的,而其他方法则不是,可能看起来有些随意。这只是某些非常聪明的开发者在一个特定时间点的选择。
我们将在下面讨论最常见的全局方法。我们首先从解码和编码 URI(包括转义和未转义的)开始,然后解析数字,最后进行评估。
URI 的解码和编码
有时你需要对字符串进行编码或解码。编码简单地说就是将一种形状转换为另一种形状。在这种情况下,我们将处理百分编码,也称为 URL 编码。在我们开始之前,可能会有一些关于 URI 和 URL 含义的混淆。URI(统一资源标识符)是某种资源的标识符。URL(统一资源定位符)是 URI 的一个子类别,它不仅是一个标识符,而且还包含有关如何访问它的信息(位置)。
让我们讨论如何对这些 URI(以及 URL,因为它们是子集)进行编码和解码。你需要这样做的一个例子是在表单中使用get
方法通过 URL 发送变量。你通过 URL 发送的这些变量被称为查询参数。
如果某个内容包含空格,这将进行解码,因为你的 URL 中不能使用空格。它们将被转换为%20
。URL 可能看起来像这样:
www.example.com/submit?name=maaike%20van%20putten&love=coding
所有字符都可以转换为以%
开头的格式。然而,在大多数情况下,这并不是必要的。URI 可以包含一定数量的字母数字字符。特殊字符需要编码。一个例子,在编码之前是:
https://www.example.com/submit?name=maaike van putten
编码后的相同 URL 是:
https://www.example.com/submit?name=maaike%20van%20putten
有两种编码和解码方法。我们将在下面讨论它们及其用例。由于 URI 不能包含空格,因此使用包含空格的变量时,使用这些方法是至关重要的。
decodeUri() 和 encodeUri()
decodeUri()
和 encodeUri()
实际上并不是真正的编码和解码,它们更多的是修复损坏的 URI。就像之前的例子中的空格一样。这对方法非常擅长修复损坏的 URI 并将它们解码回字符串。这里你可以看到它们的作用:
let uri = "https://www.example.com/submit?name=maaike van putten";
let encoded_uri = encodeURI(uri);
console.log("Encoded:", encoded_uri);
let decoded_uri = decodeURI(encoded_uri);
console.log("Decoded:", decoded_uri);
下面是输出结果:
Encoded: https://www.example.com/submit?name=maaike%20van%20putten
Decoded: https://www.example.com/submit?name=maaike van putten
如你所见,它已经替换了编码版本中的空格,并在解码版本中再次删除它们。所有其他字符都保持不变——这个编码和解码不关注特殊字符,因此将它们留在 URI 中。冒号、问号、等号、斜杠和和号是可以预期的。
这对于修复损坏的 URI 非常棒,但实际上,当你需要编码包含这些字符的字符串时(/
,
?
:
@
&
=
+
$
#
),实际上是无用的。这些可以用作 URI 的一部分,因此被跳过。这就是下一个两个内置方法派上用场的地方。
decodeUriComponent() 和 encodeUriComponent()
因此,decodeURI()
和 encodeURI()
方法可以非常有助于修复损坏的 URI,但当你只想编码或解码包含特殊字符(如 =
或 &
)的字符串时,它们就毫无用处。以下是一个例子:
https://www.example.com/submit?name=this&that=some thing&code=love
奇怪的价值,我们可以同意这一点,但它将展示我们的问题。使用 encodeURI 对此进行编码后,我们将得到:
https://www.example.com/submit?name=this&that=some%20thing&code=love
根据 URI 标准,这里实际上有 3 个变量:
-
name
(值是this
) -
that
(值是some
thing
) -
code
(值是love
)
虽然我们打算发送一个变量,name
,其值为 this&that=some thing&code=love
。
在这种情况下,你需要 decodeUriComponent()
和 encodeUriComponent()
,因为变量部分中的 =
和 &
也需要编码。目前,这种情况并不存在,它实际上会在解释查询参数(?
后的变量)时引起问题。我们只想发送一个参数:name
。但结果我们发送了三个。
让我们看看另一个例子。这是前一个章节中示例使用组件编码的结果:
let uri = "https://www.example.com/submit?name=maaike van putten";
let encoded_uri = encodeURIComponent(uri);
console.log("Encoded:", encoded_uri);
let decoded_uri = decodeURIComponent(encoded_uri);
console.log("Decoded:", decoded_uri);
最终的输出如下:
Encoded: https%3A%2F%2Fwww.example.com%2Fsubmit%3Fname%3Dmaaike%20van%20putten
Decoded: https://www.example.com/submit?name=maaike van putten
显然,你不想你的 URI 是这样的,但组件方法对于编码,例如 URL 变量很有用。如果 URL 变量包含特殊字符,如 =
和 &
,这些字符如果没有被编码,就会改变意义并破坏 URI。
使用 escape() 和 unescape() 进行编码
这些仍然是全局方法,可用于执行类似于编码(escape)和解码(unescape)的操作。这两种方法强烈建议不要使用,并且它们实际上可能从未来的 JavaScript 版本中消失,或者浏览器可能出于良好原因不支持它们。
练习 8.1
将字符串How's%20it%20going%3F
的decodeURIComponent()
输出到控制台。也将字符串How's it going?
编码并输出到控制台。创建一个网络 URL 并编码 URI:
-
将字符串作为变量添加到 JavaScript 代码中
-
使用
encodeURIComponent()
和decodeURIComponent()
将结果输出到控制台 -
创建一个带有请求参数
http://www.basescripts.com?=Hello World";
的网络 URI -
将网络 URI 编码并输出到控制台
解析数字
解析字符串到数字有不同的方法。在许多情况下,你将不得不将字符串转换为数字,例如从 HTML 网页中读取输入框。你不能用字符串进行计算,但可以用数字。根据你确切需要做什么,你可能需要这些方法中的任何一个。
使用parseInt()
创建整数
使用parseInt()
方法将字符串转换为整数。这个方法是Number
类的一部分,但它全局可用,你可以不用在前面加上Number
来使用它。这里你可以看到它是如何工作的:
let str_int = "6";
let int_int = parseInt(str_int);
console.log("Type of ", int_int, "is", typeof int_int);
我们从一个包含6
的字符串开始。然后我们使用parseInt
方法将这个字符串转换为整数,当我们记录结果时,我们将在控制台得到:
Type of 6 is number
你可以看到类型已经从string
变成了number
。在这个时候,你可能想知道如果parseInt()
尝试解析其他类型的数字,比如浮点数的字符串版本或二进制数字,会发生什么。你认为当我们这样做时会发生什么?
let str_float = "7.6";
let int_float = parseInt(str_float);
console.log("Type of", int_float, "is", typeof int_float);
let str_binary = "0b101";
let int_binary = parseInt(str_binary);
console.log("Type of", int_binary, "is", typeof int_binary);
这将记录:
Type of 7 is number
Type of 0 is number
你能理解这里的逻辑吗?首先,JavaScript 不喜欢通过崩溃或使用错误作为退出方式,所以它正在尽力让它尽可能正常工作。parseInt()
方法在遇到非数字字符时会停止解析。这是指定的行为,你在使用parseInt()
时需要记住这一点。在第一种情况下,它一遇到点就停止解析,所以结果是7
。在二进制数字的情况下,它一遇到b
就停止解析,结果是0
。到现在你可能已经能猜出这是做什么的:
let str_nan = "hello!";
let int_nan = parseInt(str_nan);
console.log("Type of", int_nan, "is", typeof int_nan);
由于第一个字符是非数字的,JavaScript 将这个字符串转换为NaN
。这是你将在控制台得到的结果:
Type of NaN is number
所以parseInt()
可能有点古怪,但它非常有价值。在现实世界中,它被大量用于通过网页结合用户输入和计算。
使用parseFloat()
创建浮点数
同样,我们可以使用parseFloat()
将字符串解析为浮点数。它的工作方式完全相同,但它也可以理解小数,并且它不会一遇到第一个点就停止解析:
let str_float = "7.6";
let float_float = parseFloat(str_float);
console.log("Type of", float_float, "is", typeof float_float);
这将记录:
Type of 7.6 is number
使用parseInt()
,这个值变成了7
,因为它一遇到非数字字符就会停止解析。然而,parseFloat()
可以处理数字中的一个点,并且将之后的数字解释为小数。你能猜出当它遇到第二个点时会发生什么吗?
let str_version_nr = "2.3.4";
let float_version_nr = parseFloat(str_version_nr);
console.log("Type of", float_version_nr, "is", typeof float_version_nr);
这将记录:
Type of 2.3 is number
策略与 parseInt()
函数类似。一旦它找到一个无法解释的字符,在这个例子中是第二个点,它将停止解析并只返回到目前为止的结果。然后要注意的另一件事是,它不会在整数后面添加 .0
,所以 6
不会变成 6.0
。这个例子:
let str_int = "6";
let float_int = parseFloat(str_int);
console.log("Type of", float_int, "is", typeof float_int);
将记录:
Type of 6 is number
最后,二进制数和字符串的行为是相同的。它会在遇到无法解释的字符时停止解析:
let str_binary = "0b101";
let float_binary = parseFloat(str_binary);
console.log("Type of", float_binary, "is", typeof float_binary);
let str_nan = "hello!";
let float_nan = parseFloat(str_nan);
console.log("Type of", float_nan, "is", typeof float_nan);
这将记录:
Type of 0 is number
Type of NaN is number
你会在需要十进制数时使用 parseFloat()
。然而,它不能与二进制、十六进制和八进制值一起工作,所以当你真正需要处理这些值或整数时,你必须使用 parseInt()
。
使用 eval() 执行 JavaScript
这个全局方法将参数作为 JavaScript 语句执行。这意味着它将执行插入其中的任何 JavaScript,就像那个 JavaScript 是直接在那个地方写的一样,而不是 eval()
。这对于处理注入的 JavaScript 可能很方便,但注入的代码伴随着巨大的风险。我们稍后会处理这些风险;让我们首先探索一个例子。这是一个非常棒的网站:
<html>
<body>
<input onchange="go(this)"></input>
<script>
function go(e) {
eval(e.value);
}
</script>
</body>
</html>
这是一个带有输入框的基本 HTML 网页。
你将在 第九章,文档对象模型 中了解更多关于 HTML 的内容。
你在输入框中插入的内容将被执行。如果我们把这个写在输入框里:
document.body.style.backgroundColor = "pink";
网站的背景会变成粉色。这看起来很有趣,对吧?然而,我们无法强调使用 eval()
时应该多么小心。根据许多开发者的说法,他们甚至可能把它称为 邪恶的。你能解释为什么这可能是吗?
答案是安全性!是的,这可能是大多数情况下在安全性方面最糟糕的事情。你将执行外部代码。这段代码可能是恶意的。这是一种支持代码注入的方法。备受尊敬的 OWASP (开放网络应用安全项目) 基金会每三年发布一次安全威胁的前 10 名。代码注入自他们第一次发布的前 10 名以来一直存在,并且现在它仍然是 OWASP 前 10 名安全威胁之一。在服务器端运行它可能导致你的服务器崩溃,你的网站关闭,或者更糟。几乎总是有比使用 eval()
更好的解决方案。除了安全风险之外,它在性能方面也很糟糕。所以仅仅因为这个原因,你可能想要避免使用它。
好吧,关于这个的最后一点。如果你知道你在做什么,你可能想在非常具体的情况下使用它。尽管它被认为是“邪恶的”,但它拥有很大的力量。在某些情况下使用它是可以接受的,例如当你创建模板引擎、你自己的解释器和所有其他 JavaScript 核心工具时。只是要小心危险,并仔细控制对这个方法的访问。还有一个最后的技巧,当你觉得你真的必须使用 eval 时,在网上快速搜索一下。很可能你会找到一个更好的方法。
数组方法
我们已经看到了数组——它们可以包含多个项目。我们也看到了数组上的一些内置方法,如 shift()
和 push()
。让我们在接下来的几节中看看更多。
对每个项目执行特定操作
我们之所以从这种方法开始,是有原因的。您可能此时正在考虑循环,但有一个内置的方法可以用来为数组中的每个元素执行一个函数。这就是 forEach()
方法。我们在 第六章,函数 中简要提到了它,但让我们更详细地考虑它。它接受需要为每个元素执行的函数作为输入。这里您可以看到一个示例:
let arr = ["grapefruit", 4, "hello", 5.6, true];
function printStuff(element, index) {
console.log("Printing stuff:", element, "on array position:", index);
}
arr.forEach(printStuff);
这段代码将写入控制台:
Printing stuff: grapefruit on array position: 0
Printing stuff: 4 on array position: 1
Printing stuff: hello on array position: 2
Printing stuff: 5.6 on array position: 3
Printing stuff: true on array position: 4
如您所见,它为数组中的每个元素调用了 printStuff()
函数。我们还可以使用索引,它是第二个参数。我们不需要控制循环的流程,我们也不能在某个点上卡住。我们只需要指定需要为每个元素执行哪个函数。这个元素将被输入到这个函数中。这被广泛使用,尤其是在更函数式编程风格的场景中,其中许多方法被链式使用,例如,处理数据。
过滤数组
我们可以使用数组上的内置 filter()
方法来改变数组中的值。filter()
方法接受一个函数作为参数,这个函数应该返回一个布尔值。如果布尔值为 true
,则元素将最终出现在过滤后的数组中。如果布尔值为 false
,则元素将被排除在外。您可以看到它是如何工作的:
let arr = ["squirrel", 5, "Tjed", new Date(), true];
function checkString(element, index) {
return typeof element === "string";
}
let filterArr = arr.filter(checkString);
console.log(filterArr);
这将在控制台输出:
[ 'squirrel', 'Tjed' ]
重要的是要意识到原始数组没有改变,filter()
方法返回一个包含通过过滤器的元素的新数组。我们在这里将这个新数组捕获到变量 filterArr
中。
检查所有元素的条件
您可以使用 every()
方法来检查数组中所有元素是否都满足某个条件。如果是这样,every()
方法将返回 true
,否则将返回 false
。我们在这里使用前一个例子中的 checkString()
函数和数组:
console.log(arr.every(checkString));
这将记录 false
,因为数组中并非所有元素都是 string
类型。
用数组的另一部分替换数组的一部分
copyWithin()
方法可以用来用数组的另一部分替换数组的一部分。在第一个例子中,我们指定了 3 个参数。第一个参数是目标位置,值将被复制到该位置。第二个参数是要复制到目标位置的起始位置,最后一个参数是要复制到目标位置的序列的结束位置;这个最后一个索引不包括在内。这里我们只是用位置 3 的值覆盖位置 0:
arr = ["grapefruit", 4, "hello", 5.6, true];
arr.copyWithin(0, 3, 4);
arr
变成了:
[ 5.6, 4, 'hello', 5.6, true ]
如果我们指定长度为 2
的范围,则从起始位置开始的前两个元素将被覆盖:
arr = ["grapefruit", 4, "hello", 5.6, true];
arr.copyWithin(0, 3, 5);
现在 arr
变成了:
[ 5.6, true, 'hello', 5.6, true ]
我们也可以完全不指定结束;它将取到字符串的末尾:
let arr = ["grapefruit", 4, "hello", 5.6, true, false];
arr.copyWithin(0, 3);
console.log(arr);
这将记录:
[ 5.6, true, false, 5.6, true, false ]
重要的是要记住,这个函数会改变原始数组的内容,但永远不会改变原始数组的长度。
映射数组的值
有时候你需要改变数组中的所有值。使用数组的map()
方法,你可以做到这一点。这个方法将返回一个包含所有新值的新数组。你将必须说明如何创建这些新值。这可以通过箭头函数来完成。它将为数组中的每个元素执行箭头函数,例如:
let arr = [1, 2, 3, 4];
let mapped_arr = arr.map(x => x + 1);
console.log(mapped_arr);
这是新映射数组在控制台输出的样子:
[ 2, 3, 4, 5 ]
使用箭头函数,map()
方法创建了一个新数组,其中每个原始数组值都增加了 1。
在数组中查找最后一个出现的位置
我们可以使用indexOf()
找到出现,就像我们已经看到的那样。要找到最后一个出现,我们可以使用数组上的lastIndexOf()
方法,就像我们对string
做的那样。
如果它能找到具有该值的最后一个元素,它将返回该元素的索引:
let bb = ["so", "bye", "bye", "love"];
console.log(bb.lastIndexOf("bye"));
这将记录2
,因为索引 2 持有最后一个bye
变量。当你请求一个不存在的东西的最后一个索引时,你认为你会得到什么?
let bb = ["so", "bye", "bye", "love"];
console.log(bb.lastIndexOf("hi"));
对的(希望如此)!它是-1
。
练习 8.2
使用filter()
和indexOf()
从数组中删除重复项。起始数组是:
["Laurence", "Mike", "Larry", "Kim", "Joanne", "Laurence", "Mike", "Laurence", "Mike", "Laurence", "Mike"]
使用数组filter()
方法,这将创建一个新数组,使用通过函数实现的测试条件通过的元素。最终结果将是:
[ 'Laurence', 'Mike', 'Larry', 'Kim', 'Joanne' ]
采取以下步骤:
-
创建一个包含人名的数组。确保你包含重复的名字。这个练习将删除重复的名字。
-
使用
filter()
方法,将数组中每个项目的结果作为匿名函数内的参数。使用值、索引和数组参数,返回过滤后的结果。你可以暂时将返回值设置为true
,因为这将使用原始数组中的所有结果构建新数组。 -
在函数内添加一个
console.log
调用,该调用将输出数组中当前项目的索引值。同时添加值,以便你可以看到具有当前索引号的项值以及数组索引值的第一匹配结果。 -
使用
indexOf()
当前值返回项目索引值,并应用条件检查是否与原始索引值匹配。这个条件只会在第一个结果上为真,所以所有后续的重复项都将为假,并且不会添加到新数组中。false
不会将值返回到新数组中。由于indexOf()
只获取数组中的第一个匹配项,所以重复项都将为假。 -
将新的唯一值数组输出到控制台。
练习 8.3
使用数组map()
方法更新数组的内 容。采取以下步骤:
-
创建一个数字数组。
-
使用数组
map
方法和一个匿名函数,返回一个更新后的数组,将数组中的所有数字乘以 2。将结果输出到控制台。 -
作为另一种方法,使用箭头函数格式,通过一行代码使用数组的
map()
方法将数组的每个元素乘以 2。 -
将结果记录到控制台。
字符串方法
我们已经处理过字符串了,你很可能已经遇到了一些字符串方法。有一些我们没有特别说明,我们将在本节中讨论它们。
字符串的合并
当你想连接字符串时,可以使用 concat()
方法。这不会改变原始字符串(们);它返回作为字符串的合并结果。你必须将结果捕获到新变量中,否则它将丢失:
let s1 = "Hello ";
let s2 = "JavaScript";
let result = s1.concat(s2);
console.log(result);
这将记录:
Hello JavaScript
将字符串转换为数组
使用 split()
方法我们可以将字符串转换为数组。同样,我们还需要捕获结果;它不会改变原始字符串。让我们使用包含 Hello JavaScript
的上一个结果。我们必须告诉 split
方法它应该在哪个字符串上分割。每次它遇到该字符串时,它都会创建一个新的数组项:
let result = "Hello JavaScript";
let arr_result = result.split(" ");
console.log(arr_result);
这将记录:
[ 'Hello', 'JavaScript' ]
如你所见,它创建了一个由空格分隔的所有元素的数组。我们可以按任何字符分割,例如逗号:
let favoriteFruits = "strawberry,watermelon,grapefruit";
let arr_fruits = favoriteFruits.split(",");
console.log(arr_fruits);
这将记录:
[ 'strawberry', 'watermelon', 'grapefruit' ]
现在已创建一个包含 3 个元素的数组。你可以根据任何内容进行分割,而你正在分割的字符串将不包括在结果中。
将数组转换为字符串
使用 join()
方法可以将数组转换为字符串。以下是一个基本示例:
let letters = ["a", "b", "c"];
let x = letters.join();
console.log(x);
这将记录:
a,b,c
x
的类型是 string
。如果你想用其他东西而不是逗号,你可以指定它,就像这样:
let letters = ["a", "b", "c"];
let x = letters.join('-');
console.log(x);
这将使用 –
而不是逗号。这是结果:
a-b-c
这可以很好地与我们在上一节中介绍的 split()
方法结合使用,它执行相反的操作,将字符串转换为数组。
处理索引和位置
能够找出字符串中某个子字符串的索引非常有用。例如,当你需要通过日志文件的用户输入搜索某个特定单词并从该索引开始创建子字符串时。以下是如何找到字符串索引的示例。indexOf()
方法返回子字符串的第一个字符的索引,一个单一的数字:
let poem = "Roses are red, violets are blue, if I can do JS, then you can too!";
let index_re = poem.indexOf("re");
console.log(index_re);
这将在控制台记录 7
,因为 re
在 are
中的第一次出现是在索引 7
。当它找不到索引时,它将返回 -1
,就像这个例子一样:
let indexNotFound = poem.indexOf("python");
console.log(indexNotFound);
它记录 -1
来指示我们正在搜索的字符串在目标字符串中不存在。通常你会在处理结果之前编写一个 if
检查来查看它是否为 -1
。例如:
if(poem.indexOf("python") != -1) {
// do stuff
}
在字符串中搜索特定子字符串的另一种方法是使用 search()
方法:
let searchStr = "When I see my fellow, I say hello";
let pos = searchStr.search("lo");
console.log(pos);
这将输出 17
,因为这是 lo
在 fellow
中的索引。与 indexOf()
类似,如果找不到,它将返回 -1
。这个例子就是这样:
let notFound = searchStr.search("JavaScript");
console.log(notFound);
search()
方法可以接受正则表达式格式作为输入,而 indexOf()
只接受一个字符串。indexOf()
比搜索方法更快,所以如果你只需要查找一个字符串,请使用 indexOf()
。如果你需要查找字符串模式,你必须使用 search()
方法。
正则表达式是定义字符串模式的特殊语法,你可以用它来替换所有出现,但我们将在 第十二章,中级 JavaScript 中处理它。
接下来,indexOf()
方法返回第一次出现的索引,但同样,我们还有一个 lastIndexOf()
方法。它返回参数字符串最后一次出现的位置。如果找不到,它返回 -1
。以下是一个示例:
let lastIndex_re = poem.lastIndexOf("re");
console.log(lastIndex_re);
这返回 24
;这是 re
在我们的诗中最后一次出现。它是第二个 are
。
有时你可能需要做相反的操作;不是寻找字符串出现的索引,而是想知道某个索引位置上的字符是什么。这就是 charAt(index)
方法派上用场的地方,其中指定的索引位置作为参数:
let pos1 = poem.charAt(10);
console.log(pos1);
这是在记录 r
,因为索引 10 的字符是 red
中的 r
。如果你询问的索引位置超出了字符串的范围,它将返回一个空字符串,就像在这个例子中发生的那样:
let pos2 = poem.charAt(1000);
console.log(pos2);
这将在屏幕上输出一个空行,如果你询问 pos2
的类型,它将返回 string
。
创建子字符串
使用 slice(start, end)
方法我们可以创建子字符串。这不会改变原始字符串,而是返回一个新的包含子字符串的字符串。它接受两个参数,第一个是开始索引的位置,第二个是结束索引。如果你省略第二个索引,它将从开始位置继续到字符串的末尾。结束索引不包括在子字符串中。以下是一个示例:
let str = "Create a substring";
let substr1 = str.slice(5);
let substr2 = str.slice(0,3);
console.log("1:", substr1);
console.log("2:", substr2);
这将输出:
1: e a substring
2: Cre
第一个只有一个参数,所以它从索引 5(包含一个 e
)开始,并从那里获取字符串的其余部分。第二个有两个参数,0
和 3
。C
在索引 0,a
在索引 3。由于最后一个索引不包括在子字符串中,它只会返回 Cre
。
替换字符串的一部分
如果你需要替换字符串的一部分,你可以使用 replace(old, new)
方法。它接受两个参数,一个是要在字符串中查找的字符串,一个是要替换旧值的新的值。以下是一个示例:
let hi = "Hi buddy";
let new_hi = hi.replace("buddy", "Pascal");
console.log(new_hi);
这将在控制台输出 Hi Pascal
。如果你没有捕获结果,它就会消失,因为原始字符串不会改变。如果你要替换的字符串在原始字符串中不存在,替换不会发生,原始字符串将被返回:
let new_hi2 = hi.replace("not there", "never there");
console.log(new_hi2);
这记录了 Hi buddy
。
最后一点需要注意的是,默认情况下它只会改变第一次出现的位置。所以这个例子只会替换新字符串中的第一个 hello
:
let s3 = "hello hello";
let new_s3 = s3.replace("hello", "oh");
console.log(new_s3);
这会记录 oh hello
。如果我们想替换所有出现的位置,我们可以使用 replaceAll()
方法。这将用指定的新字符串替换所有出现的位置,如下所示:
let s3 = "hello hello";
let new_s3 = s3.replaceAll("hello", "oh");
console.log(new_s3);
这会记录 oh oh
。
大写和小写
我们可以使用 string
上的内置方法 toUpperCase()
和 toLowerCase()
来改变字符串的字母。再次强调,这并不会改变原始字符串,所以我们需要捕获结果:
let low_bye = "bye!";
let up_bye = low_bye.toUpperCase();
console.log(up_bye);
这会记录:
BYE!
它将所有字母转换为大写。我们可以使用 toLowerCase()
来做相反的操作:
let caps = "HI HOW ARE YOU?";
let fixed_caps = caps.toLowerCase();
console.log(fixed_caps);
这会记录:
hi how are you?
让我们让它更复杂一些,比如说我们想要句子的第一个字母大写。我们可以通过结合我们现在已经看到的一些方法来实现这一点:
let caps = "HI HOW ARE YOU?";
let fixed_caps = caps.toLowerCase();
let first_capital = fixed_caps.charAt(0).toUpperCase().concat(fixed_caps.slice(1));
console.log(first_capital);
我们在这里正在链式调用方法;我们首先使用 charAt(0)
获取 fixed_caps
的第一个字符,然后通过调用 toUpperCase()
来将其转换为大写。然后我们需要字符串的其余部分,我们可以通过连接 slice(1)
来获取它。
字符串的开始和结束
有时候你可能会想检查一个字符串的开始或结束部分。你已经猜到了,string
上有内置方法可以做到这一点。我们可以想象这一章很难理解,所以这里有一点点鼓励,同时也是一个例子:
let encouragement = "You are doing great, keep up the good work!";
let bool_start = encouragement.startsWith("You");
console.log(bool_start);
这将在控制台输出 true
,因为句子以 You
开头。请注意,这是大小写敏感的。所以下面的例子将输出 false
:
let bool_start2 = encouragement.startsWith("you");
console.log(bool_start2);
如果你不在乎大小写,你可以在之前讨论的 toLowerCase()
方法中使用它,这样它就不会考虑大小写。
let bool_start3 = encouragement.toLowerCase().startsWith("you");
console.log(bool_start3);
我们现在将字符串转换为小写,这样我们就知道我们在这里只处理小写字母。然而,这里的一个重要注意事项是,这将对大字符串的性能产生影响。
再次强调,一个更高效的替代方案是使用正则表达式。对 第十二章,中级 JavaScript 感到兴奋了吗?
为了结束这一节,我们可以对检查字符串是否以特定字符串结束做同样的事情。你可以在这里看到它的实际应用:
let bool_end = encouragement.endsWith("Something else");
console.log(bool_end);
由于它不以 Something else
结尾,所以它将返回 false
。
练习 8.4
使用字符串操作,创建一个函数,该函数将返回一个字符串,其中所有单词的首字母大写,其余字母小写。你应该将句子 thIs will be capiTalized for each word
转换为 This Will Be Capitalized For Each Word
:
-
创建一个包含不同大小写字母的单词的字符串,包括大写和小写的单词混合。
-
创建一个函数,该函数接受一个字符串作为参数,这是我们将会操作的值。
-
在函数中首先将所有内容转换为小写字母。
-
创建一个空数组,用于存储当我们将单词大写时它们的值。
-
使用
split()
方法将短语转换为单词数组。 -
遍历新数组中的每个单词,以便你可以独立选择每个单词。你可以使用
forEach()
来做这个。 -
使用
slice()
从每个单词中隔离第一个字母,然后将其转换为大写。再次使用slice()
,获取不包括第一个字母的单词剩余部分。然后将这两个部分连接起来,形成现在已大写的单词。 -
将新的大写单词添加到您创建的空白数组中。循环结束时,你应该有一个包含所有单词作为新数组中单独项目的数组。
-
使用
join()
方法将更新后的单词数组转换回一个字符串,单词之间用空格分隔。 -
返回更新后带有大写单词的新字符串的值,然后可以将其输出到控制台。
练习 8.5
使用 replace()
字符串方法,通过将字符串中的元音字母替换为数字来完成这个元音替换练习。你可以从以下字符串开始:
I love JavaScript
然后将其转换为以下类似的内容:
2 l3v1 j0v0scr2pt
按以下步骤操作:
-
创建之前指定的字符串,并将其转换为小写。
-
创建一个包含元音字母的数组:a, e, i, o, u。
-
遍历数组中的每个字母,并将当前字母输出到控制台,以便你可以看到哪个字母将被转换。
-
在循环中,使用
replaceAll()
更新每个元音子字符串,使用元音数组中字母的索引值。使用
replace()
只会替换第一次出现的内容;如果你使用replaceAll()
,这将更新所有匹配的结果。 -
循环完成后,将新字符串的结果输出到控制台。
数字方法
让我们继续学习 Number
对象的一些内置方法。我们已经见过一些了,它们非常受欢迎,以至于其中一些已经被做成全局方法。
检查某个值是否(不是)一个数字
这可以用 isNaN()
来做。我们已经在讨论全局方法时见过这个方法,我们可以在它前面不加 Number
使用这个方法。通常你想要做的是相反的操作,你可以在函数前面加一个 !
来取反:
let x = 34;
console.log(isNaN(x));
console.log(!isNaN(x));
let str = "hi";
console.log(isNaN(str));
这将在控制台输出:
false
true
true
由于 x
是一个数字,isNaN
将为 false
。但这个结果取反后变为 true
,因为 x
是一个数字。字符串 hi
不是一个数字,所以它将变为 false
。那么这个呢?
let str1 = "5";
console.log(isNaN(str1));
这里正在进行一些有趣的操作,即使 5
在引号之间,JavaScript 仍然将其视为数字 5,并将输出 false
。在这个时候,我相信你希望你的伴侣、家人和同事像 JavaScript 一样理解和宽容。
检查某个值是否是有限的
到现在为止,你可能已经能够猜出在 Number
上检查某个数是否有限的那个方法的名字。它是一个非常流行的方法,并且已经被做成全局方法,它的名字是 isFinite()
。对于 NaN
、Infinity
和 undefined
,它返回 false
,对于所有其他值返回 true
:
let x = 3;
let str = "finite";
console.log(isFinite(x));
console.log(isFinite(str));
console.log(isFinite(Infinity));
console.log(isFinite(10 / 0));
这将记录:
true
false
false
false
在这个列表中,唯一的有限数是 x
。其他的都不是有限的。字符串不是一个数字,因此不是有限的。Infinity
也不是有限的,10
除以 0
返回 Infinity
(这不是一个错误)。
检查是否为整数
是的,这是用 isInteger()
做的。与 isNaN()
和 isFinite()
不同,isInteger()
没有被做成全局方法,我们必须引用 Number
对象来使用它。它确实做了你想象中的事情:如果值是整数,它返回 true
;如果不是,返回 false
:
let x = 3;
let str = "integer";
console.log(Number.isInteger(x));
console.log(Number.isInteger(str));
console.log(Number.isInteger(Infinity));
console.log(Number.isInteger(2.4));
这将记录:
true
false
false
false
由于列表中唯一的整数是 x
。
指定小数位数
我们可以使用 toFixed()
方法告诉 JavaScript 使用多少位小数。这与 Math
中的四舍五入方法不同,因为我们可以在这里指定小数位数。它不会改变原始值,所以我们必须存储结果:
let x = 1.23456;
let newX = x.toFixed(2);
这将只保留两位小数,所以 newX
的值将是 1.23
。它按正常方式四舍五入;当你要求更多一位小数时,你可以看到这一点:
let x = 1.23456;
let newX = x.toFixed(3);
console.log(x, newX);
这记录了 1.23456 1.235
作为输出。
指定精度
同样,还有一个方法可以指定精度。这又不同于 Math
类中的四舍五入方法,因为我们可以指定要查看的总数。这归结为 JavaScript 查看总数。它也在计算小数点前的数字:
let x = 1.23456;
let newX = x.toPrecision(2);
因此,这里 newX
的值将是 1.2
。同样,这里它也在进行四舍五入:
let x = 1.23456;
let newX = x.toPrecision(4);
console.log(newX);
这将记录 1.235
。
现在,让我们继续讨论一些相关的数学方法!
数学方法
Math
对象有许多方法,我们可以使用它们在数字上进行计算和操作。我们将在下面介绍最重要的几个。当你使用一个在输入时显示建议和选项的编辑器时,你可以看到所有可用的方法。
查找最大和最小数
有一个内置的 max()
方法可以找到参数中的最大数。你可以在这里看到它:
let highest = Math.max(2, 56, 12, 1, 233, 4);
console.log(highest);
它记录了 233
,因为这是最大的数。以类似的方式,我们可以找到最小的数:
let lowest = Math.min(2, 56, 12, 1, 233, 4);
console.log(lowest);
这将记录 1
,因为这是最小的数。如果你尝试用非数字参数做这个操作,你将得到 NaN
作为结果:
let highestOfWords = Math.max("hi", 3, "bye");
console.log(highestOfWords);
它没有输出 3
,因为它不是忽略文本,而是得出结论,它无法确定 hi
应该是高于还是低于 3
。因此,它返回 NaN
。
平方根和幂运算
sqrt()
方法用于计算某个数的平方根。这里你可以看到它的实际应用:
let result = Math.sqrt(64);
console.log(result);
这将记录8
,因为64
的平方根是8
。这种方法与你在学校学到的数学一样。为了将一个数提升到某个幂(例如 2³),我们可以使用pow(base, exponent)
函数。就像这样:
let result2 = Math.pow(5, 3);
console.log(result2);
我们在这里将5
提升到3
的幂(5³),所以结果是125
,这是 555 的结果。
将小数转换为整数
将小数转换为整数的方法有很多。有时你可能需要四舍五入一个数字。你可以使用round()
方法来做这件事:
let x = 6.78;
let y = 5.34;
console.log("X:", x, "becomes", Math.round(x));
console.log("Y:", y, "becomes", Math.round(y));
这将记录:
X: 6.78 becomes 7
Y: 5.34 becomes 5
如你所见,这里使用的是正常的四舍五入。也可能你不想向下取整,而是向上取整。例如,如果你需要计算你需要多少块木板,你得出结论你需要1.1
,1
将不足以完成工作。你需要2
。在这种情况下,你可以使用ceil()
方法(即天花板):
console.log("X:", x, "becomes", Math.ceil(x));
console.log("Y:", y, "becomes", Math.ceil(y));
这将记录:
X: 6.78 becomes 7
Y: 5.34 becomes 6
ceil()
方法总是向上取整到遇到的第一个整数。我们之前在生成随机数时已经使用过这个方法了!注意这里的负数,因为-5
比-6
要大。这是它的工作方式,就像你在下面的例子中看到的那样:
let negativeX = -x;
let negativeY = -y;
console.log("negativeX:", negativeX, "becomes", Math.ceil(negativeX));
console.log("negativeY:", negativeY, "becomes", Math.ceil(negativeY));
这将记录:
negativeX: -6.78 becomes -6
negativeY: -5.34 becomes -5
floor()
方法与ceil()
方法正好相反。它向下取整到最接近的整数,就像你在这里看到的那样:
console.log("X:", x, "becomes", Math.floor(x));
console.log("Y:", y, "becomes", Math.floor(y));
这将记录:
X: 6.78 becomes 6
Y: 5.34 becomes 5
再次提醒,注意这里的负数,因为它可能感觉不太直观:
console.log("negativeX:", negativeX, "becomes", Math.floor(negativeX));
console.log("negativeY:", negativeY, "becomes", Math.floor(negativeY));
这将记录:
negativeX: -6.78 becomes -7
negativeY: -5.34 becomes -6
然后还有一个最后的方法,trunc()
。对于正数,它给出的结果与floor()
相同,但它得到这些结果的方式不同。它不是向下取整,它只是简单地返回整数部分:
console.log("X:", x, "becomes", Math.trunc(x));
console.log("Y:", y, "becomes", Math.trunc(y));
这将记录:
X: 6.78 becomes 6
Y: 5.34 becomes 5
当我们使用负数进行trunc()
时,我们可以看到差异:
negativeX: -6.78 becomes -6
negativeY: -5.34 becomes –5
所以,每当你需要向下取整时,你将不得不使用floor()
,如果你需要数字的整数部分,你需要trunc()
。
指数和对数
指数是基数被提升到的数。在数学中,我们经常使用e
(欧拉数),这就是 JavaScript 中的exp()
方法所做的工作。它返回e
必须提升到的数以得到输入。我们可以使用Math
的内置exp()
方法来计算指数,以及log()
方法来计算自然对数。你可以在这里看到一个例子:
let x = 2;
let exp = Math.exp(x);
console.log("Exp:", exp);
let log = Math.log(exp);
console.log("Log:", log);
这将记录:
Exp: 7.38905609893065
Log: 2
如果你现在在数学上跟不上了,不要担心。当你需要编程时,你会弄明白的。
练习题 8.6
使用以下步骤实验Math
对象:
-
使用
Math
将PI
的值输出到控制台。 -
使用
Math
获取5.7
的ceil()
值,获取5.7
的floor()
值,获取5.7
的舍入值,并将其输出到控制台。 -
将随机值输出到控制台。
-
使用
Math.floor()
和Math.random()
从 0 到 10 获取一个数字。 -
使用
Math.floor()
和Math.random()
从 1 到 10 获取一个数字。 -
使用
Math.floor()
和Math.random()
从 1 到 100 获取一个数字。 -
创建一个函数,使用
min
和max
参数生成随机数。运行该函数 100 次,每次在控制台返回一个 1 到 100 的随机数。
日期方法
为了在 JavaScript 中处理日期,我们使用内置的 Date
对象。此对象包含许多内置函数,用于处理日期。
创建日期
创建日期有不同的方法。创建日期的一种方法是通过使用不同的构造函数。您在这里可以看到一些示例:
let currentDateTime = new Date();
console.log(currentDateTime);
这将记录当前日期和时间,在这种情况下:
2021-06-05T14:21:45.625Z
但是,我们这里不是使用内置方法,而是使用构造函数。有一个内置方法,now()
,它返回当前日期和时间,类似于无参数构造函数所做的那样:
let now2 = Date.now();
console.log(now2);
这将记录当前时间,以自 1970 年 1 月 1 日以来的秒数表示。这是一个表示 Unix 纪元的任意日期。在这种情况下:
1622902938507
我们可以向 Unix 纪元时间添加 1,000 毫秒:
let milliDate = new Date(1000);
console.log(milliDate);
它将记录:
1970-01-01T00:00:01.000Z
JavaScript 也可以将许多字符串格式转换为日期。始终注意日期格式中日期和月份的顺序以及 JavaScript 的解释器。这可能会根据地区而变化:
let stringDate = new Date("Sat Jun 05 2021 12:40:12 GMT+0200");
console.log(stringDate);
这将记录:
2021-06-05T10:40:12.000Z
最后,您还可以使用构造函数指定一个特定日期:
let specificDate = new Date(2022, 1, 10, 12, 10, 15, 100);
console.log(specificDate);
这将记录:
2022-02-10T12:10:15.100Z
请注意这里的一个非常重要的细节,第二个参数是月份。0
代表一月,11
代表十二月。
获取和设置日期元素的函数
现在我们已经看到了如何创建日期,我们将学习如何获取日期的某些部分。这可以通过许多 get
方法之一来完成。您将使用哪个取决于您需要哪个部分:
let d = new Date();
console.log("Day of week:", d.getDay());
console.log("Day of month:", d.getDate());
console.log("Month:", d.getMonth());
console.log("Year:", d.getFullYear());
console.log("Seconds:", d.getSeconds());
console.log("Milliseconds:", d.getMilliseconds());
console.log("Time:", d.getTime());
这将立即记录:
Day of week: 6
Day of month: 5
Month: 5
Year: 2021
Seconds: 24
Milliseconds: 171
Time: 1622903604171
时间如此之高,因为它是从 1970 年 1 月 1 日以来的毫秒数。您可以使用类似的 set
方法更改日期。这里需要注意的是,原始日期对象会通过这些设置方法被更改:
d.setFullYear(2010);
console.log(d);
我们已经将我们的日期对象的年份更改为 2010 年。这将输出:
2010-06-05T14:29:51.481Z
我们还可以更改月份。让我们将以下代码片段添加到我们的年份更改代码中。这将将其更改为十月。请注意,当我这样做的时候,我会反复运行代码,所以当我没有设置这些时,示例中的分钟和更小的时间单位将会变化:
d.setMonth(9);
console.log(d);
它将记录:
2010-10-05T14:30:39.501Z
这是一个奇怪的情况,为了更改日期,我们必须调用 setDate()
方法,而不是 setDay()
方法。没有 setDay()
方法,因为星期几是从特定日期中扣除的。我们不能改变 2021 年 9 月 5 日是星期日的事实。不过,我们可以更改月份中的天数:
d.setDate(10);
console.log(d);
这将记录:
2010-10-10T14:34:25.271Z
我们还可以更改小时:
d.setHours(10);
console.log(d);
现在它将记录:
2010-10-10T10:34:54.518Z
记得 JavaScript 不喜欢崩溃吗?如果您使用大于 24 的数字调用 setHours()
,它将滚动到下一个日期(每 24 小时一个),并且在使用模运算符后,hours % 24
剩余的部分将是小时。同样的过程适用于分钟、秒和毫秒。
setTime()
实际上用插入的纪元时间覆盖了完整的日期:
d.setTime(1622889770682);
console.log(d);
这将记录:
2021-06-05T10:42:50.682Z
解析日期
使用内置的parse()
方法,我们可以从字符串中解析纪元日期。它接受许多格式,但同样,你需要注意日期和月份的顺序:
let d1 = Date.parse("June 5, 2021");
console.log(d1);
这将记录:
1622851200000
如你所见,它以许多零结尾,因为我们没有在字符串中指定时间或秒。这里还有一个完全不同格式的例子:
let d2 = Date.parse("6/5/2021");
console.log(d2);
这也将记录:
1622851200000
解析的输入是日期的 ISO 格式。许多格式可以解析为字符串,但你需要小心。结果可能取决于确切的实现。确保你知道传入字符串的格式,以免混淆月份和日期,并确保你知道实现的行为了。只有当你知道字符串格式时,才能可靠地完成这项工作。例如,当你需要将来自你自己的数据库或网站日期选择器的数据转换为日期时。
将日期转换为字符串
我们也可以将日期转换回字符串。例如,使用这些方法:
console.log(d.toDateString());
这将记录以文字格式表示的日期:
Sat Jun 05 2021
这是一种不同的转换方法:
console.log(d.toLocaleDateString());
它将记录:
6/5/2021
练习题 8.7
将完整的月份名称输出到控制台中的日期。在转换到或从数组时,请记住它们是零基的:
-
设置一个日期对象,可以是未来的任何日期或过去的日期。将日期输出到控制台以查看它通常如何作为日期对象输出。
-
设置一个包含一年中所有月份名称的数组。保持它们的顺序,以便它们与日期月份输出相匹配。
-
使用
getDate()
从日期对象值中获取日期。 -
从日期对象值中获取年份,使用
getFullYear()
。 -
使用
getMonth()
从日期对象值中获取月份。 -
设置一个变量来存储日期对象中的日期,并使用数组月份名称的数值作为索引输出月份。由于数组是零基的,月份返回的值为 1-12,因此结果需要减去 1。
-
将结果输出到控制台。
章节项目
单词打乱器
创建一个函数,该函数返回单词的值,并使用Math.random()
打乱字母顺序:
-
创建一个字符串,用于存储你选择的单词值。
-
创建一个可以接受字符串单词值的参数的函数。
-
就像数组一样,字符串也有默认的长度。你可以使用这个长度来设置循环的最大值。你需要创建一个单独的变量来存储这个值,因为随着循环的进行,字符串的长度会减小。
-
创建一个空临时的字符串变量,你可以用它来存储新的打乱后的单词值。
-
创建一个
for
循环,该循环将从字符串参数中的 0 开始迭代,直到达到字符串的原始长度值。 -
创建一个变量,使用
Math.floor()
和Math.random()
乘以当前字符串的长度,通过其索引值随机选择一个字母。 -
将新字母添加到新字符串中,并从原始字符串中移除它。
-
使用
console.log()
输出由随机字母组成的新构造字符串,并在循环继续时同时输出原始字符串和新字符串。 -
通过选择从索引值开始的子字符串并将其添加到索引加一之后的剩余字符串值来更新原始字符串。输出新的原始字符串值,其中已移除字符。
-
当你遍历内容时,你会看到剩余字母的倒计时、构建中的单词的新打乱版本,以及原始单词中减少的字母。
-
返回最终结果,并使用原始字符串单词作为参数调用该函数。将此输出到控制台。
倒计时计时器
创建一个可以在控制台窗口中执行的倒计时计时器代码,并显示到达目标日期之前剩余的总毫秒数、天数、小时数、分钟数和秒数:
-
创建一个你想要倒计时的结束日期。在字符串中将其格式化为日期类型格式。
-
创建一个倒计时函数,该函数将解析
endDate()
并从当前日期减去该结束日期。这将显示以毫秒为单位的总数。使用Date.parse()
,你可以将日期的字符串表示形式转换为自 1970 年 1 月 1 日 00:00:00 UTC 以来的数值。 -
一旦你有了总毫秒数,要获取天数、小时数、分钟数和秒数,你可以采取以下步骤:
-
要获取天数,你可以将日期中的毫秒数除以,并使用
Math.floor()
移除余数。 -
要获取小时数,可以使用模运算来捕获在总天数被移除后的余数。
-
要获取分钟数,你可以使用分钟中的毫秒值,并使用模运算捕获余数。
-
通过将数字除以毫秒中的秒数并获取余数,以同样的方式获取秒数。如果你使用
Math.floor()
,你可以向下取整,移除任何将在较低值中显示的剩余小数位。
-
-
返回一个对象中的所有值,其属性名表示值所引用的时间单位。
-
创建一个函数,使用
setTimeout()
每秒(1,000 毫秒)运行update()
函数。update()
函数将创建一个变量,可以暂时保存countdown()
的对象返回值,并创建一个空变量,将用于创建输出值。 -
在同一函数中,使用
for
循环获取temp
对象变量的所有属性和值。当你遍历对象时,更新输出字符串以包含属性名和属性值。 -
使用
console.log()
将输出结果字符串打印到控制台。
自我检查测验
-
以下哪种方法可以解码以下内容?
var c = "http://www.google.com?id=1000&n=500"; var e = encodeURIComponent(c);
-
decodeURIComponent(e)
-
e.decodeUriComponent()
-
decoderURIComponent(c)
-
decoderURIComponent(e)
-
-
以下语法将在控制台输出什么?
const arr = ["hi","world","hello","hii","hi","hi World","Hi"]; console.log(arr.lastIndexOf("hi"));
-
以下代码在控制台的结果是什么?
const arr = ["Hi","world","hello","Hii","hi","hi World","Hi"]; arr.copyWithin(0, 3, 5); console.log(arr);
-
以下代码在控制台的结果是什么?
const arr = ["Hi","world","hello","Hii","hi","hi World","Hi"]; const arr2 = arr.filter((element,index)=>{ const ele2 = element.substring(0, 2); return (ele2 == "hi"); }); console.log(arr2);
摘要
在本章中,我们处理了许多内置方法。这些是 JavaScript 提供给我们的方法,我们可以使用它们来完成我们经常需要做的事情。我们回顾了最常用的全局内置方法,这些方法如此常见,以至于无需使用它们所属的对象作为前缀即可使用。
我们还讨论了数组方法、字符串方法、数字方法、数学方法和日期方法。当你对 JavaScript 更加熟悉时,你会发现自己会大量使用这些方法,并在适当的时候将它们链接起来(只要它们返回结果)。
现在我们已经熟悉了 JavaScript 的许多核心特性,接下来几章我们将深入探讨它是如何与 HTML 和浏览器协同工作,使网页生动起来的!
第九章:文档对象模型
文档对象模型 (DOM) 比您最初听起来要有趣得多。在本章中,我们将向您介绍 DOM。这是您在网页上使用 JavaScript 之前需要理解的基本概念。它抓取一个 HTML 页面并将其转换为逻辑树。如果您不知道任何 HTML,不用担心。我们从 HTML 快速入门部分开始,如果您熟悉 HTML,可以跳过这一部分。
一旦我们确信我们对 HTML 知识有相同的理解,我们将向您介绍 浏览器对象模型 (BOM)。BOM 包含 JavaScript 与浏览器交互的所有方法和属性。这是与之前访问的页面、浏览器窗口大小以及 DOM 相关的信息。
DOM 包含网页上的 HTML 元素。通过 JavaScript,我们可以选择和操作 DOM 的部分。这导致交互式网页而不是静态网页。所以,简而言之,能够与 DOM 一起工作意味着您能够创建交互式网页!
我们将涵盖以下主题:
-
HTML 快速入门
-
介绍 BOM
-
介绍 DOM
-
DOM 元素的类型
-
选择页面元素
我们可以想象您迫不及待地想要开始,所以让我们深入探讨吧。
注意:练习、项目和自我检查测验的答案可以在 附录 中找到。
HTML 快速入门
超文本标记语言 (HTML) 是塑造网页内容的语言。网络浏览器理解 HTML 代码并以我们习惯的格式展示它:网页。以下是一个非常基础的 HTML 示例:
<!DOCTYPE html>
<html>
<head>
<title>Tab in the browser</title>
</head>
<body>
<p>Hello web!</p>
</body>
</html>
这就是这个基本网页的外观:
图 9.1:基本网站
HTML 代码由元素组成。这些元素包含一个标签和属性。我们将在接下来的章节中解释这些基本概念。
HTML 元素
如您所见,HTML 由 <angle brackets>
之间的单词组成,或元素。任何打开的元素都需要关闭。我们用 <elementname>
打开,用 </elementname>
关闭。
元素之间的所有内容都是该元素的一部分。在关闭方面有一些例外,但你会根据自己的进度遇到它们。在上一个示例中,我们有许多元素,包括这两个。这是一个带有 body
标签的元素,以及一个带有 p
标签的内部元素:
<body>
<p>Hello web!</p>
</body>
因此,元素可以包含内部元素。只有当所有内部元素都已关闭时,元素才能关闭。以下是一个示例来演示这一点。以下是正确的方式:
<outer>
<sub>
<inner>
</inner>
</sub>
</outer>
而下面是错误的做法:
<outer>
<sub>
<inner>
</sub>
</inner>
</outer>
请注意,这些只是虚构的元素名称。在上一个示例中,我们在关闭 inner
元素之前关闭了 sub
。这是错误的;您必须始终在关闭外部元素之前关闭内部元素。我们称内部元素为子元素,外部元素为父元素。以下是一些正确的 HTML:
<body>
<div>
<p>
</p>
</div>
</body>
这不是正确的 HTML,因为div
在其内部元素p
之前就关闭了:
<body>
<div>
<p>
</div>
</p>
</body>
不同的元素代表不同的布局部分。我们刚才看到的p
代表段落。另一个常见的元素是h1
,它代表一个大标题。更重要的是要了解每个 HTML 页面的三个主要构建元素。HTML 元素、head 元素和 body 元素。
在 HTML 元素中,所有的 HTML 都在这里发生。你的 HTML 页面中只能有一个这样的元素。它是外层元素,所有其他元素都包含在其中。它包含其他两个顶级元素:head
和body
。如果你对head
和body
的顺序感到困惑,只需想象一个人类;头部在身体之上。
在head
元素中,我们安排了很多旨在为浏览器而非用户准备的事情。你可以想象一些元数据,比如哪些 JavaScript 脚本和哪些样式表需要包含,以及搜索引擎应该在搜索结果页面上使用什么作为描述。作为 JavaScript 开发者,我们实际上不会对 head 元素做很多操作,除了包含脚本。
这里是一个基本head
元素的例子:
<head>
<title>This is the title of the browser tab</title>
<meta name="description" content="This is the preview in google">
<script src="img/included.js"></script>
</head>
body
元素主要是将在网页上显示的内容。在 HTML 元素中只能有一个body
元素。标题、段落、图片、列表、链接、按钮等等,都是我们可能在 body 中遇到元素。它们有自己的标签,例如,img
用于图片,a
用于链接。这里有一个包括 body 中常见标签的表格。这绝对不是一个详尽的列表。
开启标签 | 结束标签 | 描述 |
---|---|---|
<p> |
</p> |
用于创建一个段落。 |
<h1> |
</h1> |
用于创建一个标题;较小的标题是 h2 到 h6。 |
<span> |
</span> |
用于需要分离的内容的通用内联容器,例如,用于布局目的。 |
<a> |
</a> |
用于超链接。 |
<button> |
</button> |
用于按钮。 |
<table> |
</table> |
创建一个表格。 |
<tr> |
</tr> |
创建一个表格行,必须在表格内使用。 |
<td> |
</td> |
在行内创建一个表格数据单元格。 |
<ul> |
</ul> |
带有项目符号的无序列表,例如。 |
<ol> |
</ol> |
带有数字的有序列表。 |
<li> |
</li> |
有序和无序列表中的列表项。 |
<div> |
</div> |
HTML 页面内的一个部分。它通常用作其他样式或部分的容器,并且可以很容易地用于特殊布局。 |
<form> |
<form> |
创建一个 HTML 表单。 |
<input> |
</input> |
创建一个用户可以输入信息的输入字段。这些可以是文本框、复选框、按钮、密码、数字、下拉菜单、单选按钮等等。 |
<input /> |
无 | 与 input 相同,但写成没有结束标签的形式,末尾的/使其自闭合。这仅适用于少数几个元素。 |
<br> |
无 | 用于创建换行(转到新行)。它不需要结束标签,因此是一个例外。 |
你能弄清楚这个 HTML 示例的作用吗:
<html>
<head>
<title>Awesome</title>
</head>
<body>
<h1>Language and awesomeness</h1>
<table>
<tr>
<th>Language</th>
<th>Awesomeness</th>
</tr>
<tr>
<td>JavaScript</td>
<td>100</td>
</tr>
<tr>
<td>HTML</td>
<td>100</td>
</tr>
</table>
</body>
</html>
它创建了一个网页,在标签标题中显示Awesome
。在页面上,有一个大标题写着Language and awesomeness
。然后有一个包含三行两列的表格。第一行包含标题Language
和Awesomeness
。第二行包含值JavaScript
和100
,第三行包含值HTML
和100
。
HTML 属性
在本快速入门课程的最后部分,我们将讨论 HTML 属性。属性会影响它们指定的元素。它们存在于指定的元素内部,并使用等号赋值。例如,a
(表示超链接)的属性是href
。这指定了链接将重定向到何处:
<a href="https://google.com">Ask Google</a>
这显示了一个带有文本Ask Google
的链接。当你点击它时,你将被发送到 Google,这可以通过href
属性的值来判断。这修改了a
元素。属性有很多,但就目前而言,你只需要知道它们会修改指定的元素。
这里有一个表格,概述了开始使用 HTML 和 JavaScript 时最重要的属性。为什么这些属性很重要将在下一章中展开。
属性名称 | 描述 | 可以用于哪些元素? |
---|---|---|
id |
给元素赋予一个唯一的 ID,例如age 。 |
所有元素 |
name |
用于给元素指定一个自定义名称。 | input 、button 、form 以及我们尚未见过的许多元素 |
class |
可以添加到元素中的特殊元数据。这可能导致特定的布局或 JavaScript 操作。 | 几乎所有在body 内部的元素 |
value |
设置添加到元素中的初始值。 | button 、input 、li 以及我们尚未见过的几个元素 |
style |
给添加到其中的 HTML 元素指定一个布局。 | 所有元素 |
当你需要它们来练习你的 JavaScript 魔法时,我们将向你介绍其他属性。
好的,这可能是其中较为简短的 HTML 快速入门课程之一。有许多优秀的资源可以找到更多信息。如果你现在需要更多信息或解释,创建并打开一个如下所示的 HTML 文件,然后继续学习!
<!DOCTYPE html >
<html>
<body>
<a href="https://google.com">Ask google</a>
</body>
</html>
我们现在将接着查看 BOM 及其不同部分。
BOM
BOM,有时也称为窗口浏览器对象,是一个神奇的“魔法”元素,使得你的 JavaScript 代码能够与浏览器进行通信。
窗口对象包含表示浏览器窗口所需的所有属性,例如窗口的大小和之前访问的网页的历史记录。窗口对象具有全局变量和函数,我们可以在探索窗口对象时看到它们。BOM 的确切实现取决于浏览器和浏览器的版本。在浏览这些部分时,这一点很重要要记住。
本章我们将探讨 BOM 的一些最重要的对象:
-
历史
-
导航器
-
位置
除了前面提到的有用对象外,我们还将更详细地考虑 DOM。但首先,我们可以探索 BOM,并使用命令console.dir(window)
查看其对象。我们将在浏览器控制台中输入这个命令。让我们先讨论如何到达那里。
如果我们转到浏览器检查面板,我们可以访问 HTML 元素和 JavaScript。到达那里的方式略有不同,但通常在浏览器中按F12
按钮或右键单击你想要查看控制台的网站,然后单击Inspect element或 macOS 设备上的Inspect。
你应该会看到一个侧边面板(或者如果你已经更改了设置,则是一个单独的窗口)弹出。
图 9.2:在浏览器中检查页面
导航到上面的图像中紧邻Elements标签旁边的Console标签。你可以输入以下命令并按Enter
键以获取有关窗口对象的信息:
console.dir(window);
此命令将产生如下视图:
图 9.3:console.dir(window)的部分输出,显示了窗口浏览器对象
console.dir()
方法显示指定对象的所有属性列表。你可以点击小三角形来打开对象并进一步检查它们。
BOM 包含许多其他对象。我们可以像处理对象时那样访问它们,例如,我们可以通过访问窗口的history
对象以及history
对象的长度的方式来获取历史记录的长度(在我的浏览器中),如下所示:
window.history.length;
练习之后,我们将更多地了解history
对象。
练习 9.1
-
返回你刚刚查看的网站并执行命令
console.dir(window)
。 -
你能找到嵌套在
window
对象中的document
对象吗?在控制台中的window
对象根目录下,你可以导航到名为document
的对象。 -
你能找到窗口的高度和宽度(以像素为单位)吗?你可以返回内窗口和外窗口。
窗口历史对象
窗口浏览器对象还包含一个history
对象。实际上,这个对象可以不使用window
前缀来编写,因为它已经被设置为全局可用,所以我们可以通过在控制台中使用console.dir(window.history)
或简单地使用console.dir(history)
命令来获取相同的对象:
图 9.4:历史对象
这个对象实际上是你用来返回上一个页面的。它有一个内置的go
函数用于这个目的。当你执行这个命令时会发生什么?
window.history.go(-1);
你可以自己在浏览器控制台中尝试一下(确保你在那个标签页中访问了多个页面)。
窗口navigator
对象
在我们刚才看到的窗口对象中,有一个navigator
属性。这个属性特别有趣,因为它包含了关于我们使用的浏览器的信息,比如它是哪个浏览器,我们使用的是哪个版本,以及浏览器运行在什么操作系统上。
这对于为某些操作系统定制网站很有用。想象一下,一个按钮在不同的操作系统(Windows、Linux 和 macOS)上会有不同的样子。
你可以使用以下命令在控制台中探索它:
console.dir(window.navigator);
如你所见,我们首先访问的是窗口,因为navigator
是window
对象的一个对象。所以它是window
对象的一个属性,我们用点号来指定。换句话说,我们以访问任何其他对象的方式访问这些window
对象。但是,在这种情况下,由于navigator
也是全局可用的,我们也可以使用这个命令在不带window
前缀的情况下访问它:
console.dir(navigator);
这里是navigator
对象可能的样子:
图 9.5:navigator
对象
窗口位置对象
window
对象的另一个相当有趣且独特的属性是location
对象。它包含了当前网页的 URL。如果你覆盖(部分)这个属性,你会强制浏览器跳转到新页面!具体如何操作因浏览器而异,但下一项练习将指导你完成这个过程。
location
对象包含一些属性。你可以在控制台中使用console.dir(window.location)
或console.dir(location)
来查看它们。输出将如下所示:
图 9.6:location
对象
location
对象上有许多对象,就像我们之前看到的那些一样。我们可以使用点符号(就像我们之前看到的对象一样)来访问嵌套的对象和属性。所以,例如,在这个浏览器中,我可以输入以下内容:
location.ancestorOrigins.length;
这将获取ancestorOrigins
对象的长度,它表示我们的页面与多少个浏览上下文相关联。这可以用来确定网页是否在一个意外的上下文中被框架化。不过,并不是所有的浏览器都有这个对象;再次强调,BOM 及其所有元素在每个浏览器中都是不同的。
按照实践练习中的步骤,自己尝试这样的魔法。
实践练习 9.2
通过 window
对象遍历到 location
对象,然后输出当前文件的 protocol
和 href
属性的值到控制台。
DOM
DOM 实际上并不复杂,理解起来。它是一种将 HTML 文档的结构以逻辑树的形式显示出来的方式。这是由于一个非常重要的规则,即内部元素必须在外部元素关闭之前关闭。
这里是一个 HTML 片段:
<html>
<head>
<title>Tab in the browser</title>
</head>
<body>
<h1>DOM</h1>
<div>
<p>Hello web!</p>
<a href="https://google.com">Here's a link!</a>
</div>
</body>
</html>
以下是将其转换为树形结构的方法:
图 9.7:非常基本的网页 DOM 的树结构
如你所见,最外层的元素 html 位于树的顶部。下一层 head 和 body 是它的子元素。head 只有一个子元素:title。body 有两个子元素:h1 和 div。而 div 有两个子元素:p 和 a。这些通常用于段落和链接(或按钮)。显然,复杂的网页有复杂的树。这个逻辑树和一系列额外的属性构成了网页的 DOM。
一个真实网页的 DOM 无法在这个书的页面上显示。但如果你能在脑海中画出这样的树,这将很快大有帮助。
额外的 DOM 属性
我们可以像检查其他对象一样检查 DOM。我们在网站的控制台中执行以下命令(再次强调,document
对象是全局可访问的,因此通过 window
对象访问它是可能的,但不是必需的):
console.dir(document);
在这种情况下,我们想查看代表 DOM 的 document
对象:
图 9.8:DOM
你真的不需要理解你在这里看到的一切,但它显示了众多内容,其中包括 HTML 元素和 JavaScript 代码。
太好了,现在你已经掌握了 BOM 的基础知识,以及与我们最相关的子对象 DOM。我们之前已经看到了 DOM 的许多属性。对我们来说,查看 DOM 中的 HTML 元素最为相关。DOM 包含了网页上的所有 HTML 元素。
这些 DOM 元素的基本知识,结合一些操作和探索 DOM 的知识,将开启许多可能性。
在下一章中,我们将专注于遍历 DOM、在 DOM 中查找元素以及操作 DOM。我们将编写的代码将真正开始看起来像真正的项目。
选择页面元素
document
对象包含许多属性和方法。为了在页面上处理元素,你首先必须找到它们。如果你需要更改某个段落的值,你必须先获取这个段落。我们称之为选择段落。选择之后,我们就可以开始修改它了。
要在您的 JavaScript 代码中选择的页面元素以及为了操作元素,您可以使用 querySelector()
或 querySelectorAll()
方法。这两个方法都可以用来通过标签名、ID 或类选择页面元素。
document.querySelector()
方法将返回文档中第一个匹配指定选择器的元素。如果没有找到匹配的页面元素,则返回结果 null
。要返回多个匹配元素,可以使用 document.querySelectorAll()
方法。
querySelectorAll()
方法将返回一个静态的 NodeList
,它表示与指定选择器组匹配的文档元素列表。我们将通过以下 HTML 片段演示 querySelector()
和 querySelectorAll()
的用法:
<!doctype html>
<html>
<head>
<title>JS Tester</title>
</head>
<body>
<h1 class="output">Hello World</h1>
<div class="output">Test</div>
</body>
</html>
我们将使用 querySelector()
选择 h1
元素。因此,如果有多个,它只会获取第一个:
const ele1 = document.querySelector("h1");
console.dir(ele1);
如果您想选择多个元素,可以使用 querySelectorAll()
。此方法将返回一个数组中匹配选择器的所有元素。在这个例子中,我们将查找 output
类的实例,这是通过在类名前加一个点来完成的。
const eles = document.querySelectorAll(".output");
console.log(eles);
选择后,你可以开始使用 DOM 的动态特性:你可以使用 JavaScript 操作元素。内容可以像变量内容一样更改,元素可以被删除或添加,样式可以调整。所有这些都可以使用 JavaScript 和用户与页面交互的方式来实现。我们已经在这里看到了 DOM 中两种最常见的选择方法,querySelector()
和 querySelectorAll()
。实际上,你可以使用这些方法选择任何你可能需要的元素。还有更多,你将在下一章中遇到,以及许多 DOM 可以被操作的方式。
练习 9.3
选择页面元素并更新内容、更改样式和添加属性。使用以下代码模板创建一个包含具有 output
类的页面元素的 HTML 文件:
<!DOCTYPE html >
<html>
<div class="output"></div>
<script>
</script>
</html>
在 script
标签内,对输出元素进行以下更改:
-
将页面元素作为 JavaScript 对象选择。
-
更新所选页面元素的
textContent
属性。 -
使用
classList
.add
对象方法,将red
类添加到元素上。 -
将元素的
id
属性更新为tester
。 -
通过
style
对象,将backgroundColor
属性设置为red
添加到页面元素上。 -
通过
document.URL
获取文档 URL 并更新输出元素的文本,使其包含文档 URL 的值。您可以先在控制台中记录它,以确保您有正确的值。
章节项目
使用 JavaScript 操作 HTML 元素
以下是一个 HTML 代码示例:
<div class="output">
<h1>Hello</h1>
<div>Test</div>
<ul>
<li id="one">One</li>
<li class="red">Two</li>
</ul>
<div>Test</div>
</div>
采取以下步骤(并进一步实验)来了解如何使用 JavaScript 代码操作 HTML 元素。
-
选择具有
output
类的元素。 -
创建另一个名为
mainList
的 JavaScript 对象,并仅选择output
元素内的ul
标签。更新该ul
标签的 ID 为mainList
。 -
搜索每个
div
的tagName
,并将它们作为数组输出到控制台。 -
使用
for
循环,将每个div
标签的 ID 设置为id
,其数值表示它们在输出中的出现顺序。仍然在循环中,交替更改output
中每个元素的背景颜色为红色或蓝色。
自我检查测验
-
前往你最喜欢的网站并打开浏览器控制台。输入
document.body
。你在控制台中看到了什么? -
如我们所知,对于对象,我们可以使用赋值运算符写入属性值并分配新值。更新你选择的网页上
document.body
对象的textContent
属性,使其包含字符串Hello World
。 -
使用我们关于对象的知识来列出 BOM 对象的属性和值。尝试在
document
对象上操作。 -
现在为
window
对象做同样的操作。 -
创建一个包含
h1
标签的 HTML 文件。使用 JavaScript 选择带有h1
标签的页面元素,并将该元素赋值给一个变量。更新该变量的textContent
属性为Hello World
。
摘要
我们从 HTML 的基础知识开始本章。我们了解到 HTML 由元素组成,这些元素可以包含其他元素。元素有一个标签指定它们的类型,并且它们可以有属性来改变元素或向元素添加一些元数据。这些属性可以被 JavaScript 使用。
我们然后查看 BOM,它代表正在使用的浏览器窗口,并包含其他对象,如 history
、location
、navigator
和 document
对象。document
对象被称为 DOM,你很可能会与之交互。文档包含网页的 HTML 元素。
我们还开始考虑如何选择文档元素并使用这些元素来操作网页。这是我们将在下一章继续探索的内容!
第十章:使用 DOM 进行动态元素操作
学习上一章的困难概念将在本章中得到回报。我们将把我们的 DOM 知识再向前迈进一步,学习如何使用 JavaScript 操作页面上的 DOM 元素。首先,我们需要学习如何导航 DOM 并选择我们想要的元素。我们将学习我们如何添加和更改属性和值,以及如何向 DOM 添加新元素。
你还将学习如何给元素添加样式,这可以用来使项目出现和消失。然后我们将向你介绍事件和事件监听器。我们将从简单开始,但到本章结束时,你将能够以多种方式操作网页,并且你将拥有创建基本 Web 应用的知识。掌握这项技能后,天空就是极限。
在旅途中,我们将涵盖以下主题:
-
基本 DOM 遍历
-
访问 DOM 中的元素
-
元素点击处理器
-
这和 DOM
-
操作元素样式
-
更改元素的类
-
操作属性
-
元素上的事件监听器
-
创建新元素
注意:练习、项目和自我检查测验的答案可以在附录中找到。
我们已经学到了很多关于 DOM 的知识。为了与我们的网页交互并创建动态网页,我们必须将我们的 JavaScript 技能与 DOM 连接起来。
基本 DOM 遍历
我们可以使用我们在上一章中看到的document
对象来遍历 DOM。这个文档对象包含所有 HTML,是网页的表示。遍历这些元素可以让你到达需要操作的元素。
这不是最常见的方法,但这将有助于你理解它的工作原理。有时,你可能实际上需要这些技术。只是不要慌张:还有其他方法可以做到这一点,它们将在本章中揭晓!
即使对于简单的 HTML 片段,也已经存在多种遍历 DOM 的方法。让我们在我们的 DOM 中寻找宝藏。我们从这个小小的 HTML 片段开始:
<!DOCTYPE html>
<html>
<body>
<h1>Let's find the treasure</h1>
<div id="forest">
<div id="tree1">
<div id="squirrel"></div>
<div id="flower"></div>
</div>
<div id="tree2">
<div id="shrubbery">
<div id="treasure"></div>
</div>
<div id="mushroom">
<div id="bug"></div>
</div>
</div>
</div>
</body>
</html>
我们现在想要遍历这个片段的 DOM 以找到宝藏。我们可以通过进入文档对象并从那里开始导航来实现这一点。在浏览器控制台中做这个练习最容易,因为这样你将直接获得关于你在 DOM 中的位置的反馈。
我们可以从文档的body
属性开始。这包含body
元素内部的所有内容。在控制台中,我们将输入:
console.dir(document.body);
我们应该得到一个非常长的对象。从该对象到我们的宝藏有几种方法。为了做到这一点,让我们讨论一下子元素和childNodes
属性。
childNodes
比children
更完整。children
只包含所有 HTML 元素,所以实际上是节点。childNodes
还包含文本节点和注释。然而,使用children
,你可以使用 ID,因此它们更容易使用。
要使用子元素到达宝藏,你必须使用:
console.dir(document.body.children.forest.children.tree2.children.shrubbery.children.treasure);
如你所见,在每一个我们选择的元素上,我们都需要再次选择子元素。所以,首先,我们从 body 中获取子元素,然后从这些子元素中选择forest
。然后从forest
中,我们想要再次获取其子元素,然后从这些子元素中选择tree2
。从tree2
中,我们想要再次获取子元素,从这些子元素中我们需要shrubbery
。然后最终,我们可以从shrubbery
中获取子元素并选择treasure
。
要使用childNodes
到达宝藏,你将不得不经常使用你的控制台,因为文本和注释节点也包含在其中。childNodes
是一个数组,所以你必须选择正确的索引来选择正确的子节点。这里有一个优势:它要短得多,因为你不需要单独选择名称。
console.dir(document.body.childNodes[3].childNodes[3].childNodes[1].childNodes[1]);
你也可以将它们组合起来:
console.dir(document.body.childNodes[3].childNodes[3].childNodes[1].children.treasure);
遍历文档的方法有很多。根据你的需求,你可能需要使用一种特定的方法。对于需要 DOM 遍历的任务,通常情况下,如果它有效,它就是一个好的解决方案。
到目前为止,我们看到了如何向下移动 DOM,但我们也可以向上移动。每个元素都知道它的父元素。我们可以使用parentElement
属性向上移动。例如,如果我们使用宝藏 HTML 示例并在控制台中输入以下内容:
document.body.children.forest.children.tree2.parentElement;
我们回到了forest
,因为它是tree2
的父元素。这可以非常有用,特别是当与getElementById()
等函数结合使用时,我们将在后面更详细地看到。
我们不仅可以上下移动,还可以左右移动。例如,如果我们这样选择tree2
:
document.body.children.forest.children.tree2;
我们可以通过以下方式到达tree1
:
document.body.children.forest.children.tree2.previousElementSibling;
并且从tree1
我们可以通过以下方式到达tree2
:
document.body.children.forest.children.tree1.nextElementSibling;
作为nextElementSibling
的替代方案,后者返回下一个元素节点,你可以使用nextSibling
,它将返回任何类型的下一个节点。
练习 10.1
在这个练习中,尝试遍历 DOM 层次结构。你可以使用这个示例 HTML 网站:
<!doctype html>
<html><head><title>Sample Webpage</title></head>
<body>
<div class="main">
<div>
<ul >
<li>One</li>
<li>Two</li>
<li>Three</li>
</ul>
</div>
<div>blue</div>
<div>green</div>
<div>yellow</div>
<div>Purple</div>
</div>
</body>
</html>
采取以下步骤:
-
创建并打开上面的示例网页,或者访问你喜欢的网站,并在控制台中通过
console.dir(document)
打开文档主体。 -
在
body.children
属性中,选择一些子元素。查看它们如何与页面内容匹配。 -
导航到并输出控制台中的下一个节点或元素。
将元素作为对象选择
现在我们知道了如何遍历 DOM,我们可以对元素进行更改。与其使用console.dir()
,我们只需输入我们想要更改的元素路径。现在我们有了这个元素的 JavaScript 对象,我们可以更改其所有属性。让我们用一个更简单的 HTML 页面来做这个例子。
<!DOCTYPE html>
<html>
<body>
<h1>Welcome page</h1>
<p id="greeting">
Hi!
</p>
</body>
</html>
我们可以通过以下代码遍历到p
元素,例如:
document.body.children.greeting;
这使我们能够直接操作元素的属性和元素本身!让我们在下一节中执行这种新获得的力量。
改变 innerText
innerText
属性关注元素的开头和结尾之间的文本,如下所示:
<element>here</element>
返回的值将是纯文本 here
。例如,如果我们去控制台并输入:
document.body.children.greeting.innerText = "Bye!";
页面上显示的消息立即从 Hi!
变为 Bye!
。innerText
返回元素的纯文本内容,在这种情况下没有问题,因为里面只有文本。然而,如果你需要选择元素内的任何 HTML,或者你想添加 HTML,你不能使用这种方法。它将 HTML 解释为文本,并将其直接输出到屏幕上。所以如果我们执行以下操作:
document.body.children.greeting.innerText = "<p>Bye!</p>";
它将在屏幕上输出 <p>Bye!</p>
,包括其周围的 HTML,就像它是一个文本字符串一样。为了解决这个问题,你需要使用 innerHTML
。
更改 innerHTML
如果你不仅想处理纯文本,或者可能想用你的值指定一些 HTML 格式,你可以使用 innerHTML
属性。这个属性不仅处理纯文本,还可以处理内联 HTML 元素:
document.body.children.greeting.innerHTML = "<b>Bye!</b>";
这将在屏幕上以粗体显示 Bye!,考虑到 b
元素而不是简单地将其作为单个字符串值打印出来。
你已经承诺过,你可以以比遍历 DOM 更方便的方式访问元素。让我们在下一节中看看具体是如何做到的。
在 DOM 中访问元素
从 DOM 中选择元素有多种方法。在获取元素后,我们能够修改它们。在接下来的章节中,我们将讨论如何通过 ID、标签名、类名和 CSS 选择器获取元素。
我们将使用内置方法来遍历 DOM 并返回符合指定条件的元素,而不是像我们刚才那样逐个遍历。
我们将使用以下 HTML 片段作为示例:
<!DOCTYPE html>
<html>
<body>
<h1>Just an example</h1>
<div id="one" class="example">Hi!</div>
<div id="two" class="example">Hi!</div>
<div id="three" class="something">Hi!</div>
</body>
</html>
让我们从通过 ID 访问元素开始。
通过 ID 访问元素
我们可以使用 getElementById()
方法通过 ID 获取元素。这个方法返回一个具有指定 ID 的元素。ID 应该是唯一的,因为 HTML 文档只会返回一个结果。对于有效的 ID 没有多少规则;它们不能包含空格,并且至少要有一个字符。与变量命名的约定一样,最好使其描述性,并避免使用特殊字符。
如果我们想立即选择具有 ID 为 two
的元素,我们可以使用:
document.getElementById("two");
这将返回完整的 HTML 元素:
<div id="two" class="example">Hi!</div>
再次强调,如果你有多个具有相同 ID 的元素,它只会返回它遇到的第一个。尽管如此,你应该避免在代码中出现这种情况。
这就是包含在 HTML 页面中的完整文件的样子,而不是简单地查询浏览器控制台:
<html>
<body>
<h1 style="color:pink;">Just an example</h1>
<div id="one" class="example">Hi!</div>
<div id="two" class="example">Hi!</div>
<div id="three" class="something">Hi!</div>
</body>
<script>
console.log(document.getElementById("two"));
</script>
</html>
在这种情况下,它将在控制台中记录具有 id="two"
的完整 HTML div
。
练习 10.2
尝试通过 ID 获取元素进行实验:
-
创建一个 HTML 元素并在元素属性中分配一个 ID。
-
使用其 ID 选择页面元素。
-
将选定的页面元素输出到控制台。
通过标签名访问元素
如果我们通过标签名请求元素,我们将得到一个数组作为结果。这是因为可能有多个具有相同标签名的元素。它将是一个 HTML 元素集合,或 HTMLCollection
,这是一个特殊的 JavaScript 对象。它基本上只是一个节点列表。在控制台中执行以下命令:
document.getElementsByTagName("div");
它将返回:
HTMLCollection(3) [div#one.example, div#two.example, div#three.something, one: div#one.example, two: div#two.example, three: div#three.something]
如您所见,所有具有 div
标签的 DOM 元素都被返回。您可以从语法中读取 ID 和类。集合中的前几个是对象:div
是名称,#
指定 ID,.
指定类。如果有多个点,则表示有多个类。然后您可以看到元素再次(namedItems
),这次作为键值对,其中 ID 作为键。
我们可以使用 item()
方法通过索引访问它们,如下所示:
document.getElementsByTagName("div").item(1);
这将返回:
<div id="two" class="example">Hi!</div>
我们也可以通过名称使用 namedItem()
方法访问它们,如下所示:
document.getElementsByTagName("div").namedItem("one");
这将返回:
<div id="one" class="example">Hi!</div>
当只有一个匹配项时,它仍然会返回一个 HTMLCollection
。只有一个 h1
标签,所以让我们演示这种行为:
document.getElementsByTagName("h1");
这将输出:
HTMLCollection [h1]
由于 h1
没有 ID 或类,它只是 h1
。而且因为它没有 ID,所以它不是 namedItem
,并且只出现一次。
练习 10.3
使用 JavaScript 通过标签名选择页面元素:
-
首先,创建一个简单的 HTML 文件。
-
使用相同的标签创建三个 HTML 元素。
-
在每个元素中添加一些内容,以便您可以区分它们
-
将一个脚本元素添加到您的 HTML 文件中,并在其中通过标签名选择页面元素并将它们存储在变量中作为一个数组
-
使用索引值选择中间元素并将其输出到控制台。
通过类名访问元素
我们可以为类名做类似的事情。在我们的示例 HTML 中,我们有两个不同的类名:example
和 something
。如果您通过类名获取元素,它将返回一个包含结果的 HTMLCollection
。以下将获取所有具有类 example
的元素:
document.getElementsByClassName("example");
这将返回:
HTMLCollection(2) [div#one.example, div#two.example, one: div#one.example, two: div#two.example]
如您所见,它只返回了具有 example
类的 div
标签。它忽略了具有 something
类的 div
。
练习 10.4
使用元素的类名选择所有匹配的页面元素。
-
创建一个简单的 HTML 文件进行工作。
-
添加三个 HTML 元素,并为每个元素添加相同的类。只要包含相同的元素类,您可以使用不同的标签。在每个元素中添加一些内容,以便您可以区分它们。
-
将一个脚本元素添加到您的文件中,并在其中通过类名选择页面元素。将结果
HTMLCollection
值分配给一个变量。 -
您可以使用索引值来选择单个
HTMLCollection
元素,就像您选择数组元素一样。从索引 0 开始,选择具有类名的页面元素,并将该元素输出到控制台。
通过 CSS 选择器访问元素
我们也可以使用 CSS 选择器来访问元素。我们使用querySelector()
和querySelectorAll()
来完成。然后我们给出 CSS 选择器作为参数,这将过滤 HTML 文档中的项目,并只返回满足 CSS 选择器的那些。
CSS 选择器可能看起来与你的第一印象略有不同。我们不是在寻找某种布局,而是使用与我们要为某些元素指定布局时相同的语法。我们还没有讨论这个问题,所以我们将在这里简要介绍。
如果我们声明p
作为 CSS 选择器,这意味着所有标签为p
的元素。这看起来可能是这样的:
document.querySelectorAll("p");
如果我们说p.example
,这意味着所有具有example
类的p
标签元素。它们也可以有其他类;只要example
在其中,就会匹配。我们也可以说#one
,这意味着选择所有 ID 为one
的元素。
这种方法与getElementById()
的结果相同。当你只需要通过 ID 选择时,选择哪一个只是口味问题——这是一个与另一位开发者讨论的绝佳话题。querySelector()
允许进行更复杂的查询,一些开发者会声称getElementById()
更易读。其他人会声称你可以在任何地方使用querySelector()
以保持一致性。这在这个阶段并不重要,但尽量保持一致。
目前不必过于担心所有这些选项;有很多,当你需要时你会弄清楚。这就是如何在 JavaScript 中使用 CSS 选择器。
使用querySelector()
第一个选项将选择与查询匹配的第一个元素。所以,在控制台中输入以下内容,仍然使用本节开头介绍的 HTML 片段:
document.querySelector("div");
应该返回:
<div id="one" class="example">Hi!</div>
它只返回第一个div
,因为那是它遇到的第一个。我们也可以要求一个具有类.something
的元素。如果你还记得,我们使用点符号来选择类,如下所示:
document.querySelector(".something");
这将返回:
<div id="three" class="something">Hi!</div>
使用这种方法,你只能使用有效的 CSS 选择器:元素、类和 ID。
练习第 10.5 节练习题
使用querySelector()
来启用单个元素选择:
-
创建另一个简单的 HTML 文件。
-
创建四个 HTML 元素,并为每个元素添加相同的类。只要元素属性中有类,它们可以有不同的标签名。
-
在每个元素中添加一些内容,以便你可以区分它们。
-
在一个脚本元素中,使用
querySelector()
来选择具有该类的第一个元素,并将其存储在一个变量中。如果querySelector()
中有多个匹配结果,它将返回第一个。 -
将元素输出到控制台。
使用querySelectorAll()
有时候,只返回第一个实例是不够的,但你想选择所有匹配查询的元素。例如,当你需要获取所有输入框并将它们清空时。这可以通过querySelectorAll()
来完成:
document.querySelectorAll("div");
这将返回:
NodeList(3) [div#one.example, div#two.example, div#three.something]
如您所见,它是一个对象类型 NodeList
。它包含所有匹配 CSS 选择器的节点。我们可以使用 item()
方法通过索引获取它们,就像我们对 HTMLCollection
所做的那样。
练习 10.6
使用 querySelectorAll()
在 HTML 文件中选取所有匹配的元素:
-
创建一个 HTML 文件,并添加四个 HTML 元素,给每个元素添加相同的类。
-
在每个元素中添加一些内容,以便您能够区分它们。
-
在脚本元素内部,使用
querySelectorAll()
选取具有该类的所有匹配元素,并将它们存储在一个变量中。 -
首先将所有元素输出到控制台,然后通过循环逐个输出。
元素点击处理程序
HTML 元素在被点击时可以执行某些操作。这是因为可以将 JavaScript 函数连接到 HTML 元素。以下是一个片段,其中指定了与元素关联的 JavaScript 函数:
<!DOCTYPE html>
<html>
<body>
<div id="one" onclick="alert('Ouch! Stop it!')">Don't click here!</div>
</body>
</html>
当 div
中的文本被点击时,会弹出一个包含文本 Ouch! Stop it!
的弹出窗口。在这里,JavaScript 是直接在 onclick
后指定的,但如果页面上有 JavaScript,您也可以像这样引用那个 JavaScript 中的函数:
<!DOCTYPE html>
<html>
<body>
<script>
function stop(){
alert("Ouch! Stop it!");
}
</script>
<div id="one" onclick="stop()">Don't click here!</div>
</body>
</html>
此代码执行的是完全相同的事情。正如您所想象的,对于更大的函数,这会是一个更好的实践。HTML 也可以引用加载到页面中的脚本。
也有一种方法可以使用 JavaScript 添加点击处理程序。我们选择要添加点击处理程序的 HTML 元素,并指定 onclick 属性。
这里是一个 HTML 片段:
<!DOCTYPE html>
<html>
<body>
<div id="one">Don't click here!</div>
</body>
</html>
如果您点击它,此时代码不会执行任何操作。如果我们想动态地向 div
元素添加点击处理程序,我们可以选择它并通过控制台指定属性:
document.getElementById("one").onclick = function () {alert("Auch! Stop!");
}
由于这是在控制台中添加的,所以当您刷新页面时,此功能将消失。
这和 DOM
this
关键字始终具有相对意义;它取决于它所在的上下文。在 DOM 中,特殊的 this
关键字指向它所属的 DOM 元素。如果我们指定一个 onclick
并将 this
作为参数发送,它将发送 onclick
所在的元素。
script tag:
<!DOCTYPE html>
<html>
<body>
<script>
function reveal(el){
console.log(el);
}
</script>
<button onclick="reveal(this)">Click here!</button>
</body>
</html>
这就是它将记录的内容:
<button onclick="reveal(this)">Click here!</button>
如您所见,它记录了它所在的元素,即 button
元素。
我们可以使用类似这样的函数来访问 this
的父级:
function reveal(el){
console.log(el.parentElement);
}
在上面的例子中,body 是按钮的父级。因此,如果我们用新函数点击按钮,它将输出:
<body>
<script>
function reveal(el.parentElement){
console.log(el);
}
</script>
<button onclick="reveal(this)">Click here!</button>
</body>
我们可以用同样的方式输出元素的任何其他属性;例如,console.log(el.innerText);
会打印出我们在 更改 innerText 部分看到的内部文本值。
因此,this
关键字是指向元素,并且我们可以像我们刚刚学习的那样从这个元素遍历 DOM。这可以非常有用,例如,当您需要获取输入框的值时。如果您发送 this
,那么您可以读取和修改触发函数的元素的属性。
练习 10.7
在一个基本的 HTML 文档中创建一个按钮,并添加 onclick
属性。示例将演示如何使用 this
引用对象数据:
-
在你的 JavaScript 代码中创建一个处理点击的函数。你可以将函数命名为
message
。 -
在
onclick
函数参数中添加此,使用this
发送当前元素对象数据。 -
在
message
函数内部,使用console.dir()
在控制台输出使用onclick
和this
发送到函数的元素对象数据。 -
在页面上添加第二个按钮,也调用相同的点击函数。
-
当按钮被点击时,你应该在控制台看到触发点击的元素,如下所示:
图 10.1:实现 onclick 属性
操作元素样式
在从 DOM 中选择正确的元素后,我们可以更改应用于它的 CSS 样式。我们可以使用 style
属性来完成此操作。以下是操作方法:
-
从 DOM 中选择正确的元素。
-
更改此元素的
style
属性的右侧属性。
我们将创建一个按钮,该按钮将切换一行文本的显示和隐藏。要使用 CSS 隐藏某些内容,我们可以将元素的 display
属性设置为 none
,例如对于 p
(段落)元素:
p {
display: none;
}
我们可以使用以下方法将其切换回可见状态:
p {
display: block;
}
我们也可以使用 JavaScript 添加此样式。以下是一个简单的 HTML 和 JavaScript 片段,它将切换文本的显示:
<!DOCTYPE html>
<html>
<body>
<script>
function toggleDisplay(){
let p = document.getElementById("magic");
if(p.style.display === "none") {
p.style.display = "block";
} else {
p.style.display = "none";
}
}
</script>
<p id="magic">I might disappear and appear.</p>
<button onclick="toggleDisplay()">Magic!</button>
</body>
</html>
如您所见,在 if
语句中,我们正在检查它是否正在隐藏,如果是,则显示它。否则,隐藏它。如果你点击按钮并且它当前是可见的,它将消失。如果你在文本消失时点击按钮,它将出现。
使用此样式元素,你可以做各种有趣的事情。你认为点击按钮时它会做什么?
<!DOCTYPE html>
<html>
<body>
<script>
function rainbowify(){
let divs = document.getElementsByTagName("div");
for(let i = 0; i < divs.length; i++) {
divs[i].style.backgroundColor = divs[i].id;
}
}
</script>
<style>
div {
height: 30px;
width: 30px;
background-color: white;
}
</style>
<div id="red"></div>
<div id="orange"></div>
<div id="yellow"></div>
<div id="green"></div>
<div id="blue"></div>
<div id="indigo"></div>
<div id="violet"></div>
<button onclick="rainbowify()">Make me a rainbow</button>
</body>
</html>
当你首次打开页面时,你会看到以下内容:
图 10.2:点击时将做奇妙事情的按钮
当你点击按钮时:
图 10.3:按钮点击时由 JavaScript 制作的美丽彩虹
让我们回顾一下这个脚本,看看它是如何工作的。首先,HTML 中有几个 div
标签,它们都具有某种颜色的 ID。在 HTML 中指定了一个 style
标签,它为这些 div
标签提供了默认布局,即 30px x 30px 的大小和白色背景。
当你点击按钮时,将执行 rainbowify()
JavaScript 函数。在这个函数中,以下事情正在发生:
-
所有
div
元素都被选中并存储在数组divs
中。 -
我们遍历这个
divs
数组。 -
对于
divs
数组中的每个元素,我们正在将style
的backgroundColor
属性设置为元素的 ID。由于所有 ID 都代表一种颜色,所以我们看到了彩虹的出现。
如你所想,你可以真正地玩得很开心。只需几行代码,你就可以在屏幕上显示各种东西。
改变元素的类
HTML 元素可以有类,正如我们所看到的,我们可以通过类的名称来选择元素。你可能还记得,类经常被用来通过 CSS 给元素一个特定的布局。
使用 JavaScript,我们可以改变 HTML 元素的类,这可能会触发与 CSS 中该类相关联的特定布局。我们将查看添加类、移除类和切换类。
向元素添加类
这可能听起来有点模糊,所以让我们看看一个例子,我们将向一个元素添加一个类,在这种情况下,这将添加布局并使元素消失。
<!DOCTYPE html>
<html>
<body>
<script>
function disappear(){
document.getElementById("shape").classList.add("hide");
}
</script>
<style>
.hide {
display: none;
}
.square {
height: 100px;
width: 100px;
background-color: yellow;
}
.square.blue {
background-color: blue;
}
</style>
<div id="shape" class="square blue"></div>
<button onclick="disappear()">Disappear!</button>
</body>
</html>
在这个例子中,我们在style
标签中指定了一些 CSS。具有hide
类的元素有一个display
: none
样式,这意味着它们被隐藏。具有square
类的元素是 100x100 像素,并且是黄色的。但是当它们同时具有square
和blue
类时,它们是蓝色的。
当我们点击消失!按钮时,disappear()
函数会被调用。这个函数在脚本标签中指定。disappear()
函数通过获取具有 ID shape
的元素的classList
属性来改变类,这个正方形就是我们看到的。我们在classList
中添加了hide
类,因此元素获得了display:
none
布局,我们就无法再看到它了。
从元素中移除类
我们也可以移除一个类。如果我们从classList
中移除hide
类,例如,我们就可以再次看到我们的元素,因为display: none
布局不再适用。
在这个例子中,我们正在移除另一个类。你能通过查看代码来推断出按下按钮会发生什么吗?
<!DOCTYPE html>
<html>
<body>
<script>
function change(){
document.getElementById("shape").classList.remove("blue");
}
</script>
<style>
.square {
height: 100px;
width: 100px;
background-color: yellow;
}
.square.blue {
background-color: blue;
}
</style>
<div id="shape" class="square blue"></div>
<button onclick="change()">Change!</button>
</body>
</html>
当按钮被按下时,改变函数会被触发。这个函数会移除blue
类,从而移除布局中的蓝色背景色,留下黄色背景色,并且正方形会变成黄色。
你可能会想知道为什么正方形一开始是蓝色的,因为它有两个background-color
的布局被 CSS 分配给了它。这是通过一个积分系统来实现的。当一个样式更具体时,它会获得更多的积分。所以,指定两个没有空格的类意味着它适用于具有这两个类的元素。这比指定一个类更具体。
在 CSS 中引用 ID,#nameId
会获得更多的积分,并且会优先于基于类的布局。这种分层允许更少的重复代码,但它可能会变得混乱,所以一定要确保将 CSS 和 HTML 很好地结合起来,以获得所需的布局。
切换类
在某些情况下,你可能想在元素还没有特定类时添加一个类,但如果它已经有了,就移除它。这被称为切换。有一个特殊的方法可以切换类。让我们改变我们的第一个例子,以便切换hide
类,这样当我们在第二次点击按钮时,类将出现,第三次点击时消失,依此类推。blue
类被移除以使其更短;在这个例子中,它除了使正方形变蓝之外没有做任何事情。
<!DOCTYPE html>
<html>
<body>
<script>
function changeVisibility(){
document.getElementById("shape").classList.toggle("hide");
}
</script>
<style>
.hide {
display: none;
}
.square {
height: 100px;
width: 100px;
background-color: yellow;
}
</style>
<div id="shape" class="square"></div>
<button onclick="changeVisibility()">Magic!</button>
</body>
</html>
点击Magic!
按钮将添加类到classList
中,如果它不存在的话,如果它已经存在,则将其移除。这意味着每次你点击按钮时,你都可以看到结果。正方形会不断出现和消失。
操作属性
我们已经看到我们可以更改class
和style
属性,但有一个更通用的方法可以用来更改任何属性。快速提醒一下,属性是 HTML 元素后面跟着等号的那些部分。例如,这个指向 Google 的 HTML 链接:
<a id="friend" class="fancy boxed" href="https://www.google.com">Ask my friend here.</a>
本例中的属性是id
、class
和href
。其他常见属性包括src
和style
,但还有很多其他属性。
使用setAttribute()
方法,我们可以在元素上添加或更改属性。这将改变页面的 HTML。如果你在浏览器中检查 HTML,你会看到更改后的属性是可见的。你可以从控制台操作并轻松查看结果,或者编写另一个包含此功能的 HTML 文件。在这个 HTML 片段中,你会看到它的实际应用:
<!DOCTYPE html>
<html>
<body>
<script>
function changeAttr(){
let el = document.getElementById("shape");
el.setAttribute("style", "background-color:red;border:1px solid black");
el.setAttribute("id", "new");
el.setAttribute("class", "circle");
}
</script>
<style>
div {
height: 100px;
width: 100px;
background-color: yellow;
}
.circle {
border-radius: 50%;
}
</style>
<div id="shape" class="square"></div>
<button onclick="changeAttr()">Change attributes...</button>
</body>
</html>
这是点击按钮之前的页面:
图 10.4:带有黄色正方形div
的页面
点击按钮后,div
的 HTML 变为:
<div id="new" class="circle" style="background-color:red;border:1px solid black"></div>
如你所见,属性已经更改。id
从shape
更改为new
。class
从square
更改为circle
,并添加了一个style
。它看起来是这样的:
图 10.5:带有黑色线条的红色圆圈页面
这是一个非常强大的工具,可以以多种方式与 DOM 交互。例如,想象一下可以用来创建图片或甚至明信片的工具。在表面之下,有很多操作正在进行。
这里需要注意的是,JavaScript 与 DOM 交互,而不是与 HTML 文件交互——因此,DOM 是唯一会发生变化的部分。如果你再次点击按钮,你将在控制台看到一个错误消息,因为没有在 DOM 中找到id="shape"
的元素,因此我们尝试在空值上调用方法。
练习 10.8
创建自定义属性:使用名称数组,以下代码将更新元素的 HTML,使用数组中的数据添加 HTML 代码。数组中的项将作为 HTML 代码输出到页面。用户可以点击页面元素,它们将显示页面元素的属性值。
图 10.6:使用名称数组创建自定义属性
由于从现在开始 HTML 将变得更加复杂,而我们只是尝试测试你的 JavaScript,我们将提供所需的 HTML 模板。你可以使用以下 HTML 模板,并将你的答案作为完成的 script
元素提供:
<!DOCTYPE html>
<html>
<head>
<title>Complete JavaScript Course</title>
</head>
<body>
<div id="message"></div>
<div id="output"></div>
<script>
</script>
</body>
</html>
执行以下步骤:
-
创建一个名称数组。你可以添加任意多个,所有的字符串值都将输出到页面中的表格内。
-
将页面元素作为 JavaScript 对象选择。
-
添加一个函数,并在 JavaScript 代码中调用该函数。该函数可以命名为
build()
,因为它将构建页面内容。在build
函数中,你将设置 HTML 表格。 -
创建一个名为
html
的表格,并在标签内循环遍历数组的元素,将结果输出到html
表格中。 -
将名为
box
的类添加到具有数组项目索引值的单元格中,并为每个额外的行添加相同的类。 -
在创建
tr
元素内的 HTML 元素时,在主row
元素中创建一个名为data-row
的属性,包含数组中项目的索引值。此外,在元素内添加另一个名为data-name
的属性,它将包含文本输出。 -
在相同的
tr
元素的属性中,也添加onclick
以调用名为getData
的函数,并将当前元素对象作为this
传递给函数参数。 -
将 HTML 代码表添加到页面中。
-
创建一个名为
getData
的函数,当 HTMLtr
元素被点击时调用。一旦tr
元素被点击,使用getAttribute
获取行值的属性值和文本输出的内容,并将它们存储在不同的变量中。 -
使用前一步存储在属性中的值,将值输出到页面上的
message
元素中。 -
当用户点击页面上的元素时,它将显示来自具有
id
为message
的元素属性的详细信息。
元素上的事件监听器
事件是发生在网页上的事情,比如点击某个元素、将鼠标移至元素上、改变元素等,还有很多。我们已经看到了如何添加 onclick
事件处理器。同样地,你可以添加 onchange
处理器或 onmouseover
处理器。不过有一个特殊条件;一个元素只能有一个事件处理器作为 HTML 属性。所以,如果它有一个 onclick
处理器,就不能同时有 onmouseover
处理器。到目前为止,我们只看到了如何使用像这样 HTML 属性添加事件监听器:
<button onclick="addRandomNumber()">Add a number</button>
使用 JavaScript 注册事件处理程序也有一种方法。我们称之为事件监听器。使用事件监听器,我们可以给一个元素添加多个事件。这样,JavaScript 就会不断检查,或者说监听,页面上的元素是否发生了特定事件。添加事件监听器是一个两步的过程:
-
选择你想要添加事件的元素
-
使用
addEventListener("event", function)
语法添加事件
即使是两步,也可以用一行代码完成:
document.getElementById("square").addEventListener("click", changeColor);
这是在获取 ID 为 square
的元素,并将 changeColor
函数作为点击事件的监听器。注意,在使用事件监听器时,我们从事件类型中移除了 on
前缀。例如,这里的 click
与 onclick
引用相同的事件类型,但我们移除了 on
前缀。
让我们考虑另一种添加事件监听器的方法(别担心,我们将在 第十一章,交互内容和事件监听器 中详细回顾这些方法),通过设置某个对象的 event
属性为一个函数。
这里有一个有趣的事实——事件监听器通常是在其他事件中添加的!
我们可以在这种情况下重用我们信任的 onclick
监听器,但另一个常见的情况是当网页加载完成时使用 onload
:
window.onload = function() {
// whatever needs to happen after loading
// for example adding event listeners to elements
}
这个函数将被执行。这在 window.onload
中很常见,但在许多其他情况下则较少见,例如 div
上的 onclick
(尽管是可能的)。让我们看看我们在网页上查看的第一个事件监听器的例子。当你点击正方形时,你能想出它会做什么吗?
<!DOCTYPE html>
<html>
<body>
<script>
window.onload = function() {
document.getElementById("square").addEventListener("click", changeColor);
}
function changeColor(){
let red = Math.floor(Math.random() * 256);
let green = Math.floor(Math.random() * 256);
let blue = Math.floor(Math.random() * 256);
this.style.backgroundColor = `rgb(${red}, ${green}, ${blue})`;
}
</script>
<div id="square" style="width:100px;height:100px;background-color:grey;">Click for magic</div>
</body>
</html>
网页从一个带有文本 点击进行魔法
的灰色正方形开始。当网页加载完成后,为这个正方形添加一个事件。每次点击时,changeColor
函数都会被执行。这个函数使用随机变量通过 RGB 颜色改变颜色。每次你点击正方形,颜色都会更新为随机值。
你可以向各种元素添加事件。到目前为止,我们只使用了 click
事件,但还有很多其他事件。例如,focus
、blur
、focusin
、focusout
、mouseout
、mouseover
、keydown
、keypress
和 keyup
。这些将在下一章中介绍,所以继续前进!
练习 10.9
尝试一种替代方法来实现与 练习 10.7 类似的逻辑。使用以下 HTML 代码作为此练习的模板,并添加 script
元素的全部内容:
<!doctype html>
<html>
<head>
<title>JS Tester</title>
</head>
<body>
<div>
<button>Button 1</button>
<button>Button 2</button>
<button>Button 3</button>
</div>
<script>
</script>
</body>
</html>
按以下步骤操作:
-
将所有页面按钮选择到一个 JavaScript 对象中。
-
遍历每个按钮,并在按钮作用域内创建一个名为
output
的函数。 -
在
output()
函数中,添加一个console.log()
方法,输出当前对象的textContent
。你可以使用this
关键字引用当前父对象。 -
当你遍历按钮时,附加一个事件监听器,当点击时调用
output()
函数。
创建新元素
在本章中,您已经看到了许多操作 DOM 的酷炫方法。还有一个重要的方法尚未介绍,那就是创建新元素并将它们添加到 DOM 中。这包括两个步骤,首先是创建新元素,其次是将它们添加到 DOM 中。
这并不像看起来那么难。下面的 JavaScript 就是这样做的:
let el = document.createElement("p");
el.innerText = Math.floor(Math.random() * 100);
document.body.appendChild(el);
它创建了一个类型为p
(段落)的元素。这是一个位于document
对象上的createElement()
函数。在创建时,您需要指定您想要创建的 HTML 元素类型,在这个例子中是p
,所以像这样:
<p>innertext here</p>
并且作为innerText
,它正在添加一个随机数字。接下来,它将元素作为 body 的新最后一个子元素添加。您也可以将其添加到另一个元素中;只需选择您想要添加的元素,并使用appendChild()
方法。
在这里,您可以看到它被整合到一个 HTML 页面中。这个页面有一个按钮,每次按下按钮时,都会添加一个p
元素。
<!DOCTYPE html>
<html>
<body>
<script>
function addRandomNumber(){
let el = document.createElement("p");
el.innerText = Math.floor(Math.random() * 100);
document.body.appendChild(el);
}
</script>
<button onclick="addRandomNumber()">Add a number</button>
</body>
</html>
这是按下按钮五次后该页面的截图。
![img/B16682_10_07.png]
图 10.7:点击按钮五次后的随机数字
一旦我们刷新页面,它就又变空了。包含源代码的文件没有改变,我们也没有将其存储在任何地方。
练习练习 10.10
购物清单:使用以下 HTML 模板,更新代码以将新项目添加到页面上的项目列表。一旦点击按钮,它将向项目列表添加一个新项目:
<!DOCTYPE html>
<html>
<head>
<title>Complete JavaScript Course</title>
<style>
</style>
</head>
<body>
<div id="message">Complete JavaScript Course</div>
<div>
<input type="text" id="addItem">
<input type="button" id="addNew" value="Add to List"> </div>
<div id="output">
<h1>Shopping List</h1>
<ol id="sList"> </ol>
</div>
<script>
</script>
</body>
</html>
执行以下步骤:
-
将页面元素作为 JavaScript 对象选择。
-
给添加按钮添加一个
onclick
事件监听器。一旦按钮被点击,它应该将输入字段的全部内容添加到列表的末尾。您可以调用addOne()
函数。 -
在
addOne()
函数中,创建li
元素并将其附加到页面上的主列表。将输入值添加到列表项的文本内容中。 -
在
addOne()
函数中,获取addItem
输入字段的当前值。使用该值创建一个具有该值的textNode
,并将其添加到列表项中。将textNode
附加到列表项。
章节项目
可折叠的折叠组件
构建一个可折叠和展开的折叠组件,当点击标题标签时,将打开页面元素,隐藏和显示内容。使用以下 HTML 作为模板,添加完成的script
元素,并使用 JavaScript 创建所需的功能:
<!doctype html>
<html>
<head>
<title>JS Tester</title>
<style>
.active {
display: block !important;
}
.myText {
display: none;
}
.title {
font-size: 1.5em;
background-color: #ddd;
}
</style>
</head>
<body>
<div class="container">
<div class="title">Title #1</div>
<div class="myText">Just some text #1</div>
<div class="title">Title #2</div>
<div class="myText">Just some text #2</div>
<div class="title">Title #3</div>
<div class="myText">Just some text #3</div>
</div>
<script>
</script>
</body>
</html>
执行以下步骤:
-
使用
querySelectorAll()
选择所有具有title
类的元素。 -
使用
querySelectorAll()
选择所有具有myText
类的元素。这些元素的数量应该与title
元素的数量相同。 -
遍历所有
title
元素,并添加事件监听器,一旦点击,将选择下一个元素的同级元素。 -
在
click
动作中选择元素,并使用具有active
类的classlist
切换元素的类。这将允许用户点击元素并隐藏或显示下面的内容。 -
添加一个函数,每次点击元素时都会调用,该函数将从所有元素中移除
class
的active
。这将隐藏所有带有myText
的元素。
交互式投票系统
以下代码将创建一个动态的人名列表,可以点击,并且它将更新与该名字被点击次数相对应的值。它还包括一个输入字段,允许您将更多用户添加到列表中,每个用户都会在列表中创建另一个可以与之交互的项目,就像默认列表项一样。
图 10.8:创建一个交互式投票系统
使用以下 HTML 代码作为模板来添加 JavaScript,并提供您的答案作为完成的script
元素。
<!DOCTYPE html>
<html>
<head>
<title>Complete JavaScript Course</title>
</head>
<body>
<div id="message">Complete JavaScript Course</div>
<div>
<input type="text" id="addFriend">
<input type="button" id="addNew" value="Add Friend">
</div>
<table id="output"></table>
<script>
</script>
</body>
</html>
采取以下步骤:
-
创建一个名为
myArray
的人名数组。这将默认为原始名单。 -
将页面元素作为 JavaScript 对象选择,以便在代码中轻松选择。
-
将事件监听器添加到添加朋友按钮上。一旦点击,这将从输入字段获取值并将这些值传递给一个函数,该函数将朋友列表添加到页面上。此外,将新朋友的名字添加到您创建的人名数组中。获取输入字段中的当前值并将该值推送到数组中,以便数组与页面上的值匹配。
-
运行一个函数来构建页面上的内容,使用
forEach()
循环获取数组中的所有项目并将它们添加到页面上。将0
作为默认的投票计数,因为所有个人都应该从零票开始。 -
创建一个主函数,该函数将创建页面元素,从父表行
tr
开始,然后创建三个表格单元格td
元素。向表格单元格添加内容,包括最后一列的投票计数,中间的人名,以及第一列的索引加 1。 -
将表格单元格添加到表格行中,并将表格行添加到页面上的输出区域。
-
添加一个事件监听器,当用户点击时,将增加该行的投票计数器。
-
从行中的最后一列获取文本内容。它应该是当前计数器的值。将计数器加一,并确保数据类型是数字,这样您就可以将其添加到它。
-
更新最后一列以显示新的点击计数器。
挂科游戏
使用数组和页面元素创建一个挂科游戏。您可以使用以下 HTML 模板:
<!doctype html>
<html><head>
<title>Hangman Game</title>
<style>
.gameArea {
text-align: center;
font-size: 2em;
}
.box,
.boxD {
display: inline-block;
padding: 5px;
}
.boxE {
display: inline-block;
width: 40px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 1.5em;
}
</style>
</head>
<body>
<div class="gameArea">
<div class="score"> </div>
<div class="puzzle"></div>
<div class="letters"></div>
<button>Start Game</button>
</div>
<script>
</script>
</body>
</html>
采取以下步骤:
-
设置一个包含您想在游戏中使用的单词或短语的数组。
-
在 JavaScript 中,创建一个主游戏对象,包含一个属性来包含当前单词解决方案,另一个属性来包含解决方案的字母数组。还应创建一个数组来包含页面元素,并与解决方案中每个字母的索引值相对应,最后添加一个属性来计算剩余要解决的字母数并在需要时结束游戏。
-
将所有页面元素选择到变量中,以便在代码中更容易访问。
-
为开始游戏按钮添加事件监听器,使其可点击,当点击时,应启动一个名为
startGame()
的函数。 -
在
startGame()
函数内部,检查words
数组是否还有剩余的单词。如果有,则通过将.display
对象设置为none
来隐藏按钮。清除游戏内容并将总数设置为0
。在游戏对象中的当前单词中分配一个值,该值应该是包含游戏单词的数组中shift()
的响应。 -
在游戏解决方案中,使用
split()
将字符串转换为包含单词解决方案中所有字符的数组。 -
创建一个名为
builder()
的函数,用于构建游戏板。在所有游戏值清除并设置后,在startGame()
函数中调用该函数。 -
创建一个单独的函数,用于生成页面元素。在参数中,获取元素的类型、新元素将附加到的父元素、新元素的输出内容以及要添加到新元素的类。使用一个临时变量创建元素,添加类,将其附加到父元素,设置
textContent
,并返回元素。 -
在
builder()
函数中,该函数在运行startGame()
时也会被调用,需要清除字母和拼图页面元素中的innerHTML
。 -
遍历游戏解决方案数组,获取解决方案中的每个字母。使用
builder()
函数生成页面元素,添加输出值为-
,设置类,并将其附加到主拼图页面元素。 -
检查值是否为空,如果是,则清除
textContent
并将边框更新为白色。如果不为空,则增加总数,使其反映必须猜测的总字母数。将新元素推入游戏拼图数组。 -
创建一个新函数来更新分数,以便可以输出当前剩余的字母数。将其添加到
builder()
函数中。 -
创建一个循环来表示 26 个字母表中的字母。你可以使用包含所有字母的数组生成字母。字符串方法
fromCharCode()
将返回数字表示形式的字符。 -
为每个字母创建元素,添加
class
为box
并将其附加到letters
页面元素。随着每个元素的创建,添加一个运行名为checker()
的函数的事件监听器。 -
一旦点击信件,我们需要调用
checker()
函数,该函数将移除主类,添加另一个类,移除事件监听器,并更新背景颜色。同时调用一个名为checkLetter()
的函数,将点击信件的值传递给参数。 -
checkLetter()
函数将遍历所有解决方案字母。添加一个条件来检查解决方案字母是否等于玩家选择的字母。确保将输入的字母转换为大写,以便可以准确匹配字母。使用游戏谜题数组和从解决方案中的字母索引更新谜题中的匹配字母。索引值在每个中都是相同的,这为将视觉表示与数组中的内容匹配提供了一个简单的方法。 -
从跟踪剩余待解总字母数的游戏全局对象中减去一个,调用
updatescore()
函数检查游戏是否结束,并更新分数。将谜题的textContent
设置为移除原始破折号的字母。 -
在
updatescore()
函数中,将分数设置为剩余字母的数量。如果剩余总数小于或等于零,则游戏结束。显示按钮,以便玩家有选择下一个短语的选项。
自我检查测验
-
以下代码将产生什么输出?
<div id="output">Complete JavaScript Course </div> <script> var output = document.getElementById('output'); output.innerText = "Hello <br> World"; </script>
-
在浏览器页面中可以看到什么输出?
<div id="output">Complete JavaScript Course </div> <script> document.getElementById('output').innerHTML = "Hello <br> World"; </script>
-
以下代码将在输入字段中看到什么?
<div id="output">Hello World</div> <input type="text" id="val" value="JavaScript"> <script> document.getElementById('val').value = document.getElementById('output').innerHTML; </script>
-
在以下代码中,当点击包含单词
three
的元素时,控制台输出什么?当点击包含单词one
的元素时,输出什么?<div class="holder"> <div onclick="output('three')">Three <div onclick="output('two')">Two <div onclick="output('one')">One</div> </div> </div> </div> <script> function output(val) { console.log(val); } </script>
-
在以下代码中,需要添加哪一行代码来在按钮点击时移除事件监听器?
<div class="btn">Click Me</div> <script> const btn = document.querySelector(".btn"); btn.addEventListener("click", myFun); function myFun() { console.log("clicked"); } </script>
摘要
在本章中,我们真正将我们的网络技能提升到了新的水平。操作 DOM 允许与网页进行各种交互,这意味着网页不再是一个静态的事件。
我们首先解释了动态网页以及如何遍历 DOM。在手动遍历元素之后,我们了解到使用getElementBy…()
和querySelector()
方法选择 DOM 中的元素有更简单的方法。在选择了它们之后,我们有了修改它们、向它们添加新元素以及使用所选元素执行所有 sorts of things 的能力。我们从一些更基本的 HTML 处理器开始,并可以将函数分配给例如 HTML 元素的onclick
属性。
我们还使用了作为参数传入的this
参数来访问被点击的元素,并且我们可以以不同的方式修改它,例如,通过更改style
属性。我们还看到了如何向一个元素添加类,创建新元素,并将它们添加到 DOM 中。最后,我们与元素上的事件监听器一起工作,这真正将我们的动态网页提升到了新的水平。通过事件监听器,我们可以为某个元素指定多个事件处理程序。所有这些新技能都使我们能够在网页浏览器中创建令人惊叹的事物。你现在实际上可以创建完整的游戏了!
下一章将带你将事件处理技能提升到新的水平,并将进一步增强你创建交互式网页的能力(而且也会变得稍微容易一些!)。
第十一章:交互式内容和事件监听器
你现在已经熟悉了文档对象模型(DOM)的基本操作。在上一章中,我们探讨了事件,并了解到事件监听器持续监控是否有特定事件发生;当事件发生时,指定的函数(事件)会被调用。
在本章中,我们将在此基础上更进一步,使用事件监听器来创建交互式网页内容。这一章将真正完善你的 DOM 知识。我们将涵盖以下主题:
-
交互式内容
-
指定事件
-
onload
事件处理器 -
鼠标事件处理器
-
事件目标属性
-
DOM 事件流
-
onchange
和onblur
-
关键事件处理器
-
拖放元素
-
表单提交
-
元素动画
注意:练习、项目和自我检查测验的答案可以在附录中找到。
介绍交互式内容
交互式内容是指能够响应用户操作的内容。例如,想象一下一个可以在网页浏览器中动态创建明信片或玩游戏的应用程序。
这种交互式内容是通过根据用户交互更改 DOM 来实现的。这些交互可以是任何操作:在输入字段中输入文本、在页面上点击某个位置、用鼠标悬停在某个元素上,或者使用键盘输入某个特定的内容。所有这些都被称为事件。我们已经看到了事件。但实际上还有更多!
指定事件
有三种指定事件的方式。我们在上一章中已经看到过这些方法,但现在让我们再次回顾一下。其中一种是基于 HTML 的,另外两种是基于 JavaScript 的。在这个例子中,我们将使用click
事件作为示例。
使用 HTML 指定事件
首先,HTML 方法:
<p id="unique" onclick="magic()">Click here for magic!</p>
以这种方式指定事件的好处是,阅读代码并预测其行为相当容易。一旦你点击段落,magic()
函数就会被触发。当然,也存在一些缺点:你只能以这种方式指定一个事件,而且也不能动态地更改事件。
使用 JavaScript 指定事件
这是使用 JavaScript 实现的第一种方法。
document.getElementById("unique").onclick = function() { magic(); };
这里发生的事情是我们正在获取表示所选事件的属性,并将我们的函数分配给它。因此,在这种情况下,我们通过其属性值unique
选择上一节中显示的p
元素,获取onclick
属性,并通过将其包裹在匿名函数中将其magic()
函数分配给它。我们也可以在这里指定确切的函数。我们可以随时用另一个函数覆盖它,使可以触发的事件更加动态。
我们现在也可以指定不同的事件,而 HTML 无法做到这一点。因此,我们也可以给它一个keyup
、keydown
或mouseover
事件,例如——我们将在本章中考虑每种事件类型。
如果我们想要为页面上的所有元素指定事件触发器,我们可以在循环中这样做,以获得更清晰的编码风格。
练习 11.1
个性化你的网页。允许用户在常规模式和暗黑模式之间切换页面显示的主题。
-
在一个简单的 HTML 文档中,设置一个布尔值变量,用于切换颜色模式。
-
使用
window.onclick
设置一个函数,当点击时在控制台输出一条消息。你可以使用布尔变量的值。 -
在函数中添加一个条件,检查
darkMode
变量是true
还是false
。 -
如果是
false
,则更新页面样式,将背景颜色设置为黑色,字体颜色设置为白色。 -
添加一个
else
响应,将背景颜色改为白色,文本颜色改为黑色。同时,相应地更新darkMode
变量的值。
使用事件监听器指定事件
最后一种方法是使用addEventListener()
方法向元素添加事件。使用这种方法,我们可以为同一事件指定多个函数,例如,当元素被点击时。
我们所探讨的两种方法——使用 HTML 事件和属性赋值——引人注目的是,事件前都会加上on
前缀。例如,onclick
、onload
、onfocus
、onblur
、onchange
等。当我们使用addEventListener()
方法时,情况并非如此,我们会在事件监听器中指定事件类型,而不使用on
前缀,如下所示,这是onclick
的替代方案:
document.getElementById("unique").addEventListener("click", magic);
请注意,我们在魔法函数后面省略了括号。我们不能用这种方式传入参数。如果你必须这样做,你将不得不将功能包装在一个匿名函数中,如下所示:
document.getElementById("unique").addEventListener("click", function() { magic(arg1, arg2) });
在本章中,我们可以使用这些方法中的任何一种来指定事件。我们主要会使用 JavaScript 选项之一。
练习 11.2
创建几个带有颜色名称的div
,并在textContent
中添加 JavaScript 以添加对每个元素的click
事件监听器。当每个元素被点击时,更新页面的背景颜色以匹配div
中的颜色名称。
onload
事件处理程序
我们在上一章中简要介绍了这个事件处理程序。onload
事件在某个元素加载后触发。这可以出于许多原因是有用的。例如,如果你想使用getElementById
选择一个元素,你必须确保这个元素已经在 DOM 中加载。这个事件最常用于window
对象,但也可以用于任何元素。当你使用它时,当窗口对象加载完成后,这个事件就会被触发。以下是使用它的方法:
window.onload = function() {
// whatever needs to happen after the page loads goes here
}
onload
类似,但它在window
和document
对象中有所不同。这种差异取决于你使用的浏览器。load
事件在文档加载过程的末尾触发。因此,你会发现文档中的所有对象都在 DOM 中,并且资源已经加载完成。
你也可以在任何元素上使用 addEventListener()
方法来处理任何事件。它也可以用于 DOM 中所有内容加载的事件。为此有一个特殊内置的事件:DOMContentLoaded()
。这个事件可以用来处理 DOM 加载的事件,当事件被设置时,会在页面上的 DOM 构造完成后立即触发。以下是设置它的方法:
document.addEventListener("DOMContentLoaded", (e) => {
console.log(e);
});
这将在所有 DOM 内容加载完成后记录到控制台。作为替代,你也会经常在 body
标签中看到它,如下所示:
<body onload="unique()"></body>
这是在主体上分配一个名为 unique()
的函数,并且当主体加载完成后会触发。你不能通过将 addEventListener()
和 HTML 结合使用来组合它们。一个将会覆盖另一个,这取决于网页的顺序。如果你需要在 DOM 加载时发生两个事件,你需要在你的 JavaScript 中调用两个 addEventListener()
。
练习 11.3
使用一个基本的 HTML 文件,下面的练习将演示使用 DOMContentLoaded
事件来展示 window
对象和 document
对象的加载顺序,这是一个在浏览器中 document
对象内容加载后触发的事件。即使 window.onload
语句先出现,window
对象也会随后加载。
-
在一个基本的 HTML 文件中,创建一个名为
message
的函数,该函数需要两个参数,第一个是一个字符串值的消息,第二个是一个事件对象。在函数内部,使用console.log
将事件和消息输出到控制台。 -
使用
window
对象,将一个onload
函数附加到事件对象上。调用该函数,将字符串值Window Ready
和事件对象传递给message
函数以进行输出。 -
创建第二个函数来捕获 DOM 内容加载,并将一个监听
DOMContentLoaded
事件的监听器添加到document
对象上。一旦该事件被触发,将事件对象和字符串值Document Ready
传递给message
输出函数。 -
改变事件监听器的顺序,将
document
事件语句放在window onload
之前:这会影响输出吗? -
使用
document
对象,添加DOMContentLoaded
事件监听器,该监听器将Document Ready
和触发的事件对象作为参数传递给函数。 -
运行脚本并查看哪个事件首先被触发;改变事件的顺序以查看输出序列是否改变。
鼠标事件处理器
有不同的鼠标事件处理器。鼠标事件是鼠标的动作。这些是我们所拥有的:
-
ondblclick
:当鼠标双击时 -
onmousedown
:当鼠标点击在元素上而没有释放点击时 -
onmouseup
:当鼠标点击在元素上释放时 -
onmouseenter
:当鼠标移动到元素上时 -
onmouseleave
:当鼠标离开一个元素及其所有子元素时 -
onmousemove
:当鼠标移过元素时 -
onmouseout
:当鼠标离开单个元素时 -
onmouseover
:当鼠标悬停在元素上时
让我们看看这些在实际中的应用。你认为这会做什么?
<!doctype html>
<html>
<body>
<div id="divvy" onmouseover="changeColor()" style="width: 100px; height: 100px; background-color: pink;">
<script>
function changeColor() {
document.getElementById("divvy").style.backgroundColor = "blue";
}
</script>
</body>
</html>
如果你将鼠标移到粉红色的正方形(具有id
divvy
的div
元素)上,它会立即变成蓝色。这是因为 HTML 中添加了onmouseover
,并指向改变正方形颜色的 JavaScript 函数。
让我们看看一个类似的稍微复杂一点的例子。
<!doctype html>
<html>
<body>
<div id="divvy" style="width: 100px; height: 100px; background-color: pink;">
<script>
window.onload = function donenow() {
console.log("hi");
document.getElementById("divvy").addEventListener("mousedown", function() { changeColor(this, "green"); });
document.getElementById("divvy").addEventListener("mouseup", function() { changeColor(this, "yellow"); });
document.getElementById("divvy").addEventListener("dblclick", function() { changeColor(this, "black"); });
document.getElementById("divvy").addEventListener("mouseout", function() { changeColor(this, "blue"); });
}
console.log("hi2");
function changeColor(el, color) {
el.style.backgroundColor = color;
}
</script>
</body>
</html>
我们仍然从粉红色的正方形开始。这个div
上连接了四个事件监听器:
-
mousedown
:当鼠标按钮被按下但尚未释放时,正方形会变成绿色。 -
mouseup
:一旦鼠标按钮被释放,正方形会变成黄色。 -
dblclick
:这是最受欢迎的。你认为双击会发生什么?双击包含两个mousedown
事件和两个mouseup
事件。在第二个mouseup
之前,它不是双击。所以,正方形会变成绿色、黄色、绿色、黑色(然后保持黑色,直到另一个事件被触发)。 -
mouseout
:当鼠标离开正方形时,它会变成蓝色并保持蓝色,直到上述三个事件之一再次被触发。
这允许进行大量的交互。你可以用这个做很多事情。仅举一个例子,比如说你想要一个非常动态的、由mouseover
驱动的产品帮助决策工具。它将包含四列,最后三列(从右到左)的内容是动态内容。第一列是用于分类的。第二列包含每个分类的一些更具体的产品分类。第三列包含单个产品,第四列显示产品信息。这需要很多事件监听器,以及大量监听器的删除和添加。
练习 11.4
我们的目的是在页面上的元素上根据各种鼠标事件改变背景颜色。在元素上mousedown
时,元素会变成绿色。当鼠标悬停在元素上时,它会变成红色。当鼠标移出元素边界时,颜色会变成黄色。当鼠标点击时,颜色会变成绿色,当鼠标释放时,它会变成蓝色。这些动作也会在控制台中记录。
-
在页面上创建一个空白元素并给它分配一个类。
-
使用元素的类名选择元素。
-
将一个变量分配给页面上的元素对象。
-
更新元素的内容,使其显示
hello world
。 -
使用元素的样式属性,更新高度和宽度,然后给它添加一个默认的背景颜色。
-
创建一个函数来处理两个参数,第一个是一个字符串形式的颜色值,第二个是触发事件的的事件对象。
-
在函数中,将颜色值输出到控制台,对于事件,将事件类型输出到控制台。
-
向元素添加事件监听器:
mousedown
、mouseover
、mouseout
和mouseup
。对于这些事件中的每一个,将两个参数发送到您创建的函数:一个颜色值和事件对象。 -
运行代码并在您的浏览器中尝试它。
事件目标属性
每当事件被触发时,一个事件变量就会变得可用。它有许多属性,您可以通过在为事件触发的函数中使用此命令来检查它:
console.dir(event);
这将显示许多属性。目前最有趣的属性之一是 target
属性。目标是触发事件的 HTML 元素。因此,我们可以用它来获取网页上的信息。让我们看看一个简单的例子。
<!doctype html>
<html>
<body>
<button type="button" onclick="triggerSomething()">Click</button>
<script>
function triggerSomething() {
console.dir(event.target);
}
</script>
</body>
</html>
在这种情况下,event.target
是 button
元素。在控制台中,将记录 button
元素及其所有属性,包括潜在的兄弟元素和父元素。
当有多个输入字段和一个按钮的 HTML 表单时,父属性可能会很有用。表单中的按钮通常是其直接父元素。通过这个父元素,可以获取输入字段的值。以下是一个示例:
<!doctype html>
<html>
<body>
<div id="welcome">Hi there!</div>
<form>
<input type="text" name="firstname" placeholder="First name" />
<input type="text" name="lastname" placeholder="Last name" />
<input type="button" onclick="sendInfo()" value="Submit" />
</form>
<script>
function sendInfo() {
let p = event.target.parentElement;
message("Welcome " + p.firstname.value + " " + p.lastname.value);
}
function message(m) {
document.getElementById("welcome").innerHTML = m;
}
</script>
</body>
</html>
这将产生一个小表单,如下所示:
图 11.1:基本 HTML 表单
一旦您在字段中输入数据并点击 提交,它看起来就像这样:
图 11.2:带有动态欢迎信息的简单 HTML 表单
使用此命令,event.target[CIT]
是 HTML 按钮:
let p = event.target.parentElement;
按钮的父元素,在这个例子中是表单,被存储在 p
变量中。p
是父元素,它代表表单元素。因此,这个命令将获取输入框的值:
p.firstname.value;
类似地,p.lastname.value
获取姓氏。我们还没有看到这一点,但使用 value
属性,您可以获取输入元素的值。
接下来,将两个输入值合并并发送到 message()
函数。此函数将 div 的内部 HTML 更改为个性化的欢迎信息,这就是为什么 Hi there! 会变成 Welcome Maaike van Putten。
练习 11.5
在页面上更改 div 元素的文本。这个练习将演示您如何从输入字段获取值并将其放置在页面元素中。它还涵盖了跟踪按钮点击和事件目标的相关细节。您可以使用以下 HTML 文档作为模板,然后添加 JavaScript:
<!doctype html>
<html>
<head>
<title>JS Tester</title>
</head>
<body>
<div class="output"></div>
<input type="text" name="message" placeholder="Your Message">
<button class="btn1">Button 1</button>
<button class="btn2">Button 2</button>
<div>
<button class="btn3">Log</button>
</div>
<script>
</script>
</body>
</html>
采取以下步骤:
-
使用上述 HTML 作为模板,添加 JavaScript 代码,选择每个页面元素,包括
div
、输入字段和button
元素。将这些元素对象分配给您的代码中的变量。 -
创建一个名为
log
的空数组,它将用于跟踪和记录所有事件。 -
创建一个函数,该函数将事件对象详情捕获到对象中,并将其添加到
log
数组。获取事件目标和创建一个对象,将其添加到存储输入值时、事件类型、目标元素的类名和目标元素的标签名的数组中。 -
在日志函数内部,获取输入内容中的值并将其分配给
div
的textContent
。 -
在信息添加到
log
数组后清除div
内容。 -
将事件监听器添加到前两个按钮上,将事件对象发送到之前步骤中创建的跟踪函数。
-
将事件监听器附加到第三个按钮上,将日志内容输出到控制台。
DOM 事件流
让我们回顾一下当你点击一个与多个元素相关联的元素时会发生什么。
我们将创建嵌套的div
元素。为了说明这一点,我们在body
上添加了一个样式。实际上,最好在head
标签中添加这个样式,甚至更好的是有一个单独的 CSS 文件,但这样读起来更简洁。这就是嵌套div
元素的外观:
图 11.3:网页中的事件冒泡
以下是与之相关的代码。脚本在底部,将在上面的部分完成后执行。它将为每个div
添加事件监听器,并将执行的操作是记录innerText
。对于嵌套div
元素的最外层元素,这将显示12345
,每行一个新数字。
所以这里的问题是,它将如何触发事件?比如说我们点击5,会执行什么?所有嵌套div
元素的事件,还是只有5的事件?如果它要执行所有这些事件,是按照从内到外的事件顺序执行,还是相反?
<!DOCTYPE html>
<html>
<body>
<style>
div {
border: 1px solid black;
margin-left: 5px;
}
</style>
<div id="message">Bubbling events</div>
<div id="output">
1
<div>
2
<div>
3
<div>
4
<div>5</div>
</div>
</div>
</div>
</div>
<script>
function bubble() {
console.log(this.innerText);
}
let divs = document.getElementsByTagName("div");
for (let i = 0; i < divs.length; i++) {
divs[i].addEventListener("click", bubble);
}
</script>
</body>
</html>
在这种情况下,它具有默认行为。它将执行所有五个事件,所以每个嵌套div
都会执行。并且它是从内到外执行的。所以它将从只有5的innerText
开始,然后是45,直到最后一个,12345:
图 11.4:控制台输出事件冒泡
这被称为事件冒泡。当你在一个元素上触发处理器时,就会发生这种情况。它首先运行自己的事件,然后是父元素,依此类推。之所以称为冒泡,是因为它从内部事件向上传播到外部,就像水泡上升一样。
你可以通过在添加事件监听器时将true
作为第三个参数来改变这种行为:
divs[i].addEventListener("click", bubble, true);
这将是结果:
图 11.5:控制台输出事件捕获
这种从外部元素到内部元素的移动被称为事件捕获。现在不再经常使用,但如果你需要实现它,可以使用addEventListener()
的useCapture
参数(第三个参数)并将其设置为true
。默认情况下是false
。
事件捕获和冒泡允许我们应用一个称为 事件委托 的原则。事件委托的概念是,我们不是在 HTML 块中的每个元素上添加事件处理器,而是定义一个包装器并将事件添加到这个包装器元素,然后它也适用于所有子元素。你将在下一个练习中应用这个原则。
练习 11.6
此示例将演示事件捕获和页面元素的委托。通过在主元素内部添加事件监听器,此示例将根据事件捕获属性对控制台消息进行排序。
所有具有 class
为 box
的 div
元素将具有相同的事件对象。我们可以将事件目标、textcontent
也添加到控制台,以便我们可以知道哪个元素被点击。
使用以下模板:
<!doctype html>
<html>
<head>
<title>JS Tester</title>
<style>
.box {
width: 200px;
height: 100px;
border: 1px solid black
}
</style>
</head>
<body>
<div class="container">
<div class="box" id="box0">Box #1</div>
<div class="box" id="box1">Box #2</div>
<div class="box" id="box2">Box #3</div>
<div class="box" id="box3">Box #4</div>
</div>
<script>
</script>
</body>
</html>
按照以下步骤操作:
-
在 JavaScript 代码中,选择所有具有类别的元素,并单独选择主容器元素。
-
向主容器添加事件监听器,当
useCapture
参数设置为false
时输出控制台值4
,当useCapture
参数设置为true
时输出1
。 -
对于每个嵌套元素,添加
click
事件监听器,当useCapture
参数设置为false
时console.log()
值为3
,当useCapture
参数设置为true
时值为2
。 -
点击页面元素以查看页面上的事件委托和输出顺序。
-
在
click
事件中,向盒元素添加到控制台的输出,输出事件目标的textContent
值。
onchange
和 onblur
另外两个常与输入框结合使用的事件是 onchange
和 onblur
。onchange
在元素改变时被触发,例如,当输入框的值改变时。onblur
在对象失去焦点时被触发;例如,当你有一个输入框的光标并且光标移动到另一个输入框时,第一个输入框的 onblur
事件将被触发。
onblur and onchange, and there is an extra function.
<!DOCTYPE html>
<html>
<body>
<div id="welcome">Hi there!</div>
<form>
<input type="text" name="firstname" placeholder="First name" onchange="logEvent()" />
<input type="text" name="lastname" placeholder="Last name" onblur="logEvent()" />
<input type="button" onclick="sendInfo()" value="Submit" />
</form>
<script>
function logEvent() {
let p = event.target;
if (p.name == "firstname") {
message("First Name Changed to " + p.value);
} else {
message("Last Name Changed to " + p.value);
}
}
function sendInfo() {
let p = event.target.parentElement;
message("Welcome " + p.firstname.value + " " + p.lastname.value);
}
function message(m) {
document.getElementById("welcome").innerHTML = m;
}
</script>
</body>
</html>
firstname
输入框有一个 onchange
事件。如果输入框中的数据值发生变化,此事件将在输入框失去焦点时立即触发。如果输入框在值未改变时失去焦点,则 onchange
不会发生任何操作。这并不适用于分配给 lastname
输入框的 onblur
,即使值没有改变,事件也会被触发。
另一个常与输入框一起使用的事件是 onfocus
,或者当与事件监听器结合使用时简称为 focus
。此事件与光标进入输入框相关联,并且当输入框被光标聚焦并且可以输入时,此事件将被触发。
练习 11.7
在页面上有两个输入字段时,JavaScript 会监听输入字段内容的变化。一旦输入字段不再聚焦,如果值已经改变,将调用改变事件。blur
和 focus
也会添加到输入字段,并在这些事件发生时记录到控制台。两个输入元素将具有相同的事件监听器,当你更改输入字段的 内容并移除焦点时,输出文本内容将使用触发事件的输入字段值更新。
使用以下 HTML 模板:
<!doctype html>
<html>
<head>
<title>JS Tester</title>
</head>
<body>
<div class="output1">
</div>
<input type="text" placeholder="First Name" name="first"><br>
<input type="text" placeholder="Last Name" name="last"><br>
<script>
</script>
</body>
</html>
现在按照以下步骤操作:
-
在 JavaScript 代码中,将 HTML 输出元素放入一个变量对象中,这样你就可以在页面上显示内容了。
-
选择两个输入字段。你可以使用
querySelector()
和"input[name='first']"
,这将允许你通过输入字段名称进行选择。 -
将事件监听器添加到第一个输入和第二个输入。事件监听器应该是一个改变事件,用于跟踪更改的值。这只会在你点击离开输入字段时调用。
-
创建一个单独的函数来处理内容的输出到页面,更新输出元素的
textContent
。 -
将输入字段的值在它们改变时发送到输出元素
textContent
。 -
添加四个额外的事件监听器,并监听每个输入的
blur
和focus
事件。一旦事件被触发,就在控制台输出事件类型值。
关键事件处理器
有几个关键事件。其中之一是 onkeypress
。onkeypress
会在按下键时触发,嗯,你可能已经猜到了,按下意味着在这里按钮被按下然后释放。如果你想事件在按下按钮(在释放之前)立即发生,你可以使用 onkeydown
事件。如果你想事件在释放时发生,你可以使用 onkeyup
事件。
我们可以用关键事件做很多事情。例如,我们可以限制在输入框中可以输入的字符。每次按键时,我们都可以检查字符并决定是否保留它。
我们可以使用以下方式获取触发事件的键:
event.key;
以下 HTML 代码中有两个输入框。你能看到这里发生了什么吗?
<!doctype html>
<html>
<body>
<body>
<div id="wrapper">JavaScript is fun!</div>
<input type="text" name="myNum1" onkeypress="numCheck()">
<input type="text" name="myNum2" onkeypress="numCheck2()">
<script>
function numCheck() {
message("Number: " + !isNaN(event.key));
return !isNaN(event.key);
}
function numCheck2() {
message("Not a number: " + isNaN(event.key));
return isNaN(event.key);
}
function message(m) {
document.getElementById('wrapper').innerHTML = m;
}
</script>
</body>
</html>
第一个检查值是否为数字,如果是数字,它将在顶部写 Number: true
;否则,它将在顶部写 Number: false
。第二个是检查值是否不是数字;如果不是数字,它将在顶部写 Not a number: true
;否则,它将在顶部写 Not a number: false
。
这就是使用 onkeypress
事件的一种方法,但我们还能做得更多。我们可以在 onkeypress
事件中添加一个 return
语句,如下所示:
onkeypress="return numCheck2()";
如果返回 true
,则键值被添加到输入框中;如果返回 false
,则键值不会被添加。
以下代码片段只允许在输入框中输入数字。每当用户尝试输入其他内容时,该函数会限制它。
<!doctype html>
<html>
<body>
<body>
<div id="wrapper">JavaScript is fun!</div>
<input type="text" name="myNum1" onkeypress="return numCheck()" onpaste="return false">
<input type="text" name="myNum2" onkeypress="return numCheck2()" onpaste="return false">
<script>
function numCheck() {
message(!isNaN(event.key));
return !isNaN(event.key);
}
function numCheck2() {
message(isNaN(event.key));
return isNaN(event.key);
}
function message(m) {
document.getElementById("wrapper").innerHTML = m;
}
</script>
</body>
</html>
如您所见,return
被包含在 onkeypress 中,以确保只能输入数字。还有一件事可能引起了您的注意:onpaste="return false"
。这是为了处理那些复制粘贴数字到非数字字段或字符到数字字段,但仍能成功输入非法字符的聪明人。
练习 11.8
通过识别按键和检测当元素获得焦点时字符的按键值,我们还可以检测内容是否被粘贴到输入字段中。
-
在你的 HTML 中创建两个输入字段。添加一个元素以输出内容。
-
使用 JavaScript 选择页面元素。你可以将一个名为
output
的变量分配给具有output
类的元素。创建另一个变量eles
,并使用querySelectorAll()
选择所有输入字段作为其值。这样,我们可以遍历节点列表并将相同的事件分配给所有匹配的元素。 -
使用
forEach()
遍历页面上的所有输入元素。将相同的事件监听器添加到所有这些元素上。 -
添加一个 keydown 事件监听器并检查值是否为数字。如果是数字,则将其添加到输出区域。
-
在
keyup
事件中,将按键值输出到控制台。 -
检查输入字段中是否有粘贴操作;如果有,则可以将单词
paste
输出到控制台。
拖放元素
对于拖放也有特殊的事件处理器。我们需要一个起点来能够拖放某个东西。让我们创建一个拖放区域的 CSS 和 HTML。
<!doctype>
<html>
<head>
<style>
.box {
height: 200px;
width: 200px;
padding: 20px;
margin: 0 50px;
display: inline-block;
border: 1px solid black;
}
#dragme {
background-color: red;
}
</style>
</head>
<body>
<div class="box">1</div>
<div class="box">2</div>
</body>
</html>
现在我们还将包括一个将要被拖放和放下的元素。为了标记一个元素为可拖动,我们需要添加draggable
属性。这是我们将在第二个 div 中包含的代码,第一个 div 围绕它:
<div class="box"> 1
<div id="dragme" draggable="true">
Drag Me Please!
</div>
</div>
然后我们需要决定当我们将可拖动元素放下时,我们将要做什么。我们需要在它可以被拖动的框内指定这一点。我们将向两个框都添加功能,以便它可以从一个拖动到另一个,然后再回到第一个。
<div class="box" ondrop="dDrop()" ondragover="nDrop()">
1
<div id="dragme" ondragstart="dStart()" draggable="true">
Drag Me Please!
</div>
</div>
<div class="box" ondrop="dDrop()" ondragover="nDrop()">2</div>
以下是将被添加到 body 末尾的脚本:
<script>
let holderItem;
function dStart() {
holderItem = event.target;
}
function nDrop() {
event.preventDefault();
}
function dDrop() {
event.preventDefault();
if (event.target.className == "box") {
event.target.appendChild(holderItem);
}
}
</script>
我们首先在脚本中指定一个变量,用于在拖动时保存的项目。当ondragstart
事件被触发时,我们将把正在拖动的元素存储在holderItem
变量中。通常,当你拖动时,由于 HTML 的设计,放下是不允许的。为了允许放下,你需要阻止默认事件,这意味着你想要放下的项目不能被放下。你可以通过以下方式做到这一点:
event.preventDefault();
通常,在你阻止默认行为之前,你会做一些检查,看看被拖动的元素是否可以放置在那个位置。在上面的例子中,我们检查被拖动的元素的类名是否为 box
。如果是这样,我们将 holderItem
作为子元素添加到盒子中。
我们创建了一个页面,允许将 HTML 元素从一个盒子移动到另一个盒子。如果你尝试在其他任何地方释放它,元素将返回到其原始位置。
练习 11.9
这将是一个“我不是机器人”的检查。拖放可以用来确保是在页面上操作的是活生生的人,而不是机器人。这个练习将演示如何在一个活动元素上创建一个视觉拖动效果,其中用户按下鼠标以创建拖动动作,一旦鼠标按钮释放,就会发生放下事件。成功操作将被记录到控制台。
你可以使用以下模板:
<!doctype html>
<html>
<head>
<title>JS Tester</title>
<style>
.box {
width: 100px;
height: 100px;
border: 1px solid black;
background-color: white;
}
.red {
background-color: red;
}
</style>
</head>
<body>
<div class="box">1
<div id="dragme" draggable="true">
Drag Me Please!
</div>
</div>
<div class="box">2</div>
<script>
</script>
</body>
</html>
前面的 HTML 为用于放置的元素创建样式,并设置了宽度、高度和边框。它创建了一个名为 red
的另一个类,并将红色背景添加到活动元素,以便显示为活动状态,以及两个具有 box
元素类别的 div
元素,用于放置。最后,我们在其中一个盒子中创建了一个嵌套的 div
,其 id
为 dragme
,并将 draggable
属性设置为 true
,添加了一些指导性文本以帮助用户。通过以下步骤完成脚本:
-
在你的 JavaScript 代码中将可拖动元素作为对象选择。
-
添加一个
dragstart
事件的监听器,其中它将可拖动元素的透明度更新为0.5
。 -
添加另一个
dragend
事件的监听器,移除不透明度的值。 -
使用
querySelectorAll()
选择所有放下盒子。 -
向所有放下盒子添加事件监听器,设置当用户触发
dragenter
事件时向元素添加red
类。这将向用户指示操作正在进行。 -
设置
dragover
,向元素添加preventDefault()
方法以禁用可能已经存在的任何动作。 -
在
dragleave
事件中,移除red
类。 -
将
drop
事件的监听器添加到盒子中,将可拖动元素添加到事件目标。 -
要以相同的方式作用于所有元素,移除元素的默认行为。你可以使用
preventDefault()
方法来禁用可能已经存在的任何动作。 -
你可以在这些事件中的任何一个上添加控制台日志消息,以更好地跟踪它们。
表单提交
当表单提交时,可以触发一个事件。这可以通过不同的方式实现,其中之一是在 form
元素上添加 onsubmit
属性。
<form onsubmit="doSomething()">
列出的函数将在提交 submit
类型的输入时被触发。
<input type="submit" value="send">
我们可以对form
元素的 HTML 做更多的事情;例如,我们可以重定向到另一个页面。我们必须指定使用method
属性发送表单值的方式,以及使用action
属性指定的页面位置。
<form action="anotherpage.html" method="get" onsubmit="doStuff()">
目前不必担心get
;这仅仅意味着值通过 URL 发送。当你使用get
时,URL 看起来是这样的:
www.example.com/anotherpage.html?name=edward
在问号之后,显示的是随 URL 发送的变量,以键值对的形式出现。这是当edward
被插入name
时创建 URL 的表单形式。
<!doctype html>
<html>
<body>
<form action="anotherpage.html" method="get">
<input type="text" placeholder="name" name="name" />
<input type="submit" value="send" />
</form>
</body>
</html>
anotherpage.html
可以使用 URL 中的变量。这可以在anotherpage.html
的 JavaScript 中这样做:
<!doctype html>
<html>
<body>
<script>
let q = window.location.search;
let params = new URLSearchParams(q);
let name = params.get("name");
console.log(name);
</script>
</body>
</html>
到目前为止,我们一直在使用action
和onsubmit
属性提交表单。action
会重定向到另一个位置。这可能是不同页面的 API 端点。onsubmit
指定了当表单提交时触发的事件。
我们还可以使用表单的onsubmit
事件做更多的事情。还记得onkeypress
中的return
用法吗?我们在这里可以做到类似的事情!如果我们让调用的函数返回一个布尔值,那么只有当布尔值为true
时,表单才会提交。
如果我们想在发送表单之前进行一些表单验证,这将非常有用。看看这段代码,看看你是否能弄清楚何时可以提交。
<!doctype html>
<html>
<body>
<div id="wrapper"></div>
<form action="anotherpage.html" method="get" onsubmit="return valForm()">
<input type="text" id="firstName" name="firstName" placeholder="First name" />
<input type="text" id="lastName" name="lastName" placeholder="Last name" />
<input type="text" id="age" name="age" placeholder="Age" />
<input type="submit" value="submit" />
</form>
<script>
function valForm() {
var p = event.target.children;
if (p.firstName.value == "") {
message("Need a first name!!");
return false;
}
if (p.lastName.value == "") {
message("Need a last name!!");
return false;
}
if (p.age.value == "") {
message("Need an age!!");
return false;
}
return true;
}
function message(m) {
document.getElementById("wrapper").innerHTML = m;
}
</script>
</body>
</html>
此表单包含三个输入字段和一个输入按钮。这些字段是用于姓氏、名字和年龄的。当其中之一缺失时,表单将不会提交,因为函数将返回false
。还会在表单上方的div
中添加一条消息,解释出了什么问题。
练习 11.10
这将涉及创建一个表单验证器。在这个练习中,你需要检查确保所需的值被输入到输入字段中。代码将检查用户输入的输入值,以匹配那些字段值的预定条件。
-
设置一个表单,在内部添加三个输入字段:
First
、Last
和Age
。添加一个提交按钮。 -
在 JavaScript 代码中,将表单选择为一个元素对象。
-
向表单添加一个提交事件监听器。
-
将
error
的默认值设置为false
。 -
创建一个名为
checker()
的函数,该函数将检查字符串的长度并将字符串长度输出到控制台。 -
为每个字段值添加条件,首先检查值是否存在,如果响应为
false
,则返回错误,然后在将错误变量更改为true
之前。 -
使用
console.log()
记录有关错误的详细信息。 -
对于年龄输入值,检查提供的年龄是否为 19 岁或以上,否则引发错误。
-
验证结束后,检查
error
是否为true
;如果是,使用preventDefault()
停止表单提交。将错误记录到控制台。
动画元素
最后,我们想向你展示你可以使用 HTML、CSS 和 JavaScript 进行动画处理。这使我们能够用我们的网页做更多酷的事情。例如,我们可以将动画作为事件触发。这可以用于许多不同的目的,例如,说明解释,将用户的注意力吸引到某个位置,或者玩游戏。
让我们给你一个非常基本的例子。我们可以使用position
键,并在 CSS 中将其设置为absolute
。这使得元素的位置相对于其最近的定位父元素。这里,那将是 body。这是当点击按钮时从左到右移动的紫色方块的代码。
<!doctype html>
<html>
<style>
div {
background-color: purple;
width: 100px;
height: 100px;
position: absolute;
}
</style>
<body>
<button onclick="toTheRight()">Go right</button>
<div id="block"></div>
<script>
function toTheRight() {
let b = document.getElementById("block");
let x = 0;
setInterval(function () {
if (x === 600) {
clearInterval();
} else {
x++;
b.style.left = x + "px";
}
}, 2);
}
</script>
</body>
</html>
我们需要给div
块一个绝对位置,因为我们依赖于 CSS 的left
属性来移动它。为了在某个东西的左边,那个东西必须是绝对的,否则left
属性不能相对于它定位。在这种情况下,我们需要在div
的左边一定数量的像素处;这就是为什么我们需要将div
的位置设置为绝对,这样移动框的位置就可以相对于其父元素的位置了。
当我们点击按钮时,会触发toTheRight()
函数。这个函数获取block
并将其存储在b
中。将x
设置为0
。然后我们使用一个非常强大的内置 JavaScript 函数:setInterval()
。这个函数会持续评估一个表达式,直到clearInterval()
被调用。当x
,即我们距离左边的测量值达到 600 时,它会这样做。它每 2 毫秒重复一次,这使得它看起来像在滑动。
你同时也可以设置不同的位置,如style.top
、style.bottom
和style.right
,或者添加新元素来创建雪花效果,或者显示不断行驶的汽车。有了这个工具箱,天空就是极限。
练习第 11.11 节练习题。
在这里,我们将点击紫色方块,并观察它在页面上移动。这个练习将演示在页面上创建一个简单交互元素的事件的创建。紫色方块每次被点击时都会移动;一旦它达到页面的边界,它将根据它撞击的侧面从左到右或从右到左改变方向。
-
设置元素的样式,在设置
position
为absolute
之前设置height
和width
。 -
创建一个你想要在页面上移动的元素。
-
使用 JavaScript 选择并存储元素。
-
设置一个包含
speed
、direction
和position
值的对象。 -
添加一个事件监听器,以便在元素被点击时触发。
-
将间隔计数器的默认值设置为
30
。 -
如果计数器小于 1,则结束间隔并清除它。
-
一旦间隔运行了 30 次使用
x
的值,元素将静止并等待再次被点击。 -
在运动过程中,检查位置值是否大于 800 或小于 0,这意味着需要改变方向。
direction
值将提供运动的方向。如果运动将盒子移出容器的边界,我们需要将其送回另一个方向。这可以通过乘以负一来实现。如果值是正的,它将变成负的,将元素送向左边。如果值是负的,它将变成正的,将元素送向右边。 -
更新元素的
style.left
位置值,分配更新后的位置值。添加px
,因为分配给样式的值是一个字符串。 -
将结果输出到控制台。
章节项目
构建你自己的分析。
确定页面中点击了哪些元素,并记录它们的 ID、标签和类名。
-
在你的 HTML 中创建一个主容器元素。
-
在主元素内部添加四个元素,每个元素都有一个
class
为box
的类和一个唯一的 ID 以及唯一的文本内容。 -
设置你的 JavaScript 代码以包含一个数组,你可以使用它来跟踪,将每次点击的详细信息添加到其中。
-
在你的代码中将主容器元素作为变量对象选择。
-
添加一个事件监听器来捕获对元素的点击。
-
创建一个处理点击的函数。从事件对象中获取目标元素。
-
检查元素是否有 ID,这样你就不会跟踪主容器上的点击。
-
设置一个对象来跟踪值;包括元素的
textContent
、id
、tagName
和className
。 -
将临时对象添加到你的跟踪数组中。
-
将你在跟踪数组中捕获的值输出到你的控制台。
星级评分系统
使用 JavaScript 创建一个完全交互和动态的星级评分组件。
图 11.6:创建星级评分系统
你可以使用以下 HTML 和 CSS 作为起始模板。提供完成的脚本元素作为你的答案。
<!DOCTYPE html>
<html>
<head>
<title>Star Rater</title>
<style>
.stars ul {
list-style-type: none;
padding: 0;
}
.star {
font-size: 2em;
color: #ddd;
display: inline-block;
}
.orange {
color: orange;
}
.output {
background-color: #ddd;
}
</style>
</head>
<body>
<ul class="stars">
<li class="star">✭</li>
<li class="star">✭</li>
<li class="star">✭</li>
<li class="star">✭</li>
<li class="star">✭</li>
</ul>
<div class="output"></div>
<script>
</script>
</body>
</html>
执行以下步骤:
-
将
ul
中所有具有class
为stars
的星星选择为一个对象,并为output
元素创建另一个对象。 -
创建另一个对象来包含对具有
class
为star
的元素调用querySelectorAll()
的结果。 -
遍历结果节点列表,将索引值加 1 的值添加到元素对象中,并添加一个监听点击的事件监听器。将名为
starRate()
的函数附加到每个star
元素的click
事件上。 -
在
starRate()
函数中,将使用事件目标和元素对象的星值(在之前步骤中设置)的值添加到输出中。 -
使用
forEach()
遍历所有星星,检查星星元素的索引值是否小于星星值;如果是,则应用class
为orange
。否则,从classList
元素中移除class
为orange
。
鼠标位置跟踪器
跟踪鼠标在元素内的 x
和 y
位置。当鼠标在元素内移动时,x
位置和 y
位置值将更新。
-
创建一个页面元素并向其添加尺寸,包括
height
和width
。创建一个名为active
的类样式,其background-color
属性为red
。最后,创建一个包含文本的输出元素。 -
选择主要的容器元素并向其添加事件监听器。监听
mouseover
、mouseout
和mousemove
事件。 -
在
mouseover
时添加active
类,在mouseout
时移除active
类。 -
在
mousemove
时调用一个函数,该函数跟踪事件元素的clientX
和clientY
位置,将它们嵌入到可读句子中,并将输出输出到输出元素。
盒子点击速度测试游戏
此处的目标是尽可能快地点击出现的红色框。该框将在容器内随机放置,并使用随机值定位。该框将有一个事件监听器,用于跟踪开始和点击时间,以计算点击事件的持续时间。您可以使用以下模板,因为这里的 HTML 有点复杂——只需添加 <script>
元素使 HTML 具有交互性!
<!DOCTYPE html>
<html>
<head>
<title>Click Me Game</title>
<style>
.output {
width: 500px;
height: 500px;
border: 1px solid black;
margin: auto;
text-align: center;
}
.box {
width: 50px;
height: 50px;
position: relative;
top: 50px;
left: 20%;
background-color: red;
}
.message {
text-align: center;
padding: 10px;
font-size: 1.3em;
}
</style>
</head>
<body>
<div class="output"></div>
<div class="message"></div>
<script>
</script>
</body>
</html>
使用 JavaScript 处理上述 HTML 代码。
-
有两个
div
元素,一个具有output
类用于游戏区域,另一个具有message
类用于向玩家提供说明。使用 JavaScript 选择这些主要元素作为对象。 -
使用 JavaScript,在
output
元素内创建另一个元素,并创建一个div
作为主要可点击对象。将名为box
的样式附加到新元素,并将其附加到output
元素。 -
使用 JavaScript 向信息区域添加用户说明:
按开始
。他们需要点击新创建的具有box
类的div
以开始游戏。 -
创建一个名为
game
的全局对象来跟踪计时器和起始时间值。这将用于计算元素显示和玩家点击之间的持续时间(以秒为单位)。将start
设置为null
。 -
创建一个函数,该函数将生成一个随机数并返回一个随机值,参数是要使用的最大值。
-
向
box
元素添加事件监听器。一旦点击,这应该开始游戏。将box
元素的显示设置为none
。使用setTimeout()
方法调用名为addBox()
的函数,并将超时设置为随机毫秒值。根据需要调整;这将是在点击对象框显示和消失之间的时间。如果起始值是null
,则向加载信息区域添加文本内容。 -
如果起始值存在,则使用当前
date
对象的getTime()
获取 Unix 时间值,从当前时间值中减去游戏开始时间,然后除以 1,000 以获得秒值。将结果输出到message
元素,以便玩家可以看到他们的分数。 -
创建一个函数来处理点击事件,当计时器结束时添加盒子。更新消息的文本内容为
点击它……
。将游戏的start
值设置为当前时间的毫秒数。将block
样式应用于元素,使其在页面上显示。 -
从可用空间(500 个总容器宽度减去 50 个盒子宽度)设置一个随机位置,在元素的上方和左侧使用
Math.random()
。 -
玩游戏并根据需要更新样式。
自我检查测验
-
你在哪里可以找到
window.innerHeight
和window.innerWidth
? -
preventDefault()
的作用是什么?
摘要
在本章中,我们处理了相当多的主题,以增加网页的交互性。我们看到了指定事件的不同方式,然后更详细地探讨了不同的事件处理器。当指定的元素(通常是document
对象)完全加载时,会触发onload
事件处理器。这非常适合包裹其他功能,因为它避免了选择尚未存在的 DOM 内容。
我们还看到了鼠标事件处理器,用于响应在网页上可以使用鼠标做的所有不同的事情。所有这些事件处理器的使用非常相似,但它们各自启用与用户的不同类型的交互。我们还看到,我们可以通过调用event.target
来访问触发事件的元素。这个属性持有触发事件的元素。
我们还更详细地探讨了onchange
、onblur
和键事件处理器。之后,我们看到了如何在表单提交时触发交互。我们研究了 HTML 的action
属性,它重定向提交的处理,以及onsubmit
事件,它处理表单提交。然后我们看到了我们可以使用所有这些事件做一些事情,例如在页面上指定拖放和动画元素。
在下一章中,我们将继续探讨一些更高级的主题,这将把你的现有 JavaScript 技能提升到新的水平!
第十二章:中级 JavaScript
本书到目前为止所提出的概念和解决方案方法并不是解决问题的关键途径。在本章中,我们将挑战你深入思考,保持好奇心,并练习优化解决方案的良好习惯。
在前面的章节中,我们承诺在本章中会有很多精彩的内容,因为一些内置方法的最佳使用需要了解正则表达式,我们将在本章中介绍。还有更多有趣的内容——以下是我们将要涵盖的主题列表:
-
正则表达式
-
函数和参数对象
-
JavaScript 提升
-
严格模式
-
调试
-
使用 cookie
-
本地存储
-
JSON
如你所见,一系列多样化的主题,但都是高级且有趣的。本章中的部分内容并不像你现在可能已经习惯的那样相互关联。它们大多是独立的话题,可以帮助真正增强你的理解,并大大提高你的 JavaScript 知识。
注意:练习、项目和自我检查测验的答案可以在附录中找到。
正则表达式
正则表达式,也称为regex,只是描述文本模式的一种方式。你可以把它们看作是更高级的字符串。存在不同的正则表达式实现。这意味着根据解释器的不同,正则表达式的写法可能会有所不同。然而,它们在一定程度上是标准化的,所以你几乎可以用相同的方式为所有版本的正则表达式编写它们。我们将使用正则表达式进行 JavaScript 编程。
正则表达式在许多情况下非常有用,例如当你需要在大文件中查找错误或检索用户正在使用的浏览器代理时。它们也可以用于表单验证,因为使用正则表达式,你可以指定字段条目(如电子邮件地址或电话号码)的有效模式。
正则表达式不仅用于查找字符串,还可以用于替换字符串。到目前为止,你可能认为,“正则表达式真是太神奇了,但有没有什么陷阱呢?”是的,不幸的是,确实有陷阱。一开始,正则表达式可能看起来就像你邻居的猫走过了你的键盘,不小心输入了一些随机的字符。例如,这个正则表达式检查有效的电子邮件:
/([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+)/g
不要害怕,在本节之后,你将能够解读正则表达式中的秘密模式。我们不会详细介绍正则表达式的所有内容,但我们将建立一个坚实的基础,这将使你能够使用它们,并在实践中扩展你的知识。
让我们从简单开始。正则表达式模式指定在两个斜杠之间。这是一个有效的正则表达式表达式:
/JavaScript/
上述表达式将匹配如果给定的字符串包含单词JavaScript
。当它匹配时,这意味着结果是正的。这可以用来做很多事情。
我们可以使用 JavaScript 内置的 match()
函数来做这件事。这个函数返回正则表达式在结果(如果有)上的匹配(如果有的话),以匹配字符串的起始位置和输入字符串的子字符串形式。
实际上还有其他使用正则表达式的内置函数,但我们会稍后介绍。match()
只是一个方便的函数,用来演示正则表达式是如何工作的。你可以在下面看到它的实际应用:
let text = "I love JavaScript!";
console.log(text.match(/javascript/));
这记录了 null
,因为它默认是大小写敏感的,因此不匹配。如果我们搜索 /ava/
或简单地搜索 /a/
,它就会匹配,因为它包含 ava
和 a
。如果你想让它不区分大小写,你可以在斜杠后指定一个 i
。在这个不区分大小写的例子中,表达式将匹配前面的字符串:
console.log(text.match(/javascript/i));
这实际上会记录结果,因为它现在是大小写不敏感的,从这个角度来看,我们的字符串确实包含 javascript
。以下是结果:
[
'JavaScript',
index: 7,
input: 'I love JavaScript!',
groups: undefined
]
结果是一个对象,包含找到的匹配项及其开始索引,以及被检查的输入。组是未定义的。你可以使用圆括号创建组,就像我们在关于组的章节中看到的那样。
你经常可以在 JavaScript 中找到正则表达式与字符串的内置搜索和替换方法的结合,我们将在下一节中介绍。
指定多个单词选项
为了指定一定范围的选项,我们可以使用这种语法:
let text = "I love JavaScript!";
console.log(text.match(/javascript|nodejs|react/i));
在这里,表达式匹配 javascript
、nodejs
或 react
。到目前为止,我们只匹配第一次出现,然后退出。所以这现在不会找到两个或更多的匹配项——它将输出与之前相同的内容:
let text = "I love React and JavaScript!";
console.log(text.match(/javascript|nodejs|react/i));
它记录了以下内容:
[
'React',
index: 7,
input: 'I love React and JavaScript!',
groups: undefined
]
如果我们想要找到所有匹配项,我们可以指定全局修饰符 g
。它与我们在大小写不敏感搜索中所做的是非常相似的。在这个例子中,我们正在检查所有匹配项,并且它是大小写不敏感的。所有修饰符都在最后一个斜杠之后。你可以像我们下面这样做,同时使用多个修饰符,或者你可以决定只使用 g
:
let text = "I love React and JavaScript!";
console.log(text.match(/javascript|nodejs|react/gi));
这返回了 React
和 JavaScript
作为结果:
[ 'React', 'JavaScript' ]
如你所见,结果现在看起来非常不同。一旦你指定了 g
,匹配函数将只返回一个匹配单词的数组。在这种情况下,这并不太令人兴奋,因为这些正是我们要求的单词。但与更复杂的模式相比,这可能会更令人惊讶。这正是我们接下来要学习的。
字符选项
到目前为止,我们的表达式相当易于阅读,对吧?字符选项是事情开始看起来,嗯,很复杂的地方。比如说我们想要搜索一个只包含一个字符等于 a
、b
或 c
的字符串。我们会这样写:
let text = "d";
console.log(text.match(/[abc]/));
这将返回 null
,因为 d
不是 a
、b
或 c
。我们可以这样包含 d
:
console.log(text.match(/[abcd]/));
这将记录以下内容:
[ 'd', index: 0, input: 'd', groups: undefined ]
由于这是一个字符范围,我们可以将其写得更短,就像这样:
let text = "d";
console.log(text.match(/[a-d]/));
如果我们想要任何字母,无论是大写还是小写,我们会这样写:
let text = "t";
console.log(text.match(/[a-zA-Z]/));
我们实际上也可以使用不区分大小写的修饰符来实现相同的效果,但这将应用于整个正则表达式模式,而你可能只需要它应用于特定的字符:
console.log(text.match(/[a-z]/i));
我们将在这两个前面的选项上得到匹配。如果我们还想包括数字,我们将写:
console.log(text.match(/[a-zA-Z0-9]/));
如你所见,我们只需连接范围来指定一个字符,就像我们可以为特定字符的可能选项连接起来一样,例如[abc]
。上面的例子指定了三个可能的范围。它将匹配任何从 a 到 z 的小写或大写字母以及所有数字字符。
这并不意味着它只能匹配一个字符的字符串;在这种情况下,它只会匹配第一个匹配的字符,因为我们没有添加全局修饰符。然而,这些特殊字符不会匹配:
let text = "äé!";
console.log(text.match(/[a-zA-Z0-9]/));
为了解决复杂字符不匹配表达式的问题,点在正则表达式中作为特殊通配符使用,可以匹配任何字符。那么你认为这会做什么?
let text = "Just some text.";
console.log(text.match(/./g));
由于它具有全局修饰符,它将匹配任何字符。这是结果:
[
'J', 'u', 's', 't',
' ', 's', 'o', 'm',
'e', ' ', 't', 'e',
'x', 't', '.'
]
但如果你只想找到点字符本身的匹配呢?如果你想使特殊字符(在正则表达式中用于指定模式的字符)具有普通含义,或者使普通字符具有特殊含义,你可以使用反斜杠来转义它:
let text = "Just some text.";
console.log(text.match(/\./g));
在这个例子中,我们通过添加前导反斜杠来转义点。因此,它不作为通配符使用,它将寻找字面匹配。这就是它将返回的内容:
[ '.' ]
有些普通字符在它们前面加上反斜杠后会获得特殊含义。我们不会深入探讨它们,但让我们看看一些例子:
let text = "I'm 29 years old.";
console.log(text.match(/\d/g));
如果我们转义d
,\d
,它匹配任何数字。我们正在进行全局搜索,所以它将指定任何数字。这是结果:
[ '2', '9' ]
我们也可以转义s
,\s
,它匹配所有空白字符:
let text = "Coding is a lot of fun!";
console.log(text.match(/\s/g));
上述例子将只返回几个空格,但制表符和其他类型的空白也被包括在内:
[ ' ', ' ', ' ', ' ', ' ' ]
非常有用的一种是\b
,它只在单词的开头匹配文本。所以,在下面的例子中,它不会匹配beginning
中的in
实例:
let text = "In the end or at the beginning?";
console.log(text.match(/\bin/gi));
这就是它最终记录的内容:
[ 'In' ]
尽管你可以检查字符是否为数字,但match()
方法属于string
对象,所以你在数值变量上实现它。例如,尝试以下操作:
let nr = 357;
console.log(nr.match(/3/g));
你应该收到一个TypeError
,表示nr.match()
不是一个函数。
分组
有很多理由将正则表达式分组。无论何时你想匹配一组字符,你都可以用括号将它们括起来。看看这个例子:
let text = "I love JavaScript!";
console.log(text.match(/(love|dislike)\s(javascript|spiders)/gi));
在这里,它将寻找love
或dislike
,后面跟着一个空格字符,然后是javascript
或spiders
,并且它会匹配所有出现的情况,而忽略它们是大写还是小写。这就是它将记录的内容:
[ 'love JavaScript' ]
就说我们在这里可以匹配大约四种组合。其中两种对我来说个人感觉更有意义:
-
喜爱蜘蛛
-
不喜欢蜘蛛
-
喜爱 JavaScript
-
不喜欢 JavaScript
当我们知道如何重复它们时,组非常强大。让我们看看如何做到这一点。你经常会发现自己需要重复某个正则表达式片段。我们为此有几个选项。例如,如果我们想匹配一个序列中的任何四个字母数字字符,我们可以简单地写下这个:
let text = "I love JavaScript!";
console.log(text.match(/[a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9]/g));
这将产生以下输出:
[ 'love', 'Java', 'Scri' ]
这是一个关于重复的糟糕方法:让我们看看更好的选项。如果我们只想让它出现 0 次或 1 次,我们可以使用问号。所以这是可选字符的例子:
let text = "You are doing great!";
console.log(text.match(/n?g/gi));
这在 g
字符之前可能或可能没有 n
。因此,这将记录:
[ 'ng', 'g' ]
争论起来,一次并不是重复的例子。让我们看看如何获得更多的重复。如果你想至少出现一次,但也可以更频繁地出现,你可以使用加号:+
。以下是一个示例:
let text = "123123123";
console.log(text.match(/(123)+/));
这将匹配组 123
一次或多次。由于这个字符串存在,它将找到匹配。这就是将被记录的内容:
[ '123123123', '123', index: 0, input: '123123123', groups: undefined ]
它匹配整个字符串,因为这里只是 123
的重复。也有这样的情况,你想要某个正则表达式片段匹配任意次数,这可以用星号 *
表示。以下是一个正则表达式模式的示例:
/(123)*a/
它将匹配任何由 123
开头,后面跟着任意数量的 a
。所以它将匹配以下内容,例如:
-
123123123a
-
123a
-
a
-
ba
关于重复的最后一件事是,我们可以更具体一些。我们使用这种语法 {min, max}
来做这件事。以下是一个示例:
let text = "abcabcabc";
console.log(text.match(/(abc){1,2}/));
这将记录:
[ 'abcabc', 'abc', index: 0, input: 'abcabcabc', groups: undefined ]
它这样做是因为它会匹配 abc
一次和两次。正如你所见,我们一直在使用组,但输出中的 groups
仍然是 undefined
。为了指定组,我们必须给它们命名。以下是一个如何做到这一点的示例:
let text = "I love JavaScript!";
console.log(text.match(/(?<language>javascript)/i));
这将输出:
[
'JavaScript',
'JavaScript',
index: 7,
input: 'I love JavaScript!',
groups: [Object: null prototype] { language: 'JavaScript' }
]
关于正则表达式,还有更多要说的,但这应该已经能够让你用它做很多酷的事情。让我们看看一些实际例子。
实际的正则表达式
正则表达式在许多情况下非常有用——任何你需要匹配特定字符串模式的地方,正则表达式都会派上用场。我们将讨论如何将正则表达式与其他字符串方法结合使用,以及如何使用它来验证电子邮件地址和 IPv4 地址。
搜索和替换字符串
在 第八章,内置 JavaScript 方法 中,我们看到了字符串上的搜索和替换方法。我们原本希望我们的搜索是不区分大小写的。猜猜看——我们可以使用正则表达式来实现这一点!
let text = "That's not the case.";
console.log(text.search(/Case/i));
在这里添加 i
修饰符会忽略大小写之间的区别。此代码返回 15
,这是匹配的起始索引位置。这不能使用正常的字符串输入来完成。
你认为我们如何使用正则表达式来改变替换方法的行怍,以便我们可以替换字符串的所有实例而不是第一个实例?再次,使用修饰符!我们使用全局修饰符(g
)来做这件事。为了感受一下区别,看看没有 g
的这个表达式:
let text = "Coding is fun. Coding opens up a lot of opportunities.";
console.log(text.replace("Coding", "JavaScript"));
这是它的输出结果:
JavaScript is fun. Coding opens up a lot of opportunities.
没有使用正则表达式时,它只替换第一次遇到的内容。这次,让我们用带有 g
全局修饰符的例子来看一下:
let text = "Coding is fun. Coding opens up a lot of opportunities.";
console.log(text.replace(/Coding/g, "JavaScript"));
结果如下:
JavaScript is fun. JavaScript opens up a lot of opportunities.
如您所见,所有实例都被替换了。
练习 12.1
查找和替换字符串。以下练习涉及在指定的字符串值中替换字符。第一个输入字段将指示哪个字符串将被替换,第二个输入字段将指示点击按钮后用哪些字符替换它们。
使用下面的 HTML 作为模板,并添加完成任务的 JavaScript:
<!doctype html>
<html>
<head>
<title>Complete JavaScript Course</title>
</head>
<body>
<div id="output">Complete JavaScript Course</div>
Search for:
<input id="sText" type="text">
<br> Replace with:
<input id="rText" type="text">
<br>
<button>Replace</button>
<script>
</script>
</body>
</html>
按照以下步骤操作:
-
使用 JavaScript 选择三个页面元素中的每一个,并将元素对象分配给变量,以便在您的代码中轻松引用。
-
为按钮添加事件监听器,以便在点击时调用函数。
-
创建一个名为
lookup()
的函数,该函数将在输出元素中查找并替换文本。将输出元素的文本内容分配给名为s
的变量,然后将我们要替换的输入值分配给另一个名为rt
的变量。 -
使用第一个输入字段的值创建一个新的正则表达式,这将允许您替换文本。使用正则表达式,使用
match()
方法检查匹配。将此与一个条件包装起来,如果找到匹配项,则执行代码块。 -
如果找到匹配项,使用
replace()
来设置新值。 -
使用新创建和更新的文本输出更新输出区域。
电子邮件验证
为了创建一个正则表达式模式,我们首先需要能够用文字描述该模式。电子邮件地址由五个部分组成,形式为 [name]@[domain].[extension]
。
下面是五个部分的解释:
-
name
: 一个或多个字母数字字符、下划线、破折号或点 -
@
: 文字字符 -
domain
: 一个或多个字母数字字符、下划线、破折号或点 -
.
: 文字点 -
extension
: 一个或多个字母数字字符、下划线、破折号或点
因此,让我们按照正则表达式的步骤来做:
-
[a-zA-Z0-9._-]+
-
@
-
[a-zA-Z0-9._-]+
-
\.
(记住,点在正则表达式中是一个特殊字符,所以我们需要转义它) -
[a-zA-Z0-9._-]+
将所有这些放在一起:
/([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+)/g
让我们看看这个正则表达式在实际中的应用:
let emailPattern = /([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+)/g;
let validEmail = "maaike_1234@email.com";
let invalidEmail = "maaike@mail@.com";
console.log(validEmail.match(emailPattern));
console.log(invalidEmail.match(emailPattern));
我们在有效和无效的电子邮件地址上测试了该模式,这是输出结果:
[ 'maaike_1234@email.com' ]
null
如您所见,它为有效的电子邮件返回结果,对于无效的电子邮件返回 null(没有匹配)。
练习 12.2
创建一个使用 JavaScript 检查输入字符串值是否使用正则表达式正确格式化的电子邮件的应用程序。查看以下模板:
<!doctype html>
<html>
<head>
<title>JavaScript Course</title>
</head>
<body>
<div class="output"></div>
<input type="text" placeholder="Enter Email">
<button>Check</button>
<script>
</script>
</body>
</html>
按照以下步骤操作:
-
使用上述模板代码开始创建你的应用程序。在 JavaScript 代码中,选择页面上的
input
、output
和button
元素作为 JavaScript 对象。 -
为按钮添加事件监听器,当点击时运行一段代码,该代码将获取输入字段中的当前值。创建一个空白的响应值,用于填充输出
div
元素的 内容。 -
使用输入字段中的字符串值和电子邮件格式表达式添加一个测试。如果测试结果为
false
,则更新响应输出为无效电子邮件
,并将输出颜色更改为红色
。 -
如果测试条件返回
true
,则添加一个确认电子邮件格式正确的响应,并将输出文本颜色更改为绿色
。 -
将响应值输出到输出元素中。
函数和参数对象
JavaScript 通过将参数添加到名为arguments
的自定义对象中来处理函数中的参数。这个对象非常像数组,我们可以用它来代替参数名。考虑以下代码:
function test(a, b, c) {
console.log("first:", a, arguments[0]);
console.log("second:", b, arguments[1]);
console.log("third:", c, arguments[2]);
}
test("fun", "js", "secrets");
这将输出:
first: fun fun
second: js js
third: secrets secrets
当你更新其中一个参数时,参数对象会相应地改变。反之亦然;
function test(a, b, c) {
a = "nice";
arguments[1] = "JavaScript";
console.log("first:", a, arguments[0]);
console.log("second:", b, arguments[1]);
console.log("third:", c, arguments[2]);
}
test("fun", "js", "secrets");
这将同时改变arguments[0]
和b
,因为它们分别与a
和arguments[1]
相关,如输出所示:
first: nice nice
second: JavaScript JavaScript
third: secrets secrets
如果函数调用时传递的参数多于函数签名中声明的参数,可以通过这种方式访问它们。然而,现代的方式是使用剩余参数(…param)
而不是arguments
对象。
如果你忘记了剩余参数是什么,可以回顾一下第六章,函数。
练习 12.3
这个练习将演示如何使用类似数组的arguments
对象并从中提取值。使用arguments
的长度属性,我们将遍历参数中的项,并返回列表中的最后一个项:
-
创建一个不带参数的函数。创建一个循环来遍历
arguments
对象的长度。这将允许遍历函数中参数的每个项。 -
设置一个名为
lastOne
的变量,并赋予其空值。 -
在遍历参数时,使用
i
的索引将lastOne
设置为当前参数的值。参数将有一个索引值,可以用来在遍历arguments
对象时引用值。 -
返回
lastOne
的值,它应该只返回最后一个参数值作为响应。 -
输出函数的响应,向函数传递多个参数,并在控制台中记录响应结果。你应该只看到列表中的最后一个项。如果你想看到每一个,可以在查看值时分别将它们输出到控制台,或者构建一个数组,然后在遍历参数时添加每个值。
JavaScript 提升
在第六章,函数中,我们讨论了我们有三种不同的变量,const
,let
和var
,我们强烈建议你应该使用let
而不是var
,因为它们的范围不同。JavaScript 提升是原因。提升是将变量的声明移动到它们定义的作用域顶部的原则。这允许你做许多其他语言中不能做的事情,而且有很好的理由。这应该看起来很正常:
var x;
x = 5;
console.log(x);
它只是输出5
。但是多亏了提升,这也一样:
x = 5;
console.log(x);
var x;
如果你尝试使用let
来做这件事,你会得到一个ReferenceError
。这就是为什么最好使用let
的原因。因为很明显,这种行为很难阅读,不可预测,而且你实际上并不需要它。
发生这种情况的原因是 JavaScript 解释器在处理文件之前将所有的var
声明移动到文件的顶部。只有声明,不是初始化。这就是为什么如果你在使用它之前没有初始化它,你会得到一个undefined
的结果。这就是为什么它应该在声明之前初始化。它是为了内存分配而设计的,但副作用是不希望的。
然而,有一种方法可以关闭这种行为。让我们在下一节中看看我们如何做到这一点!
使用严格模式
我们可以通过在代码中使用以下命令在一定程度上改变 JavaScript 的理解和宽容行为。这需要成为你代码的第一个命令:
"use strict";
这里有一些在我们不使用严格模式时可以正常工作的事情:
function sayHi() {
greeting = "Hello!";
console.log(greeting);
}
sayHi();
我们忘记声明greeting
,所以 JavaScript 通过在顶层添加一个greeting
变量来为我们做了这件事,它将输出Hello!
。然而,如果我们启用严格模式,这将给出一个错误:
"use strict";
function sayHi() {
greeting = "Hello!";
console.log(greeting);
}
sayHi();
错误:
ReferenceError: greeting is not defined
你也可以只在特定的函数中使用严格模式:只需将其添加到函数的顶部,它就只为该函数启用。严格模式还会改变一些其他事情;例如,当使用严格模式时,可用作变量和函数名称的单词更少,因为它们很可能会成为 JavaScript 未来需要为其自身语言保留的关键字。
使用严格模式是习惯在框架环境中使用 JavaScript 或稍后编写 TypeScript 的好方法。如今,这通常被认为是一种良好的实践,因此我们鼓励你在有机会时在自己的代码中使用它。然而,当与现有的较老代码一起工作时,这通常不是一个(容易的)选项。
现在我们已经了解了严格模式,是时候深入探讨另一种完全不同的模式:调试模式!调试模式是在你忙于编写或运行应用程序,但以特殊方式运行它以定位任何错误的位置时使用的。
调试
调试是一种精细的艺术。一开始,通常很难发现你的代码有什么问题。如果你在浏览器中使用 JavaScript,并且它没有按预期运行,第一步总是打开浏览器的控制台。通常它将包含可以帮助你进一步了解的错误。
如果这不能解决问题,你可以在代码的每一步中记录到控制台,并记录变量。这将给你一些关于正在发生什么的洞察。可能只是你依赖于某个恰好未定义的变量。或者也许你期望从数学计算中得到某个特定的值,但你犯了一个错误,结果与你的预期完全不同。在开发过程中使用console.log()
来查看正在发生的事情是很常见的。
断点
调试的一种更专业的方法是使用断点。这可以在大多数浏览器和集成开发环境中完成。你点击代码前的行(在 Chrome 的源面板中,但不同浏览器可能有所不同),就会出现一个点或箭头。当你的应用程序运行时,它会在这一点暂停,给你机会检查变量的值并从那里逐行检查代码。
这样,你将得到一个好的线索,了解发生了什么以及如何修复它。以下是使用 Chrome 中的断点的方法,大多数其他浏览器也有类似的功能。在 Chrome 中,转到检查面板的源选项卡。选择你想要设置断点的文件。然后你只需点击行号即可设置断点:
图 12.1:浏览器中的断点
然后尝试触发代码行,当它被触发时,它会暂停。在屏幕的非常右侧,我可以检查所有变量和值:
图 12.2:检查断点变量
现在,你可以用细齿梳子一样的方式检查你的代码:使用顶部的播放图标,你可以恢复脚本执行(直到遇到下一个断点或再次遇到相同的断点)。使用顶部的圆形箭头图标,我可以跳到下一行并再次检查下一行的值。
断点有很多选项,我们在这里没有空间全部涵盖。关于如何使用断点调试代码的更多细节,请查看你选择的代码编辑器的文档,或者查看这里的相关 Google Chrome 文档:developer.chrome.com/docs/devtools/javascript/breakpoints/
。
练习 12.4
在调试过程中,可以在编辑器中跟踪变量值。以下练习将演示如何使用编辑器的断点来检查脚本运行过程中某个点的变量值。这是一个简单的例子,但可以使用相同的过程在执行过程中特定点查找有关较大脚本的信息,或确定问题可能存在的地方。
不同编辑器中断点的操作方式存在细微差别和细微差别,因此请参阅您环境的文档以获取更详细的说明——这旨在让您了解断点在调试时能提供什么。
您可以使用以下简短脚本作为示例:
let val = 5;
val += adder();
val += adder();
val += adder();
console.log(val);
function adder(){
let counter = val;
for(let i=0;i<val;i++){
counter++;
}
return counter ;
}
如果您在浏览器控制台中测试此脚本,请记住添加<script>
标签并将脚本作为 HTML 文档打开。
此练习已在桌面编辑器中测试过,但它同样适用于浏览器控制台和其他环境。请按照以下步骤操作:
-
在您选择的编辑器中打开您的脚本,或打开浏览器Inspect面板的Sources标签页。点击您想要添加断点的代码行左侧。将出现一个点或其他指示器,以表示已设置断点:
图 12.3:设置断点
-
使用您的新断点运行代码:我已经选择了运行 | 开始调试,但具体操作可能因您的编辑器而异。如果您正在使用浏览器控制台,可以通过简单地重新加载网页来重新运行代码,并考虑您的新断点:
图 12.4:添加断点后运行代码
-
您现在应该能看到调试控制台。将有一个标签页列出代码中的变量和第一个断点处的当前值。在我的编辑器中,它被称为VARIABLES,但在 Chrome 浏览器控制台中,它是Scope标签页。
-
您可以使用菜单选项来移动到下一个断点、停止调试或重新启动断点序列。按播放图标移动到下一个断点。它将更新为 5 的值,如第 1 行指定,并在第一个断点处暂停。请注意,突出显示的行尚未运行:
图 12.5:在控制台中查看变量
-
再次按播放图标,脚本将运行,直到遇到下一个断点,此时变量的值将因第 2 行的代码而更新:
图 12.6:在脚本中通过断点前进
-
再次按播放图标以移动到下一个断点,这将使
val
的值再次增加:图 12.7:最后的断点
-
一旦达到最后一个断点,你将只会看到重新启动或停止调试器的选项。如果你按停止,它将结束调试过程:![Text
自动生成的描述
图 12.8:浏览器中的断点
在第三次断点之后,val
的最终值被揭示为 135
。写下 adder()
函数第一次和第二次调用后的 val
值,这些值是通过使用断点揭示给你的。
这是一个基本的练习,但我们邀请你测试使用断点在一些更大的脚本上,并更熟悉你在运行时如何理解你的代码。
错误处理
我们已经看到很多错误已经出现了。到目前为止,当程序遇到错误时,我们让它崩溃。处理错误还有其他方法。当我们处理依赖于某种外部输入的代码时,例如 API、用户输入或我们需要读取的文件,我们必须处理这些输入可能引起的错误。
如果我们期望某段代码抛出错误,我们可以用 catch
块包围这段代码。它可能抛出的错误将在这个块中被捕获。
你必须小心不要过度使用它,通常当你能够写出更好的代码来避免最初就出现错误时,你通常不想这样做。
这里是一个抛出错误并围绕 try
和 catch
块的代码示例。假设 somethingVeryDangerous()
函数可能会抛出错误:
try {
somethingVeryDangerous();
} catch (e) {
if (e instanceof TypeError) {
// deal with TypeError exceptions
} else if (e instanceof RangeError) {
// deal with RangeError exceptions
} else if (e instanceof EvalError) {
// deal with EvalError exceptions
} else {
//deal with all other exceptions
throw e; //rethrow
}
}
如果它抛出错误,它将结束于 catch
块。由于 Error
可能意味着许多不同的错误,我们将检查我们正在处理的特定错误,并为此错误编写特定的处理程序。我们使用 instanceof
运算符检查确切的错误类。错误处理之后,其余的代码将继续正常执行。
你还可以使用 try
catch
块做一件事,那就是添加一个 finally
块。这个 finally
块会无条件执行,无论是否抛出错误。这对于清理工作非常有用。这里有一个简单的例子:
try {
trySomething();
} catch (e) {
console.log("Oh oh");
} finally {
console.log("Error or no error, I will be logged!");
}
我们不知道这段代码的输出,因为 trySomething()
没有定义。如果它抛出错误,它会在控制台记录 Oh oh
,然后记录 Error or no error, I will be logged!
。如果 trySomething()
没有抛出错误,它只会记录最后一部分。
最后,如果你出于任何原因需要抛出错误,你可以使用 throw
关键字,如下所示:
function somethingVeryDangerous() {
throw RangeError();
}
这在需要处理超出你控制范围的事物时非常有用,例如 API 响应、用户输入或从文件读取的输入。如果发生意外情况,有时你必须抛出一个错误来适当地处理它。
练习 12.5
-
使用
throw
、try
和catch
,检查值是否为数字,如果不是,则创建一个自定义错误。 -
创建一个名为
val
的带有一个参数的函数。 -
使用
try
,并在其中添加一个条件来检查val
是否为数字,使用isNaN
。如果是真的,那么抛出一个错误,说明它不是一个数字。否则,在控制台中输出Got a number
。 -
使用
catch
捕获任何错误并将错误值输出到控制台。 -
在函数执行并输出值后添加
finally
,当函数完成时,也包括val
的值。 -
向函数发送一个字符串参数和一个数字参数的请求。在控制台中查看结果。
使用 cookies
Cookies是存储在您的计算机上并由网站使用的小型数据文件。Cookies 是为了存储有关网站用户的信息而发明的。Cookies 是具有特殊模式的字符串。它们包含键值对,这些键值对由分号分隔。
您可以创建一个 cookie 并在以后再次使用它。以下是创建 cookie 的方法:
document.cookie = "name=Maaike;favoriteColor=black";
当您在客户端运行它时(例如在您的<script>
标签中),这并不在所有浏览器中都有效。例如,在 Chrome 中,您无法从客户端设置 cookie。您必须从服务器运行代码。(我在这里使用了 Safari,但无法保证未来的支持。)另一种选择是 web 存储 API。
您也可以通过启用某些设置从命令行启动 Chrome,或者在隐私首选项下的设置中启用 cookie。不过,如果您不想这样做,请小心将其关闭。这是从 cookie 中读取的方法:
let cookie = decodeURIComponent(document.cookie);
let cookieList = cookie.split(";");
for (let i = 0; i < cookieList.length; i++) {
let c = cookieList[i];
if (c.charAt(0) == " ") {
c = c.trim();
}
if (c.startsWith("name")) {
alert(c.substring(5, c.length));
}
}
此示例使用decodeURIComponent()
获取所有 cookie,然后使用;
分割它们。这给我们留下了一个包含键值对的字符串的数组cookieList
。接下来,我们遍历所有的键值对。修剪它们(移除前后空白),并查看它们是否以name
开头。这就是我们的 cookie 键的名称。
如果我们要获取值,我们必须从键之后开始读取,所以至少是键的长度,在这个例子中是 4(name)。这已经把我们带到了索引 3。我们还想跳过索引 4 上的等号,所以我们从索引 5 开始。在这种情况下,我们正在添加一个关于名称的警告。以下是一个使用 cookie 问候用户的简单网站示例:
<!DOCTYPE html>
<html>
<body>
<input onchange="setCookie(this)" />
<button onclick="sayHi('name')">Let's talk, cookie!</button>
<p id="hi"></p>
<script>
function setCookie(e) {
document.cookie = "name=" + e.value + ";";
}
function sayHi(key) {
let name = getCookie(key);
document.getElementById("hi").innerHTML = "Hi " + name;
}
function getCookie(key) {
let cookie = decodeURIComponent(document.cookie);
let cookieList = cookie.split(";");
for (let i = 0; i < cookieList.length; i++) {
let c = cookieList[i];
if (c.charAt(0) == " ") {
c = c.trim();
}
if (c.startsWith(key)) {
console.log("hi" + c);
return c.substring(key.length + 1, c.length);
}
}
}
</script>
</body>
</html>
如果您正在编写一个新的网站,您可能不应该使用这种方法。然而,无论何时您需要处理旧代码,您很可能会遇到这种情况。现在您知道了它的含义以及如何调整它。这对您来说是个好消息!
练习 12.6
让我们创建一个 cookie 构建器。创建几个函数,允许您与页面 cookie 交互,包括通过名称读取 cookie 值、使用名称创建新 cookie 并为其设置一定数量的天数,以及删除 cookie。您可以使用以下 HTML 模板开始:
<!doctype html>
<html>
<head>
<title>Complete JavaScript Course</title>
</head>
<body>
<script>
</script>
</body>
</html>
按照以下步骤操作:
-
设置您的网页,并在 JavaScript 代码中输出
document.cookie
的值。它应该是空的。 -
创建一个函数,该函数将接受
cookieName
、cookieValue
以及你想要设置 cookie 的天数作为参数。 -
检查
days
是否有效,并在有效代码块中获取当前日期。通过将天数乘以毫秒来为 cookie 设置一个setTime
值,使其在毫秒数后过期。 -
将 cookie 过期时间的毫秒数对象转换为 UTC 字符串值。
-
将
document.cookie
设置为cookieName = cookieValue
,然后添加过期详情,最后指定path=/
。 -
创建一个函数,在指定天数后创建一个带有值和过期的测试 cookie。以相同的方式创建第二个 cookie,当你刷新你的页面时,你应该在控制台看到至少两个 cookie。
-
创建第二个函数来读取 cookie 值,将值设置为
false
,然后创建一个由分号分割的 cookie 数组。 -
遍历所有 cookie,并在等号处再次分割。这将给出索引为 0 的第一个项,作为 cookie 的名称。添加一个条件来检查名称是否等于函数参数中请求的名称。如果匹配,则分配索引中第二个项的值,这将是要删除的选定名称的 cookie 的值。在函数中返回
cookievalue
。 -
使用函数添加两个控制台日志消息,以读取你之前设置的 cookie。在控制台输出 cookie 的值。
-
要删除 cookie,你需要设置一个早于当前日期的日期。你可以创建一个带有
-1
日期的 cookie,并通过调用 cookie 创建函数发送带有其选定名称的 cookie 以进行删除。 -
尝试通过名称删除 cookie。
本地存储
我们已经将 cookie 视为保存用户数据的一种方式,但实际上有一种更现代的方法来做这件事:本地存储。本地存储是一个令人兴奋的话题,它将增强你制作智能网站的能力。使用本地存储,我们可以在我们的网络浏览器中保存键值对,并在新会话中再次使用它们(当浏览器稍后再次打开时)。信息通常存储在用户的计算机上的一个文件夹中,但这一点因浏览器而异。
这使得网站能够存储一些信息并在稍后检索它,即使是在刷新页面或关闭浏览器之后。与 cookie 相比,本地存储的优势在于它们不需要在每次 HTTP 请求中传递,这是 cookie 的情况。本地存储只是在那里等待被访问。
localStorage
对象是我们之前见过的window
对象的一个属性。我们需要了解localStorage
对象上的一些方法才能有效地使用它。首先,我们需要能够在本地存储上获取和设置键值对。每次我们想要保存某物时,我们使用setItem()
,每次我们想要稍后检索值时,我们使用getItem()
。以下是这样做的方法:
<!DOCTYPE html>
<html>
<body>
<div id="stored"></div>
<script>
let message = "Hello storage!";
localStorage.setItem("example", message);
if (localStorage.getItem("example")) {
document.getElementById("stored").innerHTML =
localStorage.getItem("example");
}
</script>
</body>
</html>
Hello storage! on the page. You can add items to storage by specifying a key and a value with the setItem method. You can access localStorage directly or via the window object. Here we specify example as the key and Hello storage! as the value and save it to local storage. Then we check whether the example key is set in local storage and output the data by writing it to the innerHTML of the div with the ID stored.
如果你回到代码中,在第二次加载页面之前关闭setItem()
行,它仍然会输出该值,因为信息是在第一次运行脚本时存储的,并且从未被删除。尽管如此,本地存储不会过期,但可以手动删除。
我们也可以使用索引来检索一个键。这在我们需要遍历键值对,但不知道键的名称时非常有用。以下是按索引检索键的方法:
window.localStorage.key(0);
在这种情况下,键是name
。为了获取关联的值,我们可以这样做:
window.localStorage.getItem(window.localStorage.key(0));
我们也可以这样删除键值对:
window.localStorage.removeItem("name");
我们可以在一次调用中从本地存储中删除所有键值对:
window.localStorage.clear();
因此,使用本地存储,你可以在关闭浏览器后保存值。这允许许多“智能”行为,因为现在你的应用能够记住事情,比如你在表单中输入的内容,你在网站上切换的设置,以及你之前查看的内容。
请不要将此视为一个可以用来绕过 cookies 和隐私问题的替代方案。本地存储与 cookies 引发的问题完全相同,只是知名度较低。你仍然需要在网站上提及你正在跟踪用户并存储信息,就像你需要为 cookies 做的那样。
练习 12.7
让我们创建一个本地存储购物清单,它将在浏览器本地存储中存储值。这是一个使用 JavaScript 将字符串转换为可使用 JavaScript 对象,然后再将其转换回可以存储在本地存储中的字符串的示例。你可以使用以下模板:
<!doctype html>
<html>
<head>
<title>JavaScript</title>
<style>
.ready {
background-color: #ddd;
color: red;
text-decoration: line-through;
}
</style>
</head>
<body>
<div class="main">
<input placeholder="New Item" value="test item" maxlength="30">
<button>Add</button>
</div>
<ul class="output">
</ul>
<script>
</script>
</body>
</html>
采取以下步骤:
-
在 JavaScript 代码中,选择所有页面元素作为 JavaScript 对象。
-
如果存在本地
tasklist
存储,则创建一个tasks
数组,其值为本地存储,否则将tasks
数组设置为空数组。使用JSON.parse
,你可以将字符串值转换为 JavaScript 中的可使用对象。 -
遍历
tasklist
数组中的所有项;它们将被存储为对象,具有名称和布尔值表示其选中状态。创建一个单独的函数来构建任务项,并将其添加到页面列表中。 -
在任务生成函数中,创建一个新的列表项和一个
textNode
。将textNode
附加到列表项上。将列表项附加到页面输出区域。如果任务被标记为完成,布尔值为true
,则添加style
类的ready
。 -
为列表项添加一个事件监听器,当点击时切换
ready
类。每次任何列表项发生变化时,你也需要将其存储到本地存储中。创建一个任务构建函数,该函数将存储并确保可视列表与本地存储列表相同。你需要清除当前任务列表数组,并从可视数据中重建,因此需要创建一个处理列表构建的函数。 -
任务构建函数将清除当前的
tasks
数组,并选择页面上的所有li
元素。遍历所有列表项,从元素中获取文本值,并检查它是否包含ready
类。如果包含ready
类,则将勾选条件标记为 true。将结果添加到tasks
数组中,这将重建数组以确保它与用户视觉上看到的一致。将数据发送到保存任务函数以将tasks
数组保存在本地存储中,这样如果页面被刷新,你将看到相同的列表。 -
在保存任务函数中,将
localstorage
项设置为任务数组。你需要将对象序列化,以便它可以放入本地存储的字符串参数中。 -
现在,当你刷新页面时,你会看到任务列表。你可以通过点击它们来勾选任务,也可以通过按下提交新项的按钮在输入字段中添加新项。
JSON
JSON代表JavaScript 对象表示法,这仅仅是一种数据格式。我们在创建 JavaScript 对象时看到了这种表示法;然而,JSON 并不意味着 JavaScript 对象,它只是使用与 JavaScript 对象类似格式的数据表示方法。它也可以轻松地转换为 JavaScript 对象。
JSON 是一种用于与 API 通信的标准,包括不是用 JavaScript 编写的 API!API 可以接受数据,例如,来自网站表单的数据,以 JSON 格式。如今,API 几乎总是以 JSON 格式发送数据。例如,当你进入网上商店时,产品通常来自连接到数据库的 API 调用。这些数据被转换为 JSON 并返回到网站。以下是一个 JSON 的示例:
{
"name" : "Malika",
"age" : 50,
"profession" : "programmer",
"languages" : ["JavaScript", "C#", "Python"],
"address" : {
"street" : "Some street",
"number" : 123,
"zipcode" : "3850AA",
"city" : "Utrecht",
"country" : "The Netherlands"
}
}
这是一个似乎描述一个人的对象。它包含键值对。键总是必须用引号括起来,但值只有在它们是字符串时才必须用引号括起来。因此,第一个键是name
,第一个值是Malika
。
值列表(或 JavaScript 数组)用[]
表示。JSON 对象包含一个languages
列表,它有方括号,还有一个对象address
。你可以通过大括号来判断这一点。
实际上,JSON 中只有几种类型:
-
值为以下类型的键值对:字符串、数字、布尔值和 null
-
包含
[
和]
的列表键值对,这些列表包含列表中的项 -
包含其他 JSON 元素的
{
和}
之间的键值对,这些元素是其他对象
这三个选项可以组合使用,因此一个对象可以包含其他对象,一个列表可以包含其他列表。我们已经在上面的例子中看到了这一点。我们的对象包含一个嵌套的地址对象。
但这可以进一步嵌套。列表也可以包含对象,这些对象可以包含带有对象的列表、列表,以此类推。这听起来可能有点复杂,这正是重点。尽管它非常简单,但所有这些选项的嵌套仍然会使 JSON 稍微复杂一些。这就是为什么我们将它放在高级主题章节中的原因。
让我们现在看看一个稍微复杂一点的例子:
{
"companies": [
{
"name": "JavaScript Code Dojo",
"addresses": [
{
"street": "123 Main street",
"zipcode": 12345,
"city" : "Scott"
},
{
"street": "123 Side street",
"zipcode": 35401,
"city" : "Tuscaloosa"
}
]
},
{
"name": "Python Code Dojo",
"addresses": [
{
"street": "123 Party street",
"zipcode": 68863,
"city" : "Nebraska"
},
{
"street": "123 Monty street",
"zipcode": 33306,
"city" : "Florida"
}
]
}
]
}
这是一个公司列表,上面有两个公司对象。公司有两个键值对:名称和地址列表。每个地址列表包含两个地址,每个地址由三个键值对组成:street
、zipcode
和 city
。
练习 12.8
这个练习将演示如何创建一个有效的 JSON 对象,该对象可以用作 JavaScript 对象。您将创建一个简单的包含名称和状态的列表,可以遍历并将结果输出到控制台。您将加载 JSON 数据到 JavaScript 并输出对象的详细内容:
-
创建一个包含 JSON 格式数据的 JavaScript 对象。该对象应包含至少两个项目,并且每个项目应是一个包含至少两个配对值的对象。
-
创建一个可以调用的函数,该函数将遍历 JavaScript JSON 对象中的每个项目,并将结果输出到控制台。使用
console.log
将数据项输出到控制台。 -
调用函数并启动 JavaScript 代码。
解析 JSON
有许多库和工具可以将 JSON 字符串解析为对象。可以使用 JSON.parse()
函数将 JavaScript 字符串转换为 JSON 对象。从其他地方接收的数据始终是 string
类型的值,因此为了将其视为对象,需要将其转换。以下是这样做的方法:
let str = "{\"name\": \"Maaike\", \"age\": 30}";
let obj = JSON.parse(str);
console.log(obj.name, "is", obj.age);
解析后,它可以被视为一个对象。因此,它将在控制台输出 Maaike is 30
。
有时也需要反过来操作。可以使用 JSON.stringify()
方法将对象转换为 JSON 字符串。它将 JavaScript 对象或值转换为 JSON 字符串。您可以在以下操作中看到它的作用:
let dog = {
"name": "wiesje",
"breed": "dachshund"
};
let strdog = JSON.stringify(dog);
console.log(typeof strdog);
console.log(strdog);
strdog
的类型变为字符串,因为它正在被字符串化。它不再具有 name
和 breed
属性。这些将变为未定义。此代码片段将在控制台输出以下内容:
string
{"name":"wiesje","breed":"dachshund"}
这对于将 JSON 数据直接存储在数据库中非常有用,例如。
练习 12.9
这个练习将演示使用 JSON 方法解析 JSON 并将字符串值转换为 JSON。使用 JavaScript 中的 JSON 方法,将 JSON 格式的字符串值转换为 JavaScript 对象,并将 JavaScript 对象转换为 JSON 对象的字符串表示:
-
创建一个包含多个项目和对象的 JSON 对象。您可以使用上一课中的 JSON 对象。
-
使用 JSON 的
stringify()
方法,将 JSON JavaScript 对象转换为字符串版本,并将其分配给名为newStr
的变量:[{"name":"Learn JavaScript","status":true},{"name":"Try JSON","status":false}]
。 -
使用
JSON.parse()
,将newStr
值转换回对象,并将其分配给名为newObj
的变量。 -
遍历
newObj
中的项目,并将结果输出到控制台。
练习 12.9 答案
let myList = [{
"name": "Learn JavaScript",
"status": true
},
{
"name": "Try JSON",
"status": false
}
];
const newStr = JSON.stringify(myList);
const newObj = JSON.parse(newStr);
newObj.forEach((el)=>{
console.log(el);
});
章节项目
邮件提取器
使用以下 HTML 作为起始模板,并添加 JavaScript 代码以创建电子邮件提取器函数:
<!doctype html>
<html>
<head>
<title>Complete JavaScript Course</title>
</head>
<body>
<textarea name="txtarea" rows=2 cols=50></textarea> <button>Get Emails</button>
<textarea name="txtarea2" rows=2 cols=50></textarea>
<script>
</script>
</body>
</html>
执行以下步骤:
-
在 JavaScript 中选择两个文本区域和按钮,并将它们设置为 JavaScript 对象。
-
为按钮添加一个事件监听器,该监听器将调用一个函数,该函数获取第一个
textarea
的内容,并过滤出仅接受电子邮件地址。 -
在提取函数中,获取第一个输入字段的内容。使用
match()
,从第一个textarea
的内容中返回匹配的电子邮件地址数组。 -
为了去除任何重复项,创建一个单独的数组,该数组将只包含唯一值。
-
遍历所有找到的电子邮件地址,并检查每个地址是否已经在
holder
数组中,如果没有,则添加它。 -
使用
join()
数组方法,现在可以将找到的电子邮件地址的结果合并在一起,并将其输出到第二个textarea
。
表单验证器
此项目是一个典型的表单结构示例,其中您检查表单中输入的值,并在内容提交之前进行验证。如果值不符合代码中的验证标准,则向用户返回响应。使用以下 HTML 和 CSS 作为起始模板:
<!doctype html>
<html>
<head>
<title>JavaScript Course</title>
<style>
.hide {
display: none;
}
.error {
color: red;
font-size: 0.8em;
font-family: sans-serif;
font-style: italic;
}
input {
border-color: #ddd;
width: 400px;
display: block;
font-size: 1.5em;
}
</style>
</head>
<body>
<form name="myform"> Email :
<input type="text" name="email"> <span class="error hide"></span>
<br> Password :
<input type="password" name="password"> <span class="error hide"></span>
<br> User Name :
<input type="text" name="userName"> <span class="error hide"></span>
<br>
<input type="submit" value="Sign Up"> </form>
<script>
</script>
</body>
</html>
执行以下步骤:
-
使用 JavaScript 选择所有页面元素,并将它们设置为 JavaScript 对象,以便在代码中更容易选择。同时选择所有具有
error
类的页面元素作为对象。 -
为提交添加事件监听器并捕获点击,防止默认表单操作。
-
遍历所有具有类
error
的页面元素,并添加hide
类,这将使它们从视图中消失,因为这是一个新的提交。 -
使用有效的电子邮件的正则表达式,将结果与电子邮件字段的输入值进行测试。
-
创建一个函数来响应错误,该函数将从触发事件的元素旁边的元素中移除
hide
类。在函数内将该元素设置为焦点。 -
如果有错误,即输入不匹配所需的正则表达式,将参数传递给您刚刚创建的错误处理函数。
-
检查密码字段输入值,确保只使用字母和数字。还要检查长度,确保它是 3-8 个字符。如果任一条件不满足,则使用错误函数添加错误,并为用户创建一条消息。将错误布尔值设置为
true
。 -
添加一个对象来跟踪表单数据创建,并通过遍历所有输入,将属性名称设置为与输入名称相同,将值设置为与输入值相同来向对象中添加值。
-
在验证函数结束之前,检查是否仍然存在错误,如果不存在,则提交表单对象。
简单数学测验
在这个项目中,我们将创建一个数学测验,允许用户回答数学问题。应用程序将检查用户的回答并评分用户对问题的回答准确性。你可以使用以下 HTML 模板:
<!doctype html>
<html>
<head>
<title>Complete JavaScript Course</title>
</head>
<body>
<span class="val1"></span> <span>+</span> <span class="val2"></span> = <span>
<input type="text" name="answer"></span><button>Check</button>
<div class="output"></div>
</body>
</html>
执行以下步骤:
-
在 JavaScript 中,将代码包裹在一个名为
app
的函数中。在app
函数内部,创建变量对象来包含所有页面元素,以便在脚本中使用,并创建一个名为game
的空对象。 -
在页面加载完成后,添加一个
DOMContentLoaded
事件监听器,调用应用初始化。 -
在
init()
函数中,给按钮添加一个事件监听器,监听点击事件,并将事件跟踪到名为checker
的函数中。同样在init
函数中,加载另一个名为loadQuestion()
的函数。 -
创建一个用于加载问题的函数,以及另一个可以生成从参数中的最小值和最大值之间的随机数的函数。
-
在
loadQuestion()
函数中,生成两个随机值并将它们添加到游戏对象中。计算这两个值相加的结果,并将该值分配到游戏对象中。 -
为需要动态数值计算问题的页面元素分配和更新
textContent
。 -
当按钮被点击时,使用三元运算符确定问题的答案是否正确。对于正确答案,颜色设置为
green
,对于错误答案,颜色设置为red
。 -
创建一个页面元素来输出所有问题并跟踪结果。在
checker()
函数中,向 HTML 中添加一个新的元素,并使用样式颜色来指示正确或错误的响应。显示第一个和第二个值以及答案,并在括号内显示用户的响应。 -
清除输入字段并加载下一个问题。
自我检查测验
-
以下正则表达式表达式将从以下单词中返回什么?
Expression / ([a-e])\w+/g "Hope you enjoy JavaScript"
-
Cookie 是文档对象的一部分吗?
-
以下代码将对 JavaScript cookie 做什么?
const mydate = new Date(); mydate.setTime(mydate.getTime() - 1); document.cookie = "username=; expires=" + mydate.toGMTString();
-
以下代码在控制台中的输出是什么?
const a = "hello world"; (function () { const a = "JavaScript"; })(); console.log(a);
-
以下代码在控制台中的输出是什么?
<script> "use strict"; myFun(); console.log(a); function myFun() { a = "Hello World"; } </script>
-
以下代码的输出是什么?
console.log("a"); setTimeout(() => { console.log("b"); }, 0); console.log("c");
摘要
在本章中,我们有一些重要且更高级的主题,我们仍然需要覆盖,但你可能在前面的书中还没有准备好。在本章之后,你应该在几个方面加深了对 JavaScript 的理解,首先是正则表达式。使用正则表达式,我们可以指定字符串的模式,并可以使用这些模式来搜索其他字符串以匹配我们的模式。
我们还考虑了函数和arguments
对象,通过它们我们可以通过索引访问参数。我们继续探讨了 JavaScript 的提升和严格模式,这使我们能够在使用 JavaScript 时遵循更多规则。习惯在严格模式下使用 JavaScript 通常是一种良好的实践,并且是使用 JavaScript 框架工作的良好准备。
调试和微调也被讨论了:我们可以使用断点或将输出记录到控制台来了解正在发生的事情。妥善处理错误可以防止程序不必要的崩溃。最后,我们探讨了 JavaScript 的 cookie 创建和本地存储的使用,以及 JSON 的使用,这是一种发送数据的语法。我们看到了不同类型的键值对以及如何解析 JSON。我们还看到了如何将键值对存储在window
对象的localStorage
中。
本章加深了我们对 JavaScript 的理解,我们学习了一些现代 JavaScript 所需了解的新知识,同时也为处理旧(遗留)代码提供了很多知识。在下一章中,我们将深入探讨一个更高级的主题:并发。这个主题是关于使用 JavaScript 代码进行多任务处理。
第十三章:并发
是时候讨论一个更高级的话题了。你已经准备好了!我们将处理异步代码和代码多任务处理的选项。这个概念被称为并发。如果你觉得这一章有点困难,不要担心;这是在 JavaScript 中进行的编程,是高级编程。我们将处理以下主题:
-
并发
-
回调
-
Promise
-
async
/await
-
事件循环
是的,这很困难,但理解如何利用并发确实可以通过加快处理速度来提高程序的性能,这足以让我们深入这个高级话题!
注意:练习、项目和自我检查测验的答案可以在附录中找到。
介绍并发
并发是指事物“同时”或并行发生。为了给出一个非代码的例子,让我们来谈谈管理我的家庭。当我在周五晚上回家时,我有一系列的任务:孩子们需要吃饭、洗澡,然后被带到床上,衣服需要叠好,洗衣机需要放衣服,而且公平地说,还有很多其他事情,但这些都足以说明例子。
如果我不能同时做很多事情,这将是一个非常艰难的夜晚,而且会非常晚。我首先会做晚饭——把披萨放进烤箱并站在旁边等待——喂孩子吃饭,然后给他们洗澡,然后把他们带到床上,然后叠衣服,再次打开机器,等待它完成。幸运的是,我能够多任务处理,所以看起来更像是这样:我把披萨放进烤箱,同时,我打开洗衣机并可能叠几件衣服,然后我喂孩子吃饭,在他们洗澡的时候做剩下的衣服,我很快就完成了。
这同样适用于你的电脑和你使用的应用程序。如果它不能同时做很多事情,你可能会非常烦恼。你不可能在写代码的同时检查邮件,你不可能在写代码的同时听音乐,还有很多其他事情。这是你的电脑在不同任务之间切换。同样的事情也可以在应用层面上发生。例如,我们可以调用某个 API,而不等待回复,而是在此期间做些有用的事情。我们可以使用并发的概念来实现这一点。
在处理并发时,JavaScript 中有三种策略你需要了解:回调、Promise以及async和await关键字。
回调
回调是我们讨论并发时应该首先理解的东西。好消息是,回调
原则并不难理解。它只是一个接受另一个函数作为参数的函数,当初始函数的其余部分完成时,这个函数会被调用。换句话说,它只是一个函数调用另一个函数,就像这样:
function doSomething(callback) {
callback();
}
function sayHi() {
console.log("Hi!");
}
doSomething(sayHi);
Hi! printed to the console.
这里有一个回调
原则实际应用的例子:
function judge(grade) {
switch (true) {
case grade == "A":
console.log("You got an", grade, ": amazing!");
break;
case grade == "B":
console.log("You got a", grade, ": well done!");
break;
case grade == "C":
console.log("You got a", grade, ": alright.");
break;
case grade == "D":
console.log("You got a", grade, ": hmmm...");
break;
default:
console.log("An", grade, "! What?!");
}
}
function getGrade(score, callback) {
let grade;
switch (true) {
case score >= 90:
grade = "A";
break;
case score >= 80:
console.log(score);
grade = "B";
break;
case score >= 70:
grade = "C";
break;
case score >= 60:
grade = "D";
break;
default:
grade = "F";
}
callback(grade);
}
getGrade(85, judge);
这里有两个函数:judge()
和 getGrade()
。我们用两个参数调用 getGrade()
函数:85
和函数 judge()
。注意,在将函数作为参数调用时,我们不包括括号。judge()
函数被存储在 callback
中。在确定成绩后,存储在回调中的函数(在本例中为 judge()
)会带着成绩被调用。
这也可以是另一个比判断更有用的函数,例如,根据测试结果发送特定的电子邮件。如果我们想要这样做,实际上我们不需要更改 getGrade()
函数;我们只需要编写一个新的函数来做这件事,并用这个新函数作为第二个参数调用 getGrade()
。
你现在可能非常失望,因为这不是很令人兴奋。在异步上下文中,回调变得非常有价值,例如,当一个函数在调用数据库并调用将要处理数据的 callback
函数之前仍在等待数据库的调用结果时。
一些 JavaScript 内置函数使用这种回调原则,例如,setTimeOut()
和 setInterval()
函数。在超时的情况下,它们将执行一个在特定时间后执行的函数,对于指定的间隔,每隔一定时间执行一次。我们已经看到这些了,但只是为了提醒:
setInterval(encourage, 500);
function encourage() {
console.log("You're doing great, keep going!");
}
作为参数插入的函数在这里被称为回调。真正理解并发性其实是从回调开始的,但是多层嵌套的回调会使代码难以阅读。
当所有这些都被写成一个函数,并且内部包含匿名函数时,这个函数的缩进也会很多。我们称之为 回调地狱 或 圣诞树问题(因为代码嵌套得如此之多,看起来像侧放的圣诞树)。
回调是一个很好的概念,但它们可以快速创建出丑陋的代码。通常有一个更好的解决方案,我们保证。
练习 13.1
这个练习将演示如何使用回调函数,通过调用回调函数创建一种从函数传递值到另一个函数的方法。我们将使用一个字符串中的全名创建一个问候回调。
-
创建一个名为
greet()
的函数,它接受一个参数fullName
。该参数应该是一个数组。将数组的项输出到控制台,并插入到问候消息字符串中。 -
创建第二个函数,它有两个参数:第一个是一个字符串,用于用户的完整名称,第二个是
callback
函数。 -
使用
split()
方法将字符串拆分成一个数组。 -
将全名数组发送到第一步中创建的
greet()
函数。 -
调用
callback
函数的过程。
Promises
使用 Promises,我们可以以稍微更容易维护的方式组织代码的顺序。Promise 是一个特殊的对象,它连接需要产生结果的代码和需要使用这个结果的下一步代码。
当我们创建一个 Promise 时,我们给它一个函数。在下面的例子中,我们使用了一个我们经常看到的约定;我们现场创建一个函数。所以,在参数列表中,我们定义了这个函数,通常也使用箭头函数。这个函数需要两个参数,这些参数是回调函数。我们在这里称它们为 resolve
和 reject
。
你可以随意命名这些参数,但 resolve
或 res
和 reject
或 rej
是最常见的。
当调用 resolve()
时,假设 Promise 是成功的,并且箭头之间的任何内容都会返回并用作 Promise 对象上 then()
方法的输入。如果调用 reject()
,则 Promise 失败,并且如果存在,Promise 对象上的 catch()
方法会使用 reject()
函数的参数执行。
这是一大堆信息,一开始可能难以理解,所以这里有一个 Promise 的例子来帮助你:
let promise = new Promise(function (resolve, reject) {
// do something that might take a while
// let's just set x instead for this example
let x = 20;
if (x > 10) {
resolve(x); // on success
} else {
reject("Too low"); // on error
}
});
promise.then(
function (value) {
console.log("Success:", value);
},
function (error) {
console.log("Error:", error);
}
);
我们首先创建一个 Promise。在创建 Promise
时,我们不知道 Promise
的值将是什么。这个值就是发送给解析函数的参数。它是一种占位符。
所以当我们对 Promise 调用 then()
时,我们基本上是在说:找出 Promise 的值,当你知道时,如果 Promise 被解析,则执行一个函数,如果被拒绝,则执行不同的函数。当一个 Promise 既没有被解析也没有被拒绝时,我们说这个 Promise 是挂起的。
then()
本身也是一个 Promise,所以当它返回时,我们可以使用结果作为下一个 then()
实例的输入。这意味着我们可以链式调用 then()
实例,这可以看起来像这样:
const promise = new Promise((resolve, reject) => {
resolve("success!");
})
.then(value => {
console.log(value);
return "we";
})
.then(value => {
console.log(value);
return "can";
})
.then(value => {
console.log(value);
return "chain";
})
.then(value => {
console.log(value);
return "promises";
})
.then(value => {
console.log(value);
})
.catch(value => {
console.log(value);
})
这将记录:
success!
we
can
chain
promises
解析函数是用箭头函数实现的。return
语句是下一个函数的 value
输入。你可以看到最后一个块是一个 catch()
函数。如果任何一个函数导致拒绝,并且因此 Promise 被拒绝,那么这个 catch()
块就会被执行,并打印出 reject()
函数发送给 catch()
方法的任何内容。例如:
const promise = new Promise((resolve, reject) => {
reject("oops... ");
})
.then(value => {
console.log(value);
return "we";
})
.then(value => {
console.log(value);
return "can";
})
.then(value => {
console.log(value);
return "chain";
})
.then(value => {
console.log(value);
return "promises";
})
.then(value => {
console.log(value);
})
.catch(value => {
console.log(value);
})
这将只记录 oops…
,因为第一个 Promise 被拒绝而不是解析。这对于创建需要等待另一个过程完成的异步过程非常有用。我们可以尝试执行一组特定的操作,如果出现问题,可以使用 catch()
方法来处理。
练习第 13.2 节的练习题
在这个练习中,你将创建一个计数器,它将使用 Promise 按顺序输出值。
-
设置一个以
Start Counting
为值的 Promise。 -
创建一个名为
counter()
的函数,该函数有一个参数,用于获取值并将其输出到控制台。 -
使用四个
then()
实例设置 Promise 的下一个函数,这些实例应该将值输出到计数器函数中,并返回一个值,该值将为后续的then()
实例提供输入。返回的值应该是one
,然后是two
,然后是three
。控制台屏幕输出应该是以下内容:Start Counting One Two Three
async 和 await
我们刚刚看到了 Promise
语法。使用 async
关键字,我们可以让一个函数返回一个 Promise。这使得 Promise 更易于阅读,看起来很像同步(非并发)代码。我们可以像上一节学的那样使用这个 Promise,或者我们可以使用更强大的 await
关键字来等待 Promise 完成。await
只在异步函数中有效。
在异步上下文中,我们也可以等待其他 Promise,如下例所示:
function saySomething(x) {
return new Promise(resolve => {
setTimeout(() => {
resolve("something" + x);
}, 2000);
});
}
async function talk(x) {
const words = await saySomething(x);
console.log(words);
}
talk(2);
talk(4);
talk(8);
你能弄清楚这段代码做了什么吗?我们连续三次调用异步函数 talk()
,没有中断。每次函数调用都在等待 saySomething()
函数。saySomething()
函数包含一个新 Promise,它通过 setTimeout()
函数在两秒后解析为 something
加 x
的值。所以两秒后,三个函数同时完成(或者对人类眼睛来说是这样的)。
如果 talk()
函数不是异步的,它会因为 await
关键字而抛出 SyntaxError
。await
只在异步函数中有效,所以 talk()
必须是异步的。如果没有这个例子中的 async
和 await
,它将把函数 saySomething()
的结果,一个挂起的 Promise
,以文字形式存储,并且每次函数调用时都记录一次:
Promise { <pending> }
Promise { <pending> }
Promise { <pending> }
我们现在已经看到了并发的基石。这应该为你在现实生活中处理并发做好准备。并发确实是一个高级话题;调试它可能会有些麻烦,但正确应用时在性能方面确实值得。
练习 13.3
这个练习将演示如何使用 await
在 async
函数中等待一个 Promise
。使用 await
和 async
创建一个带有 timeout()
的计数器,并增加全局计数器值。
-
为计数器创建一个全局值。
-
创建一个函数,它接受一个参数。返回一个新 Promise 的结果,设置一个
setTimeout()
函数,该函数将包含解析实例。 -
在
setTimeout()
中增加计数器的值,每秒增加一。用计数器的值和函数参数中的变量值解析 Promise。 -
创建一个异步函数,将全局计数器的值和函数参数的值输出到控制台。
-
创建一个变量来捕获
await
函数返回的解析值。将结果输出到控制台。 -
创建一个循环,迭代 10 次,增加值并调用
async
函数,将增量变量的值作为参数传递给函数。
结果应该看起来像以下这样:
ready 1 counter:0
ready 2 counter:0
ready 3 counter:0
x value 1 counter:1
x value 2 counter:2
x value 3 counter:3
事件循环
我们希望以解释 JavaScript 在底层如何处理异步和并发来结束本章。JavaScript 是一种单线程语言。在这个上下文中,线程意味着一个执行路径。如果只有一个路径,这意味着任务将不得不相互等待,并且一次只能发生一件事情。
这个单一的执行器是事件循环。这是一个执行实际工作的进程。你可能对此感到疑惑,因为你刚刚学习了并发和异步同时执行事情。好吧,尽管 JavaScript 是单线程的,但这并不意味着它不能外包一些任务并等待它们返回。这正是 JavaScript 能够以多线程方式执行事情的原因。
调用栈和回调队列
JavaScript 使用调用栈,它必须执行的所有操作都排队在这里。事件循环是一个不断监控这个调用栈的进程,每当有任务要做时,事件循环就会逐个执行。最上面的任务先执行。
这里有一个小脚本:
console.log("Hi there");
add(4,5);
function add(x, y) {
return x + y;
}
这是此脚本的调用栈和事件循环的可视化。
图 13.1:事件循环和调用栈的可视化
这里没有多线程在进行。但是它在这里:
console.log("Hi there");
setTimeout(() => console.log("Sorry I'm late"), 1000);
console.log(add(4, 5));
function add(x, y) {
return x + y;
}
setTimeout()
任务被外包给浏览器的 Web API(关于 API 的更多内容请见第十五章,下一步)。当它完成时,这会出现在一个特殊的地方:回调队列。当调用栈为空(并且只有在这种情况下!)时,事件循环会检查回调队列以查找要执行的工作。如果有任何等待的回调,它们将逐个执行。在每次操作之后,事件循环都会首先检查调用栈上的工作。
这是setTimeout()
外包情况下的可视化:
图 13.2:setTimeout
外包的可视化
当setTimeout()
到期时,事件循环将完成调用栈上的所有工作,并检查回调队列,并执行那里的任何任务:
图 13.3:回调队列上的任务的可视化
这就是它将输出的内容:
Hi there
9
Sorry I'm late
让我们看看你是否很好地阅读了上面的文本。你认为当我们将计时器设置为0
时会发生什么,就像这里一样?
console.log("Hi there");
setTimeout(() => console.log("Sorry I'm late"), 0);
console.log(add(4,5));
function add(x, y) {
return x + y;
}
这将输出完全相同的内容。当计时器为0
时,setTimeout()
也将被外包。回调立即放入回调队列,但事件循环甚至不会检查回调队列,直到调用栈为空。所以它仍然会在9
之后打印Sorry I'm late
,尽管计时器为0
。
章节项目
密码检查器
使用允许密码的数组,这个练习将创建一个应用程序来检查这些密码字符串值是否存在于一个列出所有接受密码的数组中。设置一个 Promise 来检查密码是否有效,并根据结果以状态为 true 解析或以状态为 false 拒绝。返回检查结果。
-
创建一个允许密码的数组。
-
创建一个登录函数,该函数将检查参数是否是包含在密码数组中的值。您可以使用
indexOf()
或includes()
方法检查数组中的值并返回结果的布尔值。includes()
方法是一个数组方法,可以检查某个值是否包含在数组中的项中。它将根据结果返回一个布尔值。 -
添加一个返回 Promise 的函数。使用
resolve
和reject
,返回一个 JavaScript 对象,其中包含布尔值true
或false
以指示密码的有效性状态。 -
创建一个检查密码的函数,将密码发送到登录函数,并使用
then()
和catch()
输出拒绝的密码或解析的密码的结果。 -
向检查函数发送几个密码,一些在数组中,一些不在。
自我检查测验
-
修复以下代码中的错误以使用
callback
函数:function addOne(val){ return val + 1; } function total(a, b, callback){ const sum = a + b; return callback(sum); } console.log(total(4, 5, addOne()));
-
记录以下代码的结果:
function checker(val) { return new Promise((resolve, reject) => { if (val > 5) { resolve("Ready"); } else { reject(new Error("Oh no")); } }); } checker(5) .then((data) => {console.log(data); }) .catch((err) => {console.error(err); });
-
需要添加哪些代码行到前面的函数中,以确保函数运行后始终有一个结果,该结果确保单词
done
输出到控制台? -
更新以下代码,使函数返回一个 Promise:
function myFun() { return "Hello"; } myFun().then( function(val) { console.log(val); }, function(err) { conole.log(err); } );
摘要
在本章中,我们讨论了并发。并发使我们的代码能够同时做很多事情,我们可以使用回调、Promise 以及async
和await
关键字来确定事情的顺序。在您的应用程序和页面上实现这些功能将大大提高用户体验!现在的用户需求很高;如果一个网站加载不够快,他们会跳转(例如,回到 Google)。并发有助于更快地交付结果。
接下来的两章将介绍使用 JavaScript 进行现代 Web 开发,并将涉及 HTML5、JavaScript 以及现代 JavaScript 框架,这些框架是真正的变革者。
第十四章:HTML5、Canvas 和 JavaScript
HTML5 于 2012 年发布,并于 2014 年成为标准,这导致浏览器支持各种新功能。HTML5 的引入影响了通过 JavaScript 可用的可能性领域。自从 HTML5 引入以来,JavaScript 在图形、视频、图形交互等方面的选项大大增加,并且变革如此之大,以至于实际上导致了浏览器对 Flash 的支持终止。
HTML5 通过添加新元素,如<header>
,使网页结构更加合理。同时,DOM 也有很大的改进,这导致了性能的提升。还有一些其他的新增功能,你将在本章中看到一些。这里值得提一下的另一个有趣(且有用)的新增功能是<canvas>
元素,我们将在本章中介绍它。
JavaScript 已经为我们提供了许多惊人的功能,但与 HTML5 结合时,在创建动态交互式 Web 应用方面有更多的可能性。这种组合使我们能够提升我们的内容展示水平。现在我们可以在浏览器中处理文件,以及绘制 HTML5 画布,并向其添加图像和文本。
在本章中,我们将探讨 HTML5 为我们带来的令人惊叹的事物。这些主题并不直接相关,但它们有一个共同点,那就是它们都是通过强大的 HTML5 和 JavaScript 团队以及当然,它们都是有趣且有用的。它们将使你能够为你的应用程序的用户创造更加动态和交互式的体验。
本章将涵盖以下主题:
-
使用 JavaScript 介绍 HTML5
-
本地文件读取器
-
地理位置信息
-
HTML5 画布
-
动态画布
-
使用鼠标在画布上绘制
-
保存动态图像
-
页面上的媒体内容
-
数字无障碍
注意:练习、项目和自我检查测验的答案可以在附录中找到。
使用 JavaScript 介绍 HTML5
HTML5 正式上是 HTML 的一个版本。与它的前辈相比,它是一个巨大的进步,使我们能够在网络浏览器中制作完整的应用程序,甚至可以在离线状态下访问。当你看到工作描述中的 HTML5 时,它通常不仅仅是指 HTML。通常,HTML5 与 JavaScript、CSS、JSON 等其他技术的组合也包括在内。
自从 HTML5 以来,我们页面的结构得到了改善。我们有新的元素,如<header>
、<nav>
和<article>
。我们可以使用<video>
元素播放视频,这意味着我们不再需要 Flash,因为 HTML5 已经提供了这些功能。正如我们之前提到的,我们可以使用<canvas>
元素在页面上创建视觉元素或表示动画、图表等视觉内容。一些过去必须使用 JavaScript 完成的事情现在可以仅使用 HTML 完成,例如向网页添加视频和音频。
DOM 的更改也提高了网页的加载时间。在本章中,我们将深入探讨一些 HTML5 特定功能。让我们从从浏览器访问文件开始。
本地文件读取器
自从 HTML5 以来,我们终于可以使用在浏览器中运行的 JavaScript 与本地文件进行交互,这是一个真正令人惊叹的功能。使用此功能,我们可以从我们的设备上传文件到我们的 Web 应用程序,并在应用程序中读取它们。这意味着我们可以将文件附加到表单中,例如,这在许多情况下都很好,无论我们出于何种目的需要上传某种类型的文件,例如,将简历添加到在线工作申请中。
让我们首先确保你使用的浏览器支持此功能。我们可以运行一个简单的脚本来检查它是否支持:
<!DOCTYPE html>
<html>
<body>
<div id="message"></div>
<script>
let message = document.getElementById("message");
if (window.FileReader) {
message.innerText = "Good to go!";
} else {
message.innerText = "No FileReader :(";
}
</script>
</body>
</html>
如果你在这个文件中打开它,当你的浏览器支持文件读取时,它应该会显示 一切正常!如果它显示 No FileReader 😦,请尝试更新你的浏览器或使用另一个浏览器。可以工作的浏览器示例包括 Chrome 和 Firefox。
上传文件
上传文件实际上比你想象的要简单。我们通过添加一个类型为 file
的输入来表示我们想要上传一个文件。以下是一个基本的脚本,它就是这样做的:
<!DOCTYPE html>
<!DOCTYPE html>
<html>
<body>
<input type="file" onchange="uploadFile(this.files)" />
<div id="message"></div>
<script>
let message = document.getElementById("message");
function uploadFile(files) {
console.log(files[0]);
message.innerText = files[0].name;
}
</script>
</body>
</html>
它提供了一个带有 选择文件 按钮的空白 HTML 页面,以及其后的 未选择文件 注释。点击按钮会弹出文件系统,你可以选择一个文件。选择文件后,JavaScript 会被触发。正如你所看到的,我们正在发送在身体中激活的属性文件。这是一个文件列表。因此,我们正在获取列表中的第 0 个索引,即列表中的第一个元素。文件以对象的形式表示。
文件对象在这里被记录到控制台,这使你能够看到所有属性及其相关值。一些重要的属性是 name
、size
、type
和 lastModified
,但还有很多其他属性。
我们将文件的名称放入我们的 div
消息的 innerText
中。因此,在屏幕上,你将看到文件名出现在 div
中。我们可以为多个文件做类似的事情。以下是同时上传多个文件的方法:
<html>
<body>
<input type="file" multiple onchange="uploadFile(this.files)" />
<div id="message"></div>
<script>
let message = document.getElementById("message");
function uploadFile(files) {
for (let i = 0; i < files.length; i++) {
message.innerHTML += files[i].name + "<br>";
}
}
</script>
</body>
</html>
我们已经将多个属性添加到我们的输入元素中。这改变了按钮上的文本;不再是 选择文件,而是现在说 选择文件,因此我们可以选择多个文件。
我们还通过添加循环稍微改变了我们的上传函数。并且,我们不再使用 innerText
,而是现在使用 innerHTML
,因为这样我们就可以使用 HTML 换行符插入一个换行。它将在屏幕上的输入框下方输出所有选定的文件名。
读取文件
有一个用于读取文件的特殊 JavaScript 对象。它有一个非常合适的名字:FileReader
。以下是我们可以如何使用它来读取一个文件。
<!DOCTYPE html>
<html>
<body>
<input type="file" onchange="uploadAndReadFile(this.files)" />
<div id="message"></div>
<script>
let message = document.getElementById("message");
function uploadAndReadFile(files) {
let fr = new FileReader();
fr.onload = function (e) {
message.innerHTML = e.target.result;
};
fr.readAsText(files[0]);
}
</script>
</body>
</html>
正如你所看到的,我们必须指定为了将我们的 HTML 和 JavaScript 连接到文件需要发生什么。我们通过添加一个 onload
事件作为发送事件数据的匿名函数来完成此操作。
可以使用FileReader
对象上的readAs()
方法之一来读取数据。我们在这里使用了readAsText()
,因为我们正在处理文本文件。这触发了实际的读取,并在完成后触发onload
函数,将读取的结果添加到我们的消息中。这接受所有文件类型,但并非所有文件类型都有意义。
为了看到有意义的内容,我们必须上传包含纯文本的内容,例如.txt
、.json
和.xml
。有了这些,我们还可以将文件发送到服务器或处理日志文件的内容。
练习 14.1
本练习将演示在您的网页中上传和显示本地图像文件的过程。使用以下 HTML 和 CSS 作为起始模板:
<!doctype html>
<html>
<head>
<title>Complete JavaScript Course</title>
<style>
.thumb {
max-height: 100px;
}
</style>
</head>
<body>
<input type="file" multiple accept="image/*" />
<div class="output"></div>
<script>
</script>
</body>
</html>
按以下步骤完成脚本元素:
-
在您的 JavaScript 代码中,将页面元素作为变量对象中的值选择。
-
向
input
字段添加事件监听器。应更改事件触发器,以便它立即调用读取器函数。 -
创建一个函数来处理所选文件的读取。
-
使用事件对象,选择触发事件的元素。获取该输入中选择的文件,并将它们分配给
files
变量。 -
遍历所有选定的文件。
-
在循环中将文件索引设置为名为
file
的变量。 -
将图像文件设置为从用户输入字段中选择的循环内的文件。
-
将新创建的
img
标签添加到页面中,创建一个页面区域以输出内容,并将新的页面元素附加到它上。 -
创建一个新的
FileReader
对象。 -
向
fileReader
对象添加一个onload
事件监听器,创建并调用一个匿名函数,将图像的源设置为来自目标元素的输出。将您刚刚创建的图像对象作为参数传递给该函数。 -
使用
readAsDataURL()
,获取当前文件对象并将其传递到文件读取器对象中,以便在onload
完成后使用,并将其添加到页面中。 -
您现在可以从计算机中选择多个图像文件,并在网页上显示它们。
使用地理位置获取位置数据
我们现在将查看窗口对象navigator
,看看我们是否可以定位浏览器的用户。这可以用于许多事情,例如,建议用户附近的餐厅位置。我们可以通过检查navigator.geolocation
来查看GeoLocation
。这是其中一种方法:
<!DOCTYPE html>
<html>
<body>
<script>
window.onload = init;
function init() {
console.dir(navigator.geolocation);
}
</script>
</body>
</html>
如果你查看日志,你可以看到GeoLocation
对象包含的内容,其中一种方法是通过获取用户的当前位置。以下是使用方法:
<!DOCTYPE html>
<html>
<body>
<script>
window.onload = init;
function init() {
navigator.geolocation.getCurrentPosition(showGeoPosition);
}
function showGeoPosition(data) {
console.dir(data);
}
</script>
</body>
</html>
这可能比你预期的要复杂一些,这是因为getCurrentPosition()
方法接受另一个方法作为参数。位置数据被发送到这个函数,而这个函数将使用这些数据作为输入。因此,我们必须将console.dir()
包裹在一个外部函数(称为showGeoPosition()
)中,该函数接受一个参数并输出这些数据,这样我们就可以在控制台中看到这些数据。然后我们可以将这个函数发送到getCurrentPosition()
函数,并查看数据。
如果你运行这个程序,你应该会得到一个GeolocationPosition
对象,其中包含一个coords
属性,包含你的纬度和经度。浏览器可能会提示你是否同意分享你的位置。如果没有任何提示,请确保你的计算机的首选项和设置允许浏览器使用你的位置。
使用这个功能,你可以获取用户的地理位置,并根据它显示个性化的内容,或者为了其他目的收集他们的位置数据,例如分析访客的位置或根据用户的位置显示建议。
HTML5 画布
我们已经提到过<canvas>
元素是 HTML5 中的新元素了吗?这是一个令人惊叹的工具,可以帮助你创建动态的 Web 应用程序。以下是设置画布的方法:
<!DOCTYPE html>
<html>
<body>
<canvas id="c1"></canvas>
<script></script>
</body>
</html>
当你打开这个页面时,你将看不到任何内容。为什么?嗯,画布元素默认是一个白色的矩形,在白色背景上你无法看到它。你可以添加一些 CSS 来给画布添加边框或给主体添加背景颜色,这样你的画布就会显现出来。
但是,我们可能想在上面放置一些内容,并且我们需要 JavaScript 来实现这一点。让我们使用 JavaScript 在上面创建一个“绘图”:
<!DOCTYPE html>
<html>
<head>
<style>
canvas {
border: 1px solid black;
}
</style>
</head>
<body>
<canvas id="c1"></canvas>
<script>
let canvas = document.getElementById("c1");
let ctx = canvas.getContext("2d");
canvas.width = 500; //px
canvas.height = 500; //px
ctx.fillRect(20, 40, 100, 100);
</script>
</body>
</html>
画布的上下文被读取并存储在ctx
变量中(上下文的常见缩写)。我们需要这个变量来在画布上绘制。我们将画布的尺寸更改为500
像素乘以500
像素。这不同于使用 CSS 设置宽度和高度;这添加了 HTML 属性的width
和height
。
在画布的上下文中使用fillRect()
方法,我们可以在画布上绘制一个矩形。它接受四个参数。前两个是图形应该添加到画布上的x和y坐标。最后两个是矩形的宽度和高度。在我们的例子中,它是一个正方形。以下是结果的样子:
图 14.1:fillRect()方法在我们 500 像素乘以 500 像素的画布上的结果
我们还可以更改我们绘制的颜色。你可以通过替换上一个 HTML 文档中的 JavaScript 代码,用以下代码来得到一个粉红色的方块:
<script>
let canvas = document.getElementById("c1");
let ctx = canvas.getContext("2d");
canvas.width = 500; //px
canvas.height = 500; //px
ctx.fillStyle = "pink";
ctx.fillRect(20, 40, 100, 100);
</script>
我们现在刚刚使用了单词 pink,但你也可以使用十六进制颜色代码为 fillStyle
属性工作,例如,对于粉红色可以是这样的:#FFC0CB
。前两个字符指定红色量(这里为 FF
),第三和第四个字符指定绿色量(C0
),最后两个字符指定蓝色量(CB
)。这些值从 00
到 FF
(十进制数中的 0 到 255)不等。
你可以使用 canvas 做的事情远不止绘制。让我们看看如何将文本添加到我们的画布上。
练习 14.2
我们将实现形状,并使用 HTML5 canvas 元素通过 JavaScript 在网页上绘制。使用 JavaScript 绘制一个矩形。输出将类似于以下内容:
图 14.2:练习结果
按以下步骤操作:
-
将 canvas 元素添加到页面中。
-
将宽度和高度设置为 640 像素,并使用 CSS 为元素添加 1 像素的边框。
-
在 JavaScript 中,选择 canvas 元素并将
Context
设置为2d
。 -
将填充样式设置为红色。
-
使用矩形创建形状的输出。
-
设置矩形的轮廓。
-
清除矩形内部的填充,使其透明并显示背景颜色。
动态画布
我们可以绘制更复杂的形状,添加图像和文本。这使得我们可以将我们的画布技能提升到下一个层次。
向画布添加线条和圆形
这里我们将看到如何绘制线条和圆形。以下是一段示例代码,用于绘制线条:
<!DOCTYPE html>
<html>
<head>
<style>
#canvas1 {
border: 1px solid black;
}
</style>
</head>
<body>
<canvas id="canvas1"></canvas>
<script>
let canvas = document.getElementById("canvas1");
let ctx = canvas.getContext("2d");
canvas.width = 100;
canvas.height = 100;
ctx.lineWidth = 2;
ctx.moveTo(0, 20);
ctx.lineTo(50, 100);
ctx.stroke();
</script>
</body>
</html>
线宽设置为 2
像素。这首先将焦点放在 0
(x) 和 20
(y)。这意味着它位于画布的非常左侧边缘,距离顶部 20
像素。这个画布较小;它是 100
x 100
像素。第二个点是 50
(x) 和 100
(y)。这就是线的样子:
图 14.3:绘制线条到画布的结果
在我们转向文本之前,这是绘制圆形的方法。
<!DOCTYPE html>
<html>
<head>
<style>
#canvas1 {
border: 1px solid black;
}
</style>
</head>
<body>
<canvas id="canvas1"></canvas>
<script>
let canvas = document.getElementById("canvas1");
let ctx = canvas.getContext("2d");
canvas.width = 150;
canvas.height = 200;
ctx.beginPath();
ctx.arc(75, 100, 50, 0, Math.PI * 2);
ctx.stroke();
</script>
</body>
</html>
我们使用 arc()
方法来创建曲线或圆形。它需要五个参数:
-
画布上的起始位置 x
-
画布上的起始位置 y
-
圆的半径
-
起始角度(以弧度为单位)
-
结束角度(以弧度为单位)
因此,如果我们不想画圆形,而是画半圆形,例如,我们必须指定不同的起始和结束角度(以弧度为单位)。这次我们使用了 stroke()
方法来实际绘制,而不是 fill()
方法:
图 14.4:使用 arc() 方法绘制圆形的结果
stroke()
只绘制线条,而 fill()
则填充整个形状。
在 canvas 中,形状和线条将根据它们绘制的顺序叠加。你首先绘制的形状会在后面的形状下面。当你实际在画布上绘画时,情况正是如此。你将在下一个练习中看到这一点。
练习 14.3
在这个练习中,你将使用画布绘制一个棒人:
图 14.5:在网页浏览器画布元素内的练习结果
-
创建页面元素并准备在画布上绘制。
-
从你的画布对象的顶部中心大致开始路径。
-
使用
arc()
设置左眼的位置,大约在刚刚绘制的弧的中心顶部左侧,然后添加另一个弧来绘制右眼。创建一个半圆来代表嘴巴(半圆的弧度角是π)并填充所有。 -
将绘制位置移动到中心并绘制一个代表鼻子的线条。
-
用从弧的中心向下的线条绘制身体,创建左臂,然后移动绘制位置来绘制右臂,其宽度是左臂的两倍。回到中心并继续向下绘制左腿,回到中心,绘制右腿的线条。
-
移动到顶部,设置颜色为蓝色,并绘制一个代表帽子的三角形。
添加文本到画布
我们也可以以类似的方式向画布添加文本。在这个例子中,我们设置了一个字体和字体大小,然后将文本写入画布:
<!DOCTYPE html>
<html>
<head>
<style>
#canvas1 {
border: 1px solid black;
}
</style>
</head>
<body>
<canvas id="canvas1"></canvas>
<script>
let canvas = document.getElementById("canvas1");
let ctx = canvas.getContext("2d");
canvas.width = 200;
canvas.height = 200;
ctx.font = "24px Arial";
let txt = "Hi canvas!";
ctx.fillText(txt, 10, 35);
</script>
</body>
</html>
使用fillText()
方法来添加文本。我们必须指定三个参数:文本、x位置和y位置。这里是结果:
图 14.6:使用fillText()
方法的结果
我们已经指定文本从顶部开始的位置为35
像素。我们可以指定文本的其他方面,例如,如下所示:
ctx.textAlign = "center";
在这里,我们使用画布上的textAlign
属性来指定文本应该如何对齐。
练习 14.4
我们将处理文本并在你的画布元素中添加文本。以下练习将演示如何动态添加文本并在你的画布元素内定位它。练习代码的结果将类似于这个图示:
图 14.7:练习结果
采取以下步骤:
-
创建一个简单的 HTML 文档,并将画布元素添加到你的页面中。将高度和宽度设置为
640
,并为元素添加一个1
像素的边框,以便你可以在页面上看到它。 -
将页面元素作为 JavaScript 变量中的值选择。
-
创建一个包含消息
Hello World
的字符串变量。 -
使用
font
属性设置字体样式,使用fillStyle
属性设置蓝色填充颜色。你还可以将文本对齐到左侧。 -
使用
fillText
将文本添加到画布上,并设置文本的x和y位置。 -
设置新的字体和红色颜色。
-
创建一个循环,并使用循环变量的值向页面画布元素添加文本。
向画布添加和上传图像
我们可以向画布添加一个图像。我们可以简单地从我们的页面中获取一个图像,并将其添加到我们的画布上:
<!DOCTYPE html>
<html>
<head>
<style>
canvas {
border: 1px solid black;
}
</style>
</head>
<body>
<canvas id="c1"></canvas>
<img id="flower" src="img/flower.jpg" />
<script>
window.onload = function () {
let canvas = document.getElementById("c1");
canvas.height = 300;
canvas.width = 300;
let ctx = canvas.getContext("2d");
let myImage = document.getElementById("flower");
ctx.drawImage(myImage, 10, 10);
};
</script>
</body>
</html>
我们在这里将其包裹在一个onload
事件监听器中,因为我们想确保在从 DOM 中获取图像之前图像已经加载完成,否则画布将保持空白。我们使用drawImage()
方法将图像添加到画布上。它需要三个参数:图像、x位置和y位置。
我们也可以在另一个画布内部使用一个画布。我们这样做的方式与使用图像时完全一样。这是一个非常强大的功能,因为它使我们能够使用用户输入的一部分绘图,例如。让我们看看如何做到这一点的一个例子:
<!DOCTYPE html>
<html>
<head>
<style>
canvas {
border: 1px solid black;
}
</style>
</head>
<body>
<canvas id="canvas1"></canvas>
<canvas id="canvas2"></canvas>
<canvas id="canvas3"></canvas>
<script>
let canvas1 = document.getElementById("canvas1");
let ctx1 = canvas1.getContext("2d");
ctx1.strokeRect(5, 5, 150, 100);
let canvas2 = document.getElementById("canvas2");
let ctx2 = canvas2.getContext("2d");
ctx2.beginPath();
ctx2.arc(60, 60, 20, 0, 2 * Math.PI);
ctx2.stroke();
let canvas3 = document.getElementById("canvas3");
let ctx3 = canvas3.getContext("2d");
ctx3.drawImage(canvas1, 10, 10);
ctx3.drawImage(canvas2, 10, 10);
</script>
</body>
</html>
我们创建了三个画布,其中两个添加了形状,第三个是前两个的组合。看起来是这样的:
图 14.8:结果:三个带有形状的画布
我们还可以将图像上传到画布上。当您想向用户展示刚刚上传的内容的预览时,例如个人资料图片,这非常有用。这非常类似于从网页中抓取<img>
元素并使用该元素,但这次我们需要从上传的文件中读取数据,创建一个新的图像元素,然后将该图像绘制到画布上。
以下代码就是这样做:
<html>
<head>
<style>
canvas {
border: 1px solid black;
}
</style>
</head>
<body>
<input type="file" id="imgLoader" />
<br>
<canvas id="canvas"></canvas>
<script>
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
let imgLoader = document.getElementById("imgLoader");
imgLoader.addEventListener("change", upImage, false);
function upImage() {
let fr = new FileReader();
fr.readAsDataURL(event.target.files[0]);
fr.onload = function (e) {
let img = new Image();
img.src = event.target.result;
img.onload = function () {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
};
console.log(fr);
};
}
</script>
</body>
</html>
每当输入字段的输入发生变化时,upImage()
方法就会被执行。这个方法做了一些事情,所以让我们来分解一下。首先,我们创建一个新的FileReader
并添加上传的文件。在这种情况下只有一个,所以我们使用索引0
。与我们已经看到的readAsText()
不同,我们现在使用readAsDataURL()
,我们可以用它来读取图像。
这将触发onload
事件。在我们的情况下,这会创建一个可以稍后添加到画布中的新图像。作为一个源,我们添加我们读取操作的结果,当图像加载完成后,我们将画布的大小更改为图片的大小,然后将图片添加进去。
这些新技能将使您能够在画布上处理图像,绘制自己的图像,从其他地方上传图像,甚至重用网页上的图像。这在许多情况下都很有用,例如,创建基本动画,或者创建上传新个人资料图片到用户个人资料的功能。
练习 14.5
我们将练习将本地图像上传到画布。以下练习将演示如何从您的本地计算机上传图像,并在浏览器中的画布元素内显示它们。
-
设置页面元素,并添加一个输入字段来上传图像。将画布元素添加到页面中。
-
在 JavaScript 中,选择输入字段和画布元素作为 JavaScript 对象。
-
添加一个事件监听器,在输入字段内容发生变化时调用上传函数。
-
创建上述函数来处理图像上传到画布的操作。使用
FileReader
创建一个新的FileReader
对象。在reader.onload
事件中,创建一个新的图像对象。 -
将
onload
监听器添加到图像对象中,以便在图像加载时,将画布的高度和宽度设置为图像高度和宽度的一半。使用ctx.drawImage()
,将图像添加到画布上。 -
将 img 的源设置为输入值的输出结果。
-
使用读取器对象并调用
readAsDataURL()
将文件输入值转换为可读的 base64 图像数据格式,该格式可用于画布中。
向画布添加动画
使用我们迄今为止看到的方法,我们已经开始创建动画。我们通过使用循环和递归,结合 timeout()
来实现这一点。这些带有(短)时间间隔的绘图会导致动画。让我们从一个基本的动画开始:
<!DOCTYPE html>
<html>
<head>
<style>
canvas {
border: 1px solid black;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
window.onload = init;
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
canvas.height = 500;
canvas.width = 500;
var pos = {
x: 0,
y: 50,
};
function init() {
draw();
}
function draw() {
pos.x = pos.x + 5;
if (pos.x > canvas.width) {
pos.x = 0;
}
if (pos.y > canvas.height) {
pos.y = 0;
}
ctx.fillRect(pos.x, pos.y, 100, 100);
window.setTimeout(draw, 50);
}
</script>
</body>
</html>
这将从位置 5
,50
开始绘制一个正方形。然后 50
毫秒后,它将在位置 10
,50
处绘制另一个正方形,然后是 15
,50
。它将不断通过 5
改变这个 x 值,直到 x 大于画布的宽度,此时它被设置为零。这样,该行最后一点白色画布也会被着色成黑色。
目前,它更像是创建一条线,而不是移动的正方形。这是因为我们不断将着色部分添加到画布上,但没有将其重置为之前的颜色。我们可以使用 clearRect()
方法来做这件事。此方法接受四个参数。前两个参数是绘制要清除的矩形的起点(即 x,y)。第三个参数是要清除的矩形的 width
,最后一个参数是 height
。为了清除整个画布,我们必须编写:
ctx.clearRect(0, 0, canvas.width, canvas.height);
将此添加到我们之前示例中的 draw 函数的开头,结果是一个移动的正方形而不是一条粗线被绘制出来,因为之前的正方形没有被保留,但画布每次都会重置,正方形是从头开始绘制的。
练习 14.6
我们将练习在页面上动画化形状和移动对象。这个练习将演示如何使用 HTML5 画布元素和 JavaScript 在页面上移动一个对象。
图 14.9:红色圆圈在画布对象边界内移动
按以下步骤创建一个红色圆圈,然后将其移动到画布边界内,看起来像是在反弹:
-
创建画布并给它应用 1 像素的边框。
-
使用 JavaScript 选择画布页面元素并准备在画布上绘图。
-
创建变量来跟踪 x 和 y 位置,以及 x 方向速度和 y 方向速度。你可以将这些设置为默认值
1
,而 x 和 y 的起始位置可以是画布尺寸的一半。 -
创建一个函数来绘制球。这将把球绘制成x和y位置的红色球弧。此外,球的尺寸应该作为一个变量设置,以便可以从它计算出边界。填充并关闭路径。
-
创建一个函数来移动球,并将该函数的间隔设置为 10 毫秒。
-
在上述移动函数中,清除当前矩形并使用绘制球函数绘制球。
-
检查球的位置。如果球在画布边界之外,你需要改变方向。这可以通过将方向乘以-1 来实现。使用新值更新x和y的位置。
使用鼠标在画布上绘图
我们已经有了所有创建一个我们可以用鼠标在上面绘图的画布的原料。让我们带你了解一下。我们将从设置画布开始:
<!DOCTYPE html>
<html>
<head>
<style>
canvas {
border: 1px solid black;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<input type="color" id="bgColor" />
<script>
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
canvas.width = 700;
canvas.height = 700;
</script>
</body>
</html>
在我们的脚本元素中,我们将添加一个当窗口加载完成时的方法。当窗口加载完成后,我们需要添加一些事件监听器:
window.onload = init; // add this line to the start of the script
function init() {
canvas.addEventListener("mousemove", draw);
canvas.addEventListener("mousemove", setPosition);
canvas.addEventListener("mouseenter", setPosition);
}
我们希望在鼠标移动时进行绘图,并且希望在鼠标移动时更改画布上的当前位置。这也是我们在mouseenter
时想要做的事情。让我们编写设置位置的代码。这将添加到脚本元素中。我们还需要添加位置变量,它再次应该在脚本开始时声明:
let pos = {
x: 0,
y: 0,
};
以及设置位置的函数:
function setPosition(e) {
pos.x = e.pageX;
pos.y = e.pageY;
}
这个函数在mousemove
和mouseenter
事件上被触发。触发此事件的事件具有pageX
和pageY
属性,我们可以使用这些属性来获取鼠标的当前位置。
在画布上绘图的最后一个必备元素是draw()
方法。下面是它可能的样子:
function draw(e) {
if (e.buttons !== 1) return;
ctx.beginPath();
ctx.moveTo(pos.x, pos.y);
setPosition(e);
ctx.lineTo(pos.x, pos.y);
ctx.lineWidth = 10;
ctx.lineCap = "round";
ctx.stroke();
}
我们从一个可能看起来很奇怪的东西开始,但这是一个确保鼠标实际上被点击的绝佳技巧。我们不希望在没有任何鼠标按钮被点击时进行绘图。这个方法通过在未点击时从方法中返回来防止这种情况。
然后我们开始绘制路径。我们始终有一个当前的x和y,所以它们被设置为坐标一,然后我们再次设置它们,并使用这些新坐标来绘制线条。我们给它一个圆角线条端点以实现平滑的线条,并设置线条宽度为10
。然后我们绘制线条,只要鼠标在移动,draw()
函数就会被再次调用。
应用程序现在可以打开并作为一个功能性的绘图工具使用。我们还可以在这里为用户提供更多选项,例如,添加一个颜色选择器来更改用户绘制的颜色。为了做到这一点,我们将在 HTML 中添加一个颜色选择器,如下所示:
<input type="color" id="bgColor" />
然后在 JavaScript 中通过添加一个事件监听器来更改选定的颜色,当输入框的值发生变化时:
let bgColor = "pink";
let bgC = document.getElementById("bgColor");
bgC.addEventListener("change", function () {
bgColor = event.target.value;
});
我们从粉红色开始,并用用户在颜色选择器中选择的颜色覆盖它。
练习 14.7
我们将创建一个在线绘图板,并包括宽度、颜色和擦除当前绘图的动态值。使用以下 HTML 作为此项目的模板,以添加 JavaScript 代码:
<!doctype html>
<html>
<head>
<style>
canvas {
border: 1px solid black;
}
</style>
</head>
<body>
<div class="controls">
<button class="clear">Clear</button> <span>Color
<input type="color" value="#ffff00" id="penColor"></span> <span>Width
<input type="range" min="1" max="20" value="10" id="penWidth"></span> </div>
</div>
<canvas id="canvas"></canvas>
<script>
</script>
</body>
</html>
执行以下步骤:
-
在 JavaScript 中将页面元素作为变量对象选择。获取输入字段并选择按钮作为对象。
-
为按钮添加事件监听器,以运行一个函数来清除当前画布。在 clear 函数中,使用
confirm()
方法检查用户是否想要擦除画布绘图。如果他们确认使用clearRect()
,则删除画布元素的内容。 -
为x和y设置全局位置对象,并通过添加鼠标事件的事件监听器来更新位置。如果鼠标移动被触发,则调用 draw 函数。设置位置以更新鼠标位置,将全局位置值设置为鼠标的x和y。
-
在 draw 函数中,检查鼠标按钮是否被按下,如果没有,则添加
return
。如果按下,我们就可以在画布上绘制。设置新的路径并移动到x和y的位置。开始一条新线,从颜色输入字段获取strokestyle
值,并从输入宽度值设置linewidth
值。添加stroke()
方法以将新线添加到页面上。
保存动态图像
我们可以将画布转换为图像,然后作为下一步将其保存。为了将其转换为图像,我们需要在脚本元素中添加以下内容:
let dataURL = canvas.toDataURL();
document.getElementById("imageId").src = dataURL;
我们正在将画布转换为数据 URL,这将成为我们图像的来源。我们希望在点击保存按钮时发生此操作。以下是按钮:
<input type="button" id="save" value="save" />
事件监听器:
document.getElementById("save").addEventListener("click", function () {
let dataURL = canvas.toDataURL();
document.getElementById("holder").src = dataURL;
});
现在每当点击保存按钮时,它将使用从画布生成的数据 URL 更新图像。画布元素内的任何内容都将转换为 base64 数据图像值,并添加到页面的 img 标签内。
在以下示例中,有一个 200x200 像素的画布和一个相同大小的空图像。当选择一种颜色时,在画布上绘制一个 100x100 像素的正方形。当点击保存按钮时,这个画布被转换为图像。然后可以保存这个图像。以下是示例的代码:
<!doctype html>
<html>
<head>
<style>
canvas {
border: 1px solid black;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<input type="color" id="squareColor" />
<br>
<img src="img/" width="200" height="200" id="holder" />
<input type="button" id="save" value="save" />
<script>
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
canvas.width = 200;
canvas.height = 200;
const penColor = document.getElementById("squareColor");
penColor.addEventListener("change", function () {
color = event.target.value;
draw(color);
});
document.getElementById("save").addEventListener("click", function () {
let dataURL = canvas.toDataURL();
document.getElementById("holder").src = dataURL;
});
function draw(color) {
ctx.fillStyle = color;
ctx.fillRect(70, 70, 100, 100);
}
</script>
</body>
</html>
以下是保存图像后的样子:
图 14.10:保存图像的结果
页面上的媒体
页面上有特殊的媒体元素。我们将向您展示如何添加音频和视频,以及如何在网页上嵌入 YouTube。
将音频播放器添加到页面非常简单:
<!DOCTYPE html>
<html>
<body>
<audio controls>
<source src="img/sound.ogg" type="audio/ogg">
<source src="img/sound.mp3" type="audio/mpeg">
</audio>
</body>
</html>
如果你希望用户能够控制暂停和播放以及音量,请指定controls
属性。如果你想让它自动播放,你必须添加autoplay
属性。通过源元素,你指定可以播放的音频文件。浏览器将只选择一个,并且会选择它支持的第一个(从上到下)。
将视频添加到网页上与添加音频非常相似。以下是这样做的方法:
<video width="1024" height="576" controls>
<source src="img/movie.mp4" type="video/mp4">
<source src="img/movie.ogg" type="video/ogg">
</video>
经常你会想要链接到 YouTube。以下是这样做的方法:
<iframe
width="1024"
height="576"
src="img/v6VTv7czb1Y"
>
</iframe>
你将不得不使用iframe
元素。这是一个特殊的元素,它允许在当前网页内嵌入另一个网页。然后你可以将 YouTube 嵌入链接作为源。embed
之后的最后一段代码来自视频 URL。
视频的高度和宽度属性可以被改变以使视频变大或变小。如果你想全屏显示,你可以这样更改宽度和高度:
<iframe
width="100%"
height="100%"
src="img/v6VTv7czb1Y"
>
</iframe>
如果你只想让它占据屏幕的一部分,你可以相应地调整宽度和高度属性。
你也可以使用autoplay
属性来自动播放这些视频。如果你在多个视频上使用自动播放,那么它们都不会自动播放,以保护访客免受网页上所有噪音的干扰。如果你的视频在浏览器中开始发出噪音,这通常被认为是令人烦恼的。添加muted
属性可以避免这种情况。
HTML 中的数字无障碍性
对于视觉障碍人士或无法使用鼠标的人来说,数字无障碍性非常重要。为了使用互联网,即使视力很小或没有视力,也需要屏幕阅读器。这是一款特殊的软件,它读取屏幕上的内容或使用连接到计算机的特殊设备将其转换为盲文。无法使用鼠标的人通常会依赖语音来给计算机下达指令。
早期的网络应用在无障碍性方面非常糟糕。幸运的是,WAI-ARIA 创建了一个技术规范,说明了如何使互联网数字无障碍。如果正确实现,动态部分可以被识别,通过向 HTML 添加语义和元数据,它对外部工具的使用性更好。
语义可能是这里最重要的部分之一。这归结为使用正确的 HTML 元素来实现正确的目的。如果某个元素应该被点击,最好将其制作成<button>
元素,而不是例如<span>
。如果它是一个按钮,那么可以使用Tab
键导航到它,并使用Enter
键点击它。
这同样适用于标题。你可以使用特殊类创建看起来像标题的东西,并给它一个布局,但屏幕阅读器正在寻找h1
、h2
和h3
。你应该始终使用标题元素来表示标题。这有助于屏幕阅读器并提高你网站的可用性。而且作为一个额外的好处,它还有助于你在 Google 上获得更高的排名,因为爬虫也会检查标题以了解你网站上什么内容是重要的。
使用描述性的标签和链接文本也很重要。如果链接部分只是点击此处,那没有帮助。像点击此处注册夏季活动这样的描述要好得多。
在这本书的整个过程中,我们也对我们的输入框做了一些错误的事情。为了使输入字段可访问,您必须添加一个标签元素。这将使屏幕阅读器更容易识别输入框的内容。所以这通常是不良的做法:
<input type="text" id="address" />
这要好得多,因为现在屏幕阅读器也可以读取它了(因此视力受损的人可以理解它):
<label for="address">Address:</label>
<input type="text" id="address" />
最后一个你可能已经知道的是图像的alt
属性。如果屏幕阅读器遇到图像,它将读取alt
描述。所以请确保这些描述是描述性的,即使图像不是很重要。由于显然无法知道它不是重要的,因为你无法看到图像,所以你唯一知道的是你无法看到一些图片。以下是添加alt
文本的方法:
<img src="img/umbrella.jpg" width="200" height="200" alt="rainbow colored umbrella" />
这些技巧对于练习和测试目的并不是很重要,但当你准备创建专业应用程序时,它们非常有用。考虑到可访问性会使你的应用程序对每个人来说都更容易访问。而且正如我所说,谷歌(目前)将通过提高你的排名来奖励这种良好的行为,你的应用程序将更有利可图,因为更多的人可以使用它!
章节项目
创建矩阵效果
此练习将创建一个从顶部到底部移动的文本连续动画。最终产生的效果将显示字符在画布元素中向下移动,并在接近屏幕底部时消失和淡出,因为将会有更多的新字符添加到画布中替代它们。随机字符可以是 0 或 1,并且将根据数字在相应位置上就位,这代表字符绘制的垂直位置。
画布将被填充为黑色背景,这将使用不透明度在重新绘制时创建淡出效果:
图 14.11:期望的矩阵效果
请按照以下步骤操作:
-
创建一个简单的 HTML 文档,并在 JavaScript 中创建一个画布元素,并将
getContent
元素作为2d
添加。 -
选择那个画布元素,并将高度和宽度属性设置为 500x400。将其添加到文档的主体中。
-
创建一个名为
colVal
的空数组,并创建一个循环向数组中添加若干项,这些项的值将为 0。您需要添加到数组中的项数可以通过将宽度除以十来确定,这应该是每列之间的宽度。数组中的值将是您将要设置的fillText()
方法的起始垂直位置。 -
创建一个主要矩阵函数,以 50 毫秒的间隔运行。
-
将
fillStyle
设置为黑色,不透明度为.05,这样当它覆盖在现有元素上时,会产生一种渐变效果。 -
将画布字体颜色设置为绿色。
-
使用数组映射,迭代
colVal
数组中当前的所有项,该数组包含输出文本的垂直位置。 -
在映射中设置要显示的字符。我们希望它在 0 和 1 之间交替,因此使用
Math.random()
生成文本输出的 0 或 1 值。你可以使用三元运算符来完成此操作。 -
使用索引值乘以 10 来设置 x 的位置,这是每个新字母的开始。使用
colVal
数组中的索引,这将创建移动字符的单独列。 -
使用 ctx
fillText()
方法在画布中创建字符,将输出字符设置为随机的 0 或 1 值,使用posX
作为列x位置,以及posY
,它是colVal
数组中项的值,作为输出y轴的位置。 -
添加一个条件来检查y的位置是否大于 100 加上 0-300 的随机值。数字越大,它在y位置上的下落时间就越长。这是随机的,所以不是所有数字都结束在同一个位置。这将产生初始下落后的一种错落效果。
-
如果y的位置没有超过随机值和 100,则将索引项的值增加 10。将这个y的值重新分配给
colVal
数组中的项,这样它就可以在下一个迭代中使用。这将使字母在下一个绘制回合中在画布上向下移动 10 像素。
倒计时时钟
这个练习将产生一个实时倒计时时钟,它将显示输入日期字段中剩余的天数、小时、分钟和秒。调整输入日期字段将更新倒计时时钟。它还将使用本地存储来捕获和保存输入字段的值,因此如果刷新页面,输入字段将仍然保留日期值,倒计时时钟可以继续从输入字段向下计数到该日期值。你可以使用以下 HTML 模板:
<!doctype html>
<html>
<head>
<title>JavaScript</title>
<style>
.clock {
background-color: blue;
width: 400px;
text-align: center;
color: white;
font-size: 1em;
}
.clock>span {
padding: 10px;
border-radius: 10px;
background-color: black;
}
.clock>span>span {
padding: 5px;
border-radius: 10px;
background-color: red;
}
input {
padding: 15px;
margin: 20px;
font-size: 1.5em;
}
</style>
</head>
<body>
<div>
<input type="date" name="endDate">
<div class="clock"> <span><span class="days">0</span> Days</span> <span><span class="hours">0</span>
Hours</span> <span><span class="minutes">0</span> Minutes</span> <span><span class="seconds">0</span>
Seconds</span>
</div>
</div>
<script>
</script>
</body>
</html>
我们已经创建了页面元素,包括类型为日期的输入,主要的clock
容器,并为days
、hours
、minutes
和seconds
添加了 span 标签。它们已经根据需要进行了标记并应用了 CSS 样式。
你可以采取以下步骤:
-
将页面元素作为 JavaScript 对象选择,以及将主时钟输出区域作为 JavaScript 对象的值。
-
为
timeInterval
创建变量以及一个全局布尔值,该布尔值可以用来停止时钟计时器。 -
检查本地存储中是否已经设置了倒计时项。如果有,则使用该值。
-
创建一个条件和函数来启动时钟,将其设置为保存的值,并将输入字段日期值设置为本地存储中保存的值。
-
添加一个事件监听器,当输入字段的值改变时调用一个函数。如果已改变,清除间隔并设置新的
endDate
值到本地存储中。 -
使用新的
endDate
输入值启动时钟的启动时钟函数。 -
创建一个用于启动计时的函数,该函数用于启动计数器。在该函数内,你可以创建一个更新计数器并将新的时钟时间值输出到页面时钟容器区域的函数。
-
在此函数内,检查
timeLeft
是否小于计数器时间。创建一个单独的函数来处理此情况。如果它更少,停止计时器。 -
如果剩余时间更多且在对象内有值,则通过属性名输出对象,并将你在剩余时间函数对象中使用的属性名与你在网页元素中使用的类名匹配,以便它们匹配,你可以节省重写它们的时间。遍历所有对象值并将它们分配到
innerHTML
页面元素中。 -
在剩余时间函数中,获取当前日期。使用
Date.parse()
解析日期并计算直到计数器结束的总毫秒数。将总天数、小时数、分钟数和秒数作为响应对象返回,以便在更新函数中使用。 -
如果计数器为假且已通过结束时间,则清除间隔。如果计数器仍然有效,设置间隔每 1,000 毫秒运行一次更新函数。
在线绘画应用
创建一个绘图应用,用户可以在画布元素中使用鼠标绘图。当用户在画布元素内且点击鼠标按钮时,按住按钮会在画布元素内添加线条,产生绘图效果。绘图铅笔的颜色和宽度可以动态更改以增加功能。此外,此应用还将包括一个按钮来保存并从画布元素下载图像,以及清除当前画布内容。
你可以使用以下模板并添加 JavaScript 代码:
<!doctype html>
<html>
<head>
<title>Canvas HTML5</title>
<style>
#canvas {
border: 1px solid black;
}
</style>
</head>
<body>
<canvas id="canvas" width="600" height="400"></canvas>
<div>
<button class="save">Save</button>
<button class="clear">clear</button>
<span>Color: <input type="color" value="#ffff00" id="penColor"></span>
<span>Width: <input type="range" min="1" max="20" value="10" id="penWidth"></span>
</div>
<div class="output"></div>
<script>
</script>
</body>
</html>
我们创建了一个保存按钮和一个清除按钮,一个用于颜色输入的 HTML5 颜色类型输入框,以及范围类型来获取笔宽度的数值。我们还添加了页面元素用于画布和输出区域。
请按照以下步骤操作:
-
使用 JavaScript,将所有页面元素作为 JavaScript 对象选择,并设置画布元素用于绘图。
-
设置一个变量来跟踪笔的位置。
-
在画布上添加一个事件监听器来跟踪鼠标移动。更新笔位置到
lastX
和lastY
位置,然后将位置设置为clientX
和clientY
。创建一个在笔位置绘图的函数并调用绘图函数。 -
对于
mousedown
,将draw
设置为true
,对于mouseup
和mouseout
,将draw
设置为false
。 -
在绘图函数中,从笔的位置值开始移动路径,并将笔的样式设置为笔的颜色,将笔的宽度设置为笔的宽度。这些可以通过点击输入并更新它们的 HTML 值来更改。添加笔的样式并关闭绘图路径。
-
为清除按钮添加事件监听器。如果点击,创建一个函数来确认用户想要删除并清除绘图,然后如果确认,调用
clearRect()
来清除画布内容。 -
为保存图片添加另一个事件监听器。当点击时,它应该调用一个函数,该函数使用
toDataURL
作为基于 64 的图像数据获取画布对象。你可以将其记录到控制台以查看其外观。 -
创建一个 img 元素并将其预置于输出区域元素。将
src
路径设置为dataURL
值。 -
要设置图片下载,创建一个锚点标签,将其附加到 HTML 页面元素内的任何位置,并创建一个文件名。你可以使用
Math.random()
生成一个唯一的文件名。将超链接设置为下载属性,并将href
路径设置为dataURL
路径,然后使用click()
方法触发点击。一旦点击,就删除链接元素。
自我检查测验
-
以下哪种说法是准备绘图的正确方式?
const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); var canvas = document.getElementById("canvas"); var ctx = getContext("canvas"); var ctx = canvas.getContext("canvas");
-
以下代码将做什么?
<canvas id="canvas"></canvas> <script> var canvas = document.getElementById("canvas"); var ctx = canvas.getContext("2d"); canvas.height = 600; canvas.width = 500; ctx.beginPath(); ctx.fillStyle = "red"; ctx.arc(50, 50, 50, 0, Math.PI * 2); ctx.fill(); ctx.closePath(); </script>
-
没有错误,代码有误
-
画一个红色方块
-
画一个红色圆圈
-
画半个圆
-
-
在画布元素内绘制线条需要哪三种方法,以及它们的顺序是什么?
摘要
在本章中,我们讨论了许多使用 HTML5 对我们 JavaScript 工具箱的改进。这些新技能将真正增强我们构建交互式 Web 应用的能力。我们从本地文件读取器开始,它使我们能够使用几种方法上传和读取文件,例如readAsText()
方法。然后我们看到了如何获取用户的GeoLocation
。这可以很好地用于个性化建议,例如,用于餐厅或停车位。
画布是我们在网页上可以做到的另一项惊人的功能。画布允许我们绘图、写文本、添加图像(通过绘制和上传),以及创建完整的动画。所有这些都可以使用画布上的方法完成。
我们然后查看页面上的媒体以及如何添加音频和视频。最后,我们讨论了数字可访问性的主题以及如何确保你的网站对所有人都是可访问的,无论是否有屏幕阅读器。
到目前为止,我们可以这么说,你已经做到了!你已经通过许多基本和高级的 Web 开发主题。在最后一章,我们将探讨你下一步要采取的步骤,将你的技能提升到下一个层次,超越纯 JavaScript,这正是本书的重点。
第十五章:下一步
你已经走得很远了!在这个阶段,你应该已经掌握了 JavaScript 的基础知识。你能够创建应用程序、编写巧妙的脚本,并且能够阅读大量的代码。这为接下来的重要步骤打下了坚实的基础。在本章中,我们将通过实践和探索 JavaScript 提供的无限可能性中你感兴趣的部分,将你所学的知识提升到下一个层次。
我们不会在这里过多地详细介绍所有这些主题。这些细节很快就会过时,并且互联网上有大量针对每个主题精心制作的教程和信息。可能性很大,在你阅读这段文字的时候,我们推荐的框架和库已经过时了。好消息是,下一个大事件使用相同概念的可能性非常大。
本章将作为你在 JavaScript 中下一步的起点。我们将涵盖以下主题:
-
库和框架
-
学习后端
-
下一步
注意:练习、项目和自我检查测验的答案可以在附录中找到。
库和框架
让我们从库和框架开始。库基本上是预先编程的 JavaScript 模块,你可以使用它们来加速你的开发过程。它们通常为你做一件特定的事情。框架非常相似,它们也是预先编程的,但它们不仅仅为你做一件事情,而是安排了一系列的事情。这就是为什么它被称为框架,它确实为你提供了一个坚实的起点,并且通常要求你的文件有一定的结构才能做到这一点。框架通常是一组提供一站式解决方案的库。或者至少是多合一的解决方案。你最终甚至会发现自己在框架之上使用外部库。
以一个非代码的例子来说明,如果我们开始建造一辆汽车,我们可以从头开始,并且自己制作汽车的所有部件。这正是我们在这本书中到目前为止所做的事情。有了库,我们可以获得现成的部件——在我们的汽车例子中,我们可以获得完全建成的椅子,我们只需要将其安装到我们已建造的汽车底盘上。如果我们使用框架来制作汽车,我们会得到汽车的骨架,其中已经包含了所有必要的部件,而且它可能已经能够驾驶了。我们只需要专注于定制汽车并确保它包含我们想要和需要的所有特殊功能。在这样做的时候,我们必须牢记我们已有的汽车骨架,并继续以这种方式进行。
如您所想,如果我们使用库和框架,我们的汽车项目将会更快完成。此外,使用库和框架时遇到的问题也会更少,因为预先准备的部分已经被许多人测试过了。如果我们从头开始制作自己的汽车座椅,那么一年后它们可能就不再舒适了,而标准的解决方案已经被彻底检查过了。
因此,库和框架不仅加快了进程,还为你提供了一个更稳定、经过更好测试的解决方案。难道没有缺点吗?当然有。其中最重要的可能是灵活性,因为你将不得不遵循你所使用的框架的结构。在某种程度上,这也可以是一个优点,因为它通常要求你采用良好的编码风格,这将提高代码质量。
另一个缺点是,你将不得不在所使用的框架或库更新时不断更新你的应用程序。这非常重要,尤其是当更新是针对安全问题的修复时。一方面,框架和库非常可靠,但由于它们被广泛使用,黑客发现弱点并不罕见。如果他们找到了一个,这将给他们提供许多应用程序的机会,包括你的应用程序。另一方面,你自己的代码可能比平均框架要弱得多。
然而,在许多情况下,破解你定制的应用程序可能成本太高。例如,当你只是有一个在线的爱好项目时,你可能不会支付大量的赎金给黑客,而且你应用程序中的数据也可能不值得黑客的努力。而一个试图利用常用框架在随机数量网站上弱点的脚本却是常见的。为了最小化风险,经常更新你的依赖项,并关注你库或框架所有者报告的弱点。
库
技术上,我们无法使用框架和库做比不用它们更多的事情。也就是说,如果我们不考虑时间。框架和库使我们能够更快地开发出高质量的软件,这也是它们如此受欢迎的原因。
我们将在这里讨论一些最受欢迎的库。这绝对不是一个独家列表,它也非常动态,所以一年后其他库或框架可能更受欢迎。这就是为什么我们不会在这里涵盖完整的教程和如何开始的原因。我们只会解释基本原理并展示一些代码片段。然而,这仍然是你在开发生涯中迈出下一步的稳固基础。
许多库可以通过在 HTML 的头部添加一个 script 标签来包含在页面中,如下所示:
<script src="img/librarycode.js"></script>
我们将从讨论一些常见的库开始。
jQuery
jQuery 可以说是最著名的 JavaScript 库。在过去,它会被编译成特定浏览器的最新版本的 JavaScript,使用起来非常方便。如今,它只是以不同的方式编写我们在书中看到的一些东西。你可以通过代码中的美元符号数量轻松识别 jQuery。你还可以通过在网站的控制台中输入 $
或 jQuery
来判断一个网站是否使用了 jQuery,如果它返回 jQuery 对象,则表示该网站正在使用 jQuery。jQuery 库主要关注从 DOM 中选择 HTML 元素,并与它们交互和操作。它大致看起来像这样:
$(selector).action();
使用美元符号表示你想要开始使用 jQuery,而使用选择器可以选中 HTML 中的元素。这里的符号有点像 CSS:
-
一个简单的字符串值可以定位一个 HTML 元素:
$("p")
-
一个单词或短语前的点号表示你想要选择具有特定类的所有元素:
$(".special")
-
一个井号可以定位具有特定 ID 的元素:
$("#unique")
-
你也可以使用任何其他 CSS 选择器,包括更复杂的链式选择器
下面是一个例子,jQuery 库从第 3 行开始导入到 script
元素中:
<html>
<head>
<script src="img/jquery.min.js"></script>
</head>
<body>
<p>Let's play a game!</p>
<p>Of hide and seek...</p>
<p class="easy">I'm easy to find!</p>
<button id="hidebutton">Great idea</button>
<button id="revealbutton">Found you!</button>
<script>
$(document).ready(function () {
$("#hidebutton").click(function () {
$("p").hide();
});
$("#revealbutton").click(function () {
$(".easy").show();
});
});
</script>
</body>
</html>
这就是页面看起来像这样:
图 15.1:包含简单 jQuery 脚本的页面
当你点击 好主意 按钮时,所有段落都将被隐藏。这是在 jQuery 添加的事件中完成的。首先,我们选择了具有 ID hidebutton
的按钮,然后我们调用它的 click
函数,该函数指定了点击时会发生什么。在该函数中,我们声明将选择所有 p
元素并将它们隐藏。hide
是一个特殊的 jQuery 函数,它将 display:none
样式添加到 HTML 元素中。
因此,点击后,所有段落都消失了。当我们点击 找到你了 时,只有一个返回,最后一个读作 我容易找到。这是因为当具有 ID revealbutton
的按钮被点击时,它会选择所有具有类 easy
的元素,并使用 jQuery 的 show
函数移除样式中的 display:none
。
这就是 jQuery 真正的核心所在:
-
掌握选择器
-
了解一些额外的或不同命名的函数来操作元素
你可以在你的代码中使用 jQuery,但这不会扩大你使用 JavaScript 做更多事情的可能性。它只会让你用更少的代码字符做同样的事情。jQuery 非常受欢迎的原因是,在浏览器标准化程度较低的时候,它增加了许多价值,在这种情况下,使用 jQuery 实际上提供了跨多个浏览器标准化 JavaScript 的解决方案。如今,这已经没有多少用处了,如果你要编写新的代码,最好直接使用 JavaScript。然而,无论何时你在处理旧代码,你很可能遇到 jQuery,因此了解它是如何工作的将肯定有助于你处理这些情况。
在撰写本文时,你可以在以下位置找到 jQuery 文档:api.jquery.com/
。
D3
D3 代表三个“D”:数据驱动文档。它是一个 JavaScript 库,可以帮助基于数据进行文档操作,并且可以使用 HTML、SVG 和 CSS 来可视化数据。对于需要包含任何类型数据表示的仪表板来说,它非常有用。
使用 D3,你可以用很多特性制作几乎任何你想要的图表。它可能看起来相当令人畏惧,因为需要设置图表图形的所有设置。深入其中,将其分解成小块,将确保你克服任何障碍。下面是一个非常基本的示例,使用 D3 向 SVG 添加三个球体:
<!DOCTYPE html>
<html>
<head>
<script src="img/d3.v7.min.js"></script>
<style>
svg {
background-color: lightgrey;
}
</style>
</head>
<body>
<svg id="drawing-area" height=100 width=500></svg>
<script>
let svg = d3.select("#drawing-area");
svg.append("circle")
.attr("cx", 100).attr("cy", 50).attr("r", 20).style("fill", "pink");
svg.append("circle")
.attr("cx", 200).attr("cy", 20).attr("r", 20).style("fill", "black");
svg.append("circle")
.attr("cx", 300).attr("cy", 70).attr("r", 20).style("fill", "grey");
</script>
</body>
</html>
D3 库在第一个script
标签中导入。svg
变量是通过在具有 ID drawing-area
的svg
上使用d3.select
方法创建的。
我们并没有完全公正地对待 D3 的可能性——在这种情况下,这并不比只用 canvas 做更多。然而,你可以制作漂亮的数据动画,例如缩放效果、可排序的条形图、球体的旋转效果等等。不过,这些代码将占据这本书的多个页面。
在撰写本文时,你可以在以下位置找到完整的文档:devdocs.io/d3~4/
。
Underscore
Underscore 是一个 JavaScript 库,可以概括为函数式编程的工具包。函数式编程可以被认为是一种编程范式,它围绕使用描述性函数的序列进行,而不是单独的例子。面向对象编程(OOP)也是一种编程范式,它全部关于对象及其状态,数据可以被封装并隐藏在外部代码之外。在函数式编程中,函数非常重要,但需要关注的州更少。这些函数总是用不同的参数做同样的事情,并且可以很容易地串联起来。
1, 2, and 3:
<!DOCTYPE html>
<html>
<head>
<script src="img/underscore-umd-min.js"></script>
</head>
<body>
<script>
_.each([1, 2, 3], alert);
</script>
</body>
</html>
有许多其他用于过滤、分组元素、转换元素、获取随机值、获取当前时间等功能。
这段代码可能也解释了名字的由来,因为我们通过下划线访问 Underscore 函数。不过,你首先需要安装 Underscore,否则解释器将不理解语法。
在撰写本文时,你可以在以下位置找到完整的文档:devdocs.io/underscore/
。
React
React 是我们将要讨论的最后一个前端库。如果你认为 React 是一个框架,你并不完全错误,但也不完全正确。我们之所以将 React 视为一个库,是因为你需要使用其他一些库才能达到它像框架一样的效果。
React 用于构建美观且动态的用户界面。它将页面拆分为不同的组件,并且当数据发生变化时,数据在组件之间发送和更新。以下是一个非常基础的示例,它只是触及了 React 能做什么的皮毛。这个 HTML 将在页面上显示这句话:Hi Emile, what's up?:
<div id="root"></div>
当以下 JavaScript 与之关联时,它将这样做:
ReactDOM.render(
<p> Hi Emile, what's up?</p>,
document.getElementById('root');
);
这只会在 React 库可用时才有效。它将渲染 DOM,用 render
函数的第一个参数替换 div
的 innerHTML
。我们可以通过在头部添加一个 script
元素来实现这一点,而不需要在我们的系统上安装任何东西。完整的脚本看起来像这样:
<!DOCTYPE html>
<html>
<head>
<script src="img/react.development.js" crossorigin></script>
<script src="img/react-dom.development.js" crossorigin></script>
</head>
<body>
<div id="root"></div>
<script>
let p = React.createElement("p", null, "Hi Emile, what's up?");
ReactDOM.render(
p,
document.getElementById("root");
);
</script>
</body>
</html>
这将使用在 script
标签中手动创建的 React 元素将 Hi Emile, what's up? 写入页面。不过,对于大型项目来说,你不应该这样做。使用包管理器(如 Node Package Manager (NPM))设置 React 和所有你需要的东西要更有价值。这将允许你轻松管理所有依赖项并保持你的代码井井有条。
在撰写本文时,更多信息可以在这里找到:reactjs.org/docs/getting-started.html
。
框架
框架更复杂,通常你需要在你的电脑上安装它们。如何在特定框架的在线文档中找到如何安装的信息。并且,每当你完成编码并想要运行你的代码时,你都需要运行一个命令,该命令将处理你的代码,使其成为浏览器可以理解的东西。当我们这样做时,我们“提供”应用程序。
Vue.js
Vue.js 是一个轻量级的 JavaScript 框架。它可以用来构建用户界面和 单页应用程序 (SPAs)。使用 Vue.js 编写的用户界面可能在你第一次遇到它时难以理解。看看这个代码示例:
<!DOCTYPE html>
<html>
<script src="img/vue"></script>
<body>
<div id="app">
<p v-if="!hide">
Let's play hide and seek. <br />
Go to the console and type: <br />
obj._data.hide = true <br />
</p>
</div>
<script>
let obj = new Vue({
el: "#app",
data: {
hide: false,
},
});
</script>
</body>
</html>
这是一个简单的 HTML 页面,从 Vue 导入了一个 JavaScript 链接。在 <p>
标签的 HTML 中有一些奇怪的事情发生:有一个 v-if
元素。这个元素只有在 v-if
中的条件为真时才会显示。
在这种情况下,它正在查看我们 Vue 实例中数据对象的 hide
属性。如果你将这个 hide
的值改为 true
,否定的 hide
语句将变为 false
,元素将消失。这本来也是我们可以不用 Vue 就做到的,但那样的话,我们就需要指定一个 JavaScript 事件来改变值,并使用 JavaScript 来编辑 CSS 以隐藏段落。
你甚至可以看到对你来说全新的 HTML 元素。这是因为这些不是常规的 HTML 元素,而是来自 Vue,它允许你定义自己的元素。你可能会遇到如下这样的 HTML:
<div id="custom-component">
<maaike></maaike>
</div>
当你打开与之关联的网页时,它会显示:
Maaike says: good job!
maaike component. Here is the snippet:
<script>
Vue.component("maaike", {
template: "<p>Maaike says: good job!</p>",
});
new Vue({ el: "#app" });
</script>
在前面的代码中,创建了一个新的 Vue 组件,实际上它可以持有数据,也可以有函数,但这个组件非常基础,只是为了说明我们可以在template
属性中添加 HTML 模板。指定了一段段落。当网页加载时,<maaike>
组件将被模板中的内容所替换。
一页的内容可以来自多个文件。通常这些组件各自都有自己的文件。一旦你深入到 Vue.js 中,你会了解到更多官方的 Vue 工具。实际上,它是一个非常适合初学者的框架,因为它非常清晰,是理解框架的绝佳起点。
在撰写本文时,你可以在以下链接找到完整的 Vue 文档:v3.vuejs.org/guide/introduction.html
。
Angular
Angular 是一个由 Google 发起并(目前)维护的框架。Angular 比 Vue.js 重得多,但可以被视为一个完整的包。这意味着 Angular 占用更多的磁盘空间,通常这意味着编译和安装速度较慢。查看 Angular 代码与 Vue.js 并没有太大的区别。然而,Angular 使用 TypeScript 而不是 JavaScript。TypeScript 是 JavaScript 的超集,会被编译成 JavaScript,但它更严格,也有不同的语法。
Angular 可以通过 HTML 中的ng
属性来识别。我们不会展示一个完整的示例,但这里有一个 HTML 示例,它将显示待办事项列表上的所有任务(当周围的代码设置正确时):
<ul>
<li ng-repeat="task in tasks">
{{task}}<span ng-click="deleteTask($index)">Done</span>
</li>
</ul>
ng-repeat
属性指定了重复操作,对于任务列表上的每个任务,它应该创建一个<li>
元素。task
也可以在<li>
内部作为变量使用,如{{ task }}
所示。
还有另一个 Angular 特有的功能,ng-click
,它告诉 Angular 当元素被点击时应该做什么。这与 JavaScript 的onclick
事件类似,但现在它可以动态绑定。这意味着在编写代码时,你不需要了解onclick
。显然,你可以在 JavaScript 中通过指定将导致onclick
属性(以及必要时整个元素)变化的事件来实现相同的功能,但这需要编写更多的代码。这适用于 Angular 中的任何内容:它可以用 JavaScript 完成,但这需要更多的工作(这实际上可能是一个低估,取决于情况的复杂性)。
在撰写本文时,你可以在以下链接找到完整的文档:angular.io/docs
。
学习如何使用 React、Angular 或 Vue 等库和框架是一个非常合理甚至可以说是必须的下一步,如果你希望成为一名前端开发者。在作者看来,这些选项的难度并没有太大的区别。哪个是最好的选择取决于你想要工作的地点和你所在的地区,因为这些框架和库在不同地区有不同的偏好。
学习后端
到目前为止,我们只处理了前端。前端是运行在客户端的部分,可以是用户使用的任何设备,例如手机、笔记本电脑或平板电脑。为了使网站能够执行有趣的功能,我们还需要后端。例如,如果你想登录到一个网站,这个网站需要以某种方式知道这个用户是否存在。
这就是服务器端代码,后端的工作。这是在用户的设备上运行,而是在某个服务器上运行的代码,通常由托管网站的公司的所有或租赁。托管网站通常意味着他们通过将网站放置在可以接受外部请求的 URL 上的服务器,使其对整个互联网可用。
服务器上的代码执行了许多与更深层逻辑和数据相关的事情。例如,一个电子商务商店在商店中有许多来自数据库的商品。服务器从数据库中获取商品,解析 HTML 模板,并将 HTML、CSS 和 JavaScript 发送到客户端。
登录也是如此:当你在一个网站上输入用户名和密码并点击登录时,服务器上的代码会被触发。这段代码将验证你输入的详细信息与数据库中的信息是否匹配。如果你有正确的详细信息,它将发送一个登录用户的门户页面给你。如果你输入了错误的详细信息,它将向客户端发送错误信息。
在本节中,我们将介绍前端和后端之间通信的基础,并展示你如何使用 Node.js 编写后端代码。
APIs
API(应用程序编程接口)本质上是一个用更多代码编写的代码接口。可以使用(例如)URL 来向 API 发出请求。这将触发一段特定的代码,这段代码将返回一个特定的响应。
这一切都很抽象,所以让我们用一个例子来说明。如果我们有一个酒店网站,人们能够在线预订房间是有意义的。这需要我们有一种 API。每当用户填写完所有字段并点击 提交预订 时,API 将通过调用 URL 并将用户输入的所有数据发送到该端点(一个特定的 URL)来触发,例如:www.api.hotelname.com/rooms/book
。这个 API 将处理和验证我们的数据,当一切正常时,它将在我们的数据库中存储房间预订,并可能向我们的客人发送确认邮件。
当酒店职员去检查预订时,将使用其中一个端点发起另一个 API 调用。例如,这个端点可能看起来像这样:www.api.hotelname.com/reservations
。这将首先检查我们的员工是否以正确的角色登录,如果是,它将从数据库中检索所选日期范围内的所有预订,并将包含结果的页面发送回我们的员工,然后员工可以看到所有预订。因此,API 是逻辑、数据库和前端之间的连接点。
API 通过 Hypertext Transfer Protocol (HTTP) 调用工作。HTTP 只是一个用于双方(客户端和服务器,或服务器和另一个服务器,其中请求服务器充当客户端)之间通信的协议。这意味着它必须遵守对方期望的某些约定和规则,对方将以某种方式做出回应。例如,这意味着使用特定的格式来指定头信息,使用 GET 方法获取信息,使用 POST 方法在服务器上创建新信息,以及使用 PUT 方法更改服务器上的信息。
可以使用 API 做更多的事情,例如,你的计算机和打印机也通过 API 进行通信。然而,从 JavaScript 的角度来看,这并不太相关。
你将在 AJAX 部分看到如何消费这些 API。你也可以编写自己的 API,而如何做到这一点的最终基础知识可以在 Node.js 部分找到。
AJAX
AJAX 代表 Asynchronous JavaScript and XML,这是一个误称,因为如今更常见的是使用 JSON 而不是 XML。我们使用它来从前端向后台发起调用,而不需要刷新页面(异步)。AJAX 不是一个编程语言或库,它是浏览器内置的 XMLHttpRequest
对象和 JavaScript 语言的组合。
作为前端开发者,你可能现在不会在日常工作中使用纯 AJAX,但它被隐藏在表面之下,所以了解它是如何工作的不会有害。以下是一个使用 AJAX 调用后端的示例:
let xhttp = new XMLHttpRequest();
let url = "some valid url";
xhttp.load = function () {
if (this.status == 200 && this.readyState == 4) {
document.getElementById("content").innerHTML = this.responseText;
}
};
xhttp.open("GET", url, true);
xhttp.send();
这不是一个工作示例,因为没有有效的 URL,但它演示了 AJAX 是如何工作的。它设置了当请求被加载时需要执行的操作,在这种情况下,用链接返回的内容替换元素 ID 为content
内的 HTML。这可以是一个文件的链接,或者是一个调用数据库的 API。当数据库中有其他(或没有)数据时,它可以给出不同的响应。这个响应是 JSON 格式的,但它也可以是 XML 格式的。这取决于服务器是如何编写的。
现在更常见的是使用Fetch API进行 AJAX 请求。这与我们可以用XMLHttpRequest
做到的事情类似,但它提供了一套更灵活、更强大的功能。例如,在下面的代码中,我们通过json()
方法从 URL 获取数据,将其转换为 JSON,并将其输出到控制台:
let url = "some valid url";
fetch(url)
.then(response => response.json())
.then(data => console.log(data));
Fetch API 与承诺(promises)一起工作,这一点到现在应该已经很熟悉了。所以当承诺被解决后,会通过then
创建一个新的承诺,当这个承诺被解决后,下一个then
会被执行。
在撰写本文时,更多相关信息可以在这里找到:developer.mozilla.org/en-US/docs/Web/Guide/AJAX/Getting_Started
.
练习题 15.1
创建一个 JSON 文件,并使用fetch
将结果作为可用的对象返回到你的 JavaScript 代码中:
-
创建一个 JSON 对象,并将其保存到名为
list.json
的文件中。 -
使用 JavaScript,将文件名和路径分配给名为
url
的变量。 -
使用
fetch
向文件 URL 发起请求。将结果作为 JSON 返回。 -
一旦响应对象准备就绪,遍历数据并将结果输出到 JSON 文件中每个项目的控制台。
Node.js
我们可以使用 Node.js 在 JavaScript 中编写 API。Node.js 是一个非常聪明的运行环境,它采用了 Google JavaScript 引擎,进行了扩展,并使得 JavaScript 能够在服务器上运行,通过 JavaScript 与文件系统协议和 HTTP 进行交互。正因为如此,我们可以使用 JavaScript 进行后端开发。这意味着你可以只用一种语言(连同 HTML 和 CSS)来编写前后端。如果没有 Node.js,你将不得不使用其他语言,如 PHP、Java 或 C#来编写后端。
为了运行 Node.js,你首先需要设置它,然后运行node nameOfFile.js
命令。你可以在官方 Node.js 文档中找到如何在你的系统上设置它的方法。通常需要下载和安装某些东西,然后你就可以完成了。
在撰写本文时,下载说明可以在nodejs.org/en/download/
找到。
这里有一些代码示例,它将接收可以编写在 Node.js 中的 HTTP 调用:
const http = require("http");
http.createServer(function(req, res){
res.writeHead(200, {"Content-Type": "text/html"}); //header status
let name = "Rob";
res.write(`Finally, hello ${name}`); //body
res.end();
}).listen(8080); //listen to port 8080
console.log("Listening on port 8080... ");
我们首先导入 http
模块。这是一个需要导入以运行的外部代码文件。http
模块随 Node.js 一起提供,但其他模块可能需要安装。你将使用包管理器来完成这项工作,例如 NPM,它将帮助安装所有依赖项并管理所有外部模块的不同版本。
上面的代码设置了一个监听端口 8080
的服务器,每次访问时,它将返回 Finally, hello Rob
。我们使用导入的 http
模块的 createServer
方法创建服务器。然后我们说明了对我们的服务器进行调用时需要发生什么。我们以 200 状态(表示“OK”)响应,并将 Finally, hello Rob
写入响应。然后我们指定默认端口 8080
作为监听端口。
这个例子使用了 Node.js 的内置 http
模块,这对于创建 API 非常强大。这绝对是一件事,值得有一些经验。能够编写自己的 API 将使你能够自己编写完整的应用程序。当我们添加 Express 时,这变得更加容易。
使用 Express Node.js 框架
Node.js 不是一个框架,也不是一个库。它是一个运行环境。这意味着它可以运行和解释编写的 JavaScript 代码。有针对 Node.js 的框架,目前 Express 是最受欢迎的一个。
这里是一个非常基础的 Express 应用程序——同样,你将不得不首先设置 Node.js,然后添加 Express 模块(如果你使用 NPM,npm install express
将会完成)并使用 node nameOfRootFile.js
命令运行它:
const express = require('express');
const app = express();
app.get('/', (request, response) => {
response.send('Hello Express!');
});
app.listen(3000, () => {
console.log('Express app at http://localhost:3000');
});
运行此代码并访问 localhost:3000
(假设你在本地主机上运行它),你将在浏览器中看到消息 Hello Express!。在你运行 Node
应用的终端中,它将在加载后打印控制台日志消息。
你可以在 Node.js 文档中找到更多信息,撰写本文时,该文档的地址如下:nodejs.org/en/docs/
。
对于 Express 模块,你可以访问 expressjs.com/en/5x/api.html
。
下一步
在这本书和这一章中,你已经学到了很多关于 JavaScript 的知识,通过这一章,你应该对可能的下一步行动有一个大致的想法。这一章并没有深入教授所有这些主题,因为关于每个主题都可以(并且已经)写出整本书,但你应该有一个很好的方向去寻找你的下一步行动,并在决定采取哪一步时考虑什么。
最佳的学习方式是通过实践。所以我们强烈建议你提出一个有趣的项目想法,然后尝试去实现它。或者,有了这些知识,你可能觉得自己已经准备好进入 JavaScript 的初级职位!你还可以在线做教程,甚至作为一个初级成员加入项目团队,使用 Upwork 或 Fiverr 等自由职业平台来获取项目。这些项目很难找到,我们可以想象你将首先学习一个框架或获得一些 Node.js 的经验。然而,如果你能在招聘过程中展示你的技能和潜力,这通常在工作时是可能的。
章节项目
处理 JSON
在本地创建一个 JSON 文件,连接到 JSON 和数据,并将 JSON 文件中的数据输出到你的控制台:
-
创建一个扩展名为 JSON 的文件,命名为
people.json
。 -
在
people.json
中创建一个包含多个对象的数组。数组中的每个项目都应该是一个具有相同结构的对象,使用first
、last
和topic
作为属性名。确保在属性名和值周围使用双引号,因为这才是正确的 JSON 语法。 -
使用相同的对象结构为每个项目添加三个或更多条目。
-
创建一个 HTML 文件并添加一个 JavaScript 文件。在 JavaScript 文件中,使用
people.json
作为 URL。使用fetch
连接到 URL 并检索数据。由于这是一个 JSON 格式的文件,一旦获取到响应数据,就可以使用fetch
中的.json()
方法将其格式化为 JSON。 -
将数据的全部内容输出到控制台。
-
使用
foreach
循环遍历数据中的项目,并将值输出到控制台。您可以使用模板字符串并输出每个值。
列表制作项目
创建一个列表,将其保存到本地存储,这样即使刷新页面,数据也会在浏览器中持续存在。如果页面首次加载时本地存储为空,则设置一个 JSON 文件,将其加载到本地存储,并保存为默认列表以开始列表:
-
设置一个 HTML 文件,添加一个
div
来输出列表结果,以及一个带有按钮的输入字段,可以点击。 -
使用 JavaScript,将页面元素作为对象添加,以便在代码中使用。
-
创建你的默认 JSON 文件(可以是空的),并使用名为
url
的变量将文件的路径添加到你的 JavaScript 代码中。 -
为按钮元素添加一个事件监听器,该监听器将运行一个名为
addToList()
的函数。 -
在
addToList()
中,检查输入字段的值长度是否为 3 或更多。如果是,则创建一个具有名称和输入字段值的对象。创建一个名为myList
的全局变量来保存列表,并在addToList()
中将新的对象数据推入myList
。 -
创建一个名为
maker()
的函数,该函数将创建页面元素并将文本添加到元素中,将其附加到输出元素。在addToList()
函数中调用maker()
以添加新项目。 -
此外,将项目保存到本地存储,以便
myList
的视觉内容与本地存储中保存的值同步。为此,创建一个名为savetoStorage()
的函数,并在每次更新脚本中的myList
时调用它。 -
在
savetoStorage()
函数中,使用setItem
将myList
的值设置到localStorage
中。您需要将myList
转换为字符串值以保存到localStorage
中。 -
向代码中添加
getItem()
以从localStorage
检索myList
的值。为myList
数组设置一个全局变量。 -
添加一个事件监听器来监听
DOMContentLoaded
。在函数中,检查是否从本地存储加载了值。如果是,则从本地存储获取myList
并将其从字符串转换为 JavaScript 对象。清除输出元素的 内容。遍历myList
中的项目,使用之前创建的maker()
函数将它们添加到页面中。 -
如果
localStorage
没有内容,使用fetch
加载带有默认值的 JSON 文件。一旦数据加载,将其分配给全局的myList
值。遍历myList
中的项目,使用maker()
函数将它们输出到页面。别忘了在之后调用savetoStorage()
,这样存储将包含与页面上可见的相同列表项。
自我检查测验
-
什么是 JavaScript 库和框架?
-
如何判断一个网页是否使用了 jQuery 库?
-
哪个库包含了大量用于操作数据的功能?
-
当 Node.js 安装后,如何运行一个 Node.js 文件?
摘要
在本章中,我们探索了一些继续你的 JavaScript 之旅并不断提升自己的可能性。我们首先讨论了前端以及库和框架。库和框架都是预先编写的代码,你可以在项目中使用,但库通常解决一个问题,而框架提供了一种标准解决方案,通常控制你构建应用程序的方式,并可能带来一些限制。另一方面,框架非常适合你想要在 Web 应用程序中做的很多事情。
我们接着转向查看后端。后端是运行在服务器上的代码,当我们使用 Node.js 时,我们可以用 JavaScript 编写这段代码。Node.js 是一个可以处理 JavaScript 的运行时引擎,并且为 JavaScript 提供了一些额外的功能,这些功能在使用浏览器中的 JavaScript 时是没有的。
就这样。在这个阶段,你对 JavaScript 有了非常扎实的理解。你已经看到了所有主要的构建块,并在许多小练习和大型项目中进行了大量实践。有几件事是肯定的:你永远不会完成作为 JavaScript 程序员的学业,而且随着你不断进步,你将不断地用你可以制作的东西来让自己感到惊讶。
不要忘记享受乐趣!
附录 – 练习题、项目和自我检查测验答案
第一章,JavaScript 入门
练习题
练习题 1.1
4 + 10
14
console.log("Laurence");
Laurence
undefined
练习题 1.2
<!DOCTYPE html>
<html>
<head>
<title>Tester</title>
</head>
<body>
<script>
console.log("hello world");
</script>
</body>
</html>
练习题 1.3
<!DOCTYPE html>
<html>
<head>
<title>Tester</title>
</head>
<body>
<script src="img/app.js"></script>
</body>
</html>
练习题 1.4
let a = 10; // assign a value of 10 to variable a
console.log(a); // This will output 10 into the console
/*
This is a multi-line
Comment
*/
项目
创建 HTML 文件和链接 JavaScript 文件
<!doctype html>
<html>
<head>
<title>JS Tester</title>
</head>
<body>
<script src="img/myJS.js"></script>
</body>
</html>
// console.log("Laurence");
/*
This is my comment
Laurence Svekis
*/
自我检查测验
-
<script src="img/myJS.js"></script>
. -
编号
-
通过使用
/*
和*/
打开和关闭它。 -
使用
//
注释掉该行。
第二章,JavaScript 基础
练习题
练习题 2.1
console.log(typeof(str1));
console.log(typeof(str2));
console.log(typeof(val1));
console.log(typeof(val2));
console.log(typeof(myNum));
练习题 2.2
const myName = "Maaike";
const myAge = 29;
const coder = true;
const message = "Hello, my name is " + myName + ", I am " + myAge+" years old and I can code JavaScript: " + coder + ".";
console.log(message);
练习题 2.3
let a = window.prompt("Value 1?");
let b = window.prompt("Value 2?");
a = Number(a);
b = Number(b);
let hypotenuseVal = ((a * a) + (b * b))**0.5;
console.log(hypotenuseVal);
练习题 2.4
let a = 4;
let b = 11;
let c = 21;
a = a + b;
a = a / c;
c = c % b;
console.log(a, b, c);
项目
英里到千米的转换器
//Convert miles to kilometers.
//1 mile equals 1.60934 kilometers.
let myDistanceMiles = 130;
let myDistanceKM = myDistanceMiles * 1.60934;
console.log("The distance of " + myDistanceMiles + " miles is equal to " + myDistanceKM + " kilometers");
BMI 计算器
//1 inch = 2.54 centimetres.
//2.2046 pounds in a kilo
let inches = 72;
let pounds = 180;
let weight = pounds / 2.2046; // in kilos
let height = inches * 2.54; // height in centimetres
console.log(weight, height);
let bmi = weight/(height/100*height/100);
console.log(bmi);
自我检查测验
-
字符串
-
数字
-
行 2
-
world
-
Hello world!
-
无论用户输入什么
-
71
-
4
-
16
和536
-
true
false
true
true
false
第三章,JavaScript 多值
练习题
练习题 3.1
const myList = ["Milk", "Bread", "Apples"];
console.log(myList.length);
myList[1] = "Bananas";
console.log(myList);
练习题 3.2
const myList = [];
myList.push("Milk", "Bread", "Apples");
myList.splice(1, 1, "Bananas", "Eggs");
const removeLast = myList.pop();
console.log(removeLast);
myList.sort();
console.log(myList.indexOf("Milk"));
myList.splice(1, 0, "Carrots", "Lettuce");
const myList2 = ["Juice", "Pop"];
const finalList = myList.concat(myList2, myList2);
console.log(finalList.lastIndexOf("Pop"));
console.log(finalList);
练习题 3.3
const myArr = [1, 2, 3];
const bigArr = [myArr, myArr, myArr];
console.log(bigArr[1][1]);
console.log(bigArr[0][1]);
console.log(bigArr[2][1]);
练习题 3.4
const myCar = {
make: "Toyota",
model: "Camry",
tires: 4,
doors: 4,
color: "blue",
forSale: false
};
let propColor = "color";
myCar[propColor] = "red";
propColor = "forSale";
myCar[propColor] = true;
console.log(myCar.make + " " + myCar.model);
console.log(myCar.forSale);
练习题 3.5
const people = {friends:[]};
const friend1 = {first: "Laurence", last: "Svekis", id: 1};
const friend2 = {first: "Jane", last: "Doe", id: 2};
const friend3 = {first: "John", last: "Doe", id: 3};
people.friends.push(friend1, friend2, friend3);
console.log(people);
项目
操作数组
theList.pop();
theList.shift();
theList.unshift("FIRST");
theList[3] = "hello World";
theList[2] = "MIDDLE";
theList.push("LAST");
console.log(theList);
公司产品目录
const inventory = [];
const item3 = {
name: "computer",
model: "imac",
cost: 1000,
qty: 3
}
const item2 = {
name: "phone",
model: "android",
cost: 500,
qty: 11
}
const item1 = {
name: "tablet",
model: "ipad",
cost: 650,
qty: 1
}
inventory.push(item1, item2, item3);
console.log(inventory);
console.log(inventory[2].qty);
自我检查测验
-
是的。你可以在使用
const
声明的数组中重新分配值,但不能重新声明数组本身。 -
Length
-
输出如下:
-1 1
-
你可以执行以下操作:
const myArr = [1,3,5,6,8,9,15]; myArr.splice(1,1,4); console.log(myArr);
-
输出如下:
[empty × 10, "test"] undefined
-
输出如下:
undefined
第四章,逻辑语句
练习题
练习题 4.1
const test = false;
console.log(test);
if(test){
console.log("It's True");
}
if(!test){
console.log("False now");
}
练习题 4.2
let age = prompt("How old are you?");
age = Number(age);
let message;
if(age >= 21){
message = "You can enter and drink.";
}else if(age >= 19){
message = "You can enter but not drink.";
}else{
message = "You are not allowed in!";
}
console.log(message);
练习题 4.3
const id = true;
const message = (id) ? "Allowed In" : "Denied Entry";
console.log(message);
练习题 4.4
const randomNumber = Math.floor(Math.random() * 6);
let answer = "Something went wrong";
let question = prompt("Ask me anything");
switch (randomNumber) {
case 0:
answer = "It will work out";
break;
case 1:
answer = "Maybe, maybe not";
break;
case 2:
answer = "Probably not";
break;
case 3:
answer = "Highly likely";
break;
default:
answer = "I don't know about that";
}
let output = "You asked me " + question + ". I think that " + answer;
console.log(output);
练习题 4.5
let prize = prompt("Pick a number 0-10");
prize = Number(prize);
let output = "My Selection: ";
switch (prize){
case 0:
output += "Gold ";
case 1:
output += "Coin ";
break;
case 2:
output += "Big ";
case 3:
output += "Box of ";
case 4:
output += "Silver ";
case 5:
output += "Bricks ";
break;
default:
output += "Sorry Try Again";
}
console.log(output);
项目
评估数字游戏答案
let val = prompt("What number?");
val = Number(val);
let num = 100;
let message = "nothing";
if (val > num) {
message = val + " was greater than " + num;
} else if (val === num) {
message = val + " was equal to " + num;
} else {
message = val + " is less than " + num;
}
console.log(message);
console.log(message);
朋友检查游戏答案
let person = prompt("Enter a name");
let message;
switch (person) {
case "John" :
case "Larry" :
case "Jane" :
case "Laurence" :
message = person + " is my friend";
break;
default :
message = "I don't know " + person;
}
console.log(message);
石头剪刀布游戏答案
const myArr = ["Rock", "Paper", "Scissors"];
let computer = Math.floor(Math.random() * 3);
let player = Math.floor(Math.random() * 3);
let message = "player " + myArr[player] + " vs computer " + myArr[computer] + " ";
if (player === computer) {
message += "it's a tie";
} else if (player > computer) {
if (computer == 0 && player == 2) {
message += "Computer Wins";
} else {
message += "Player Wins";
}
} else {
if (computer == 2 && player == 0) {
message += "Player Wins";
} else {
message += "Computer Wins";
}
}
console.log(message);
自我检查测验
-
one
-
这是那个
-
login
-
欢迎,这是一个用户:John
-
醒醒,是早晨了
-
结果:
-
true
-
false
-
true
-
true
-
-
结果:
100 was LESS or Equal to 100 100 is Even
第五章,循环
练习题
练习题 5.1
const max = 5;
const ranNumber = Math.floor(Math.random() * max) + 1;
//console.log(ranNumber);
let correct = false;
while (!correct) {
let guess = prompt("Guess a Number 1 - " + max);
guess = Number(guess);
if (guess === ranNumber) {
correct = true;
console.log("You got it " + ranNumber);
} else if (guess > ranNumber) {
console.log("Too high");
} else {
console.log("Too Low");
}
}
练习题 5.2
let counter = 0;
let step = 5;
do {
console.log(counter);
counter += step;
}
while (counter <= 100);
练习题 5.3
const myWork = [];
for (let x = 1; x < 10; x++) {
let stat = x % 2 ? true : false;
let temp = {
name: `Lesson ${x}`, status: stat
};
myWork.push(temp);
}
console.log(myWork);
练习题 5.4
const myTable = [];
const rows = 4;
const cols = 7;
let counter = 0;
for (let y = 0; y < rows; y++) {
let tempTable = [];
for (let x = 0; x < cols; x++) {
counter++;
tempTable.push(counter);
}
myTable.push(tempTable);
}
console.table(myTable);
练习题 5.5
const grid = [];
const cells = 64;
let counter = 0;
let row;
for (let x = 0; x < cells + 1; x++) {
if (counter % 8 == 0) {
if (row != undefined) {
grid.push(row);
}
row = [];
}
counter++;
let temp = counter;
row.push(temp);
}
console.table(grid);
练习题 5.6
const myArray = [];
for (let x = 0; x < 10; x++) {
myArray.push(x + 1);
}
console.log(myArray);
for (let i = 0; i < myArray.length; i++) {
console.log(myArray[i]);
}
for (let val of myArray) {
console.log(val);
}
练习题 5.7
const obj = {
a: 1,
b: 2,
c: 3
};
for (let prop in obj) {
console.log(prop, obj[prop]);
}
const arr = ["a", "b", "c"];
for (let w = 0; w < arr.length; w++) {
console.log(w, arr[w]);
}
for (el in arr) {
console.log(el, arr[el]);
}
练习题 5.8
let output = "";
let skipThis = 7;
for (let i = 0; i < 10; i++) {
if (i === skipThis) {
continue;
}
output += i;
}
console.log(output);
或者,可以使用以下代码,将continue
替换为break
:
let output = "";
let skipThis = 7;
for (let i = 0; i < 10; i++) {
if (i === skipThis) {
break;
}
output += i;
}
console.log(output);
项目
数学乘法表
const myTable = [];
const numm = 10;
for(let x=0; x<numm; x++){
const temp = [];
for(let y = 0; y<numm; y++){
temp.push(x*y);
}
myTable.push(temp);
}
console.table(myTable);
自我检查测验
-
结果:
0 3 6 9
-
结果:
0 5 1 6 2 7 [1, 5, 7]
第六章,函数
练习题
练习题 6.1
function adder(a, b) {
return a + b;
}
const val1 = 10;
const val2 = 20;
console.log(adder(val1, val2));
console.log(adder(20, 30));
练习题 6.2
const adj = ["super", "wonderful", "bad", "angry", "careful"];
function myFun() {
const question = prompt("What is your name?");
const nameAdj = Math.floor(Math.random() * adj.length);
console.log(adj[nameAdj] + " " + question );
}
myFun();
练习题 6.3
const val1 = 10;
const val2 = 5;
let operat = "-";
function cal(a, b, op) {
if (op == "-") {
console.log(a — b);
} else {
console.log(a + b);
}
}
cal(val1, val2, operat);
练习题 6.4
const myArr = [];
for(let x=0; x<10; x++){
let val1 = 5 * x;
let val2 = x * x;
let res = cal(val1, val2, "+");
myArr.push(res);
}
console.log(myArr);
function cal(a, b, op) {
if (op == "-") {
return a - b;
} else {
return a + b;
}
}
练习题 6.5
let val = "1000";
(function () {
let val = "100"; // local scope variable
console.log(val);
})();
let result = (function () {
let val = "Laurence";
return val;
})();
console.log(result);
console.log(val);
(function (val) {
console.log(`My name is ${val}`);
})("Laurence");
练习题 6.6
function calcFactorial(nr) {
console.log(nr);
if (nr === 0) {
return 1;
}
else {
return nr * calcFactorial(--nr);
}
}
console.log(calcFactorial(4));
练习题 6.7
let start = 10;
function loop1(val) {
console.log(val);
if (val < 1) {
return;
}
return loop1(val - 1);
}
loop1(start);
function loop2(val) {
console.log(val);
if (val > 0) {
val--;
return loop2(val);
}
return;
}
loop2(start);
练习题 6.8
const test = function(val){
console.log(val);
}
test('hello 1');
function test1(val){
console.log(val);
}
test1("hello 2");
项目
创建递归函数
const main = function counter(i) {
console.log(i);
if (i < 10) {
return counter(i + 1);
}
return;
}
main(0);
设置超时顺序
const one = ()=> console.log('one');
const two = ()=> console.log('two');
const three = () =>{
console.log('three');
one();
two();
}
const four = () =>{
console.log('four');
setTimeout(one,0);
three();
}
four();
自我检查测验
-
10
-
Hello
-
答案:
Welcome Laurence My Name is Laurence
-
19
-
16
第七章,类
练习题
练习题 7.1
class Person {
constructor(firstname, lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
}
let person1 = new Person("Maaike", "van Putten");
let person2 = new Person("Laurence", "Svekis");
console.log("hello " + person1.firstname);
console.log("hello " + person2.firstname);
练习题 7.2
class Person {
constructor(firstname, lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
fullname(){
return this.firstname + " " + this.lastname;
}
}
let person1 = new Person("Maaike", "van Putten");
let person2 = new Person("Laurence", "Svekis");
console.log(person1.fullname());
console.log(person2.fullname());
练习题 7.3
class Animal {
constructor(species, sounds) {
this.species = species;
this.sounds = sounds;
}
speak() {
console.log(this.species + " " + this.sounds);
}
}
Animal.prototype.eat = function () {
return this.species + " is eating";
}
let cat = new Animal("cat", "meow");
let dog = new Animal("dog", "bark");
cat.speak();
console.log(dog.eat());
console.log(dog);
项目
员工跟踪应用程序
class Employee {
constructor(first, last, years) {
this.first = first;
this.last = last;
this.years = years;
}
}
const person1 = new Employee("Laurence", "Svekis", 10);
const person2 = new Employee("Jane", "Doe", 5);
const workers = [person1, person2];
Employee.prototype.details = function(){
return this.first + " " + this.last + " has worked here " +
this.years + " years";
}
workers.forEach((person) => {
console.log(person.details());
});
菜单项价格计算器
class Menu {
#offer1 = 10;
#offer2 = 20;
constructor(val1, val2) {
this.val1 = val1;
this.val2 = val2;
}
calTotal(){
return (this.val1 * this.#offer1) + (this.val2 * this.#offer2);
}
get total(){
return this.calTotal();
}
}
const val1 = new Menu(2,0);
const val2 = new Menu(1,3);
const val3 = new Menu(3,2);
console.log(val1.total);
console.log(val2.total);
console.log(val3.total);
自我检查测验
-
class
-
使用以下语法:
class Person { constructor(firstname, lastname) { this.firstname = firstname; this.lastname = lastname; } }
-
继承
-
答案:
-
True
-
False
-
True
-
True
-
False
-
-
B
第八章,内置 JavaScript 方法
练习题
练习题 8.1
const secretMes1 = "How's%20it%20going%3F";
const secretMes2 = "How's it going?";
const decodedComp = decodeURIComponent(secretMes1);
console.log(decodedComp);
const encodedComp = encodeURIComponent(secretMes2);
console.log(encodedComp);
const uri = "http://www.basescripts.com?=Hello World";
const encoded = encodeURI(uri);
console.log(encoded);
练习题 8.2
const arr = ["Laurence", "Mike", "Larry", "Kim", "Joanne", "Laurence", "Mike", "Laurence", "Mike", "Laurence", "Mike"];
const arr2 = arr.filter ( (value, index, array) => {
console.log(value,index,array.indexOf(value));
return array.indexOf(value) === index;
});
console.log(arr2);
练习题 8.3
const myArr = [1,4,5,6];
const myArr1 = myArr.map(function(ele){
return ele * 2;
});
console.log(myArr1);
const myArr2 = myArr.map((ele)=> ele*2);
console.log(myArr2);
练习题 8.4
const val = "thIs will be capiTalized for each word";
function wordsCaps(str) {
str = str.toLowerCase();
const tempArr = [];
let words = str.split(" ");
words.forEach(word => {
let temp = word.slice(0, 1).toUpperCase() + word.slice(1);
tempArr.push(temp);
});
return tempArr.join(" ");
}
console.log(wordsCaps(val));
练习题 8.5
let val = "I love JavaScript";
val = val.toLowerCase();
let vowels = ["a","e","i","o","u"];
vowels.forEach((letter,index) =>{
console.log(letter);
val = val.replaceAll(letter,index);
});
console.log(val);
练习题 8.6
console.log(Math.ceil(5.7));
console.log(Math.floor(5.7));
console.log(Math.round(5.7));
console.log(Math.random());
console.log(Math.floor(Math.random()*11)); // 0-10
console.log(Math.floor(Math.random()*10)+1); // 1-10;
console.log(Math.floor(Math.random()*100)+1); // 1-100;
function ranNum(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
for (let x = 0; x < 100; x++) {
console.log(ranNum(1, 100));
}
练习题 8.7
let future = new Date(2025, 5, 15);
console.log(future);
const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
let day = future.getDate();
let month = future.getMonth();
let year = future.getFullYear();
let myDate = `${months[month-1]} ${day} ${year}`;
console.log(myDate);
项目
单词打乱器
let str = "JavaScript";
function scramble(val) {
let max = val.length;
let temp = "";
for(let i=0;i<max;i++){
console.log(val.length);
let index = Math.floor(Math.random() * val.length);
temp += val[index];
console.log(temp);
val = val.substr(0, index) + val.substr(index + 1);
console.log(val);
}
return temp;
}
console.log(scramble(str));
倒计时器
const endDate = "Sept 1 2022";
function countdown() {
const total = Date.parse(endDate) - new Date();
const days = Math.floor(total / (1000 * 60 * 60 * 24));
const hrs = Math.floor((total / (1000 * 60 * 60)) % 24);
const mins = Math.floor((total / 1000 / 60) % 60);
const secs = Math.floor((total / 1000) % 60);
return {
days,
hrs,
mins,
secs
};
}
function update() {
const temp = countdown();
let output = "";
for (const property in temp) {
output += (`${property}: ${temp[property]} `);
}
console.log(output);
setTimeout(update, 1000);
}
update();
自我检查测验
-
decodeURIComponent(e)
-
4
-
["Hii", "hi", "hello", "Hii", "hi", "hi World", "Hi"]
-
["hi", "hi World"]
第九章,文档对象模型
练习题
练习题 9.1
练习题 9.2
console.log(window.location.protocol);
console.log(window.location.href);
练习题 9.3
<script>
const output = document.querySelector('.output');
output.textContent = "Hello World";
output.classList.add("red");
output.id = "tester";
output.style.backgroundColor = "red";
console.log(document.URL);
output.textContent = document.URL;
</script>
项目
使用 JavaScript 操作 HTML 元素
const output = document.querySelector(".output");
const mainList = output.querySelector("ul");
mainList.id = "mainList";
console.log(mainList);
const eles = document.querySelectorAll("div");
for (let x = 0; x < eles.length; x++) {
console.log(eles[x].tagName);
eles[x].id = "id" + (x + 1);
if (x % 2) {
eles[x].style.color = "red";
} else {
eles[x].style.color = "blue";
}
}
自我检查测验
-
你应该会看到一个表示 HTML 页面
body
对象内包含的元素列表的对象。 -
document.body.textContent = "Hello World";
-
代码如下:
for (const property in document) { console.log(`${property}: ${document[property]}`); }
-
代码如下:
for (const property in window) { console.log(`${property}: ${document[window]}`); }
-
代码如下:
<!doctype html> <html> <head> <title>JS Tester</title> </head> <body> <h1>Test</h1> <script> const output = document.querySelector('h1'); output.textContent = "Hello World"; </script> </body> </html>
第十章,使用 DOM 动态操作元素
练习题
练习题 10.1
练习题 10.2
<!doctype html>
<html>
<head>
<title>Canvas HTML5</title>
</head>
<body>
<div id="one">Hello World</div>
<script>
const myEle = document.getElementById("one");
console.log(myEle);
</script>
</body>
</html>
练习题 10.3
<!doctype html>
<html>
<head>
<title>Dynamic event manipulation</title>
</head>
<body>
<div>Hello World 1</div>
<div>Hello World 2</div>
<div>Hello World 3</div>
<script>
const myEles = document.getElementsByTagName("div");
console.log(myEles[1]);
</script>
</body>
</html>
练习题 10.4
<!doctype html>
<html>
<head>
<title>Canvas HTML5</title>
</head>
<body>
<body>
<h1 class="ele">Hello World</h1>
<div class="ele">Hello World 1</div>
<div class="ele">Hello World 3</div>
<script>
const myEles = document.getElementsByClassName("ele");
console.log(myEles[0]);
</script>
</html>
练习题 10.5
<!doctype html>
<html>
<head>
<title>Canvas HTML5</title>
</head>
<body>
<body>
<h1 class="ele">Hello World</h1>
<div class="ele">Hello World 1</div>
<div class="ele">Hello World 3</div>
<p class="ele">Hello World 4</p>
<script>
const myEle = document.querySelector(".ele");
console.log(myEle);
</script>
</html>
练习题 10.6
<!doctype html>
<html>
<head>
<title>JS Tester</title>
</head>
<body>
<div class="container">
<div class="myEle">One</div>
<div class="myEle">Two</div>
<div class="myEle">Three</div>
<div class="myEle">Four</div>
<div class="myEle">Five</div>
</div>
<script>
const eles = document.querySelectorAll(".myEle");
console.log(eles);
eles.forEach((el) => {
console.log(el);
});
</script>
</body>
</html>
练习题 10.7
<!doctype html>
<html>
<head>
<title>JS Tester</title>
</head>
<body>
<div>
<button onclick="message(this)">Button 1</button>
<button onclick="message(this)">Button 2</button>
</div>
<script>
function message(el) {
console.dir(el.textContent);
}
</script>
</body>
</html>
练习题 10.8
<script>
const message = document.querySelector("#message");
const myArray = ["Laurence", "Mike", "John", "Larry", "Kim",
"Joanne", "Lisa", "Janet", "Jane"];
build();
//addClicks();
function build() {
let html = "<h1>My Friends Table</h1><table>";
myArray.forEach((item, index) => {
html += `<tr class="box" data-row="${index+1}"
data-name="${item}" onclick="getData(this)">
<td>${item}</td>`;
html += `<td >${index + 1}</td></tr>`;
});
html += "</table>";
document.getElementById("output").innerHTML = html;
}
function getData(el) {
let temp = el.getAttribute("data-row");
let tempName = el.getAttribute("data-name");
message.innerHTML = `${tempName } is in row #${temp}`;
}
</script>
练习题 10.9
<script>
const btns = document.querySelectorAll("button");
btns.forEach((btn)=>{
function output(){
console.log(this.textContent);
}
btn.addEventListener("click",output);
});
</script>
练习题 10.10
<script>
document.getElementById("addNew").onclick = function () {
addOne();
}
function addOne() {
var a = document.getElementById("addItem").value;
var li = document.createElement("li");
li.appendChild(document.createTextNode(a));
document.getElementById("sList").appendChild(li);
}
</script>
项目
可折叠手风琴组件
<script>
const menus = document.querySelectorAll(".title");
const openText = document.querySelectorAll(".myText");
menus.forEach((el) => {
el.addEventListener("click", (e) => {
console.log(el.nextElementSibling);
remover();
el.nextElementSibling.classList.toggle("active");
})
})
function remover() {
openText.forEach((ele) => {
ele.classList.remove("active");
})
}
</script>
交互式投票系统
<script>
window.onload = build;
const myArray = ["Laurence", "Mike", "John", "Larry"];
const message = document.getElementById("message");
const addNew = document.getElementById("addNew");
const newInput = document.getElementById("addFriend");
const output = document.getElementById("output");
addNew.onclick = function () {
const newFriend = newInput.value;
adder(newFriend, myArray.length, 0);
myArray.push(newFriend);
}
function build() {
myArray.forEach((item, index) => {
adder(item, index, 0);
});
}
function adder(name, index, counter) {
const tr = document.createElement("tr");
const td1 = document.createElement("td");
td1.classList.add("box");
td1.textContent = index + 1;
const td2 = document.createElement("td");
td2.textContent = name;
const td3 = document.createElement("td");
td3.textContent = counter;
tr.append(td1);
tr.append(td2);
tr.append(td3);
tr.onclick= function () {
console.log(tr.lastChild);
let val = Number(tr.lastChild.textContent);
val++;
tr.lastChild.textContent = val;
}
output.appendChild(tr);
}
</script>
挂挂人游戏
<script>
const game = { cur: "", solution: "", puzz: [], total: 0 };
const myWords = ["learn Javascript", "learn html",
"learn css"];
const score = document.querySelector(".score");
const puzzle = document.querySelector(".puzzle");
const letters = document.querySelector(".letters");
const btn = document.querySelector("button");
btn.addEventListener("click", startGame);
function startGame() {
if (myWords.length > 0) {
btn.style.display = "none";
game.puzz = [];
game.total = 0;
game.cur = myWords.shift();
game.solution = game.cur.split("");
builder();
} else {
score.textContent = "No More Words.";
}
}
function createElements(elType, parentEle, output, cla) {
const temp = document.createElement(elType);
temp.classList.add("boxE");
parentEle.append(temp);
temp.textContent = output;
return temp;
}
function updateScore() {
score.textContent = `Total Letters Left : ${game.total}`;
if (game.total <= 0) {
console.log("game over");
score.textContent = "Game Over";
btn.style.display = "block";
}
}
function builder() {
letters.innerHTML = "";
puzzle.innerHTML = "";
game.solution.forEach((lett) => {
let div = createElements("div", puzzle, "-", "boxE");
if (lett == " ") {
div.style.borderColor = "white";
div.textContent = " ";
} else {
game.total++;
}
game.puzz.push(div);
updateScore();
})
for (let i = 0; i < 26; i++) {
let temp = String.fromCharCode(65 + i);
let div = createElements("div", letters, temp,"box");
let checker = function (e) {
div.style.backgroundColor = "#ddd";
div.classList.remove("box");
div.classList.add("boxD");
div.removeEventListener("click", checker);
checkLetter(temp);
}
div.addEventListener("click", checker);
}
}
function checkLetter(letter) {
console.log(letter);
game.solution.forEach((ele, index) => {
if (ele.toUpperCase() == letter) {
game.puzz[index].textContent = letter;
game.total--;
updateScore();
};
};
)
}
</script>
自我检查测验
-
Hello <br> World
-
Hello
World
-
Hello World
-
当点击
three
时,输出为three
。当点击one
时,输出为:one
two
three
-
btn.removeEventListener("click", myFun);
第十一章,交互式内容和事件监听器
练习题
练习题 11.1
<!DOCTYPE html>
<html>
<head>
<title>Laurence Svekis</title>
</head>
<body>
<script>
let darkMode = false;
window.onclick = () => {
console.log(darkMode);
if (!darkMode) {
document.body.style.backgroundColor = "black";
document.body.style.color = "white";
darkMode = true;
} else {
document.body.style.backgroundColor = "white";
document.body.style.color = "black";
darkMode = false;
}
}
</script>
</body>
</html>
练习题 11.2
<!doctype html>
<html>
<body>
<div>red</div>
<div>blue</div>
<div>green</div>
<div>yellow</div>
<script>
const divs = document.querySelectorAll("div");
divs.forEach((el)=>{
el.addEventListener("click",()=>{
document.body.style.backgroundColor = el.textContent;
});
})
</script>
</body>
</html>
练习题 11.3
<!doctype html>
<html>
<head>
<title>JS Tester</title>
</head>
<body>
<script>
document.addEventListener("DOMContentLoaded", (e) => {
message("Document ready", e);
});
window.onload = (e) => {
message("Window ready", e);
}
function message(val, event) {
console.log(event);
console.log(val);
}
</script>
</body>
</html>
练习题 11.4
<!doctype html>
<html>
<head>
<title>JS Tester</title>
</head>
<body>
<div class="output"></div>
<script>
const output = document.querySelector(".output");
output.textContent = "hello world";
output.style.height = "200px";
output.style.width = "400px";
output.style.backgroundColor = "red";
output.addEventListener("mousedown", function (e) {
message("green", e);
});
output.addEventListener("mouseover", function (e) {
message("red", e);
});
output.addEventListener("mouseout", function (e) {
message("yellow", e);
});
output.addEventListener("mouseup", function (e) {
message("blue", e);
});
function message(elColor, event) {
console.log(event.type);
output.style.backgroundColor = elColor;
}
</script>
</body>
</html>
练习题 11.5
<script>
const myInput = document.querySelector("input[name='message']");
const output = document.querySelector(".output");
const btn1 = document.querySelector(".btn1");
const btn2 = document.querySelector(".btn2");
const btn3 = document.querySelector(".btn3");
const log = [];
btn1.addEventListener("click", tracker);
btn2.addEventListener("click", tracker);
btn3.addEventListener("click", (e) => {
console.log(log);
});
function tracker(e) {
output.textContent = myInput.value;
const ev = e.target;
console.dir(ev);
const temp = {
message: myInput.value,
type: ev.type,
class: ev.className,
tag: ev.tagName
};
log.push(temp);
myInput.value = "";
}
</script>
练习题 11.6
<script>
const main = document.querySelector(".container");
const boxes = document.querySelectorAll(".box");
main.addEventListener("click", (e) => {
console.log("4");
},false);
main.addEventListener("click", (e) => {
console.log("1");
},true);
boxes.forEach(ele => {
ele.addEventListener("click", (e) => {
console.log("3");
console.log(e.target.textContent);
},false);
ele.addEventListener("click", (e) => {
console.log("2");
console.log(e.target.textContent);
},true);
});
</script>
练习题 11.7
<script>
const output = document.querySelector(".output1");
const in1 = document.querySelector("input[name='first']");
const in2 = document.querySelector("input[name='last']");
in1.addEventListener("change", (e) => {
console.log("change");
updater(in1.value);
});
in1.addEventListener("blur", (e) => {
console.log("blur");
});
in1.addEventListener("focus", (e) => {
console.log("focus");
});
in2.addEventListener("change", (e) => {
console.log("change");
updater(in2.value);
});
in2.addEventListener("blur", (e) => {
console.log("blur");
});
in2.addEventListener("focus", (e) => {
console.log("focus");
});
function updater(str) {
output.textContent = str;
}
</script>
练习题 11.8
<!doctype html>
<html>
<head>
<title>JS Tester</title>
</head>
<body>
<div class="output"></div>
<input type="text" name="myNum1">
<input type="text" name="myNum2">
<script>
const eles = document.querySelectorAll("input");
const output = document.querySelector(".output");
eles.forEach(el => {
el.addEventListener("keydown", (e) => {
if (!isNaN(e.key)) {
output.textContent += e.key;
}
});
el.addEventListener("keyup", (e) => {
console.log(e.key);
});
el.addEventListener("paste", (e) => {
console.log('pasted');
});
});
</script>
</body>
</html>
练习题 11.9
<script>
const dragme = document.querySelector("#dragme");
dragme.addEventListener("dragstart", (e) => {
dragme.style.opacity = .5;
});
dragme.addEventListener("dragend", (e) => {
dragme.style.opacity = "";
});
const boxes = document.querySelectorAll(".box");
boxes.forEach(box => {
box.addEventListener("dragenter", (e) => {
e.target.classList.add('red');
});
box.addEventListener("dragover", (e) => {
e.preventDefault();
});
box.addEventListener("dragleave", (e) => {
//console.log("leave");
e.target.classList.remove('red');
});
box.addEventListener("drop", (e) => {
e.preventDefault();
console.log("dropped");
e.target.appendChild(dragme);
});
});
function dragStart(e) {
console.log("Started");
}
</script>
练习题 11.10
<!doctype html>
<html>
<head>
<title>JS Tester</title>
</head>
<body>
<form action="index2.html" method="get">
First: <input type="text" name="first">
<br>Last: <input type="text" name="last">
<br>Age: <input type="number" name="age">
<br><input type="submit" value="submit">
</form>
<script>
const form = document.querySelector("form");
const email = document.querySelector("#email");
form.addEventListener("submit", (e) => {
let error = false;
if (checker(form.first.value)) {
console.log("First Name needed");
error = true;
}
if (checker(form.last.value)) {
console.log("Last Name needed");
error = true;
}
if (form.age.value < 19) {
console.log("You must be 19 or over");
error = true;
}
if (error) {
e.preventDefault();
console.log("please review the form");
}
});
function checker(val) {
console.log(val.length);
if (val.length < 6) {
return true;
}
return false;
}
</script>
</body>
</html>
练习题 11.11
<!doctype html>
<html>
<style>
div {
background-color: purple;
width: 100px;
height: 100px;
position: absolute;
}
</style>
<body>
<div id="block"></div>
<script>
const main = document.querySelector("#block");
let mover = { speed: 10, dir: 1, pos: 0 };
main.addEventListener("click", moveBlock);
function moveBlock() {
let x = 30;
setInterval(function () {
if (x < 1) {
clearInterval();
} else {
if (mover.pos > 800 || mover.pos < 0) {
mover.dir *= -1;
}
x--;
mover.pos += x * mover.dir;
main.style.left = mover.pos + "px";
console.log(mover.pos);
}
}, 2);
}
</script>
</body>
</html>
项目
构建自己的分析工具
<!doctype html >
<html>
<head>
<title>JS Tester</title>
<style>.box{width:200px;height:100px;border:1px solid black}</style>
</head>
<body>
<div class="container">
<div class="box" id="box0">Box #1</div>
<div class="box" id="box1">Box #2</div>
<div class="box" id="box2">Box #3</div>
<div class="box" id="box3">Box #4</div>
</div>
<script>
const counter = [];
const main = document.querySelector(".container");
main.addEventListener("click",tracker);
function tracker(e){
const el = e.target;
if(el.id){
const temp = {};
temp.content = el.textContent;
temp.id = el.id;
temp.tagName = el.tagName;
temp.class = el.className;
console.dir(el);
counter.push(temp);
console.log(counter);
}
}
</script>
</body>
</html>
星级评分系统
<script>
const starsUL = document.querySelector(".stars");
const output = document.querySelector(".output");
const stars = document.querySelectorAll(".star");
stars.forEach((star, index) => {
star.starValue = (index + 1);
star.addEventListener("click", starRate);
});
function starRate(e) {
output.innerHTML =
`You Rated this ${e.target.starValue} stars`;
stars.forEach((star, index) => {
if (index < e.target.starValue) {
star.classList.add("orange");
} else {
star.classList.remove("orange");
}
});
}
</script>
鼠标位置追踪器
<!DOCTYPE html>
<html>
<head>
<title>Complete JavaScript Course</title>
<style>
.holder {
display: inline-block;
width: 300px;
height: 300px;
border: 1px solid black;
padding: 10px;
}
.active {
background-color: red;
}
</style>
</head>
<body>
<div class="holder">
<div id="output"></div>
</div>
<script>
const ele = document.querySelector(".holder");
ele.addEventListener("mouseover",
(e) => { e.target.classList.add("active"); });
ele.addEventListener("mouseout",
(e) => { e.target.classList.remove("active"); });
ele.addEventListener("mousemove", coordin);
function coordin() {
let html = "X:" + event.clientX + " | Y:" + event.clientY;
document.getElementById("output").innerHTML = html;
}
</script>
</body>
</html>
盒子点击速度测试游戏
<script>
const output = document.querySelector('.output');
const message = document.querySelector('.message');
message.textContent = "Press to Start";
const box = document.createElement('div');
const game = {
timer: 0,
start: null
};
box.classList.add('box');
output.append(box);
box.addEventListener('click', (e) => {
box.textContent = "";
box.style.display = 'none';
game.timer = setTimeout(addBox, ranNum(3000));
if (!game.start) {
message.textContent = 'Loading....';
} else {
const cur = new Date().getTime();
const dur = (cur - game.start) / 1000;
message.textContent = `It took ${dur} seconds to click`;
}
});
function addBox() {
message.textContent = 'Click it...';
game.start = new Date().getTime();
box.style.display = 'block';
box.style.left = ranNum(450) + 'px';
box.style.top = ranNum(450) + 'px';
}
function ranNum(max) {
return Math.floor(Math.random() * max);
}
</script>
自我检查测验
-
Window 对象模型
-
preventDefault()
方法如果可以取消事件,则会取消该事件。属于该事件的默认行为将不会发生。
第十二章,中级 JavaScript
练习题
练习题 12.1
<script>
const output = document.getElementById("output");
const findValue = document.getElementById("sText");
const replaceValue = document.getElementById("rText");
document.querySelector("button").addEventListener("click", lookUp);
function lookUp() {
const s = output.textContent;
const rt = replaceValue.value;
const re = new RegExp(findValue.value, "gi");
if (s.match(re)) {
let newValue = s.replace(re, rt);
output.textContent = newValue;
}
}
</script>
练习题 12.2
<script>
const output = document.querySelector(".output");
const emailVal = document.querySelector("input");
const btn = document.querySelector("button");
const emailExp =
/([A-Za-z0-9._-]+@[A-Za-z0-9._-]+\.[A-Za-z0-9]+)\w+/;
btn.addEventListener("click", (e) => {
const val = emailVal.value;
const result = emailExp.test(val);
let response = "";
if (!result) {
response = "Invalid Email";
output.style.color = "red";
} else {
response = "Valid Email";
output.style.color = "green";
}
emailVal.value = "";
output.textContent = response;
});
</script>
练习题 12.3
function showNames() {
let lastOne = "";
for (let i = 0; i < arguments.length; i++) {
lastOne = arguments[i];
}
return lastOne;
}
console.log(showNames("JavaScript", "Laurence", "Mike", "Larry"));
练习题 12.4
15
45
练习题 12.5
function test(val) {
try {
if (isNaN(val)) {
throw "Not a number";
} else {
console.log("Got number");
}
} catch (e) {
console.error(e);
} finally {
console.log("Done " + val);
}
}
test("a");
test(100);
练习题 12.6
<script>
console.log(document.cookie);
console.log(rCookie("test1"));
console.log(rCookie("test"));
cCookie("test1", "new Cookie", 30);
dCookie("test2");
function cCookie(cName, value, days) {
if (days) {
const d = new Date();
d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000));
let e = "; expires=" + d.toUTCString();
document.cookie = cName + "=" + value + e + "; path=/";
}
}
function rCookie(cName) {
let cookieValue = false;
let arr = document.cookie.split("; ");
arr.forEach(str => {
const cookie = str.split("=");
if (cookie[0] == cName) {
cookieValue = cookie[1];
}
});
return cookieValue;
}
function dCookie(cName) {
cCookie(cName, "", -1);
}
</script>
练习题 12.7
<script>
const userTask = document.querySelector(".main input");
const addBtn = document.querySelector(".main button");
const output = document.querySelector(".output");
const tasks = JSON.parse(localStorage.getItem("tasklist")) || [];
addBtn.addEventListener("click", createListItem);
if (tasks.length > 0) {
tasks.forEach((task) => {
genItem(task.val, task.checked);
});
}
function saveTasks() {
localStorage.setItem("tasklist", JSON.stringify(tasks));
}
function buildTasks() {
tasks.length = 0;
const curList = output.querySelectorAll("li");
curList.forEach((el) => {
const tempTask = {
val: el.textContent,
checked: false
};
if (el.classList.contains("ready")) {
tempTask.checked = true;
}
tasks.push(tempTask);
});
saveTasks();
}
function genItem(val, complete) {
const li = document.createElement("li");
const temp = document.createTextNode(val);
li.appendChild(temp);
output.append(li);
userTask.value = "";
if (complete) {
li.classList.add("ready");
}
li.addEventListener("click", (e) => {
li.classList.toggle("ready");
buildTasks();
});
return val;
}
function createListItem() {
const val = userTask.value;
if (val.length > 0) {
const myObj = {
val: genItem(val, false),
checked: false
};
tasks.push(myObj);
saveTasks();
}
}
</script>
练习题 12.8
let myList = [{
"name": "Learn JavaScript",
"status": true
},
{
"name": "Try JSON",
"status": false
}
];
reloader();
function reloader() {
myList.forEach((el) => {
console.log(`${el.name} = ${el.status}`);
});
}
练习题 12.9
let myList = [{
"name": "Learn JavaScript",
"status": true
},
{
"name": "Try JSON",
"status": false
}
];
const newStr = JSON.stringify(myList);
const newObj = JSON.parse(newStr);
newObj.forEach((el)=>{
console.log(el);
});
项目
邮件提取器
<script>
const firstArea = document.querySelector(
"textarea[name='txtarea']");
const secArea = document.querySelector(
"textarea[name='txtarea2']");
document.querySelector("button").addEventListener("click", lookUp);
function lookUp() {
const rawTxt = firstArea.value;
const eData = rawTxt.match(
/([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+)/gi);
const holder = [];
for (let x = 0; x < eData.length; x++) {
if (holder.indexOf(eData[x]) == -1) {
holder.push(eData[x]);
}
}
secArea.value = holder.join(',');
}
</script>
表单验证器
<script>
const myForm = document.querySelector("form");
const inputs = document.querySelectorAll("input");
const errors = document.querySelectorAll(".error");
const required = ["email", "userName"];
myForm.addEventListener("submit", validation);
function validation(e) {
let data = {};
e.preventDefault();
errors.forEach(function (item) {
item.classList.add("hide");
});
let error = false;
inputs.forEach(function (el) {
let tempName = el.getAttribute("name");
if (tempName != null) {
el.style.borderColor = "#ddd";
if (el.value.length == 0 &&
required.includes(tempName)) {
addError(el, "Required Field", tempName);
error = true;
}
if (tempName == "email") {
let exp = /([A-Za-z0-9._-]+@[A-Za-z0-9._-]+\.[A-Za-z0-9]+)\w+/;
let result = exp.test(el.value);
if (!result) {
addError(el, "Invalid Email", tempName);
error = true;
}
}
if (tempName == "password") {
let exp = /[A-Za-z0-9]+$/;
let result = exp.test(el.value);
if (!result) {
addError(el, "Only numbers and Letters",
tempName);
error = true;
}
if (!(el.value.length > 3 &&
el.value.length < 9)) {
addError(el, "Needs to be between 3-8 " +
"characters", tempName);
error = true;
}
}
data[tempName] = el.value;
}
});
if (!error) {
myForm.submit();
}
}
function addError(el, mes, fieldName) {
let temp = el.nextElementSibling;
temp.classList.remove("hide");
temp.textContent = fieldName.toUpperCase() + " " + mes;
el.style.borderColor = "red";
el.focus();
}
</script>
简单数学测验
<!doctype html>
<html>
<head>
<title>Complete JavaScript Course</title>
</head>
<body>
<span class="val1"></span> <span>+</span>
<span class="val2"></span> = <span>
<input type="text" name="answer"></span><button>Check</button>
<div class="output"></div>
<script>
const app = function () {
const game = {};
const val1 = document.querySelector(".val1");
const val2 = document.querySelector(".val2");
const output = document.querySelector(".output");
const answer = document.querySelector("input");
function init() {
document.querySelector("button").addEventListener(
"click", checker);
loadQuestion();
}
function ranValue(min, max) {
return Math.floor(Math.random() * (max - min + 1) +
min);
}
function loadQuestion() {
game.val1 = ranValue(1, 100);
game.val2 = ranValue(1, 100);
game.answer = game.val1 + game.val2;
val1.textContent = game.val1;
val2.textContent = game.val2;
}
function checker() {
let bg = answer.value == game.answer ? "green" : "red";
output.innerHTML +=
`<div style="color:${bg}">${game.val1} +
${game.val2} = ${game.answer} (${answer.value})
</div>`;
answer.value = "";
loadQuestion();
}
return {
init: init
};
}();
document.addEventListener('DOMContentLoaded', app.init);
</script>
</body>
</html>
自我检查测验
-
匹配的范围是从
a
到e
,并且区分大小写。它将返回单词的其余部分:enjoy avaScript
。 -
是的。
-
它将清除网站上的 cookies。
-
hello world
-
变量 a 未定义。
-
a
c
b
第十三章,并发
练习题
练习题 13.1
function greet(fullName){
console.log(`Welcome, ${fullName[0]} ${fullName[1]}`)
}
function processCall(user, callback){
const fullName = user.split(" ");
callback(fullName);
}
processCall("Laurence Svekis", greet);
练习题 13.2
const myPromise = new Promise((resolve, reject) => {
resolve("Start Counting");
});
function counter(val){
console.log(val);
}
myPromise
.then(value => {counter(value); return "one"})
.then(value => {counter(value); return "two"})
.then(value => {counter(value); return "three"})
.then(value => {counter(value);});
练习题 13.3
let cnt = 0;
function outputTime(val) {
return new Promise(resolve => {
setTimeout(() => {
cnt++;
resolve(`x value ${val} counter:${cnt}`);
}, 1000);
});
}
async function aCall(val) {
console.log(`ready ${val} counter:${cnt}`);
const res = await outputTime(val);
console.log(res);
}
for (let x = 1; x < 4; x++) {
aCall(x);
}
项目
密码检查器
const allowed = ["1234", "pass", "apple"];
function passwordChecker(pass) {
return allowed.includes(pass);
}
function login(password) {
return new Promise((resolve, reject) => {
if (passwordChecker(password)) {
resolve({
status: true
});
} else {
reject({
status: false
});
}
});
}
function checker(pass) {
login(pass)
.then(token => {
console.log("Approve:");
console.log(token);
})
.catch(value => {
console.log("Reject:");
console.log(value);
})
}
checker("1234");
checker("wrong");
自我检查测验
-
更新后的代码如下:
function addOne(val){ return val + 1; } function total(a, b, callback){ const sum = a + b; return callback(sum); } console.log(total(4, 5, addOne));
-
控制台将显示错误信息
Error: 哎呀
。 -
更新后的代码如下:
function checker(val) { return new Promise((resolve, reject) => { if (val > 5) { resolve("Ready"); } else { reject(new Error("Oh no")); } }); } checker(5) .then((data) => {console.log(data); }) .catch((err) => {console.error(err); }) .finally(() => { console.log("done");});
-
更新后的代码如下:
async function myFun() { return "Hello"; } myFun().then( function(val) { console.log(val); }, function(err) { conole.log(err); }
第十四章,HTML5,Canvas 和 JavaScript
练习题
练习 14.1
<script>
const message = document.getElementById("message");
const output = document.querySelector(".output");
const myInput = document.querySelector("input");
myInput.addEventListener("change", uploadAndReadFile);
function uploadAndReadFile(e) {
const files = e.target.files;
for (let i = 0; i < files.length; i++) {
const file = files[i];
const img = document.createElement("img");
img.classList.add("thumb");
img.file = file;
output.appendChild(img);
const reader = new FileReader();
reader.onload = (function (myImg) {
return function (e) {
myImg.src = e.target.result;
};
})(img);
reader.readAsDataURL(file);
}
}
</script>
练习 14.2
<!doctype html>
<html>
<head>
<title>Canvas HTML5</title>
<style>
#canvas {
border: 1px solid black;
}
</style>
</head>
<body>
<canvas id="canvas" width="640" height="640">Not Supported</canvas>
<script>
const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext("2d");
ctx.fillStyle = "red";
ctx.fillRect(100, 100, 500, 300); //filled shape
ctx.strokeRect(90, 90, 520, 320); // outline
ctx.clearRect(150, 150, 400, 200); //transparent
</script>
</body>
</html>
练习 14.3
<!doctype html>
<html>
<head>
<title>Canvas HTML5</title>
<style>
#canvas {
border: 1px solid black;
}
</style>
</head>
<body>
<canvas id="canvas" width="640" height="640">Not Supported</canvas>
<script>
const canvas = document.querySelector("#canvas");
const ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.fillStyle = "red";
ctx.arc(300, 130, 100, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.fillStyle = "black";
ctx.arc(250, 120, 20, 0, Math.PI * 2);
ctx.moveTo(370, 120);
ctx.arc(350, 120, 20, 0, Math.PI * 2);
ctx.moveTo(240, 160);
ctx.arc(300, 160, 60, 0, Math.PI);
ctx.fill();
ctx.moveTo(300, 130);
ctx.lineTo(300, 150);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(300, 230);
ctx.lineTo(300, 270);
ctx.lineTo(400, 270);
ctx.lineTo(200, 270);
ctx.lineTo(300, 270);
ctx.lineTo(300, 350);
ctx.lineTo(400, 500);
ctx.moveTo(300, 350);
ctx.lineTo(200, 500);
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = "blue";
ctx.moveTo(200, 50);
ctx.lineTo(400, 50);
ctx.lineTo(300, 20);
ctx.lineTo(200, 50);
ctx.fill();
ctx.stroke();
</script>
</body>
</html>
练习 14.4
<!doctype html>
<html>
<head>
<title>Canvas HTML5</title>
<style>
#canvas {
border: 1px solid black;
}
</style>
</head>
<body>
<canvas id="canvas" width="640" height="640">Not Supported</canvas>
<script>
const canvas = document.querySelector("#canvas");
const ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.fillStyle = "red";
ctx.arc(300, 130, 100, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.fillStyle = "black";
ctx.arc(250, 120, 20, 0, Math.PI * 2);
ctx.moveTo(370, 120);
ctx.arc(350, 120, 20, 0, Math.PI * 2);
ctx.moveTo(240, 160);
ctx.arc(300, 160, 60, 0, Math.PI);
ctx.fill();
ctx.moveTo(300, 130);
ctx.lineTo(300, 150);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(300, 230);
ctx.lineTo(300, 270);
ctx.lineTo(400, 270);
ctx.lineTo(200, 270);
ctx.lineTo(300, 270);
ctx.lineTo(300, 350);
ctx.lineTo(400, 500);
ctx.moveTo(300, 350);
ctx.lineTo(200, 500);
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = "blue";
ctx.moveTo(200, 50);
ctx.lineTo(400, 50);
ctx.lineTo(300, 20);
ctx.lineTo(200, 50);
ctx.fill();
ctx.stroke();
</script>
</body>
</html>
练习 14.5
<!doctype html>
<html>
<head>
<title>Canvas HTML5</title>
<style>
#canvas {
border: 1px solid black;
}
</style>
</head>
<body>
<div><label>Image</label>
<input type="file" id="imgLoader" name="imgLoader">
</div>
<div><canvas id="canvas"></canvas></div>
<script>
const canvas = document.querySelector("#canvas");
const ctx = canvas.getContext("2d");
const imgLoader = document.querySelector("#imgLoader");
imgLoader.addEventListener("change", handleUpload);
function handleUpload(e) {
console.log(e);
const reader = new FileReader();
reader.onload = function (e) {
console.log(e);
const img = new Image();
img.onload = function () {
canvas.width = img.width / 2;
canvas.height = img.height / 2;
ctx.drawImage(img, 0, 0, img.width / 2,
img.height / 2);
}
img.src = e.target.result;
}
reader.readAsDataURL(e.target.files[0]);
}
</script>
</body>
</html>
练习 14.6
<!doctype html>
<html>
<head>
<title>Canvas HTML5</title>
<style>
#canvas {
border: 1px solid black;
}
</style>
</head>
<body>
<div><canvas id="canvas"></canvas></div>
<script>
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const ballSize = 10;
let x = canvas.width / 2;
let y = canvas.height / 2;
let dirX = 1;
let dirY = 1;
function drawBall() {
ctx.beginPath();
ctx.arc(x, y, ballSize, 0, Math.PI * 2);
ctx.fillStyle = "red";
ctx.fill();
ctx.closePath();
}
function move() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBall();
if (x > canvas.width - ballSize || x < ballSize) {
dirX *= -1;
}
if (y > canvas.height - ballSize || y < ballSize) {
dirY *= -1;
}
x += dirX;
y += dirY;
}
setInterval(move, 10);
</script>
</body>
</html>
练习 14.7
<script>
window.onload = init;
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
canvas.style.border = "1px solid black";
const penColor = document.querySelector("#penColor");
const penWidth = document.querySelector("#penWidth");
document.querySelector(".clear").addEventListener(
"click", clearImg);
canvas.width = 700;
canvas.height = 700;
let pos = {
x: 0,
y: 0,
};
function init() {
canvas.addEventListener("mousemove", draw);
canvas.addEventListener("mousemove", setPosition);
canvas.addEventListener("mouseenter", setPosition);
}
function draw(e) {
if (e.buttons !== 1) return;
ctx.beginPath();
ctx.moveTo(pos.x, pos.y);
setPosition(e);
ctx.lineTo(pos.x, pos.y);
ctx.strokeStyle = penColor.value;
ctx.lineWidth = penWidth.value;
ctx.lineCap = "round";
ctx.stroke();
}
function setPosition(e) {
pos.x = e.pageX;
pos.y = e.pageY;
}
function clearImg() {
const temp = confirm("Clear confirm?");
if (temp) {
ctx.clearRect(0, 0, canvas.offsetWidth,
canvas.offsetHeight);
}
}
</script>
项目
创建矩阵效果
<!doctype html>
<html>
<head>
<title>Canvas HTML5</title>
</head>
<body>
<div class="output"></div>
<script>
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.setAttribute("width", "500");
canvas.setAttribute("height", "300");
document.body.prepend(canvas);
const colVal = [];
for(let x=0;x<50;x++){
colVal.push(0);
}
function matrix() {
ctx.fillStyle = "rgba(0,0,0,.05)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "green";
colVal.map((posY, index) => {
let output = Math.random()<0.5?0:1;
let posX = (index * 10) + 10;
ctx.fillText(output, posX, posY);
if (posY > 100 + Math.random() * 300) {
colVal[index] = 0;
} else {
colVal[index] = posY + 10;
}
});
}
setInterval(matrix, 50);
</script>
</body>
</html>
倒计时时钟
<script>
const endDate = document.querySelector("input[name='endDate']");
const clock = document.querySelector(".clock");
let timeInterval;
let timeStop = true;
const savedValue = localStorage.getItem("countdown") || false;
if (savedValue) {
startClock(savedValue);
let inputValue = new Date(savedValue);
endDate.valueAsDate = inputValue;
}
endDate.addEventListener("change", function (e) {
e.preventDefault();
clearInterval(timeInterval);
const temp = new Date(endDate.value);
localStorage.setItem("countdown", temp);
startClock(temp);
timeStop = true;
});
function startClock(d) {
function updateCounter() {
let tl = (timeLeft(d));
if (tl.total <= 0) {
timeStop = false;
}
for (let pro in tl) {
let el = clock.querySelector("." + pro);
if (el) {
el.innerHTML = tl[pro];
}
}
}
updateCounter();
if (timeStop) {
timeInterval = setInterval(updateCounter, 1000);
} else {
clearInterval(timeInterval);
}
}
function timeLeft(d) {
let currentDate = new Date();
let t = Date.parse(d) - Date.parse(currentDate);
let seconds = Math.floor((t / 1000) % 60);
let minutes = Math.floor((t / 1000 / 60) % 60);
let hours = Math.floor((t / (1000 * 60 * 60)) % 24);
let days = Math.floor(t / (1000 * 60 * 60 * 24));
return {
"total": t,
"days": days,
"hours": hours,
"minutes": minutes,
"seconds": seconds
};
}
</script>
在线绘画应用
<script>
const canvas = document.querySelector("#canvas");
const ctx = canvas.getContext("2d");
const penColor = document.querySelector("#penColor");
const penWidth = document.querySelector("#penWidth");
const btnSave = document.querySelector(".save");
const btnClear = document.querySelector(".clear");
const output = document.querySelector(".output");
const mLoc = {
draw: false,
x: 0,
y: 0,
lastX: 0,
lastY: 0
};
canvas.style.border = "1px solid black";
btnSave.addEventListener("click", saveImg);
btnClear.addEventListener("click", clearCanvas);
canvas.addEventListener("mousemove", (e) => {
mLoc.lastX = mLoc.x;
mLoc.lastY = mLoc.y;
//console.log(e);
mLoc.x = e.clientX;
mLoc.y = e.clientY;
draw();
});
canvas.addEventListener("mousedown", (e) => {
mLoc.draw = true;
});
canvas.addEventListener("mouseup", (e) => {
mLoc.draw = false;
});
canvas.addEventListener("mouseout", (e) => {
mLoc.draw = false;
});
function saveImg() {
const dataURL = canvas.toDataURL();
console.log(dataURL);
const img = document.createElement("img");
output.prepend(img);
img.setAttribute("src", dataURL);
const link = document.createElement("a");
output.append(link);
let fileName = Math.random().toString(16).substr(-8) +
".png"
link.setAttribute("download", fileName);
link.href = dataURL;
link.click();
output.removeChild(link);
}
function clearCanvas() {
let temp = confirm("clear canvas?");
if (temp) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
}
function draw() {
if (mLoc.draw) {
ctx.beginPath();
ctx.moveTo(mLoc.lastX, mLoc.lastY);
ctx.lineTo(mLoc.x, mLoc.y);
ctx.strokeStyle = penColor.value;
ctx.lineWidth = penWidth.value;
ctx.stroke();
ctx.closePath();
}
}
</script>
自我检查测验
-
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
-
绘制一个红色圆圈。
-
moveTo()
,lineTo()
,stroke()
ctx.moveTo(100, 0); ctx.lineTo(100, 100); ctx.stroke();
第十五章,下一步
练习题
练习 15.1
[
{
"name": "Learn JavaScript",
"status" : true
},
{
"name": "Try JSON",
"status" : false
}
]
const url = "list.json";
fetch(url).then(rep => rep.json())
.then((data) => {
data.forEach((el) => {
console.log(`${el.name} = ${el.status}`);
});
});
项目
使用 JSON
<!DOCTYPE html>
<html>
<head><title>Working with JSON Project</title></head>
<body>
<script src="img/myscript.js"></script>
</body>
</html>
// myscript.js
let url = "people.json";
fetch(url)
.then(response => response.json())
.then(data => {
console.log(data);
data.forEach(person => {
console.log(`${person.first} ${person.last} - ${person.topic}`);
});
});
// people.json
[
{
"first": "Laurence",
"last": "Svekis",
"topic": "JavaScript"
},
{
"first": "John",
"last": "Smith",
"topic": "HTML"
},
{
"first": "Jane",
"last": "Doe",
"topic": "CSS"
}
]
列表制作项目
<!DOCTYPE html>
<html>
<head>
<title>JavaScript List Project</title>
</head>
<body>
<div class="output"></div>
<input type="text"><button>add</button>
<script>
const output = document.querySelector(".output");
const myValue = document.querySelector("input");
const btn1 = document.querySelector("button");
const url = "list.json";
btn1.addEventListener("click", addToList);
let localData = localStorage.getItem("myList");
let myList = [];
window.addEventListener("DOMContentLoaded", () => {
output.textContent = "Loading......";
if (localData) {
myList = JSON.parse(localStorage.getItem("myList"));
output.innerHTML = "";
myList.forEach((el, index) => {
maker(el);
});
} else {
reloader();
}
});
function addToList() {
if (myValue.value.length > 3) {
const myObj = {
"name": myValue.value
}
myList.push(myObj);
maker(myObj);
savetoStorage();
}
myValue.value = "";
}
function savetoStorage() {
console.log(myList);
localStorage.setItem("myList", JSON.stringify(myList));
}
function reloader() {
fetch(url).then(rep => rep.json())
.then((data) => {
myList = data;
myList.forEach((el, index) => {
maker(el);
});
savetoStorage();
});
}
function maker(el) {
const div = document.createElement("div");
div.innerHTML = `${el.name}`;
output.append(div);
}
</script>
</body>
</html>
自我检查测验
-
预先编写的 JavaScript 模块,您可以使用它们来加速您的开发过程。
-
打开控制台并输入
$
或jQuery
,如果您得到一个jQuery
对象作为响应,则页面有对$
或jQuery
的引用。 -
UnderscoreJS.
-
在文件目录的终端中输入
node
和文件名。