JavaScript-编程入门指南-全-

JavaScript 编程入门指南(全)

原文:Get Programming with JavaScript

译者:飞龙

协议:CC BY-NC-SA 4.0

第一部分:控制台上的核心概念

用 JavaScript 开始编程 从介绍你在每个程序中使用的核心概念开始。所提出的思想构成了你随后编写所有代码的基础。讨论从轻松开始,并逐步深入,提供了大量的示例。贯穿全书的主旨是组织,你将了解如何使用变量存储和检索值,如何使用对象和数组分组值,以及如何使用函数分组指令。

到 第一部分 结束时,你将构建一个名为 The Crypt 的冒险游戏的可用版本。玩家将能够探索地点图,从房间移动到房间,从墓穴移动到墓穴,收集宝藏。第一章 设置了场景,介绍了编程、JavaScript 和 JS Bin,这是你的冒险发生的在线代码环境。游戏开始吧!

第一章:编程、JavaScript 和 JS Bin

本章涵盖

  • 编程

  • JavaScript

  • JS Bin

  • 我们持续的示例:The Crypt

用 JavaScript 开始编程 是对编程的实用介绍。通过动手代码示例、精心安排的解释、支持的视频教程和各种各样的示例,这本书将帮助你建立知识和技能,并让你踏上成为编码专家的道路。

本章首先简要概述了编程以及使用 JavaScript 进行编程,然后介绍了 JS Bin,这是你在学习过程中将充分利用的在线编程沙盒。最后,你将遇到 The Crypt,这是我们书中涵盖概念的不断发展的背景。

1.1. 编程

编程是向计算机提供一系列它理解的指令格式。程序无处不在,运行火星探测器、大型强子对撞机、引擎管理系统、金融市场、无人机、手机、平板电脑、电视和医疗设备。程序的力量和多功能性令人惊叹。程序可以只有几行长,也可以有几百万行长,由简单的构建块构建出复杂的解决方案。

在计算机的电子深处是一个由二进制、计数器、寄存器、总线以及内存分配组成的世界。存在低级编程语言,让我们能够在那个世界中工作,被称为机器代码和汇编语言。幸运的是,我们已经创造了高级语言,它们更容易阅读、理解和使用。我们可以编写几乎任何人都能理解的代码;以下是一些接近高级语言允许的伪代码:

increase score by 100
if score is greater than 5000 print "Congratulations! You win!"
otherwise load new level

不同的语言规定了编写此类代码的方式;一些使用比其他更多的符号,一些使用更自然的单词。以下是它在 JavaScript 中的可能样子:

score = score + 100;
if (score > 5000) {
    alert("Congratulations! You win!");
} else {
    loadNewLevel();
}

括号、花括号和分号都是语言语法的一部分,是设置代码的规则,以便计算机可以理解。你编写的代码将被自动翻译成计算机执行的底层代码。

在之前的 JavaScript 代码片段中是loadNewLevel();指令,用于加载游戏的新关卡,可能是在程序的某个地方。程序的其他部分将包含更多代码,详细说明了如何逐步加载新关卡。编程的艺术之一是将较大的程序分解成执行特定任务的较小部分。然后将这些小部分组合起来,以实现主程序的目的。

有许多许多编程语言。你可能听说过的有 Java、C、PHP、Python 和 Swift。让我们看看为什么你可能会选择 JavaScript。

1.2. JavaScript

JavaScript 是一种极其流行的编程语言,主要在网页浏览器中使用,但在其他环境中也越来越受欢迎。在网页上,它增加了交互性,从简单的动画效果到表单验证,再到完整的单页应用程序。现在,使用 Node.js 编写的服务器——使文件、网页和其他资源在互联网上可用的程序——现在正使用 JavaScript 编写。其他程序也可以用 JavaScript 编写,如 Photoshop 和 Minecraft,一些数据库存储 JavaScript,并允许你用 JavaScript 查询数据。随着越来越多的网络化对象被添加到物联网中,JavaScript 在编程传感器、机器人、无人机和 Arduino 风格的电子设备方面变得越来越受欢迎。

学习编程赋予你一项伟大的技能,它灵活、有用、刺激、富有创造力、有趣、有回报,并且需求量大。用 JavaScript 学习编程让你能够接触到世界上使用最广泛的语言之一,让你能够开发各种用途、设备、平台和操作系统上的应用程序。

1.3. 通过做和思考学习

学习跟随思考。《用 JavaScript 编程入门》的哲学是,通过在线沙盒中实验程序,亲自找出哪些可行哪些不可行,并通过尝试挑战,你将不得不仔细思考每一章的概念。这种思考将导致理解和学习。

沙盒让你可以运行程序并获得即时反馈。有时反馈会出乎意料,迫使你质疑你所认为的已知信息。一些想法可能很快就会变得清晰,而其他想法可能需要更长的时间;仔细考虑和进一步的实验可能是必要的。好奇心、承诺和韧性是学习任何事物时的关键态度,它们当然会帮助你成为一个更好的程序员。

这并不意味着学习编程会是一件苦差事!恰恰相反。即使在我编程超过 30 年后,我仍然发现将代码转化为有用且/或有趣的应用几乎是一种魔法。简单语句的行当组合在一起,能够实现如此多样的结果,这令人惊讶。看到别人使用你创造的东西变得更加高效、更有条理,或者只是更有乐趣,这是一种特权,也是一种快乐。

因此,请做好探索发现的准备,如果你一开始发现某些概念难以理解,不要气馁。花时间做练习,不要忘记 《用 JavaScript 编程入门》 网站上的资源;它提供了链接到列表、解决方案、视频和进一步阅读的内容,网址为 www.room51.co.uk/books/getProgramming/index.html。学习编程是值得努力的。

1.4. JS Bin

JavaScript 最常见的是由网页浏览器运行。浏览器从服务器加载网页,该网页可能包含 JavaScript 代码或指向代码的链接,然后浏览器获取该代码。浏览器逐行执行代码,执行指令。在 《用 JavaScript 编程入门》 的第一部分 中,你避免了编写和加载网页以及链接到代码文件的额外考虑。你将注意力集中在 JavaScript 语言本身上。要做到这一点,你可以使用 JS Bin,这是一个免费的在线服务。

JS Bin 是一个在线沙盒,用于开发和共享网页和 JavaScript 程序。本书中的所有代码列表都可在 JS Bin 上找到 (www.jsbin.com),以便你亲自动手实践代码,并让你进行实验和学习。

当你第一次访问该网站时,你会看到一个包含 Dave the BinBot 图片和一些帮助你入门的有用链接的标题部分,如图 图 1.1 所示。请随意探索,但不要因为可能遇到的任何复杂信息而气馁。探索完毕后,通过点击 Dave 左侧的 X 关闭标题。(你还可以关闭 JS Bin 有时显示的任何欢迎信息或其他信息。)

图 1.1. JS Bin 显示 HTML、CSS、输出和信息面板

图片 01fig01_alt.jpg

1.4.1. JS Bin 面板

JS Bin 是一个用于开发网页和应用程序的工具。除了顶部的信息面板外,它还有五个可显示的面板:HTML、CSS、JavaScript、控制台和输出。点击 JS Bin 工具栏上的面板名称可以切换面板的开启或关闭。在第一部分 中,你将只使用 JavaScript 和控制台面板,第二部分 将使用 HTML 面板,第三部分 将添加 CSS 和输出面板。你一开始将只使用 JavaScript 和控制台面板,所以请切换这两个面板开启,其他面板关闭;参见 图 1.2。

图 1.2. JS Bin 显示 JavaScript 和控制台面板。

HTML

HTML 用于结构化网页的内容。文本、图像、视频和表单是内容的例子。

CSS

层叠样式表(Cascading Style Sheets)允许您指定内容应该如何呈现。您可以定义背景颜色、字体细节、边距、大小等。

JavaScript

JavaScript 允许您向网页添加行为和交互性。或者,您也可以在网页上下文中之外编写程序。

控制台

控制台可以被程序用来向用户和开发者显示信息。程序中的警告和错误可能会在这里显示。控制台是交互式的;您可以在其中输入以了解程序的状态。它通常不在完成的应用程序中使用,但在学习过程中,您会很好地利用它作为一种快速简单的方式与您的程序交互。

输出

输出面板显示了在 HTML、CSS 和 JavaScript 面板中定义的网页预览。它显示了访问者通常在浏览器中看到的页面内容。

1.4.2. 跟随 JS Bin 上的代码列表

您将通过在 JS Bin 的 JavaScript 面板中添加代码行来编写程序。随着您覆盖更多语言的功能,程序将从简单开始,逐渐增加复杂性。对于书中第一部分中的大多数代码列表,您可以通过以下步骤在 JS Bin 上测试代码:

1.  在 JS Bin 的文件菜单上选择新建。

2.  切换面板,使 JavaScript 和 Console 面板可见。

3.  在 JavaScript 面板中输入代码。

4.  点击运行。

5.  在控制台面板上检查结果。

图 1.3 显示了 JS Bin 截图中的步骤。

图 1.3. 在 JS Bin 上运行 JavaScript 的步骤

书中的大多数列表也都有一个链接,指向 JS Bin 上的相同代码。JS Bin 上的列表包括与代码相关的额外信息和练习,这些将在 1.4.4 和 1.4.5 节中进一步讨论。

1.4.3. 将日志记录到控制台

在不同的点上,您希望程序通过在控制台面板上显示信息来输出信息。要在控制台上显示信息,请使用console.log命令。运行此列表中的程序将在控制台上显示以下内容:

> Hello World!
列表 1.1. 使用console.log显示信息 (jsbin.com/mujepu/edit?js,console)
console.log("Hello World!");

您将要在显示的消息之间放置引号内的括号。

注意列表标题中包含一个 JS Bin 链接。点击链接查看 JS Bin 上的实时代码。要执行 JavaScript 面板中的代码,请点击控制台面板顶部的运行按钮。您将在控制台上看到消息“Hello World!”。

尝试点击运行几次。每次点击都会执行代码,并将“Hello World!”记录到控制台。您可以通过点击清除来清除控制台中的所有消息。

当您在 JS Bin 上跟踪代码链接时,程序可能会自动运行。如果您注册了账户,您可以在 JS Bin 的偏好设置中关闭自动运行。

1.4.4. 代码注释

除了代码语句外,此书的 JS Bin 列表还包括注释,这些注释不是程序的一部分,但对解释代码的功能很有用。以下是 JS Bin 上列表 1.1 的第一个代码块注释:

/* Get Programming with JavaScript
 * Listing 1.1
 * Using console.log
 */

除了可以跨越多行的代码块注释外,您有时还会看到单行注释:

// This is a single-line comment

在 JS Bin 上,注释通常以绿色显示。程序员如果觉得代码需要一些解释以便其他程序员理解,就会在代码中添加注释。当程序执行时,计算机将忽略注释。

1.4.5. 进一步冒险

在 JS Bin 上,用 JavaScript 编程入门的大多数代码列表都附带一组小练习,称为进一步冒险,作为代码后的注释。有些很简单,有些是重复的,有些是具有挑战性的。学习编程的最佳方式是编程,所以我敦促您跳进来尝试挑战。您可以在 Manning 论坛上获得帮助,许多任务的解决方案在书籍的网站上提供,网址为 www.manning.com/books/get-programming-with-javascriptwww.room51.co.uk/books/getProgramming/index.html

1.4.6. 错误信息

当您向 JavaScript 面板添加代码时,JS Bin 会持续检查错误。您会在 JavaScript 面板的底部看到红色错误区域。不要担心,直到您完成添加一行代码。如果错误区域仍然存在,点击它以查看错误信息。

例如,尝试从列表 1.1 的代码行末尾删除分号。图 1.4 显示了 JS Bin 对删除的分号所显示的错误。

图 1.4. JS Bin 错误区域(先关闭后打开)

分号表示代码行的结束。每行以分号结束的代码称为语句。如果您停止输入但行末没有分号,JS Bin 将会抱怨。程序可能仍然会运行,JavaScript 会尝试在它认为应该放置分号的地方插入分号,但最好还是自己放置分号;JS Bin 中的错误信息鼓励良好的实践。

JS Bin 尽力提供有助于您解决问题的错误信息。逐个从代码行的末尾删除更多字符,并观察错误信息如何更新。

1.4.7. 行号

图 1.4 中的错误信息告诉了你错误发生的行号。你只有一行代码,所以错误在第一行。程序可以相当长,所以看到行号是有帮助的。你不需要手动添加行号;在这个例子中,JS Bin 文本编辑器会自动为你添加。它们不是程序的一部分;它们在编写和测试代码时帮助你。图 1.5 显示了一个较长的程序,其中包含几个错误。现在不必担心理解代码,但看看你是否能找到 JS Bin 在图中报告的错误。如果没有行号,这将困难得多,尤其是如果程序更长的话。

图 1.5. 行号在查找错误时很有帮助。

图片 1.5

要在 JS Bin 中切换行号的显示,请双击 JavaScript 面板左上角的“JavaScript”一词(见图 1.5。图 1.5)。当你双击时,菜单会打开和关闭,但行号应该从隐藏切换到可见(或反之亦然)。如果你已经注册,你还可以在 JS Bin 个人资料中开启行号。

1.4.8. 获取账户

在 JS Bin 上注册一个免费账户是值得的。你的工作将被保存,你将能够设置更多的偏好设置。当你开始编写自己的程序时,这是一个尝试你的想法并获得即时预览和反馈的好地方。

1.5. 密室——我们的运行示例

在整本书中,你正在开发一个名为 The Crypt 的基于文本的冒险游戏。玩家将能够在一个地图上探索位置,从一个地方移动到另一个地方,捡起帮助解决挑战和克服障碍的物品。每一章的最后部分将使用你在该章中学到的内容来进一步开发游戏。你将能够看到编程概念是如何帮助你构建最终组合成大型程序的各个部分的。

1.5.1. 玩 The Crypt

游戏将显示玩家当前位置描述,以及在那里找到的任何物品和出口,如图 1.6 所示。图 1.6。

图 1.6. 玩 The Crypt

图片 1.6

玩家可以输入命令来从一个地方移动到另一个地方,捡起他们发现的物品,并使用这些物品来克服挑战。

你需要为游戏中的所有不同元素编写代码。但别担心——你一步一步来,随着你的进步,我会介绍你需要知道的内容。你可以在 JS Bin 上玩这个游戏,output.jsbin.com/yapiyic

1.5.2. 构建 The Crypt 的步骤

在 第一部分 中,当你学习 JavaScript 的核心概念时,你编写代码来表示游戏中的玩家和地点,并让玩家从一个地方移动到另一个地方,捡起他们找到的物品。图 1.7 展示了你将创建的用于玩家、地点、地图和整个游戏的组件。不要担心图中所有的术语——随着你阅读本书的进展,你将详细地了解它们。每个章节都将使用类似的图表来突出整个游戏背景下的讨论内容。

图 1.7. 《密室》第一部分 part 1 中的游戏元素

两个 部分 1 和 2 都将使用 JS Bin 上的控制台来显示游戏信息和接受用户的输入。表 1.1 展示了游戏元素如何对应于 第一部分 中涵盖的 JavaScript。

表 1.1. 《密室》第一部分 part 1 中的游戏元素和 JavaScript
游戏元素 任务 JavaScript 章节
玩家 决定我们需要了解的每个玩家的信息 变量 第二章
在一个地方收集玩家信息 对象 第三章
在控制台上显示有关玩家的信息 函数 第四章–第七章
创建每个玩家收集到的物品列表 数组 第八章
组织玩家创建代码 构造函数 第九章
地点 创建许多可供探索的地方,它们具有相似的结构 构造函数 第九章
使用方括号表示法将地方连接起来 方括号表示法 第十章
游戏 添加简单的移动、收集物品和显示信息的函数 方括号表示法 第十章
地图 将有出口的地方连接起来 方括号表示法 第十章

第二部分 为玩家增加了挑战,阻止他们离开,直到玩家使用适当的物品来解决谜题。编程的重点更多地在于组织你的代码,隐藏其工作原理,检查用户输入,以及构建可以重用和交换的模块,使项目更加灵活。

图 1.8 展示了游戏如何分为地图数据模块、用于创建玩家和地点的构造函数、用于在控制台上显示信息的视图以及用于运行游戏和连接所有部件的控制器。再次强调,这里展示的图表是为了给你一个预期的感觉以及完整游戏由更小的构建块组成——在这个阶段,你不需要理解所有术语。你可以保持好奇和兴奋!每个构建块将在接下来的 400 页中完全解释;花时间探索概念并玩转代码。

图 1.8. 《密码学》中第二部分的游戏元素

图片 01fig08_alt

第三部分更新了显示以使用 HTML 模板,修改了游戏在运行时加载数据,用玩家和地点信息填充模板,并引入了文本框和按钮,以便玩家可以通过网页输入命令(图 1.9)。

图 1.9. 《密码学》中第三部分的游戏元素

图片 01fig09_alt

第四部分可在网上找到,展示了如何使用 Node.js 在服务器上存储游戏数据。

1.6. 更多示例和实践

虽然《密码学》是我们学习 JavaScript 的持续背景,但每一章都包含其他示例,以展示在多种情况下概念的应用。一些较小的示例也会随着你的进步而发展,让你看到新概念如何帮助改进示例。特别是,你将查看一个测验应用、一个健身追踪应用、一个电影评分网页和一个新闻头条页面。

1.7. 浏览器支持

浏览器一直在进化。《用 JavaScript 编程入门》中的一些 JavaScript 代码示例可能在较旧的浏览器中(例如 Internet Explorer 8 及以下版本)无法工作。书籍网站上的讨论将提供替代方法,以便在主要方法在代码示例中显示不满意的浏览器上运行代码。

1.8. 摘要

  • 程序是一系列计算机需要遵循的指令。

  • 高级语言让我们能够编写更容易阅读和理解的指令。

  • JavaScript 是世界上使用最广泛的编程语言之一。它最强烈地与为网页添加交互性相关联,但也用于服务器端编程、作为应用程序的脚本语言,以及编程机器人和其他设备的方式。

  • 学习源于思考。因此,参与书中的实际示例,保持好奇、投入和坚韧。

  • JS Bin,一个在线代码沙盒,将帮助你专注于 JavaScript,并在你实验和实践时提供快速反馈。

  • 我们的主要运行示例是《密码学》。它为你提供了一个学习编程概念和从简单元素构建相对复杂程序的环境。

  • 进一步的例子将帮助你通过广度来加深理解,并欣赏你学到的概念是如何在更广泛的各种情况下应用的。

第二章. 变量:在程序中存储数据

本章涵盖的内容

  • 使用变量存储和使用信息

  • 声明变量

  • 给变量赋值

  • 在控制台上显示变量值

《用 JavaScript 编程入门》被编写成对编程的温和介绍。因此,本章是对温和介绍的温和介绍。就冒险而言,你几乎不出门。你可以把它看作是旅程的打包阶段,至关重要——你不想在没有护照的情况下到达机场,或者在没有自拍杆的情况下到达奥斯卡——但这不是主要事件。

几乎没有例外,程序都会存储、操作和显示数据。无论你是编写一个博客系统、分析引擎性能、预测天气,还是发送探测器在 10 年后登陆彗星,你都需要考虑你将使用的数据以及这些数据可能采取的值。为了在程序中处理数据,你使用变量。

2.1. 什么是变量?

一个 变量 是程序中的一个命名值。每次你在程序中使用这个名称时,它就会被相应的值所替换。你可以创建一个名为 score 的变量,并给它赋值为 100。然后,如果你告诉计算机“显示分数”,它就会显示 100。现在,变量可以改变,因此得名,所以程序中稍后,比如响应玩家采取的一些动作,你可以更新分数。如果你将 50 加到 score 上,并告诉计算机“显示分数”,现在它会显示 150

那么你如何使用 JavaScript 来实现这个魔法呢?

2.2. 声明变量和赋值

让计算机知道你想要存储的信息需要两个步骤:

1. 你需要 设置一个名称 来在程序中引用你的数据,比如 scoreplayerNametaxRate

2. 你需要 将名称与要存储的值相链接:比如 将分数设置为 100让‘George’成为 playerName让税率是 12%

在 2.2.3 节 中,你将看到如何在一个 JavaScript 语句中完成这两个步骤,即给变量命名和赋值。现在,你慢慢来,为每个步骤使用单独的语句。

2.2.1. 声明变量

你一直梦想着通过你为下一个移动应用热潮 The Fruitinator! 的设计而一举成名,玩家被送回过去用他们的 Smoothie 9mm 拍打水果,在前进的过程中累积创纪录的分数。你的程序需要跟踪这些分数。这意味着设置一个变量。

用名称表示值的过程称为 变量声明。你通过使用 var 关键字来声明变量。以下列表显示了声明名为 score 的变量的所需代码语句。

列表 2.1. 声明一个变量 (jsbin.com/potazo/edit?js,console)
var score;

var 关键字告诉计算机将语句中的下一个单词转换为一个变量。图 2.1 注释了 列表 2.1 中的代码语句。

图 2.1. 声明一个变量

图片 02fig01

那就这样!你已经声明了一个名为 score 的变量,准备进行一些散裂水果的动作。这是基于水果的系统中的第一行代码,可能正是毁灭人类的开始。让我们开始计分吧。

2.2.2. 给变量赋值

你的程序现在知道了变量 score。但你是如何给它赋值的呢?你使用谦逊的等号 =。实际上,在 JavaScript 中,它并不那么谦逊。它无处不在,执行着许多重要的任务。(一个平滑的操作员。)图 2.2 展示了等号在工作中的样子,列表 2.2 展示了它在上下文中的使用。

图 2.2. 给变量赋值

图片 02fig02

列表 2.2. 给变量赋值 (jsbin.com/yuvoju/edit?js,console)

图片 018fig01_alt

你将变量 score 赋值为 100。一般来说,你将等号右边的值赋给等号左边的变量 (图 2.3)。当你使用等号赋值时,JavaScript 给它一个特殊名称,即 赋值运算符

图 2.3. 等号被称为赋值运算符。

图片 02fig03

你已经声明了一个变量并给它赋了值。现在是时候在控制台上显示它了。以下列表的输出应该看起来像这样:

> 100
列表 2.3. 使用变量 (jsbin.com/huvime/edit?js,console)
var score;
score = 100;
console.log(score);

使用在 第一章 中介绍的 console.log 函数,你告诉计算机显示 score 变量的值,无论它此时是什么。你刚刚给它赋值为 100,所以这个值出现在控制台上。

为什么不直接使用 console.log(100) 打印出 100 呢?嗯,变量的值通常在程序执行过程中会改变。通过使用变量而不是 字面量 值,你的程序可以使用当前值,无论它们在此时是什么。下一个列表显示了 score 的值在控制台上的显示,改变了值,并显示了新的值,如下所示:

> 100
> 150
列表 2.4. 变量变化 (jsbin.com/jasafa/edit?js,console)
var score;
score = 100;
console.log(score);

score = 150;
console.log(score);

你使用了相同的指令 console.log(score) 两次,但程序在控制台上打印了两个不同的值。你的指令使用了变量 score。因为它的值已经改变,所以输出也改变了。

你为score变量分配了数字,100然后是150,作为值。文本也很简单;只需将你想分配的文本用引号括起来。下面的列表显示了控制台上的两条消息:

> Hello World!
> Congratulations! Your tweet has won a prize ...
列表 2.5. 将文本分配给变量 (jsbin.com/hobiqo/edit?js,console)

程序员称文本的部分为字符串,因为它们是字符的字符串,或序列。正如你在列表 2.5 中看到的,为了表示字符串,你将文本放在引号内。引号可以是双引号,"Hello World!",或者单引号,'Congratulations!',只要它们匹配即可。如果没有引号,JavaScript 会尝试将文本解释为指令或变量。

2.2.3. 单步声明和赋值

你已经看到了如何分两步声明变量并分配它们值。也可以在单个语句中声明变量并给它赋值,如图 2.4 所示。

图 2.4. 你在单个语句中声明变量并给它赋值。

列表 2.6 和 2.7 实现了完全相同的结果,在显示以下消息之前声明变量并分配它们值:

> Kandra is in The Dungeon of Doom
列表 2.6. 分两步声明和分配 (jsbin.com/vegoja/edit?js,console)

列表 2.7. 单步声明和分配 (jsbin.com/dorane/edit?js,console)

在列表 2.7 中,你将等号右侧的值分配给左侧新声明的变量。在这两个程序中,你通过使用加号+将文本片段连接起来来创建控制台上显示的消息。连接文本片段称为字符串连接+字符串连接运算符

如果你声明变量时知道它的值,那么这种单步方法可以是一种将值分配给变量的整洁方式。有时,在声明时可能不知道值;可能需要进行一些计算,需要用户输入,或者你正在等待网络响应。在这种情况下,声明和分配将是分开的。程序员通常会在程序顶部声明变量,即使他们不会在稍后分配值。

2.2.4. 在自己的赋值中使用变量

当你给变量赋值时,JavaScript 会评估赋值运算符右侧的表达式,并将结果分配给变量。

var score;
score = 100 + 50;

JavaScript 评估表达式100 + 50,并将结果150分配给变量score

表达式中的值可能不会是硬编码的数字,如10050;它们更有可能是变量。以下是一个例子,使用变量callOut-ChargecostPerHournumberOfHours来计算雇佣水管工做工作的总费用:

total = callOutCharge + costPerHour * numberOfHours;

*符号用于乘法;它是乘法运算符。你也可以使用进行减法,/进行除法。

由于 JavaScript 首先评估右侧的表达式,然后再将其值赋给左侧的变量,因此你甚至可以使用变量的当前值来设置其新值。比如说,在你的应用程序的感官游戏水果机中,一个玩家刚刚打翻了草莓;那值 50 分!玩家需要更新分数。

> Your score was 100
> Great splat!!!
> New score: 150
> Way to go!

图 2.5 展示了在赋值更新分数时使用当前分数的语句,代码列表 2.8 是你的更新分数的程序。

图 2.5. 使用涉及自身计算结果的变量更新

列表 2.8. 使用变量的当前值来设置其新值 (jsbin.com/kijuce/edit?js,console)

在代码中,你使用score + 50的表达式,利用score的当前值100来得到结果150。这个值150随后被赋值给score。接下来是水果机...金桔。价值 100 分!金桔很棘手。永远不要相信金桔。

2.3. 选择好的变量名

在迄今为止的所有代码示例中,没有任何强制要求你必须使用你给变量取的名字。你试图选择那些能帮助阅读代码的人理解变量用途的名字。你几乎有完全的自由选择,但必须小心不要触及 JavaScript 的底线;JavaScript 为自身用途预留了一些名字,并且还有一些关于有效变量名的规则。

2.3.1. 关键词和保留字

JavaScript 有一组关键词,如varfunction,它们是语言本身的一部分,并控制着每个程序中的动作和属性。它还预留了一些保留字,这些字可能在未来的语言中作为关键词出现。你不能将这些关键词或保留字用作变量名。其他关键词的例子有ifswitchdoyield,完整的列表可以在 Mozilla 开发者网络(mng.bz/28d9)上找到。但不要仅凭我的话,去 JS Bin 试一试将这些词作为变量名使用,如图 2.6 所示。

图 2.6. JavaScript 有一些不能用作变量名的词。

你不需要学习关键词和保留字的列表;随着你编程经验的积累,你会自然而然地掌握大部分,而且当你尝试使用它们时,通常会出现错误。但如果你发现程序运行不正常,而你又不确定原因时,请记住它们。

2.3.2. 变量命名规则

既然关键字和保留字已经排除在外,那么其他所有内容都包含在内了吗?并不完全是这样——还有一些额外的规则。变量名可以以任何字母、美元符号$或下划线_开头。后续字符可以是这些字符中的任何一个或数字。不允许有空格。列表 2.9 包括一个有效名称块和一个无效名称块。如果你访问 JS Bin 上的代码,你会看到它报告了一个长长的错误列表。看看并尝试理解它们,但如果你不完全理解它们,不要担心;列表中故意包含了无效名称,因为 JavaScript 对此并不高兴。

列表 2.9. 有效和无效变量名 (jsbin.com/biqawu/edit?js,console)

图片

JavaScript 是区分大小写的。变量名中字符的大小写变化将产生不同的变量。scoreScoreSCORE是三个不同的名称。这些差异可能很难发现,所以像下一节讨论的那样,保持一致性是值得的。

2.3.3. 骆驼命名法

你可能已经注意到了你使用的变量名中的字母大小写。在像costPerHourplayerNameselfieStickActivated这样的变量名中,由多个单词组成,第一个单词是小写,后面的单词以大写字母开头。这被称为骆驼命名法,这是一种极其普遍的约定,可以帮助使名称更易读。

一些程序员选择使用下划线而不是空格来分隔变量名中的单词,例如cost_per_hourplayer_nameselfie_stick_activated。你如何命名变量取决于你;这是你编程风格的一部分。在《用 JavaScript 编程入门》中,我将坚持使用骆驼命名法。

2.3.4. 使用描述性变量名

尝试给你的变量命名,使其能够描述它们的作用或功能。你可以自由选择名称,但costPerHourcph更容易理解。其他程序员可能需要在将来阅读和更新你的代码,当你某一天再次回到它时,你会感谢自己。随着你的程序不断增长,涉及越来越多的变量、对象和函数,好的变量名真的可以帮助你跟随程序的流程并理解其目的。因此,请保持变量名简单、直接且具有描述性。

你已经看到了变量的用途,如何声明和分配它们,以及什么是一个好的名称。但你知道在程序中需要哪些变量吗?分析你试图解决的问题并规划一个适合你用户的解决方案是程序设计的重要部分。在下一节中,你将花点时间考虑你正在进行的示例“密室”中需要表示玩家的信息。

2.4. 密室—玩家变量

如第一章讨论的,The Crypt 包含了许多元素:玩家、地点、游戏、地图和挑战。在设计构建游戏时,你需要考虑所有这些元素的性质。现在,你专注于玩家,如图 2.7 所示。

图 2.7. 《The Crypt》中的元素

当玩家从一个地方移动到另一个地方时,程序需要知道什么来创建有趣和具有挑战性的冒险?你可能需要记录名字、健康、携带的物品或位置。或者可能是脚的毛茸茸程度或光剑的颜色。一些信息在游戏过程中可能保持不变,而一些信息可能会改变。

编程艺术的一部分是抽象,知道要包含哪些信息以及要排除哪些信息。可能只是玩家脚毛的多少在游戏中扮演了一定的角色,但很可能这比你需要知道的多。你应该仔细思考玩家完成他们的任务时将使用哪些数据。

表 2.1 展示了一些你可能希望在程序中表示每个玩家时包含的属性。

表 2.1. 可能的玩家属性
属性 它是用来做什么的? 示例值
名称 用于显示玩家信息和与其他玩家互动时。 "Kandra"、"Dax"
健康 由怪物和毒药减少。由食物和药水增加。 68
地点 玩家在地图上的位置? "老图书馆"
脚的毛茸茸程度 衡量玩家在没有靴子的情况下在寒冷条件下的应对能力。 94
物品 跟踪玩家捡起的物品。 "一把生锈的钥匙"、"一瓶紫色药水"、"奶酪"

你可能需要其他属性,并且如果需要,你可以添加和删除属性。声明玩家属性可能如下所示:

var playerName = "Kandra";
var playerHealth = 50;

程序员的专业技能之一是能够模拟情况并预测完成程序所需的变量。提前做对的事情越多,程序需要大改动的可能性就越小——没有人希望这样。就像你不想在机场意识到自己忘了护照一样,你也不想在你编写了大量代码后发现遗漏了程序的关键部分。

2.5. 概述

  • 变量允许你在程序运行时存储数据供程序使用。

  • 通过在var关键字后跟一个名称来声明变量:

    var costPerHour;
    
  • 为你的变量选择简单、描述性的名称,避免使用 JavaScript 的关键词和保留字。

  • 使用赋值运算符,即等号=来给变量赋值:

    costPerHour = 40;
    

    你将等号右侧的值赋给等号左侧的变量。

  • 在表达式中使用变量:

    total = callOutCharge + costPerHour * numberOfHours;
    
  • 作为你程序规划的一部分,考虑你需要哪些变量以及它们将持有哪些类型的数据。

第三章. 对象:分组你的数据

本章涵盖

  • 使用 JavaScript 对象组织信息

  • 创建对象

  • 向对象添加属性

  • 使用点符号访问属性

  • 对象的示例

在第二章中,你学习了如何声明变量并分配值,并考虑了可以用来模拟《The Crypt》中的玩家的变量。随着你的程序增长,你使用的变量数量也在增加;你需要方法来组织所有这些数据,以便使你的程序更容易理解,并更容易在未来进行更新和添加。

有时将项目分组并作为一个整体来看是有意义的。考虑一个急救包;我们很高兴地将其视为一个单独的项目——“你打包了急救包吗?”“递给我急救包。”“我们急需急救包,现在!”——但当需要时,会迅速将注意力转向其内容——“请从急救包中递给我消毒剂和绷带。”许多项目被一个单一的对象整洁地封装起来。

本章介绍了 JavaScript 对象,这是一种简单高效的方法,可以将变量组合在一起,以便你可以将它们作为一个组而不是单个变量传递。

3.1. 组织的需求

你的冒险故事图书馆正在增长,你决定编写一个程序来跟踪你宝贵的收藏。以下列表显示了你在控制台上生成此测试输出的变量:

> The Hobbit by J. R. R. Tolkien
列表 3.1. 使用变量表示书籍 (jsbin.com/fucuxah/edit?js,console)

首先,使用var关键字声明两个变量,bookTitlebookAuthor。你将使用这两个名称在程序中存储和访问值。然后,将字符串(文本)分配给你的新创建的变量。你用引号包裹字符串,这样 JavaScript 就不会尝试将它们解释为关键字或变量名。最后,你将一条消息记录到控制台。你通过使用连接运算符(+符号)将三个字符串连接起来构建这条消息。

虽然可能还处于早期阶段,但你肯定不止有一本书。当你购买更多书籍时,如何应对所需的变量?你可以为每本书使用不同的前缀。接下来的列表将书籍数量增加到三本,并将这些消息打印到控制台:

> There are three books so far ...
> The Hobbit by J. R. R. Tolkien
> Northern Lights by Philip Pullman
> The Adventures of Tom Sawyer by Mark Twain
列表 3.2. 使用前缀区分书籍变量 (jsbin.com/qowagi/edit?js,console)

这在某种程度上是可行的。但随着书籍数量和每本书的事实增加,变量的数量就难以管理了。能够将一本书的所有信息组合在一起,使用单个变量来访问,将会很有帮助。

3.2. 创建对象

就像请求急救包比单独请求剪刀、消毒剂、绷带和创可贴更容易一样,请求book1比单独请求book1Titlebook1Authorbook1ISBN等更容易。JavaScript 为我们提供了创建对象来分组变量的能力。定义一个新对象时,使用非常具体的语法。让我们来看一个完整的例子,然后再将其分解成几个阶段。

列表 3.3 展示了如何创建一本书作为对象而不是作为单独的变量。图 3.1 展示了当你将book对象记录到控制台时在 JS Bin 上的输出。

图 3.1。你在 JS Bin 的控制台上记录一个对象。

03fig01_alt.jpg

列表 3.3。一本书作为一个对象(jsbin.com/ruruko/edit?js,console)
var book;

book = {
    title : "The Hobbit",
    author : "J. R. R. Tolkien",
    published : 1937
};

console.log(book);

当你在 JS Bin 上运行列表 3.3 时,控制台会显示你新创建的book对象的所有属性,并告诉你它是一个对象。注意,它以字母顺序显示属性。对象本身并不对属性进行排序;JS Bin 纯粹是为了显示而选择了排序方式。

让我们分解对象创建过程,以更清楚地了解正在发生什么以及所有不同的符号表示什么。

3.2.1. 创建一个空对象

在第二章中,你看到变量可以被声明,但不能在程序稍后分配值。你可能必须等待一些用户输入或服务器的响应,或者从传感器读取,才能知道要分配给变量的值。同样,你可以创建一个没有任何属性的空对象,知道将来会在某个时候添加属性。

要创建一个对象,使用大括号,如下面的列表所示。

列表 3.4。创建一个空对象(jsbin.com/kaqatu/edit?js,console)

030fig01.jpg

你创建一个空对象,一个没有任何属性的空对象,并将其分配给变量book。没有属性就没有什么用处,你将在 3.4 节中看到如何向现有对象添加新属性。但你是如何创建具有现有属性的图书对象的?

3.2.2. 属性作为键值对

在列表 3.3 中的书中包含三个属性:它的标题、它的作者和它的出版年份。这些属性的值是"The Hobbit""J. R. R. Tolkien"1937。在 JavaScript 对象中,属性的名称被称为。对于book,键是titleauthorpublished。在创建对象时,你通过在大括号内包含键和值(由冒号分隔)来添加属性。图 3.2 显示了属性定义。

图 3.2。使用键值对设置属性。

03fig02.jpg

键值对的另一个名称是名称-值对,但在这本书中我们将坚持使用键和值。

在下一个列表中,你创建一个只有一个属性的对象。

列表 3.5. 具有一个属性的对象 (jsbin.com/robupi/edit?js,console)
var book;

book = {
    title : "The Hobbit"
};

你声明一个变量,然后创建一个对象并将其赋值给该变量。该对象有一个属性。属性的键是 title,其值是 "The Hob``bit"。我们通常简单地说,booktitle 属性是 "The Hobbit"

属性值不仅限于数字和字符串字面量,如 50"The Hob``bit"。你还可以使用之前声明的变量作为值。以下列表将一本书的名称赋给一个变量,然后使用该变量作为对象属性的值。

列表 3.6. 使用变量作为属性值 (jsbin.com/bafige/edit?js,console)

图片

拥有一个只有一个属性的对象有点奢侈;你不妨继续使用变量。让我们看看如何创建具有多个属性的对象。

当你需要多个属性时,用逗号分隔键值对。图 3.3 展示了对象定义中的两个属性,而列表 3.7 创建了两个对象,每个对象都有两个属性。

图 3.3. 具有两个属性的对象定义

图片

列表 3.7. 具有多个属性的对象 (jsbin.com/bawiqev/edit?js,console)

图片

现在你已经创建了一个对象,你需要能够访问它的属性。

3.3. 访问对象属性

我们对急救包作为一个可以传递给每个人、从地方到地方携带的单个对象的概念感到舒适。只有当我们需要使用急救包时,我们才会考虑里面的内容:消毒剂、剪刀、绷带等等。

对于 JavaScript 对象,要访问对象属性的值,你可以使用 点符号。将变量的名称与属性的名称、其键,用点或句号连接起来。对于一个急救包作为对象,你可能使用 kit.antiseptickit.scissorskit.bandages。对于书籍,要访问名为 book 的变量所赋的对象的 author 属性,你写 book.author (图 3.4)。

图 3.4. 使用点符号访问对象属性

图片

在下一个列表中,你将 book 对象的 titleauthor 属性打印到控制台,以给出以下输出:

> The Hobbit
> J. R. R. Tolkien
列表 3.8. 使用点符号访问属性值 (jsbin.com/funiyu/edit?js,console)

图片

你在代码清单 3.8 中将对象属性中的冒号对齐,以帮助提高可读性。尽管 JavaScript 会忽略额外的空格,但缩进代码块并对齐值可以使你的程序更容易阅读和跟踪,尤其是随着程序规模的增大。代码越容易阅读,就越容易维护和更新,无论是对你还是对其他程序员来说。

将多个单独的变量替换为一个单一的对象有助于你管理程序的复杂性。当细节隐藏直到你需要它们时,你可以更清晰地思考程序的工作方式。你将一本书视为程序中的单个实体,直到你需要访问书的标题、作者或出版日期。可能看起来用单个变量和三个属性替换三个变量并没有改进,但当你开始在第七章中使用具有函数的对象和在第八章中使用数组时,它们的效率和清晰度将更加明显。

你使用属性值就像使用变量一样。以下代码清单中的代码将每本书的标题与字符串" by "及其作者连接起来,以生成以下输出:

> The Hobbit by J. R. R. Tolkien
> Northern Lights by Philip Pullman
代码清单 3.9. 连接字符串属性 (jsbin.com/yoweti/edit?js,console)
var book1;
var book2;

book1 = {
    title: "The Hobbit",
    author: "J. R. R. Tolkien"
};

book2 = {
    title: "Northern Lights",
    author: "Philip Pullman"
};

console.log(book1.title + " by " + book1.author);
console.log(book2.title + " by " + book2.author);

3.4. 更新对象属性

在测验应用中,玩家一个接一个地尝试问题。尝试的问题数量、正确的问题数量和分数会随着时间的推移而变化。你可以创建一个具有初始值的玩家对象,并在尝试问题时随时更新它们。使用点符号来更改已存在的属性或向对象添加新属性,如下面的代码清单所示。

代码清单 3.10. 使用点符号更新属性 (jsbin.com/mulimi/edit?js,console)

在代码清单 3.10 中,你的代码在创建对象时将attemptedcorrect属性设置为初始值,但随后将它们更新为新的值。它使用赋值运算符=将运算符右侧的值1赋给左侧的属性player1.attempted。你设置了attemptedcorrect属性,然后立即更新它们;在实际的测验应用中,这种变化将是对玩家回答问题的响应。

在创建对象后,你可以向其中添加新的属性。在代码清单 3.10 中,你将值50赋给player1对象的score属性。

player1.score = 50;

在创建对象时,你没有设置score属性;如果该属性尚不存在,自动赋值会创建该属性。

就像使用变量一样,你可以在计算中使用属性并将结果赋回属性。下一个代码清单显示了更新玩家属性的代码:

> Max has scored 0
> Max has scored 50
列表 3.11. 在计算中使用属性 (jsbin.com/cuboko/edit?js,console)

图片

当你更新 score 属性(列表中加粗),JavaScript 首先评估赋值右侧的表达式。因为 player1.score0,表达式变为 0 + 50,结果是 50。然后 JavaScript 将该值赋给左侧,即回到 score 属性。所以,你将 player1.score0 更新到 50

3.5. 进一步示例

虽然 The Crypt 程序的开发为你提供了一个持续引入和讨论新概念的背景,但更广泛的示例将有助于加深你对所提出不同想法的理解。你还可以在整本书中回顾一些这些示例,随着你掌握这些技术,应用新的技巧。

这些示例都使用花括号创建一个对象,然后通过使用 var 关键字创建的变量将对象赋值,这是一个步骤。

3.5.1. 编写博客

一个博客由博客文章组成。了解每个作者的更多信息,能够用关键词标记文章,并为每篇文章添加评论,将是有益的。现在,这里有一个表示单个文章的最小对象。

列表 3.12. 一篇博客文章 (jsbin.com/jiculu/edit?js,console)
var post = {
    id : 1,
    title : "My Crazy Space Adventure",
    author : "Philae",
    created : "2015-06-21",
    body : "You will not believe where I just woke up!! Only on a comet..."
};

3.5.2. 创建日历

日历事件显然涉及日期。JavaScript 确实有一个 Date 对象,但你在这本书中不会使用它。下一个列表展示了以特定格式表示日期的字符串。

列表 3.13. 日历事件 (jsbin.com/viroho/edit?js,console)

图片

注意如何在 notes 属性中处理撇号。在撇号前的反斜杠阻止 JavaScript 将其视为字符串的结尾。这个反斜杠被称为转义字符,不会显示出来。

event.notes = 'Don\'t forget the portfolio!';

使用转义字符来显示字符串已用双引号包裹时的双引号。

var story = "She looked at me. \"What did you say?\" she asked.";

JavaScript 也使用反斜杠转义字符来指定特殊字符,如制表符和换行符。你将在整本书中看到它的实际应用。

日历包含许多事件对象。在第九章 [kindle_split_017.html#ch09] 中,你将看到如何通过研究构造函数来简化创建具有相似结构对象的流程。

3.5.3. 天气如何?

在线天气信息服务提供你可以用于程序中的天气数据。数据通常使用 JSON(JavaScript 对象表示法——见第二十章 [kindle_split_030.html#ch20])格式化,这与你在本章学习过的对象非常相似。数据可以非常详细,具有许多属性。下一个列表展示了这些服务之一提供的位置数据的简化版本。

列表 3.14. 天气应用的定位 (jsbin.com/diguhe/edit?js,console)
var location = {
    "city"      : "San Francisco",
    "state"     : "CA",
    "country"   : "US",
    "zip"       : "94101",
    "latitude"  : 37.775,
    "longitude" : -122.418,
    "elevation" : 47.000
};

属性键位于双引号内。JavaScript 允许你将键,即属性名,用引号括起来,无论是单引号还是双引号,尽管到目前为止你在示例中还没有这样做。实际上,如果属性名不满足第二章中讨论的有效变量名规则,则必须使用引号。你将在第十章中更详细地了解如何处理此类属性名。JSON 规范,它规定了程序应该如何将 JavaScript 对象数据作为文本在互联网上传输,要求所有键都必须使用双引号。由于有时这是必需的,许多程序员建议始终将属性名放在引号内,以避免不一致和潜在的错误。

此示例还使键值对中的冒号对齐。你认为这有帮助吗?将其与该节中的其他示例进行比较。它们是否易于阅读和跟随?你不必严格坚持一种风格或另一种风格,尽管程序员随着时间的推移采用特定的风格习惯是很常见的。

3.5.4. 测试效应

一种很好的学习方法就是经常自我测试。一个测验应用可以将其问题和答案表示为类似下一个列表中的对象属性。

列表 3.15. 测验应用的问题和答案(jsbin.com/damoto/edit?js,console)
var questionAndAnswer = {
    question: "What is the capital of France?",
    answer1: "Bordeaux",
    answer2: "F",
    answer3: "Paris",
    answer4: "Brussels",
    correctAnswer: "Paris",
    marksForQuestion: 2
};

测验应用可能包含一小套问题类型。列表 3.15 是多项选择题类型的一个示例。每种问题类型都会有固定的展示形式。模板是展示类似结构数据的副本的好方法,我们将在第十九章中更详细地探讨它们。

3.5.5. 创建你自己的

想想你想要创建的程序。你可以设计哪些对象来表示程序中的实体?前往 JS Bin,尝试构建对象并在控制台显示属性。也许你可以分享你的创作或在该论坛上提出由它们激发的任何问题,该论坛位于forums.manning.com/forums/get-programming-with-javascript

3.6. 密码学——玩家对象

你现在将应用你对 JavaScript 对象的了解来应用到《密码学》中。图 3.5 显示了本节重点,即玩家对象,在我们正在进行的游戏示例的整体结构中的位置。

图 3.5. 《密码学》中的元素

在第二章中,你考虑了在《密码学》中为玩家存储的信息类型。对于单个玩家,你开始时可以使用这些变量:

playerName = "Kandra";
playerHealth = 50;
playerPlace = "The Dungeon of Doom";
playerItems = "a rusty key, The Sword of Destiny, a piece of cheese";

然后你需要为游戏中的每个玩家复制这些变量,可能通过使用变量前缀如player1Nameplayer2Name等。

显然,使用 JavaScript 对象作为组织单个玩家所有信息的手段要整洁得多。列表 3.16 展示了如何将玩家表示为一个对象,并在控制台上显示一些属性。输出如下:

> Kandra
> Kandra is in The Dungeon of Doom
> Kandra has health 50
> Items: a rusty key, The Sword of Destiny, a piece of cheese
列表 3.16. 玩家对象(jsbin.com/qelene/edit?js,console)
var player;

player = {
    name: "Kandra",
    health: 50,
    place: "The Dungeon of Doom",
    items: "a rusty key, The Sword of Destiny, a piece of cheese"
};

console.log(player.name);
console.log(player.name + " is in " + player.place);
console.log(player.name + " has health " + player.health);
console.log("Items: " + player.items);

列表中的最后四行只是为了显示玩家信息。每次你想显示玩家信息时都要重复这些代码行似乎有点繁琐。如果能写一次代码,然后按需调用它们,那就太好了。

你很幸运!JavaScript 允许你定义函数,以便在需要时执行代码块。函数非常强大,将有助于简化玩家属性显示和多个玩家对象的创建。在接下来的四章中,你将深入了解函数。

3.7. 概述

  • 将相关的变量作为对象的属性分组。

  • 使用花括号定义对象,属性之间用逗号分隔:

    var player = { name : "Hadfield", location : "The ISS" };
    
  • 对于每个属性,使用键值对,键和值之间用冒号分隔:

    name : "Hadfield"
    
  • 使用点符号访问属性值。如果对象被分配给一个变量,则使用点将属性名与变量名连接起来:

    player.name
    
  • 在表达式中使用属性,就像使用变量一样:

    console.log(player.name + " is in " + player.location);
    
  • 使用赋值运算符=为属性赋值:

    player.location = "On a space walk";
    
  • 在需要时随时向现有对象添加新属性:

    player.oxygen = 96;
    

第四章. 函数:按需执行代码

本章涵盖

  • 使用函数组织指令

  • 定义函数——指定按需执行的代码

  • 调用函数——按需执行代码

  • 减少代码中的重复

  • 使程序更容易阅读和更新

《用 JavaScript 编程入门》的主要主题之一是通过良好的组织来管理复杂性。在第二章中,你将信息存储在变量中,并看到为这些变量选择好名字如何帮助你理解它们在程序中的作用。在第三章中,你将变量作为对象的属性分组。你可以专注于整个对象,或者当需要时深入细节。在本章中,你将了解另一种重要的组织代码和避免重复的方法,即函数。

4.1. 注意重复

随着你编写的程序变得越来越长和复杂,你会发现自己在重复类似的代码块,只有细微的差异。常见的任务,如显示文本、动画图像或保存到数据库,可能需要经常执行。你需要注意到这些重复的代码片段;它们是函数的理想选择。

函数是一种编写一次代码但多次使用的方法。第 4.2 节探讨了如何创建函数。本节探讨了 JavaScript 重复的一些示例。

4.1.1. 以文本形式显示对象属性

程序使用对象和变量来存储各种信息——个人资料、帖子、文档和照片——你叫什么,就有人在某处将其存储在计算机上。一个常见的任务是将这些信息显示给用户。比如说,你有一些代表电影的对象,需要将每部电影的详细信息显示在控制台上。预期的输出类型如图 4.1 所示。

图 4.1. 在 JS Bin 控制台上显示的电影信息

正如你在下面的列表中可以看到的,创建图 4.1 中输出所需的代码包括对 console.log 的五次调用。而这只是针对一部电影。

列表 4.1. 在控制台上显示对象的属性 (jsbin.com/besudi/edit?js,console)

如果你每次想要显示电影信息,并且对于每部电影,都必须编写这五行代码,那么这将会变得相当重复。而且,如果你决定更改显示的信息,你将不得不遍历代码中所有出现的地方,并确保它们被一致地更改。

下一个列表显示了为三部不同电影重复的代码。

列表 4.2. 显示类似对象的信息 (jsbin.com/gewegi/edit?js,console)
console.log("Movie information for " + movie1.title);
console.log("------------------------------");
console.log("Actors: " + movie1.actors);
console.log("Directors: " + movie1.directors);
console.log("------------------------------");

console.log("Movie information for " + movie2.title);
console.log("------------------------------");
console.log("Actors: " + movie2.actors);
console.log("Directors: " + movie2.directors);
console.log("------------------------------");

console.log("Movie information for " + movie3.title);
console.log("------------------------------");
console.log("Actors: " + movie3.actors);
console.log("Directors: " + movie3.directors);
console.log("------------------------------");

可能会有超过三部电影,以及需要显示信息的地方。如果你后来需要将单词 information 更改为 info,你必须确保找到所有使用它的地方。

三个包含五个语句的代码块几乎完全相同。唯一不同的是显示的是哪部电影属性。如果能定义一个语句块,并让 JavaScript 在需要时使用该块,那就太好了。这正是函数的作用!

下一个部分有一个更多重复代码的例子。(别担心,在例子变得过于重复之前,我们会接触到函数!)

4.1.2. 添加税费并显示摘要

像给价格加税这样的简单任务会反复发生。你计算税费并将其加到价格上,以得到总成本。

> price = $140
> tax @ 15% = $21
> total cost = $161

下面的列表显示了一个为三个不同交易添加税费的程序。两个运算符 */ 分别执行乘法和除法。

列表 4.3. 添加税费以找到总成本 (jsbin.com/kawocu/edit?js,console)

哇!这真是重复得令人发狂。除了 console.log 的代码块外,你还在重复计算的结构。每次你想执行计算时,本质上都是相同的代码。放心,你将学会一种更好的编写此程序的方法。进入函数的世界吧!

4.2. 定义和调用函数

正如对象是一系列属性的集合一样,函数是一系列语句或指令的集合。函数帮助你避免重复,使你的代码更加组织化,更容易更新和维护。命名良好的函数也应该使你的程序更容易理解。如果你发现你的函数在程序中被大量使用,并且在其他程序中也有用,你可以创建包含在其他项目中的有用函数库。

在上一节中,你看到了两个程序的例子,其中具有相同结构的代码块被重复使用。为了减少代码膨胀,你想要用类似以下内容替换那些代码块:

showMovieInfo();
showCostBreakdown();

这两个函数,showMovieInfoshowCostBreakdown,应该产生与列表 4.2 和 4.3 中的代码块相同的输出,你应该能够反复使用它们。让我们看看这种按需代码魔法的实现方式。

4.2.1. 定义新函数

使用以下部分定义函数,如图 4.2 所示:

图 4.2. 函数定义的各个部分

  • function关键字

  • 括号,()

  • 大括号之间的代码块,{}

代码块包含了你每次使用函数时想要执行的指令列表。这个指令列表也被称为函数体

函数定义的各个部分通常是这样安排的:

function () {
    // Lines of code to be executed go here
}

一旦你定义了一个函数,你可以像任何值一样将其赋值给变量。接下来的列表定义了一个函数,用于在控制台显示“Hello World!”,并将该函数赋值给变量sayHello

列表 4.4. 一个简单的函数定义和赋值(jsbin.com/tehixo/edit?js,console

在列表 4.4 中很容易看到组成函数定义的不同部分:function关键字、空括号和函数体的代码块。函数体只有一个语句,console.log(Hello World!);。到目前为止,你只定义了函数,准备以后使用。函数体内的代码在运行函数之前不会执行——你将在 4.2.3 节中看到如何执行它。

以下列表展示了更多定义函数并将它们赋值给变量的例子。

列表 4.5. 两个额外的函数定义和赋值(jsbin.com/xezani/edit?js,console

4.2.2. 函数表达式和函数声明

在之前的例子中,你一直使用函数表达式来定义函数并将它们通过赋值操作符赋值给变量。

var findTotal = function () { ... };   // The function expression is in bold.

你还可以使用一种称为函数声明的替代语法。而不是先定义函数然后将其赋值给变量,你可以在定义函数时作为定义的一部分声明函数的名称。

function findTotal () { ... }  // Declare a name with the function

你可以将定义函数的两种方式视为等效;有一些细微的差别,但在这里我们不会深入探讨。用 JavaScript 编程入门 在 第一部分 和 第二部分 中始终使用函数表达式,以突出创建和分配不同值之间的相似性,包括对象、函数和数组。

var numOfDays = 7;                      // Assign a number
var player = { ... };                   // Create and assign an object 
var findTotal = function () { ... };    // Define and assign a function 
var items = [];                         // Create and assign an array (ch8)

目前不必担心函数声明语法,甚至不必担心声明和表达式之间的区别。当你再次在 第三部分 遇到声明语法时,你会对函数有更多的了解。

仅定义函数不足以在控制台显示“Hello World!”,计算总和或显示菜单。你需要一种方法告诉函数执行其指令列表。

4.2.3. 使用函数

一旦你将一个函数赋值给一个变量,每次你想执行函数体内的语句时,你只需写出变量名后跟括号,()

sayHello();
findTotal();
displayMenu();

调用函数的其他名称是 调用 函数或 调用 函数。

在 列表 4.6 中,你调用了 sayHello 函数三次。它显示了字符串 "Hello World!" 三次,如下所示:

> Hello World!
> Hello World!
> Hello World!
列表 4.6. 调用 sayHello 函数三次 (jsbin.com/vozuxa/edit?js,console)

下一个列表使用 findTotal 函数更新 result 变量。然后它在控制台上显示整个计算:

> 1000 + 66 = 1066
列表 4.7. 使用 findTotal 函数显示计算 (jsbin.com/hefuwa/edit?js,console)
var number1 = 1000;
var number2 = 66;
var result;
var findTotal;

findTotal = function () {
    result = number1 + number2;
};

findTotal();

console.log(number1 + " + " + number2 + " = " + result);

列表 4.8 调用了 displayMenu 函数来,嗯,显示菜单。(这些函数确实做了它们所说的!)

> Please choose an option:
> (1) Print log
> (2) Upload file
> (9) Quit
列表 4.8. 显示菜单 (jsbin.com/cujozo/edit?js,console)
var displayMenu;

displayMenu = function () {
    console.log("Please choose an option:");
    console.log("(1) Print log");
    console.log("(2) Upload file");
    console.log("(9) Quit");
};

displayMenu();

使用空括号作为调用函数的符号可能看起来有些奇怪。但,正如你将在 第五章 中看到的,它们并不总是空的 ... [神秘音乐提示]

定义

对于那些喜欢术语的人来说,当调用函数时添加到变量末尾的括号,(),被称为 函数调用操作符函数调用操作符

4.2.4. 逐步使用函数

表 4.1 总结了定义和调用函数所使用的步骤。

表 4.1. 定义和调用函数所使用的步骤
动作 代码 注释
声明一个变量 var sayHello; 为你在程序中使用保留名称。

| 定义一个函数 | function () { console.log("Hello World!");

} | 函数体内的代码在此点不会执行。 |

| 赋值给变量 | sayHello = function () { console.log("Hello World!");

}; | 将函数赋值给变量为你提供了一个可以用来调用函数的标签。|

调用函数 sayHello(); 函数体内的代码将被执行。

| 需要时重复调用函数 | sayHello(); sayHello();

sayHello(); | 每次调用函数时,函数体内的代码都会被执行。|

表格的第二行,定义一个函数,通常不会单独出现;你更有可能定义一个函数并将其赋值给变量,就像第三行中看到的,赋值给变量。 (你将在后面的章节中看到,你可以将函数定义作为数组(列表)的元素赋值,并将它们传递给其他函数——它们并不总是赋值给变量。)

4.3. 减少重复

在 列表 4.2 和 4.3 中,你看到了在没有函数可用时所需的重复代码块。现在是时候控制这些失控的代码,修剪那些快速生长的杂草,让孩子们节食,限制开支——你明白我的意思;让我们减少重复!

4.3.1. 用于将对象属性显示为文本的函数

返回到 列表 4.2 中的代码,你简化了电影信息的显示。将电影显示代码一次性写在函数中,并在需要时简单地调用该函数。

列表 4.9. 使用函数显示对象属性 (jsbin.com/toqopo/edit?js,console)
var showMovieInfo;

showMovieInfo = function () {
    console.log("Movie information for " + movie.title);
    console.log("------------------------------");
    console.log("Actors: " + movie.actors);
    console.log("Directors: " + movie.directors);
    console.log("------------------------------");
};

代码将新函数赋值给 showMovieInfo 变量。通过编写变量名后跟括号,即 showMovieInfo() 来调用函数,如下一列表所示。你应在控制台上得到以下输出,与之前在 图 4.1 中看到的预期目标相匹配。

> Movie information for Inside Out
> ------------------------------
> Actors: Amy Poehler, Bill Hader
> Directors: Pete Doctor, Ronaldo Del Carmen
> ------------------------------
列表 4.10. 调用 showMovieInfo 函数 (jsbin.com/menebu/edit?js,console)

使用 movie 变量,该变量由 showMovieInfo 函数使用,你可以切换函数将使用哪部电影的信息。列表 4.11 展示了如何在电影之间切换。三个不同电影的信息被打印到控制台上。

> Movie information for Inside Out
> ------------------------------
> Actors: Amy Poehler, Bill Hader
> Directors: Pete Doctor, Ronaldo Del Carmen
> ------------------------------
> Movie information for Spectre
> ------------------------------
> Actors: Daniel Craig, Christoph Waltz
> Directors: Sam Mendes
> ------------------------------
> Movie information for Star Wars: Episode VII – The Force Awakens
> ------------------------------
> Actors: Harrison Ford, Mark Hamill, Carrie Fisher
> Directors: J.J.Abrams
> ------------------------------
列表 4.11. 使用相同函数处理多个对象 (jsbin.com/mavutu/edit?js,console)

4.3.2. 添加税费和显示摘要的函数

列表 4.12 展示了一个用于为销售添加税费并显示每笔交易摘要的函数。你需要重复的大部分代码都在两个函数 calculateTaxdisplaySale 中,你依次为每笔销售对象调用它们。输出结果如下所示。

price = $140
tax @ 15% = $21
total cost = $161
price = $40
tax @ 10% = $4

total cost = $44
price = $120
tax @ 20% = $24
total cost = $144

与 JS Bin 上的所有列表一样,在程序下方有 “进一步冒险” 的建议,这些建议可以帮助你探索代码并加深理解。在这种情况下,一个挑战是减少函数调用的重复;calculateTaxdisplaySale 总是同时被调用。虽然有两个不同的函数——它们做不同的事情——但你能避免每次为每个 sale 对象都调用它们吗?如果你已经连接,点击上面的链接 列表 4.12 现在前往 JS Bin 并开始冒险。如果你在远离技术的印刷版书籍中阅读,那么对你来说就是纸和笔!大多数问题的解决方案都可以在 Get Programming with JavaScript 网站上找到,网址为 www.room51.co.uk/books/getProgramming/listings.html

列表 4.12. 使用函数添加和显示税 (jsbin.com/raqiri/edit?js,console)

这两个函数在其定义中使用了 sale 变量,访问对象上的属性,如 sale.pricesale.taxRate 等。函数体内的代码在程序调用这两个函数之前不会运行,到那时程序将已将一个 sale 对象分配给 sale 变量。

函数名 calculateTaxdisplaySale 有助于使 列表 4.12 中的程序更易于遵循和理解。第 4.4 节 更详细地探讨了这些想法。

4.4. 使代码更易于阅读和更新

随着你的程序变得越来越长和复杂,你通过将它们分解成命名良好的对象和函数来管理这种复杂性。任何阅读你的代码的人都可以跟随其流程并理解各个部分以及整体的目的。

查看以下代码片段;即使你不了解函数如何工作的细节,你也应该能感受到正在发生的事情。

...
var balance = getAccountBalance();

displayBalance();

addInterest()
addBonus();
setAccountBalance();

displayBalance();
...

每个函数都应该有一个单一、明确的目的。如果你需要调查一个函数的功能,你应该能够在一个地方找到它的定义。让我们看看更新一个函数的例子。

4.4.1. 更新 showMovieInfo 函数

在 列表 4.11 中,你创建了一个 showMovieInfo 函数来显示电影对象的信息。能够将显示代码块封装到一个函数中是非常棒的。但是,当多个电影的信息挤在一起在控制台上时,很难挑选出特定电影的个别事实。添加空白行将有助于更清楚地看到每个电影。

> Movie information for Inside Out
> ------------------------------
> Actors: Amy Poehler, Bill Hader
> Directors: Pete Doctor, Ronaldo Del Carmen
> ------------------------------
>
> Movie information for Spectre
> ------------------------------
> Actors: Daniel Craig, Christoph Waltz
> Directors: Sam Mendes
> ------------------------------
>
> Movie information for Star Wars: Episode VII – The Force Awakens
> ------------------------------
> Actors: Harrison Ford, Mark Hamill, Carrie Fisher

> Directors: J.J.Abrams
> ------------------------------
>

因为你的显示代码都在 showMovieInfo 函数内部,你可以直接去那里添加一个额外的 console.log 调用来创建空白行,如下一个列表所示。你的组织工作已经开始显现成效了!

列表 4.13. 更新你的显示函数以添加空白行 (jsbin.com/cijini/edit?js,console)

图片

所以很接近!因为 JS Bin 控制台将字符串包裹在引号中,所以你并没有得到空白行。你得到的是空引号。但是如果你检查浏览器的自己的控制台(见在线指南www.room51.co.uk/guides/browser-consoles.html),你应该看到预期的空白行,如图 4.3 所示。

图 4.3. JS Bin 在引号中显示空字符串,但 Safari 浏览器不这样做。

图片

如果这个例子是更大(可能更大)程序的一部分,而你没有将玩家显示逻辑安全地封装在单个函数中,你就必须检查所有代码以找到应该进行更改的行。文本编辑器和开发环境应该有工具来帮助,但它们并不是万无一失的,你的程序可能会出现未修正的代码的隐蔽角落。你尝试一下,一开始,一切似乎都很好。然后后来有运行和,嗯,尖叫。避免噩梦——使用函数。

4.5. The Crypt——显示玩家信息

现在,你将应用你对 JavaScript 函数的知识到The Crypt。图 4.4 显示了本节重点,通过使用函数显示玩家信息,如何融入我们正在进行的游戏示例的整体结构。

图 4.4. The Crypt 的元素

图片

在第三章中,你看到了如何将有关玩家的信息组合成一个单一的 JavaScript 对象。你使用花括号创建对象,并使用键值对设置属性,如下所示:

var player;

player = {
    name: "Kandra",
    health: 50,
    place: "The Dungeon of Doom"
};

一旦将新对象分配给player变量,你就可以通过点符号来获取属性值。显示玩家信息涉及到将属性记录到控制台。

console.log(player.name + " is in " + player.place);

在游戏中,你可能需要多次显示玩家信息,并且针对多个玩家。你应该使用你对函数的新知识来使信息显示更高效;使其按需编码——可以通过调用函数来执行的代码。

4.5.1. 显示玩家信息的函数

来自列表 4.13(#ch04ex13)的showMovieInfo函数看起来正是你需要的那种函数。而showMovieInfo显示有关电影的信息,列表 4.14 显示了执行类似任务的showPlayerInfo函数,产生以下输出:

> Kandra
> ------------------------------
> Kandra is in The Dungeon of Doom
> Kandra has health 50
> ------------------------------
>
> Dax
> ------------------------------
> Dax is in The Old Library
> Dax has health 40
> ------------------------------
列表 4.14. 显示玩家信息的函数 (jsbin.com/mafade/edit?js,console)

图片

图片

太棒了!这完成了任务。现在只需调用一次函数就能显示玩家信息。遗憾的是,你必须不断将不同的玩家分配给player变量才能使其工作;如果能以某种方式告诉函数“显示 player1 的信息”或“显示 player2 的信息”会更好。好吧,在接下来的三章中,我们将详细探讨如何在函数之间传递信息。灵活性、可重用性、效率,我们来了!

4.6. 概述

  • 函数是一段你只编写一次但多次使用的代码块。它应该有一个清晰、单一的目的。

  • 你可以通过使用函数关键字、括号和花括号中的函数体来定义一个函数:

    function () {
        // Statements go here in the function body
    };
    
  • 你可以使用等号=将函数分配给一个变量,这也就是所谓的赋值运算符:

    showPlayerInfo = function () { ... };
    
  • 一旦将函数分配给变量,你就可以通过在变量名末尾添加括号来调用或调用该函数:

    addTax();
    showPlayerInfo();
    evadeRaptor();
    
  • 注意重复;具有相同结构且仅在使用值或变量上略有变化的代码段。将重复的代码移动到函数中。

  • 给函数起一个清晰的名字,以传达其目的。使用函数来组织你的代码,使你的程序更容易遵循和维护。

第五章. 参数:向函数传递数据

本章内容涵盖

  • 使用参数定义函数,准备接受数据

  • 调用函数,通过参数传递数据

函数是组织代码的一种基本手段;你只需编写一次,然后多次使用。但到目前为止,你的函数都绑定在周围的变量值上。是时候让你的函数自由了,让它们命名自己的变量,并将它们需要的数据传递给它们。

5.1. 函数重用和多功能性

你到目前为止使用的函数都依赖于在程序其他地方声明的并赋予值的变量。在下面的列表中,showMessage函数依赖于一个名为message的变量,该变量在函数定义外部声明。

列表 5.1. 依赖于函数外部的变量(jsbin.com/taqusi/edit?js,console)

图片

showMessage函数定义中,你使用了一个名为message的变量。message变量必须存在,函数才能执行其任务。如果你更改变量的名称,函数就会出错。如果message被重命名为msg,如下一列表所示,并在 JS Bin 上运行程序,你应该得到一个类似这样的错误:“引用错误:找不到变量:message。”(不同的浏览器可能会给出略有不同的错误信息。)

列表 5.2. 通过更改变量名来分解函数(jsbin.com/yaresa/edit?js,console)

图片

函数体内的指令可以使用你在程序其他地方定义的变量,但这会将函数与外部变量耦合;更好的做法是在调用函数时将函数所需的信息传递给函数。这有助于避免函数所需的变量被误命名、遗漏、删除或被程序的其他部分更改,并使跟踪程序流程和发现错误变得更加容易。

我们不希望傲慢的摇滚之神函数在表演前在他们的化妆室里要求特定的变量;我们希望随和的函数,它们可靠且愿意在世界任何地方展示自己的才华。通过将函数与变量解耦,你使函数更易于携带;函数定义可以被移动到程序的其他部分,或者在其他程序或代码库中重用,而不会造成混乱和引发错误。

那么,这种解耦是如何实现的呢?

5.2. 将信息传递给函数

向函数传递信息分为两个阶段:当你定义函数时和当你调用函数时:

1. 当你定义函数时,你设置变量名称,称为参数,以便在调用函数时使用。

2. 当你调用函数时,你包括数据以分配给你在步骤 1 中命名的变量。

在本章中,你将看到许多示例(书中其余部分还有很多),但没有什么比实践更能使知识变得牢固。JS Bin 上每个代码列表的进一步冒险部分应该能帮助你开始。

5.2.1. 向函数传递一个参数

是时候在你的函数定义中使用那些空括号了!当你调用函数时传递信息,就像这样放在括号之间:

showMessage("It's full of stars!");
showPlayerInfo("Kandra");
getMovieActors("The Hobbit");
square(12);

你为每个四个函数传递了一些信息供其代码使用。括号中包含的每个值都称为参数。这里显示的四个函数都包含一个参数。这些参数是“它充满了星星!”,“Kandra”,“霍比特人”,和 12。

要使用括号中的信息,你需要使函数准备好接受它。你定义函数时添加一个参数。参数表明函数期望你在调用它时提供一些信息,如图 5.1 所示。

图 5.1. 在定义函数时在括号中包含参数

对于前面的四个函数,函数定义可能如下所示:

showMessage = function (message) { ... };
showPlayerInfo = function (playerName) { ... };
getMovieActors = function (movieTitle) { ... };
square = function (numberToSquare) { ... };

每个函数定义都包含一个参数,用粗体显示。该参数是一个只能在函数体内使用的变量。

让我们更新 列表 5.1 中的 showMessage 函数,使其接受一个消息,而不是依赖于外部变量。在定义函数时,包括一个 message 参数。message 参数作为变量在函数体内可用。现在当你调用函数时,你将要在括号中传递的消息传递给函数,即 showMessage("It's full of stars!")。函数添加额外的文本并显示以下内容:

> The message is: It's full of stars!
列表 5.3. 将信息传递给函数 (jsbin.com/xucemu/edit?js,console)

当你在 列表 5.3 的末尾调用 showMessage 函数时,你将字符串 “It’s full of stars!” 包含在括号中。像这样包含在函数调用括号中的值称为 参数。程序将参数分配给名为 message 的变量。然后函数使用 message 变量生成记录到控制台的字符串,即 "The message is: It's full of stars!"

表 5.1 列出了声明带参数的函数以及用不同参数调用它的步骤。

表 5.1. 定义和调用函数以及传递数据的步骤
操作 代码 注释
声明一个变量 var showMessage; 为程序中的使用预留名称。

| 带参数定义函数 | function (message) {

} | 预留变量名,message,用于函数体内。|

| 使用参数 | function (message) { console.log(message);

} | 该参数作为变量在函数体内可用。|

| 将函数赋值给变量 | showMessage = function (message) { console.log(message); |

}; | 将函数赋值给变量为你提供了一个可以用来调用函数的标签。|

使用参数调用函数 showMessage("It's full of stars!"); 函数体内的代码使用括号中分配给 message 的参数执行。

| 重复使用不同参数调用函数 | showMessage("It's full of stars!"); showMessage("Yippee!"); |

showMessage("Cowabunga!"); | 每次调用函数时,参数都被分配给 message 参数。|

你可以使用你选择的任何文本调用 showMessage 函数。文本被分配给 message 变量,并用作记录到控制台的全消息的一部分。

在 列表 5.4 中,你使用三个不同的参数调用 showMessage 函数,导致控制台上有三条不同的消息:

> The message is: It's full of stars!
> The message is: Hello to Jason Isaacs
> The message is: Hello to Jason Isaacs and Stephen Fry
列表 5.4. 使用不同参数调用相同函数 (jsbin.com/zavavo/edit?js,console)

由于你在函数定义中声明了参数名称,showMessage 函数不再依赖于其他地方的变量名称,这使得它更不易损坏。解耦完成。

列表 5.5 显示了平方函数的定义,包括一个 numberToSquare 参数。该函数将你传递给它的数字作为参数进行平方。你调用该函数四次,得到以下输出:

> 10 * 10 = 100
> -2 * -2 = 4
> 1111 * 1111 = 1234321
> 0.5 * 0.5 = 0.25
列表 5.5. 使用平方函数 (jsbin.com/vequpi/edit?js,console)

参数与参数

你知道那些括号会派上用场!

当你 定义函数 时括号中包含的名称在函数体中作为变量可用。它们被称为 参数,表明你期望在调用函数时包含信息。

var myExample;
myExample = function (parameter) { ... }

当你 调用函数 时括号中包含的值被分配给参数变量,用于函数体中。这些值被称为 参数

myExample(argument);

不要过于担心术语;适应这些术语可能需要一点时间。在你创建和使用了一些函数之后,即使偶尔混淆了术语 参数参数,你也会对正在发生的事情有一个直观的感觉。

5.2.2. 向函数传递多个参数

你可以定义完成工作所需的任何参数的函数。只需在定义的括号中用逗号分隔参数即可(图 5.2)。

图 5.2. 在函数定义中包含多个参数

假设你想要一个函数将两个数字相加。如果你有两对数字,30 和 23,以及 2.8 和 -5,正确的输出应该是

> The sum is 53
> The sum is -2.2

你怎么做这个?

列表 5.6. 具有两个参数的函数 (jsbin.com/siyelu/edit?js,console)

当你调用 showSum 函数时,程序会自动将你提供的两个参数分配给定义中的两个参数,number1number2。在 列表 5.6 中的第一次调用 showSum 时,就像函数体变成了

var number1 = 30;
var number2 = 23;
var total = number1 + number2;
console.log("The sum is " + total);

你可以定义具有所需数量的参数的函数。随着参数数量的增加,人们(包括你!)在使用你的函数时犯错误的可能性也更大;他们可能在调用函数时遗漏参数或参数顺序错误。克服这个问题的巧妙方法是将对象传递给函数。函数定义只需要一个参数,函数体可以访问它需要的任何属性。你将在第七章(kindle_split_015.html#ch07)中了解如何使用对象与函数。

5.3. 密室—显示玩家信息

现在,你将应用你对 JavaScript 函数参数的知识到 密室 中。图 5.3 显示了本节重点,通过使用带参数的函数显示玩家信息,如何融入我们正在进行的游戏示例的整体结构。

图 5.3. 密室元素

在第四章中,您编写了一个showPlayerInfo函数作为代码按需。您可以在需要时随时在控制台上显示玩家信息,只需调用该函数即可。不幸的是,它依赖于代码其他地方设置的player变量。让我们更新showPlayerInfo函数,设置参数以便您可以直接传递所需的信息。

为了显示每个玩家的信息,您将任务分解为子任务,并为每条信息创建函数。然后您可以这样显示玩家的信息:

showPlayerName("Kandra");
showPlayerHealth("Kandra", 50);
showPlayerPlace("Kandra", "The Dungeon of Doom");

每个函数都有特定的任务要做。如果您想一次性显示所有信息,您可以将单个函数包装在一个主函数中,并传递给它所需的所有信息:

showPlayerInfo("Kandra", "The Dungeon of Doom", 50);

在接下来的几节中,您定义了四个函数,并看到它们与玩家对象一起工作。前三个函数非常相似;注意哪些发生了变化,哪些保持不变。注意您在定义函数时如何使用参数,以及调用它们时如何使用参数。

5.3.1. 显示玩家的名字

您的第一个函数的任务是显示玩家的名字。仅此而已。没有铃声或哨声。下一个列表显示了showPlayerName函数的定义,并使用两个不同的名字调用该函数以产生以下输出:

> Kandra
> Dax
列表 5.7. 显示玩家的名字 (jsbin.com/yubahi/edit?js,console)

在实际的The Crypt程序中,您不太可能使用像"Kandra""Dax"这样的字面值调用showPlayerName函数。您更有可能使用变量。特别是,JavaScript 对象将代表玩家。下一个列表更新了代码以使用几个玩家对象。

列表 5.8. 通过对象属性显示玩家的名字 (jsbin.com/juhewi/edit?js,console)

名字部分就到这里。接下来是健康状态。

5.3.2. 显示玩家的健康状态

下一个列表中的showPlayerHealth函数定义包含了两个参数,playerNameplayerHealth,以产生如下输出:

> Kandra has health 50
> Dax has health 40
列表 5.9. 显示玩家的健康状态 (jsbin.com/nomija/edit?js,console)

在列表 5.9 中调用showPlayerHealth时使用了字面值"Kandra"50。在最终程序中,每个玩家的信息都分配给了玩家对象的属性。您调用showPlayerHealth时更有可能使用这些属性而不是硬编码的值。下一个列表更新了代码以包含玩家对象。

列表 5.10. 通过对象属性显示玩家的健康状态 (jsbin.com/zufoxi/edit?js,console)

姓名:检查。健康:检查。这样就只剩下位置了。

5.3.3. 显示玩家的位置

在以下列表中定义的 showPlayerPlace 函数也包含两个参数,这次是 playerNameplayerPlace,并且会输出如下内容:

> Kandra is in The Dungeon of Doom
> Dax is in The Old Library
列表 5.11. 显示玩家的位置 (jsbin.com/yifahe/edit?js,console)
var showPlayerPlace;

showPlayerPlace = function (playerName, playerPlace) {
    console.log(playerName + " is in " + playerPlace);
};

showPlayerPlace("Kandra", "The Dungeon of Doom");
showPlayerPlace("Dax", "The Old Library");

再次强调,从 列表 5.11 中的硬编码字面值切换到对象属性,在下一个列表中给出了更新版本。

列表 5.12. 通过对象属性显示玩家的位置 (jsbin.com/mejuki/edit?js,console)
var player1;
var player2;
var showPlayerPlace;

showPlayerPlace = function (playerName, playerPlace) {
    console.log(playerName + " is in " + playerPlace);
};

player1 = {
    name: "Kandra",
    place: "The Dungeon of Doom",
    health: 50
};

player2 = {
    name: "Dax",
    place: "The Old Library",
    health: 40
};

showPlayerPlace(player1.name, player1.place);
showPlayerPlace(player2.name, player2.place);

您已经有了显示玩家个别信息的三个函数。现在,是时候将它们组合在一起使用了。

5.3.4. 整合所有内容——显示玩家的信息

showPlayerInfo 函数使用您的三个单独的函数——showPlayerNameshowPlayerHealthshowPlayerPlace——并添加了一些格式化,以显示每个玩家的属性。一个玩家的输出看起来像这样:

>
> Kandra
> ----------------------------
> Kandra is in The Dungeon of Doom
> Kandra has health 50
> ----------------------------
>

下一个列表省略了三个组件函数,以专注于新的函数。它们包含在 JS Bin 中。

列表 5.13. 显示玩家的信息 (jsbin.com/likafe/edit?js,console)
var showPlayerInfo;

showPlayerInfo = function (playerName, playerPlace, playerHealth) {
    console.log("");

    showPlayerName(playerName);

    console.log("----------------------------");

    showPlayerPlace(playerName, playerPlace);
    showPlayerHealth(playerName, playerHealth);

    console.log("----------------------------");
    console.log("");
};

showPlayerInfo("Kandra", "The Dungeon of Doom", 50);
showPlayerInfo("Dax", "The Old Library", 40);

您每次调用 showPlayerInfo 函数时都使用三个参数。它反过来将所需的参数传递给 showPlayerNameshowPlayerHealthshowPlayerPlace 函数。

最后,您将所有部分组合成一个单独的列表,如下所示。它将每个变量声明和赋值作为一个单独的步骤,并在调用 showPlayerInfo 时使用玩家对象的属性,如 player1.name,而不是像 "Kandra" 这样的字面值。

列表 5.14. 使用属性显示玩家的信息 (jsbin.com/loteti/edit?js,console)
var showPlayerName = function (playerName) {
    console.log(playerName);
};

var showPlayerHealth = function (playerName, playerHealth) {
    console.log(playerName + " has health " + playerHealth);
};

var showPlayerPlace = function (playerName, playerPlace) {
    console.log(playerName + " is in " + playerPlace);
};

var showPlayerInfo = function (playerName, playerPlace, playerHealth) {
    console.log("");
    showPlayerName(playerName);
    console.log("----------------------------");
    showPlayerPlace(playerName, playerPlace);
    showPlayerHealth(playerName, playerHealth);
    console.log("----------------------------");
    console.log("");
};

var player1 = {
    name: "Kandra",
    place: "The Dungeon of Doom",
    health: 50
};

var player2 = {
    name: "Dax",
    place: "The Old Library",
    health: 40
};

showPlayerInfo(player1.name, player1.place, player1.health);
showPlayerInfo(player2.name, player2.place, player2.health);

5.4. 概述

  • 定义一个带有命名 参数 的函数,以表明您在调用函数时预期传递数据。定义中的参数由逗号分隔:

    function (param1, param2) { ... }
    
  • 在函数体内使用参数,就像它们是变量一样。

  • 使用 参数 调用函数。当您运行程序时,参数会自动分配给参数以在函数体内使用:

    myFunction(arg1, arg2)
    

第六章. 返回值:从函数获取数据

本章涵盖

  • 从函数返回信息

  • return 关键字

  • 在控制台提示符中进行实验

在第四章中,你发现了函数如何通过一次编写多次使用来提高你的效率。在第五章中,你通过在每次调用时传递信息使函数变得更加灵活;一个函数可以根据你给出的参数以不同的方式行事并产生不同的输出。在本章中,你给函数机会通过返回它们工作的结果来“回应”。你还可以在控制台提示符中直接调用函数来调查它们返回的值。

6.1. 从函数返回数据

通常,让函数为你做一些工作并返回该工作的结果是非常有用的。然后你可以根据需要使用该结果。在列表 5.6 中,你看到了一个showSum函数,它在控制台上显示两个数的和。可能有一个add函数,它只是将数字相加并返回结果会更好。与showSum总是显示结果在控制台上不同,使用add,你可以选择显示函数返回的结果,将其用于进一步的计算,通过网络发送,或将其保存到数据库中。

6.1.1. 返回值替换函数调用

你迄今为止编写的许多函数都是根据需求执行代码并在控制台上记录一些内容。它们帮助你将程序分解成可理解的块。通过将函数分配给有良好命名的变量,你使程序更容易理解。以下是一个显示The Crypt中玩家信息的代码片段的示例:

showPlayerName("Kandra");
showLine();
showPlayerPlace("Kandra", "The Dungeon of Doom");
showPlayerHealth("Kandra", 50);
showLine();

即使你不知道函数是如何执行它们的任务的,它们的名称也能给你一个很好的关于代码意图的印象。

函数还可以返回信息:计算的结果、构造的文本片段或数据库中的数据。你可以将返回的值分配给变量或将其用作其他函数的参数。以下示例以加粗的形式显示了调用四个函数addgetPlayerPlacefindPlanetPositiongetMessage

var sum = add(50, 23);
var placeInfo = getPlayerPlace("Kandra", "The Dungeon of Doom");
console.log(findPlanetPosition("Jupiter"));
console.log(getMessage());

每个函数都返回一个值,返回的值替换了函数调用。假设函数返回以下加粗的值,前面的四个语句变为

var sum = 73;
var placeInfo = "Kandra is in The Dungeon of Doom";
console.log("Jupiter: planet number 5");
console.log("I'm going on an adventure!");

图 6.1 展示了调用add函数时发生的情况。

图 6.1. 你调用add函数,它返回值73

要从函数返回一个值,请使用return关键字。

6.1.2. return关键字

通过使用return关键字从函数返回一个值。语句中return之后的任何内容都是替换函数调用的值。

列表 6.1 展示了getMessage函数的定义。它包括一个返回语句,一个以return关键字开始的语句:

return "I'm going on an adventure!";

函数返回字符串 "I'm going on an adventure!",因为字符串跟在 return 关键字后面。程序将字符串赋给 response 变量并将其记录到控制台以显示

> I'm going on an adventure!
列表 6.1。从函数返回值 (jsbin.com/yucate/edit?js,console)

072fig01_alt.jpg

getMessage 函数总是返回相同的值。通常,通过在调用函数时传递的参数信息来确定返回值。

6.1.3。使用参数确定返回值

在 第五章 中,你通过在函数定义中包含参数和在函数调用中包含参数将信息传递给函数。你可以在函数体中使用这些信息来确定函数返回的值,因此你可以用不同的参数多次调用函数以产生不同的返回值。

下面的列表显示了一个 getHelloTo 函数,该函数返回一个包含作为参数传递的名称的字符串。程序将返回值赋给一个变量并将其记录到控制台。

> Hello to Kandra
列表 6.2。使用参数确定返回值 (jsbin.com/nijijo/edit?js,console)

072fig02_alt.jpg

在 列表 6.2 中,你将返回值赋给变量 fullMessage 并将其记录到控制台。这个变量是多余的;你可以直接记录返回值,如下一个列表所示,其中你两次调用 getHelloTo 函数以产生以下输出:

> Hello to Kandra
> Hello to Dax
列表 6.3。使用返回值作为参数 (jsbin.com/yapic/edit?js,console)

073fig01_alt.jpg

函数可以返回任何类型的值:字符串、数字、对象,甚至其他函数。让我们看看一个返回数字的例子。

图 6.2 展示了对 add 函数的调用,add(50, 23)。图中显示了传递给 add 的参数是如何用于计算返回值的。

图 6.2。add 函数计算返回值。

06fig02_alt.jpg

下一个列表显示了使 add 函数执行其操作的代码。特别是要注意 return 关键字。

列表 6.4。返回两个数字的和 (jsbin.com/haqapu/edit?js,console)

074fig01_alt.jpg

add(50, 23) 调用 add 函数,将 50 赋值给 number1,将 23 赋值给 number2。然后 number1 的值与 number2 的值相加,并将结果赋给 total 变量。然后 return 关键字结束函数,并用 total 的值替换函数调用。

var sum = add(50, 23);

变为

var sum = 73;

因为 add(50, 23) 返回 73

记住,赋值运算符 = 通过将其右侧的值赋给左侧的变量来工作。如果右侧是函数调用,那么它将函数的返回值赋给变量。

在 列表 6.4 中的 add 函数的 total 变量实际上并不需要;它被分配了一个值,然后立即返回。你可以直接返回计算结果,就像 列表 6.5 中的 totalCost 函数那样。考虑到管道工的调用费用和每小时收费,totalCost 计算了一定小时数的总费用。所以,假设调用费用是 30 美元,管道工每小时收费 40 美元,那么三小时工作的总费用是多少?totalCost(30, 40, 3) 应该返回结果 $150

列表 6.5. 一个有三个参数的函数 (jsbin.com/jedigi/edit?js,console)

当你调用 totalCost 函数时,它会评估 return 关键字右侧的计算并返回值。它遵循通常的算术规则:它首先执行乘法,然后执行加法:30 + 40 * 3 = 30 + 120 = 150。

6.2. 在控制台提示符中进行实验

程序员通常不会在生产环境中使用控制台,即在发布的程序和网站上。他们在开发时使用它,当设计和编写程序时。它为程序员(也就是你)提供了一个方便的方式来记录值和错误,并在程序运行时调查程序的状态。这种交互性,获得即时反馈的机会,使得控制台对于学习非常有用——尤其是通过实验学习。现在是时候带着你在本章中看到的函数去进行一次发现之旅了。

6.2.1. 调用函数

下一个列表包括之前列表中的四个函数。如果你运行程序,它不会在控制台产生任何输出;它不包含任何对 console.log 的调用。

列表 6.6. 一组返回值的函数 (jsbin.com/lijufo/edit?js,console)
var getMessage;
var getHelloTo;
var add;
var totalCost;

getMessage = function () {
    return "I'm going on an adventure!";
};

getHelloTo = function (name) {
    return "Hello to " + name;
};

add = function (number1, number2) {
    return number1 + number2;
};

totalCost = function (callOutCharge, costPerHour, numberOfHours) {
    return callOutCharge + costPerHour * numberOfHours;
};

运行程序会将四个函数分配给程序顶部声明的四个变量。然后你可以从控制台提示符访问这些变量。你可以调用分配给变量的函数来调查它们的返回值。

点击 JS Bin 上的列表链接,确保运行程序。在控制台提示符中输入

> getMessage()

然后按 Enter。getMessage 函数运行并显示其返回值:

"I'm going on an adventure!"

在提示符下,按键盘上的上箭头键。它应该会显示你最后输入的行。(你可以使用上下箭头在控制台导航到之前的输入。)按 Enter 键重新提交命令。getMessage 函数总是返回相同的字符串。如果你查看其函数体,你可以看到它返回一个字符串字面量,一个硬编码的值。

return "I'm going on an adventure!";

程序中的下一个函数 getHelloTo 在其定义中包含一个 name 参数。name 参数允许返回值根据你调用函数时传递的参数而变化。

getHelloTo = function (name) {
    return "Hello to " + name;
};

尝试在控制台使用不同的参数每次调用该函数:

> getHelloTo("Jason")
  "Hello to Jason"
> getHelloTo("Rosemary")
  "Hello to Rosemary"

你可以立即看到改变参数如何影响返回值。

6.2.2. 声明新变量

除了让你访问程序中声明的变量外,控制台还允许你声明新变量并为其赋值。

直接从上一节继续,在控制台输入

> var friend

并按 Enter 键。控制台会显示你之前输入的值。因为你没有为friend变量赋值,所以undefined被记录到控制台。

undefined

给它一个值。输入

> friend = "Amber"

并按 Enter 键。控制台会显示新的值。

"Amber"

最后,使用你新的friend变量作为getHelloTo函数的参数。

> getHelloTo(friend)
  "Hello to Amber"

尝试使用addtotalCost函数,传递不同的参数,并检查控制台上显示的返回值,例如:

> add(30, 12)
  42
> totalCost(10, 20, 3)
  70

控制台不仅仅是一个记录程序消息的地方。你真的可以深入到你的程序中,检查它们是否按预期行为。不要等待许可;跳进去测试所有的列表。

现在,让我们利用你对返回值的新知识来改进《The Crypt》显示玩家信息的方式。

6.3. 《The Crypt》——构建玩家信息字符串

图 6.3 显示了本节重点,即通过返回值显示玩家信息,在我们正在进行的游戏示例的整体结构中的位置。

图 6.3. 《The Crypt》的元素

图片

在第五章中,你将显示玩家信息的任务分配给多个函数,每个函数都有特定的任务要做。

showPlayerName("Kandra");
showPlayerPlace("Kandra", "The Dungeon of Doom");
showPlayerHealth("Kandra", 50);

每个函数都会在控制台记录一条信息字符串。但如果你不想在控制台上显示信息呢?你可能想通过电子邮件发送它,作为对网络请求的响应,或者将其附加到网页上的现有元素。如果函数构建信息字符串并按需返回它们,将更加灵活。

6.3.1. 为玩家姓名、健康和位置构建字符串

你将需要的信息作为参数传递给你的新函数,它们会返回包含该信息的字符串。你期望的返回值类型如下所示:

getPlayerName("Kandra");                 // --> "Kandra" 
getPlayerHealth("Kandra", 50);           // --> "Kadra has health 50" 
getPlayerPlace("Kandra", "The Dungeon"); // --> "Kandra is in The Dungeon"

首先是getPlayerName。目前它只返回给定的名称,这似乎是浪费时间。但定义一个函数允许你以相同的方式访问所有玩家信息;获取名称类似于获取健康或位置。这也使得在以后更新代码时,如果你决定更改名称的显示方式,更容易一些。以下列表显示了getPlayerName函数的定义和对其的一个示例调用,产生以下输出

> Kandra
列表 6.7. 获取玩家姓名的字符串(jsbin.com/hijeli/edit?js,console)
var getPlayerName;

getPlayerName = function (playerName) {
    return playerName;
};

console.log(getPlayerName("Kandra"));

接下来的两个函数getPlayerHealthgetPlayerPlace非常相似,都是根据传入的信息构建简单的字符串。下一个列表包括这两个函数的定义和一些使用示例,在控制台上生成以下内容:

> Kandra has health 50
> Kandra is in The Dungeon of Doom
列表 6.8. 获取玩家健康和位置信息的字符串 (jsbin.com/pemore/edit?js,console)
var getPlayerHealth;
var getPlayerPlace;

getPlayerHealth = function (playerName, playerHealth) {
    return playerName + " has health " + playerHealth;
};

getPlayerPlace = function (playerName, playerPlace) {
    return playerName + " is in " + playerPlace;
};

console.log(getPlayerHealth("Kandra", 50));
console.log(getPlayerPlace("Kandra", "The Dungeon of Doom"));

从一个主getPlayerInfo函数中调用三个函数将给出你想要的显示字符串。下一节将展示这些组件是如何组装的。

6.3.2. 玩家信息函数——组装组件

在所有组件就绪后,你现在可以构建主函数来生成关于玩家的信息字符串。你希望输出看起来像这样:

>
> Kandra
> ********************
> Kandra is in The Dungeon of Doom
> Kandra has health 50
> ********************
>

为了构建分隔玩家信息的字符行,你使用一个返回由星号符号组成的行的getBorder函数。接下来的列表显示了getPlayerInfo的定义。它包括对未在打印列表中显示但包含在 JS Bin 版本中的其他函数的调用。

列表 6.9. 获取玩家信息的字符串 (jsbin.com/javuxe/edit?js,console)

getPlayerInfo函数逐步构建玩家信息字符串,将调用函数返回的字符串附加到playerInfo变量上。在每个阶段附加的额外字符串\n是一个换行符;换行符后面的文本将在控制台上显示在新的一行。你使用+=运算符将字符串附加到现有字符串的末尾。

本章的最后一个示例将所有代码汇集到一个列表中,列表 6.10,以及用于测试getPlayerInfo函数的两个玩家对象。它还在getBorder函数中使用不同的分隔符字符,为两个玩家生成如下输出:

>
> Kandra
> ================================
> Kandra is in The Dungeon of Doom
> Kandra has health 50
> ================================
>
>
> Dax
> ================================
> Dax is in The Old Library
> Dax has health 40
> ================================
>
列表 6.10. 使用对象显示玩家信息 (jsbin.com/puteki/edit?js,console)

你已经成功地将函数从总是打印输出到控制台切换到返回玩家信息的字符串。然后你选择如何处理这些字符串。

将关于玩家的单个信息位传递给函数有点麻烦。不同的函数需要不同的信息位,你必须确保将参数按照正确的顺序放置。如果能直接将整个玩家对象作为参数传递,并让函数访问它们需要的任何属性,那就简单多了。在第七章中,你会看到使用 JavaScript 对象作为参数和返回值是多么有用。

6.4. 概述

  • 使用return关键字从函数中传递信息:

    return "Pentaquark!";
    return 42;
    return true;
    
  • 将函数调用作为值分配给变量或用作参数。函数返回的值替换了函数调用:

    var particle = getDiscovery();
    console.log(getPlayerPlace("Kandra"));
    
  • 在函数定义中包含参数,并使用传递给函数的参数来确定返回的值:

    var sum = add(28, 14);
    
  • 使用控制台来探索和测试程序。

  • 在控制台提示符中调用函数,并查看它们的返回值显示。

  • 在控制台声明变量,分配值,并将它们用作函数的参数。

第七章。对象参数:与对象一起工作的函数

本章涵盖

  • 使用对象作为参数

  • 在函数内部访问对象属性

  • 在函数内部添加新的对象属性

  • 从函数中返回对象

  • 将函数设置为对象的属性

这是介绍函数的四个章节中的最后一个。到现在为止,你已经了解了如何使用函数按需执行代码,以及如何通过参数、参数和return关键字在函数之间传递信息。你也看到了如何使用对象将值收集在一起作为命名属性。那么,现在是时候将函数和对象结合起来,以提升生产力、效率和可读性了。

记得第三章中的急救包吗?我们将这个包视为一个可以传递、打包在背包中并讨论的单个对象。这种封装,或者说将集合视为单一实体,是我们作为人类处理复杂信息的重要部分,无论是在语言还是在记忆中。当需要时,我们可以考虑构成这个包的元素:消毒剂、敷料、绷带等等。

在本章中,你将使用相同的封装概念,将对象作为参数传递给函数并作为返回值返回。

7.1. 使用对象作为参数

能够将对象传递给函数非常有用,特别是如果函数需要访问大量的对象属性。你只需要在函数定义中一个参数,调用函数时不需要一个长的参数列表。"showPlayerInfo(player1)"比"showPlayerInfo(player1.name, player1.location, player1.health)"更整洁、更容易理解,且更不容易出错。

你可以将相同的信息传递给函数,但将其封装在一个单独的对象中,而不是作为单独的值(图 7.1)。

图 7.1. 将单个对象作为参数比多个参数更整洁。

图片

7.1.1. 访问对象参数的属性

受到新地平线号、好奇号、罗塞塔号和菲莱号的太空探险的启发,你决定编写一个快速的应用程序来显示有关太阳系的信息。该应用程序的一个功能是显示有关行星的信息。

你的第一个列表显示了具有 planet 参数的 getPlanetInfo 函数。当你使用 planet 对象作为参数调用该函数时,函数体返回一个使用行星的一些属性构建的字符串。该代码产生以下输出:

> Jupiter: planet number 5
列表 7.1. 将对象作为参数传递给函数 (jsbin.com/tafopo/edit?js,console)

将对象作为单个参数传递给函数既整洁又方便。无需确保你已按正确顺序包含所有必需的参数——单个参数就能完成工作。

一旦你将一个对象传递给一个函数,JavaScript 会自动将其分配给函数定义中包含的参数。

你通过参数访问对象的属性,如图 7.2 所示。函数通过参数完全控制对象;它甚至可以添加新属性。

图 7.2. 在函数体内可以访问对象属性。

7.1.2. 向对象参数添加属性

当你将一个对象作为参数传递给一个函数时,函数体内的代码可以访问该对象的所有属性。它可以读取它们、更改它们、删除它们,也可以添加新的属性。

列表 7.2 显示了具有 planet 参数的两个函数。当你将包含名称和半径的行星对象传递给 calculateSizes 函数时,该函数向对象添加两个新属性,areavolume(图 7.3)。

图 7.3. calculateSizes 函数向 planet 对象添加新属性。

displaySizes 函数使用这两个新属性在控制台上打印有关行星的信息:

> Jupiter
> surface area = 61426702271.128 square km
> volume = 1431467394158943.2 cubic km
列表 7.2. 向对象添加属性的函数 (jsbin.com/qevodu/edit?js,console)

你创建 planet 对象并将其分配给 planet1 变量:

planet1 = { name: "Jupiter", radius: 69911 };

当你调用 calculateSizes 函数时,你将其参数 planet1 传递给它:

calculateSizes(planet1);

JavaScript 将 planet1 指向的对象分配给 planet 参数,以便在函数内部使用。函数使用 planet 参数向对象添加两个新属性,planet.areaplanet.volume。当你调用 displaySizes 函数时,对象已经有了显示所有所需信息所需的两个新属性:

displaySizes(planet1);

除了将对象传递给函数外,你还可以从函数返回对象。

7.2. 从函数中返回对象

正如将对象作为参数传递给函数是一种高效地将信息移动到所需位置的方法一样,使用对象作为返回值也是如此。函数可以操作你传递给它们的对象,然后返回它们,或者它们可以返回在函数体内创建的新对象。

本节探讨了两个示例:第一个使用多个参数来构建一个新的行星对象,第二个使用两个对象参数在 2D 空间中创建一个点。

7.2.1. 构建行星——对象创建函数

在你的太阳系应用程序中,你决定简化行星的创建。你编写了一个函数,将关键事实传递给它,然后它返回一个相应设置属性的 planet 对象。你的 buildPlanet 函数让你可以创建像这样的行星:

planet1 = buildPlanet("Jupiter", 5, "Gas Giant", 69911, 1);

图 7.4 展示了当你调用 buildPlanet 时 JavaScript 如何将参数分配给参数。你使用这些参数来创建一个新对象。

图 7.4. 当你调用 buildPlanet 时,JavaScript 将参数分配给参数。该函数使用这些参数来创建一个对象。

图片

列表 7.3 展示了 buildPlanet 函数的定义。它还有一个 getPlanetInfo 函数,它使用该函数来获取用于显示的行星信息。一旦显示,信息看起来像这样:

JUPITER: planet 5
NEPTUNE: planet 8

注意这里行星名称是大写的。getPlanetInfo 函数使用了内置的 JavaScript 函数 toUpperCase,该函数将字符串转换为大写。toUpperCase 和一些其他 JavaScript 函数将在 第 7.3 节 中讨论。

列表 7.3. 创建行星的函数 (jsbin.com/coyeta/edit?js,console)

图片

buildPlanet 函数创建的对象中的键值对一开始看起来有点奇怪:

name: name, position: position,等等。

对于每个键值对,键位于冒号左侧,值位于冒号右侧。你使用参数作为值,所以对于函数调用

planet1 = buildPlanet("Jupiter", 5, "Gas Giant", 69911, 1);

对象创建代码变为

name: "Jupiter",
position: 5,
type: "Gas Giant",
radius: 69911,
sizeRank: 1

7.2.2. 二维空间中的点

热衷于创建一个显示太阳系中行星的动画,你开始研究二维坐标。所有坐标都与两个值相关联,x 和 y。这似乎是使用对象的一个明显的地方。每个点都是一个具有 x 和 y 属性的对象:

point1 = { x : 3 , y : 4 };
point2 = { x : 0 , y : -2 };

你可以使用点符号来访问单个值:point1.xpoint2.y,等等。

作为初始实验,你编写了一个程序,在 x 方向上移动一个点一定量,在 y 方向上移动一定量。因为位置的变化也有 x 和 y 的分量,所以你也使用一个对象来表示。例如,要表示向右移动四格和向下移动两格,你使用对象 { x : 4, y : -2 }

列表 7.4 包含一个 move 函数,该函数接受两个参数,一个初始点对象和一个变化对象。如果你从第一个点开始,按照指定的变化移动,它将返回一个表示最终位置的新点。

该程序使用 showPoint 函数生成以下输出:

( 2 , 5 )
Move 4 across and 2 down
( 6 , 3 )
列表 7.4. 在 2D 中移动一个点 (jsbin.com/baxuvi/edit?js,console)

在列表 7.4 中传递给move函数的第二个参数使用对象字面量 { x : 4, y : -2 } 编写。你可以先将其分配给一个变量,但由于你只使用一次,直接量值也行。这两个点被绘制出来,它们的坐标在图 7.5 中显示。

图 7.5. 在 4 个单位向右和 2 个单位向下移动之前和之后的点(使用desmos.com绘制——一个用 JavaScript 编写的应用程序)

你已经看到了对象被传递到函数中,对象从函数中返回,以及在列表 7.4 中,一个使用对象作为参数并返回对象的函数。这就是函数使用对象。你还可以将函数设置为对象的属性。

7.3. 方法——将函数设置为对象的属性

在 JavaScript 中,你可以像使用数字、字符串和对象一样使用函数作为值。这意味着你可以将它们作为参数传递,从其他函数中返回它们,并将它们设置为对象的属性。在本节中,继续探讨函数与对象一起工作的主题,你将查看一个将函数设置为对象属性的示例。

7.3.1. 命名空间——组织相关函数

让我们编写一些函数来帮助格式化在控制台上显示的文本。显示文本是交互式控制台应用程序的一个重要部分,所以你创建的辅助函数的数量将会增加。现在,你从两个非常简单的函数开始:blank返回一个空字符串,newLine返回一个换行符。

因为所有函数都与同一项工作相关,所以将它们收集在一起是件好事。你可以通过将每个函数设置为单个对象的属性来实现这一点。称之为spacer

var spacer = {};

一旦你有一个对象,你就可以将函数设置为属性。首先,这是一个返回空字符串的函数:

spacer.blank = function () {
    return "";
};

图 7.6 说明了如何创建函数并将其分配给spacer对象的blank属性。

图 7.6. 创建一个函数并将其分配给对象属性

有时在字符串中包含换行符很有用,以便将其分散到多行。特殊的转义序列 "\n" 被称为换行符。添加一个函数来返回换行符:

spacer.newLine = function () {
    return "\n";
};

现在你已经将两个函数设置为spacer对象的属性。当我们以这种方式使用对象收集函数时,我们称之为命名空间newLineblank函数属于spacer命名空间。你可以通过添加括号来调用函数,就像平常一样:

console.log(spacer.blank());
console.log("Line 1" + spacer.newLine() + "Line 2");
console.log(spacer.blank());

那段代码产生以下输出:

>
> Line 1
> Line 2
>

单个函数不必逐个添加到命名空间中。你可以使用对象字面量语法,即带有逗号分隔键值对的括号,来设置带有属性的命名空间:

spacer = {
    blank: function () {
        return "";
    },

    newLine: function () {
        return "\n";
    }
};

作为对象属性设置的功能被称为方法。目前,spacer对象有两个方法,blanknewLine。你将在 7.3.4 和 7.3.5 部分回到spacer,在那里你将添加更多方法,并在 JS Bin 上对其进行操作。

JavaScript 包含了许多有用的对象和方法。在你扩展spacer对象之前,让我们来调查一下MathString方法。

7.3.2. Math 方法

Math是 JavaScript 中内置的一个命名空间,它提供了所有与数学计算相关的属性和函数。列表 7.5 展示了Math.minMath.max方法的使用。它们分别返回两个数字中的较小值和较大值。程序产生以下输出:

3 is smaller than 12
-10 is smaller than 3
列表 7.5. 使用Math.minMath.max (jsbin.com/moyoti/edit?js,console)

当一起使用时,Math.minMath.max对于确保一个值在指定的范围内非常有用。比如说,一个lineLength变量必须介于 0 和 40 之间(包括 0 和 40)。为了强制lineLength至少为零,你可以使用

lineLength = Math.max(0, lineLength);

如果lineLength大于零,它将是最大的,其值不会改变。但是,如果lineLength小于零,则零将是最大的,并且lineLength将被赋值为零。

同样,你可以强制lineLength小于或等于 40:

lineLength = Math.min(40, lineLength);

列表 7.6 展示了这样的约束是如何工作的。line是一个返回指定长度分隔线的函数。长度必须在 0 到 40 之间。尝试显示长度为 30、40 和 50 的行会产生以下输出:

> ==============================
> ========================================
> ========================================

注意,最后两行长度都是 40。尽管指定了最后一行的长度为 50,但line函数将长度限制为 40。

列表 7.6. 使用Math.minMath.max来约束参数 (jsbin.com/qiziyo/edit?js,console)

substr方法返回字符串的一部分,将在下一节中讨论。

Math方法有很多,用于各种数学任务,其中许多经常被使用。你可以在www.room51.co.uk/js/math.html上调查其中的一些。

7.3.3. 字符串方法

对于你创建的每个字符串,JavaScript 都提供了一系列方法。这些函数帮助你以各种方式操作字符串。下一个列表使用toUpperCase方法将字符串转换为大写,如下所示:

> Jupiter becomes JUPITER
列表 7.7. 将字符串转换为大写 (jsbin.com/jizaqu/edit?js,console)

你使用点符号在planet字符串上调用方法。作为一个方法,toUpperCase能够使用附加到它的planet变量的值;你不需要将planet作为参数传递给函数。

虽然字符串方法可以作用于它们附加的变量,但它们仍然是函数,你也可以传递参数给它们。图 7.7 显示了substr方法如何使用message的值和两个参数。

图 7.7. 方法可以使用参数以及它们附加的对象的值。

下面的列表展示了使用substr方法的示例,在控制台显示子字符串。

> choose to go
列表 7.8. 查找子字符串 (jsbin.com/mesisi/edit?js,console)
var message = "We choose to go to the Moon!";

console.log(message.substr(3, 12));

substr方法接受两个参数:原始字符串中的起始位置和要返回的字符数。在指定字符串中字符的位置时,计数是从零开始的:第一个字符是位置 0,第二个位置 1,第三个位置 2,依此类推。(我知道一开始从零开始可能有点奇怪;实际上,这在编程语言中是非常常见的。)

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
W e c h o o s e t o g o

substr(3, 12)从位置 3 的字符c开始,返回长度为 12 的字符串,从位置 3 到位置 14。

但你不必把所有时间都花在计算字符串中字符的位置上,尽管这听起来可能很有趣;你可以使用indexOf方法代替。indexOf方法返回指定搜索字符串在字符串中首次出现的位置,或称索引

下一个列表使用indexOf查找字符串中M字符的位置。然后将该位置传递给substr以获取长度为 3 的子字符串,在控制台产生牛叫声Moo

列表 7.9. 使用indexOf查找字符 (jsbin.com/bidabi/edit?js,console)
var message = "The cow jumped over the Moon!";

var charIndex = message.indexOf("M");

console.log(message.substr(charIndex, 3));

你可以使用indexOf查找比单个字符更长的搜索字符串。以下是一个使用列表 7.9 中的message的示例:

message.indexOf("cow");    // Returns 4
message.indexOf("the");    // Returns 20
message.indexOf("not");    // Returns -1

注意,indexOf是区分大小写的,所以theThe是不同的,如果字符串未找到,则返回-1

就像Math一样,JavaScript 中有许多String方法可用。再次强调,本书的网站为你提供了所有信息:www.room51.co.uk/js/string-methods.html

“那么,字符串现在是对象了吗?”

在列表 7.7 中,字符串"Jupiter"被分配给planet变量。然后调用toUpper``Case方法:planet.toUpperCase();

如果方法是被设置为对象属性的函数,那么我们是如何在字符串上调用方法的呢?

好吧,在幕后,每次我们访问字符串值时,JavaScript 都会创建一个特殊的String对象来包装该值。该对象包括所有方便的字符串方法。一旦语句执行完毕,该对象就会被销毁。

因此,不,字符串不是对象,但 JavaScript 提供了一种有用的方式来为我们提供处理它们的属性和方法。

7.3.4. spacer—为你的命名空间添加更多方法

在 第 7.3.1 节 中,你创建了一个带有 blanknewLine 函数的 spacer 对象。你现在可以添加一些更有趣的函数来帮助你格式化输出。

  • line 是你的行分隔函数,升级版!它返回长度在 0 到 40 个字符之间的行,现在可以选择五种华丽的字符!("*", "+", "=", "-", 或 " ")。

    spacer.line(10, "*");
    spacer.line(6, "+");
    
    > **********
    > ++++++
    
  • wrap 返回添加了前缀和后缀字符的填充到指定长度的文本。

    spacer.wrap("Saturn", 10, "=");
    spacer.wrap("Venus", 12, "-");
    spacer.wrap("Mercury", 14, "+");
    
    > = Saturn =
    > - Venus    -
    > + Mercury    +
    
  • box 返回填充到指定长度的文本,上方和下方有一行字符。

    spacer.box("Saturn", 10, "=");
    spacer.box("Venus", 12, "-");
    
    ==========
    = Saturn =
    ==========
    
    ------------
    - Venus    -
    ------------
    

以下列表显示了新方法的代码。注意 spacer.wrap 使用 spacer.line 用空格字符填充文本,而 spacer.box 使用 spacer.linespacer.wrap 来生成其框轮廓。在列表之后,你将详细了解新方法的工作原理。

列表 7.10. 将函数作为对象属性组织 (jsbin.com/kayono/edit?js,console)

小贴士

注意 wrapbox 方法如何使用相同的参数顺序:textlengthcharacterline 方法只有这三个参数中的两个,但也是一致的:lengthcharacter。这种有意识的参数顺序选择减少了调用方法时的错误概率。

7.3.5. 深层次命名空间探索

那么,你这些奇妙的新方法是如何工作的呢?大胆且好奇地沉浸在一些深层次的命名空间探索中。记住,你可以在 JS Bin 上尝试代码,无论是在 JavaScript 面板还是在提示符中。例如,对于接下来的 line 方法说明,请在控制台提示符中声明并分配 longString 变量:

> var longString = "**********----------==========++++++++++          "

然后,你可以尝试 indexOf 方法:

> longString.indexOf("*")
  0
> longString.indexOf("=")
  20

俏皮地玩,慢慢来。但如果你第一次没有完全跟上,不要担心;这些方法使用了你刚刚接触到的许多想法。坚持下去;学习跟随思考。

行方法

wrapbox 方法都使用了可靠的 line 方法。它返回由五个重复字符组成的一定长度的字符串:

line: function (length, character) {
    var longString = "****************************************";
    longString += "----------------------------------------";
    longString += "========================================";
    longString += "++++++++++++++++++++++++++++++++++++++++";
    longString += "                                        ";

    length = Math.max(0, length);
    length = Math.min(40, length);
    return longString.substr(longString.indexOf(character), length);
  },

该方法首先创建一个由所有可用字符组成的长字符串。它使用 += 操作符将新字符串追加到现有字符串中。变量 longString 最终形成以下形式的字符串:

longString = "**********----------==========++++++++++          ";

这个片段有每种字符 10 个,而实际方法有 40 个。代码使用字符串方法 indexOf 来找到当你调用 line 时指定的第一个匹配字符的位置。以下是一个示例,使用之前显示的较短的片段:

longString.indexOf("*");  // 0
longString.indexOf("-");  // 10

longString.indexOf("=");  // 20
longString.indexOf("+");  // 30
longString.indexOf(" ");  // 40

找到请求的字符首次出现的位置后,代码接着使用 substr 来获取指定长度的子字符串:

longString.substr(10, 6);  // ------
longString.substr(30, 3);  // +++

line 方法不使用硬编码的值;它使用参数 lengthcharacter 来保存调用时传入的参数:

// Get the index of the first matching character
var firstChar = longString.indexOf(character);

// Get a string of the requested length, starting at the first matching
// character
var requestedLine = longString.substr(firstChar, length);

额外的变量实际上并不需要,尽管它们可以被用来使方法更容易理解。line 方法找到子字符串,并一次性返回它:

return longString.substr(longString.indexOf(character), length);

注意到空格是可用的字符之一。空格字符串可以用来在控制台上填充标题和框。wrap 方法就是用这种方法来做的。

wrap 方法

要形成某些框文本的中间行,wrap 方法返回一个指定长度的字符串。character 参数指定了字符串的第一个和最后一个字符:

wrap : function (text, length, character) {
  var padLength = length - text.length - 3;
  var wrapText = character + " " + text;     
  wrapText += spacer.line(padLength, " ");
  wrapText += character;
  return wrapText;
},

下面是 wrap 方法返回的长度递增的字符串:

spacer.wrap("Neptune", 11, "=");  // = Neptune = 
spacer.wrap("Neptune", 12, "=");  // = Neptune  =
spacer.wrap("Neptune", 13, "=");  // = Neptune   =

该方法通过在左侧用空格填充最后一个字符,使整个字符串达到正确的长度(图 7.8)。

图 7.8. 包裹的文本由字符 + 空格 + 文本 + 填充 + 字符组成。

要找到填充的长度,首先从整个字符串期望的长度开始,减去你要包裹的文本长度,然后再减去三个字符,以考虑到第一个字符、前导空格和最后一个字符:

var padLength = length – text.length – 3;

JavaScript 在所有字符串上提供了 length 属性:

var text = "Neptune";
text.length; // 7

计算出所需的填充长度后,wrap 方法接着请求 line 方法的帮助来获取该长度的空格字符串:

spacer.line(padLength, " ");

wrap 方法通过连接所有部分来构建要返回的字符串:字符、空格、文本、填充、字符。

框方法

最后,box 方法使用 linewrap 来将字符串包裹在指定长度的框中:

spacer.line(11, "*");              //  ***********
spacer.wrap("Neptune", 11, "*");   //  * Neptune *
spacer.line(11, "*");              //  ***********

该方法使用 newLine,这样返回的单个字符串可以在控制台上跨越多行:

box: function (text, length, character) {
  var boxText = spacer.newLine();
  boxText += spacer.line(length, character) + spacer.newLine();
  boxText += spacer.wrap(text, length, character) + spacer.newLine();
  boxText += spacer.line(length, character) + spacer.newLine();
  return boxText;
}

太棒了!spacer 命名空间现在有了一些有用的方法来格式化控制台上的信息显示。它们在《The Crypt》中将会非常有用。所以,让我们混合它们吧!

7.4. 《The Crypt》– 玩家对象作为参数

现在,你将应用关于 JavaScript 对象作为参数、返回值和命名空间的知识到《The Crypt》中。图 7.9 显示了本节的重点,即通过使用带有对象的函数来显示玩家信息,如何融入到我们正在进行的游戏示例的整体结构中。

图 7.9. 《The Crypt》中的元素

在 第六章 中,你建立了一系列函数来帮助显示游戏中玩家的信息。这些函数依赖于不同玩家属性的单独参数。以下是 getPlayerInfo 函数的调用示例:

getPlayerInfo(player1.name, player1.place, player1.health);

你在本章中已经看到,你可以简单地传递一个玩家对象作为参数,让函数挑选出它需要的属性。getPlayerInfo 函数调用应该是

getPlayerInfo(player1);

很整洁!

你可以使用 spacer 命名空间中的辅助方法来格式化文本——boxwrap 就能解决问题。但你怎么能找到合适的框长度来紧密包裹信息?place 字符串可能是最长的,但 health 字符串有时可能会更长?

> ====================================          > +++++++++++++++++++++
> = Kandra is in The Dungeon of Doom =          > + Dax is in Limbo   +
> = Kandra has health 50             =          > + Dax has health 40 +
> ====================================          > +++++++++++++++++++++

你必须检查哪个是最长的。Math.max 方法可以解决这个问题:

var longest = Math.max(place.length, health.length);

不要忘记两端的外边框和单空格:

var longest = Math.max(place.length, health.length) + 4;

以下列表使用了 spacer 命名空间的方法,因此 JS Bin 上的实时示例包括该代码。

列表 7.11. 使用对象显示玩家信息 (jsbin.com/beqabe/edit?js,console)

7.5. 概述

  • 使用对象作为参数,并在函数体内访问它们的属性:

    var getPlayerHealth = function (player) {
        return player.name + " has health " + player.health;
    };
    getPlayerHealth(player1);
    
  • 在函数体内更新对象并添加新属性:

    var calculateSizes = function (rectangle) {
        rectangle.area = rectangle.width * rectangle.height;
        rectangle.perimeter = 2 * (rectangle.width + rectangle.height);
    };
    
  • 使用 return 关键字从函数中返回新或现有对象:

    var getRectangle = function (width, height) {
        return {
            width: width,
            height: height,
            area: width * height
        };
    };
    
  • 通过将函数设置为对象的属性来创建 方法

  • 使用对象作为 命名空间 来收集相关的函数和属性:

    var spacer = {};
    spacer.newLine = function () {
        return "\n";
    };
    
  • 利用 Math 对象及其方法,如 Math.maxMath.min

  • 使用字符串的 length 属性和字符串方法,如 indexOfsubstr

第八章. 数组:将数据放入列表

本章涵盖

  • 将值分组为列表

  • 创建一个数组

  • 访问数组中的元素

  • 操作数组中的元素

  • 使用 forEach 遍历每个元素

到目前为止,你所学到的几乎所有内容都是关于组织你的数据或组织你的代码。本章继续这一主题,但有所变化:它不仅仅是关于分组项目;现在你可以将它们排序。

The Crypt 中,你将最终拥有玩家收集他们在旅途中找到的物品的能力;使用数组,他们可以开始他们的个人宝藏积累。

8.1. 创建数组和访问元素

处理列表是编程的基本部分。博客文章、测验问题、股票价格、电子邮件、文件、推文和银行交易都表现为列表。实际上,你刚刚读到的就是一个列表的列表!有时顺序无关紧要,有时则很重要。在 JavaScript 中,有序列表被称为 数组,就像在许多编程语言中一样。

数组中的项称为其 元素,你通常想以某种方式处理这些元素。你可能想

  • 对每个元素执行某些操作,例如在控制台上显示它或将其增加 20%

  • 仅找到符合特定条件的某些元素,例如 Lady Gaga 的所有推文、给定月份的博客文章或正确回答的问题

  • 将所有元素组合成一个单一值,例如找到价格列表的总和或每场比赛平均得分的数量

JavaScript 中的数组对象提供了帮助您执行所有这些操作以及更多操作的功能。但是,我们可能有些过于急切了。让我们回到起点,了解如何创建一个数组。

8.1.1. 创建数组

要创建一个数组,请使用方括号。一旦创建,你可以将数组分配给一个变量,这样你就可以在代码中引用它。图 8.1 说明了这个过程。

图 8.1. 使用方括号创建数组

图片

以下列表创建了两个数组,并在控制台上显示它们,以给出以下输出:

> [3, 1, 8, 2]
> ["Kandra", "Dax", "Blinky"]
列表 8.1. 创建数组(jsbin.com/cevodu/edit?js,console)

图片

逗号分隔元素,这些元素可以是数字、字符串、对象、函数或任何数据类型,甚至可以是数组的数组。就像对象的大括号和函数的关键字function一样,方括号告诉 JavaScript 创建一个数组。一旦创建,你可以将数组分配给一个变量,将其设置为属性,将其包含在另一个数组中,或者将其传递给一个函数。

列表 8.2 创建了一些表示要访问地点的对象数组,thisYearnextYear。图 8.2 显示了 JS Bin 如何显示对象数组。

图 8.2. JS Bin 在列表 8.2 中用方括号显示了两个数组。

图片

列表 8.2. 在数组中使用现有对象(jsbin.com/gizulu/edit?js,console)

图片

8.1.2. 访问数组元素

你已经创建了一个数组并将其分配给一个变量,所以现在你可以将这个变量传递给函数,比如console.log。在某个时候,你将想要访问构成数组的元素,剥去外壳以获取里面的精华。嗯,那些方括号是双面刀;当你定义数组时,它们包围着列表,你使用它们来访问单个元素。

如图 8.3 所示,你使用索引指定每个元素,这是一个表示元素在列表中位置的整数。数组中的第一个元素索引为 0,第二个元素索引为 1,依此类推。你可以将索引视为从数组开始处的偏移量;第一个元素距离开始为零,第二个元素距离开始为一,依此类推。

图 8.3. 数组的每个元素都有一个索引,从 0 开始。

图片

要检索给定索引处的元素的值,请将索引放在分配给数组的变量的名称后面的方括号内,如图图 8.4 所示。

图 8.4. 使用方括号和索引访问分配给变量的数组元素

图片

这里,你创建了一个数组并将其分配给一个变量:

var scores;
scores = [ 3, 1, 8, 2 ];

要获取数组中第三项的值,8,将索引 2(因为从 0 开始)放在变量名称后面的方括号中:

scores[2];

你可以将 scores[2] 赋值给另一个变量,将其设置为对象的属性,在表达式中使用它,或者将其作为函数的参数传递。列表 8.3 使用 scores 数组中的值创建字符串,并在控制台上显示以下内容:

> There are 4 scores:
> The first score is 3
> The second score is 1
> The third score is 8
> The fourth score is 2

它还使用了数组的 length 属性,它简单地给出了数组中的元素数量。

列表 8.3. 访问数组元素 (jsbin.com/qemufe/edit?js,console)

现在,假设你有一个 days 数组,包含一周中每一天的名称,你想要获取一周中特定一天的名字,比如第四天。

var days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"];

在 JavaScript 的索引和描述特定元素的单词之间存在不匹配。一周的第一天索引为 0。一周的第四天索引为 3。你可能想在不同的时间访问不同的日子,所以你可以使用一个变量,dayInWeek,来保存你想要的一周中的哪一天。

// I want the fourth day of the week
dayInWeek = 4;

但使用 dayInWeek 作为数组的索引会给你错误的一天。索引 4 会给你一周中的第五天。

下一个列表展示了这段代码。它在控制台上显示了两天:错误的一天(即,不是一周中的第四天)然后是正确的一天:

> Friday
> Thursday
列表 8.4. 使用变量作为索引 (jsbin.com/veyexa/edit?js,console)

第一次调用 console.log 显示了错误的日子,因为 dayInWeek 变量没有考虑到数组是从零开始的;它们从索引 0 开始,而不是 1。第二次调用 console.log 通过从 dayInWeek 减去一来修复这个问题;一周中的第四天索引为 3。

好吧。现在是时候戴上你的帽子了。在 列表 8.5 中,你将定义一个函数并将其添加到混合中。当地的铅笔博物馆记录每天通过其大门的游客数量。老板要求你创建一个程序,当给定一周的游客数量数组时,将显示特定一天有多少游客:

> There were 132 visitors on Tuesday

你决定编写一个函数,getVisitorReport,来生成报告并返回它。然后你就有选择在控制台、网页或电子邮件中显示报告的选项。在下一个列表中,你生成了一份关于星期二的报告并在控制台上显示它。

列表 8.5. 将数组传递给函数 (jsbin.com/bewebi/edit?js,console)

你创建一个包含访客数量的数组,将其分配给 visitors 变量,并将其作为参数传递给 getVisitorReport 函数。在函数内部,数组被分配给 visitorArray 变量,并用于生成报告。报告从函数返回,分配给 report 变量,并在控制台上显示。函数中的 += 运算符将其右侧的值添加到左侧的变量中。因为 visitorReport 变量已被分配了一个字符串,+= 将其右侧的值连接到左侧的变量。

因此,你可以创建一个数组并访问其元素。但是,一旦数组中有数据,你就有很多种方法可以操作它。让我们从一些最常见的工作方式开始。

8.2. 数组方法

数组是 JavaScript 语言提供的一种对象类型,用于帮助你管理列表。JavaScript 还为你提供了一系列你可以用来操作数组的函数。当我们将函数分配给对象的属性时,我们称这些函数为对象的 方法;数组是一种对象类型,因此它们的函数也称为方法。

在本节中,你将查看与数组一起工作时可用的许多方法中的一些:pushpopsplice 允许你添加和删除元素,slice 允许你获取连续元素,join 允许你连接数组元素以形成字符串,而 forEach 允许你将每个元素作为参数传递给指定的函数。这些方法总结在 表 8.1 中。更多数组方法和示例可以在本书的网站上找到,网址为 www.room51.co.uk/js/array-methods.html

表 8.1. 数组方法
方法 用于什么? 示例
push 将一个元素追加到数组的末尾。 items.push("Put me last");
pop 从数组的末尾删除一个项目。 wasLast = items.pop();
join 连接数组中的所有元素,在每对元素之间插入一个可选的字符串。 allItems = items.join(",");
slice 从现有数组中创建一个包含一系列元素的数组。传入开始和停止范围的索引。 section = items.slice(2, 5);
splice 通过添加和/或删除连续元素来更改数组。传入开始删除元素的索引、要删除的元素数量以及要添加的任何元素。 out = items.splice(1,2,"new");

| forEach | 将每个元素依次传递给指定的函数。 | items.forEach(function (item){ console.log(item); |

} |

让我们从 pushpopjoin 开始。

8.2.1. 添加和删除元素

在 列表 8.6 中,你创建了一个空数组并将其分配给 items 变量。使用 push 方法向数组中添加了三个元素。一旦添加了这三个元素,你就在控制台上记录了整个数组:

> ["The Pyramids", "The Grand Canyon", "Bondi Beach"]

然后使用 pop 方法移除最后一个元素并显示它。最后,再次将整个数组记录到控制台,这次是将元素连接成一个字符串。

> Bondi Beach was removed
> The Pyramids and The Grand Canyon
列表 8.6. 使用 push、pop 和 join 操作数组 (jsbin.com/faqabu/edit?js,console)

111fig01_alt.jpg

JavaScript 将 pushpopjoin 函数作为属性提供给了每个数组。因为它们是数组的属性,所以你可以使用点符号来调用这些函数,例如 items.push(itemToAdd),就像访问任何对象的属性一样。

8.2.2. 切片和拼接数组

为了演示两个数组方法 slicesplice,你继续使用你的假日目的地数组。在 JS Bin 控制台中一起玩。命令可以在控制台上跨越多行;按 Shift-Enter 移动到新行而不执行语句。如果你不小心按下了 Enter 并执行了一个未完成的语句,你可能可以通过按键盘上的上箭头键来恢复你的上一个输入。你输入的语句以 > 开头。控制台会自动显示每个调用的函数的返回值。我在粗体中显示了这些值。

> var items = [
    "The Pyramids",
    "The Grand Canyon",
    "Bondi Beach",
    "Lake Garda"]
  undefined

> items
  ["The Pyramids", "The Grand Canyon", "Bondi Beach", "Lake Garda"]
切片

slice 方法返回一个新的数组,该数组由原始数组的一部分组成。它不会改变原始数组。参数是你想要的第一元素的索引和第一个你不想的后续元素的索引。记住,第一个元素的索引是 0。

> items.slice(0, 2)
  ["The Pyramids", "The Grand Canyon"]

> items.slice(2, 3)
  ["Bondi Beach"]

items.slice(2, 3) 表示你想要从索引 2 开始的项目,但不包括从索引 3 开始的项目。换句话说,你只想得到索引为 2 的项目。

如果你想获取第一个参数指定之后的所有元素,则省略第二个参数。如果你想获取整个数组,则省略两个参数。

> items.slice(2)
  ["Bondi Beach", "Lake Garda"]

> items.slice()
  ["The Pyramids", "The Grand Canyon", "Bondi Beach", "Lake Garda"]
切片

splice 方法会改变原始数组。它允许你从数组中删除项目,并且可以选择性地插入新项目。要删除项目,指定要删除的第一个元素的索引和要删除的元素数量。该方法返回一个包含被删除元素的数组。

> items.splice(2, 1)
  ["Bondi Beach"]

> items
  ["The Pyramids", "The Grand Canyon", "Lake Garda"]

> items.splice(0, 2)
  ["The Pyramids", "The Grand Canyon"]

> items
  ["Lake Garda"]

要在数组中插入新元素,将它们作为参数添加到起始索引和要删除的项目数量之后。在这个例子中,没有删除任何项目:

> items.splice(0, 0, "The Great Wall", "St Basil's")
  []

> items
  ["The Great Wall", "St Basil's", "Lake Garda"]

在这个例子中,有一个项目被移除了:

> items.splice(1, 1, "Bondi Beach", "The Grand Canyon")
  ["St Basil's"]

> items
  ["The Great Wall", "Bondi Beach", "The Grand Canyon", "Lake Garda"]

当你在 The Crypt 中处理玩家和地点项目时,你会使用 slicesplice 两个数组方法。

8.2.3. 使用 forEach 遍历每个元素

如果你有一个想要在控制台显示的项目列表,你可以为每个项目手动调用一个函数:

showInfo(items[0]);
showInfo(items[1]);
showInfo(items[2]);

不幸的是,通常无法提前知道列表中会有多少个项目,因此你无法在调用 showInfo 之前硬编码正确的调用次数。此外,随着元素数量的增加,你不想手动为每个元素调用函数。

你需要的是一个方法,让 JavaScript 为列表中的每个元素调用给定的函数,无论有多少个元素。这正是 forEach 方法所做的。要为 items 数组中的每个元素调用 showInfo,将单独的调用替换为

items.forEach(showInfo);

forEach 方法 迭代 数组,依次将每个元素作为参数传递给括号中指定的函数,如图 8.5 所示。

图 8.5. items.forEachitems 数组中的每个项目传递给 showInfo 函数。

列表 8.7 展示了 forEach 的实际应用,在控制台显示 items 数组的元素:

> The Pyramids
> The Grand Canyon
> Bondi Beach
列表 8.7. 使用 forEach 迭代数组 (jsbin.com/sokosi/edit?js,console)

在列表 8.7 中,你的用于显示每个项目的函数被分配给 showInfo 变量。然后你将 showInfo 变量作为参数传递给 forEach

如果你打算只使用一次函数,作为 forEach 的参数,你可以在 forEach 中直接创建该函数并传递,无需额外的变量。列表 8.8 中的代码直接将函数定义传递给 forEach。你还可以添加额外的信息来设置场景并改进输出:

> Dream destinations:
> - The Pyramids
> - The Grand Canyon
> - Bondi Beach
列表 8.8. 使用内联函数调用 forEach (jsbin.com/yapecu/edit?js,console)

forEach 方法实际上将三个参数传递给指定的函数:当前元素的元素、当前元素的索引和整个数组。你可以在传递给 forEach 的函数的定义中包含额外的参数来捕获额外的参数。

items.forEach(function (item, index, wholeArray) {
    // item is the current item being passed to the function   
    // index is the index of the current item
    // wholeArray is the same as 'items'
});

列表 8.9 展示了所有三个参数的实际应用。它使用 forEachplayers 数组中的每个玩家传递给 showArguments 函数,产生以下输出:

> Item: Dax
> Index: 0
> Array: Dax,Jahver,Kandra
> Item: Jahver
> Index: 1
> Array: Dax,Jahver,Kandra
> Item: Kandra
> Index: 2
> Array: Dax,Jahver,Kandra
列表 8.9. 使用 forEach 传递的参数 (jsbin.com/suvegi/edit?js,console)

forEach 方法为你调用函数。在列表 8.9 中,它调用 showArguments 函数。它为 players 数组中的每个元素调用该函数。它始终将三个参数传递给它调用的函数,尽管你不必使用所有三个。

你可以直接在数组上调用数组方法,如 forEach,而不需要变量。列表 8.10 重新编写了列表 8.9,没有将数组或函数分配给变量。

列表 8.10. 使用 forEach 传递的参数——紧凑版 (jsbin.com/pagahe/edit?js,console)
["Dax", "Jahver", "Kandra"].forEach(function (item, index, wholeArray) {
    console.log("Item: " + item);
    console.log("Index: " + index);
    console.log("Array: " + wholeArray);
});

如果您只使用一次数组和函数,列表 8.10 中的紧凑语法可能是合适的。但是,列表 8.9 中的较长形式更易于阅读,因此如果代码的含义在上下文中不明显,可能最好选择较长的版本。能够写出类似

players.forEach(showScore);

这样的内容可以帮助您和其他程序员更好地理解您的代码。

为了进一步展示使用索引参数,您将在 列表 8.11 中去商店。(如果您正在玩冒险游戏,那么您可能是在为旅行购买装备。)您购买了四种类型的物品,但每种物品的数量不同。程序计算总成本并显示,如下所示:

> The total cost is $41.17

它使用两个数组,一个用于存储每个物品购买的数量,另一个用于存储它们的成本。

列表 8.11. 计算购物账单总额 (jsbin.com/zizixu/edit?js,console)

图片

列表 8.11 使用索引来匹配当前物品成本与正确数量的物品。为了使这可行,数组必须保持相同的顺序。我希望你已经注意到 i 并不是一个非常描述性的变量名!由于需要索引变量非常普遍,大多数程序员都乐于使用简短的名称——ijk——来命名它们。它们更快地输入,并且这是一个如此根深蒂固的约定,大多数阅读你代码的人都会期望它们被用作计数器或索引。如果你更愿意将变量命名为 indexitemIndex 或类似名称,那也是可以的。

作为最后一个例子,让我们回到您的测验问题。多项选择题有一系列可能的答案,需要显示给正在参加测验的人。这似乎非常适合数组和使用 forEach;请参见 列表 8.12。您甚至可以有一个问题-答案对象数组。现在,让我们坚持一个单独的问题,如下所示显示:

> What is the capital of France?
> A – Bordeaux
> B – F
> C – Paris
> D - Brussels
列表 8.12. 显示多项选择题 (jsbin.com/lobahu/edit?js,console)

图片

当您在 第二部分 中研究用户交互时,您将了解如何实际回答问题。

8.3. The Crypt——玩家物品数组

现在,您将应用您对 JavaScript 数组的了解来 The Crypt。图 8.6 显示了本节重点,即通过使用数组显示玩家物品列表,如何融入我们正在进行的游戏示例的整体结构。

图 8.6. The Crypt 的元素

图片

Get Programming with JavaScript 的 第一部分 中,我们介绍了一些核心概念,以帮助您在 The Crypt 中建模和使用玩家。您有变量来存储和检索玩家信息,对象来收集玩家属性,数组以便您可以列出玩家收集的物品,数组方法来添加和删除集合中的物品,以及函数来显示每个玩家的信息。

列表 8.13 将所有这些概念结合起来:创建一个玩家,显示他们的信息,拾取一个新物品,并显示更新后的信息。你使用在第七章(kindle_split_015.html#ch07)中开发的 spacer 命名空间来格式化输出。组成输出的元素在 图 8.7 中突出显示。

图 8.7. 在控制台上显示 Player 对象时显示的元素

![08fig07_alt.jpg]

为了节省空间,列表中没有显示 spacer(它没有改变),但在 JS Bin 上有。此列表中的代码基于第七章(kindle_split_015.html#ch07)中的玩家显示代码,增加了列出玩家物品的功能。

列表 8.13. 显示玩家物品 (jsbin.com/mecude/edit?js,console)

图片

图片

Player 对象现在包含一个 items 数组:

var player1 = {
    name: "Kandra",
    place: "The Dungeon of Doom",
    health: 50,
    items: ["a trusty lamp"]
};

它最初只有一个元素,但你可以通过使用 push 数组方法添加另一个:

player1.items.push("a rusty key");

getPlayerItems 使用 forEachitems 数组中的每个项目传递给一个函数。该函数使用 += 将项目附加到字符串中,构建玩家所有物品的列表:

player.items.forEach(function (item) {
    itemString += "   - " + item + spacer.newLine();
});

showPlayerInfo 函数调用 getPlayerInfo 来检索玩家信息字符串,然后在控制台上显示信息。

你开始积累大量帮助显示玩家的函数;你真的应该组织它们。你可以将它们收集到一个命名空间中,或者由于它们都与玩家相关,可以将它们作为每个 Player 对象的一部分。JavaScript 提供了一种简化创建许多类似对象的方法,包括与它们一起工作的方法:构造函数。这就是你在第九章(kindle_split_017.html#ch09)中调查的内容,在那里你还在 The Crypt 中构建玩家将探索的地方。

8.4. 概述

  • 使用方括号中的逗号分隔值列表创建一个数组:

    [ "Kandra", "Dax", true, 50 ]
    
  • 将数组分配给变量,然后通过在变量名后添加方括号中的索引来访问其元素。以下代码显示 "Dax"

    var items = [ "Kandra", "Dax", true, 50 ]
    console.log(items[1]);
    
  • 记住使用基于零的索引为数组元素。items[1] 指的是 items 数组中的第二个元素。你可以将索引视为从数组开始的一个偏移量:第一个元素距离开始零个单位,第二个元素距离开始一个单位,依此类推。

  • 使用数组方法,JavaScript 提供的函数,来添加、删除、连接和遍历数组元素。本章涵盖的方法是 pushpopjoinslicespliceforEach

第九章. 构造函数:使用函数构建对象

本章涵盖

  • 使用函数创建对象

  • 关键字 new

  • 使用构造函数创建对象

  • 特殊变量 this

  • 定义玩家和地点的构造函数

程序通常创建许多类似的对象是很常见的——一个博客文章可能有数百篇文章,而日历可能有数千个事件——并且包括用于处理这些对象的功能。而不是手动使用花括号构建每个对象,你可以使用函数来简化流程。你想要改变

planet1 = {
    name: "Jupiter",
    position: 8,
    type: "Gas Giant"
};

showPlanet = function (planet) {
    var info = planet.name + ": planet " + planet.position;
    info += " - " + planet.type;
    console.log(info);
};

showPlanet(planet1);

into

planet1 = buildPlanet("Jupiter", 8, "Gas Giant");
planet1.show();

其中 buildPlanet 函数为你创建行星对象,并自动添加 show 方法。你在 buildPlanet 函数的函数体中定义了对象的蓝图,然后使用该蓝图在需要时生成新对象。这样就将你的对象创建代码集中在一个地方;你在整本书中看到,这种组织方式使得理解、维护和使用你的程序变得更加容易。

你甚至可以使用 构造函数 来创建更多类似的对象;编写生成对象的函数是如此常见,以至于 JavaScript 包含了一种内置的方式来简化流程。使用构造函数,创建和显示行星看起来是这样的:

planet1 = new Planet("Jupiter", 8, "Gas Giant");
planet1.show();

构造函数标准化了对象的创建——标准化代码通常是一件好事——并提供了一种识别对象的方法,使得区分行星、玩家、文章和位置等变得更容易。它们还通过使用 原型(在第四部分中介绍)提供了一种在许多对象之间共享单个函数的方法。

在 第 9.1 节 中,你编写自己的函数来更容易地构建具有属性和方法的对象。在 第 9.2 节 中,你研究构造函数,并了解它们如何简化流程。现在是时候让生产线运转起来了。

9.1. 使用函数构建对象

而不是使用花括号手动构建每个对象,创建一个函数来处理繁重的工作。只需将所需信息传递给函数,它就会为你返回一个崭新的对象。在更大的程序中,你可能会想在代码的多个地方创建类似的对象。拥有一个可以调用的单一函数可以避免重复,如果随着时间的推移你需要你的对象执行的操作发生变化,这也使得修改变得容易。

第 9.1.1 节 展示了一个简单的对象创建函数,而 第 9.1.2 节 则在此基础上添加了方法到创建的对象中。

9.1.1. 添加属性

图 9.1 展示了一个 buildPlanet 函数,帮助你创建每个行星对象。你将行星数据作为参数传递给函数,它返回一个具有这些参数作为属性的对象。你刚刚开始学习 JavaScript,就已经在构建行星了!

图 9.1. buildPlanet 函数创建并返回一个对象。

第一个列表显示了 buildPlanet 函数的代码。注意它是如何开始创建一个对象,并以返回对象结束的。

列表 9.1. 使用函数创建对象 (jsbin.com/jiroyo/edit?js,console)

buildPlanet 函数所采取的步骤对于理解 JavaScript 的构造函数如何简化对象创建过程至关重要。你可以在 第 9.2 节 中查看构造函数。但首先,通过添加方法来增强你创建的对象的功能。

9.1.2. 添加方法

除了设置一些初始属性外,你还可以让 buildPlanet 添加方法。记住,方法 是你分配给对象属性的函数。与其定义一个外部函数来显示每个行星对象的信息,不如将其直接嵌入对象本身。

在下一个列表中,你在返回对象之前为每个行星对象添加了一个 showPlanet 方法。输出结果如下:

> Jupiter: planet 5 - Gas Giant
列表 9.2. 为构造对象添加方法 (jsbin.com/zogure/edit?js,console)

showPlanet 方法中,你构建了一个包含行星属性(namepositiontype)的信息字符串,然后将该字符串记录到控制台。你使用 += 运算符将字符串附加到 info 变量。

你可以包含所需数量的方法,以赋予你的对象所需的功能。每次调用 buildPlanet 时,它都会返回一个具有执行程序中工作所需属性和方法的对象。你可以使用这个函数,生成你想要的任意数量的行星对象。行星数量再多也不会有问题,对吧?

列表 9.3 使用 buildPlanet 函数构建了三个行星(好吧,还不完全是一个生产线,但你能理解这个概念!)并在控制台上显示它们的详细信息:

> Jupiter: planet 5 – Gas Giant
> Neptune: planet 8 – Ice Giant
> Mercury: planet 1 – Terrestrial
列表 9.3. 构造对象的数组 (jsbin.com/jiweze/edit?js,console)

在 列表 9.3 中,你创建了一个包含三个元素的数组,这些元素是每次调用 buildPlanet 返回的对象。你将这个数组赋值给 planets 变量,然后使用 forEach 方法遍历数组。你不需要将三个行星分别赋值给单独的变量;对象可以通过数组索引访问,第一个行星位于索引 0:

planets[0].name    // "Jupiter"
planets[2].type    // "Terrestrial"

在定义 buildPlanet 时,你将对象创建移动到了函数中,一旦设置了属性,就返回了新生的行星。但 JavaScript 可以为你创建和返回对象。让我们看看它是如何做到的。

9.2. 使用构造函数构建对象

创建自己的函数来构建对象并附加方法应该让你了解了对象生产线中涉及的步骤。这是一种构建对象的常见方式,JavaScript 提供了自己的标准机制——构造函数

buildPlanet 函数中, 创建了一个空对象并设置了其属性, 返回了对象。

var buildPlanet = function (name, position, type) {
    var planet = {};              // You create an empty object 

    planet.name = name;           //
    planet.position = position;   // Assign properties
    planet.type = type;           //

    return planet;                // You return the object
};

好吧,JavaScript 为你提供了保障;使用构造函数,它会免费为你创建空对象并返回它。你仍然可以设置属性,但其余的都是自动完成的。那么是什么神秘的调用将一个普通函数转换成了构造函数呢?两个关键字:thisnew

9.2.1. 构造函数

在 JavaScript 中,你定义一个 构造函数 就像定义任何其他函数一样,但需要在 new 关键字之后调用它。如果你有一个 Planet 函数,你可以这样创建新的行星对象:

planet1 = new Planet("Jupiter", 5, "Gas Giant");
planet2 = new Planet("Neptune", 8, "Ice Giant");

要将 Planet 函数用作构造函数,你只需在调用 Planet 之前加上 new 关键字。构造函数的命名惯例是以大写字母开头,这样程序员在调用它们时就知道要使用 new 关键字。图 9.2 展示了构造函数如何自动创建并返回一个对象。

图 9.2. 使用 new 关键字调用构造函数。构造函数会自动创建并返回一个对象。

以下列表显示了完整的 Planet 构造函数以及使用 new 调用 Planet 的示例。

列表 9.4. Planet 构造函数 (jsbin.com/bixico/edit?js,console)

在函数体内,但只有当你用 new 调用函数时,JavaScript 才会为你创建一个空对象并将其分配给特殊变量 this。你可以想象 Planet 函数的隐藏的第一行是这样的

var this = {};

然后,你可以像在之前的 buildPlanet 函数中处理 planet 一样设置 this 的属性。该函数会自动返回分配给 this 的对象,因此不需要添加 return 语句。你可以想象 Planet 函数的隐藏的最后一行是这样的

return this;

当你执行 列表 9.4 中的代码时,新创建的对象将替换 Planet 构造函数的调用,并分配给 planet 变量。接下来的

var planet = new Planet( "Jupiter", 5, "Gas Giant" );

becomes

var planet = {
    name: "Jupiter",
    position: 5,
    type: "Gas Giant",
    showPlanet: function () {
        var info = this.name + ": planet " + this.position;
        info +=  " - " + this.type;
        console.log(info);
    }
};

你可以为 this 对象添加任意多的属性和方法。列表 9.5 扩展了 Planet 构造函数,为生成的对象添加了 moons 属性和 addMoon 方法:

> Jupiter: planet 5 – Gas Giant
> Moons: Io, Europa.
列表 9.5. 在 Planet 构造函数中包含月球数组 (jsbin.com/wiguya/edit?js,console)

记住从 第八章,push 方法向数组的末尾添加一个新元素,而 join 方法将数组的所有元素连接成一个字符串,元素之间可以有一个可选的分隔字符串。

行星 月球!你的勤奋是无边无际的。Io,Io,我们开始工作吧 ....

9.2.2. 世界构建——使用 Planet 构造函数

列表 9.6 使用了 Planet 构造函数的更新实现。当你调用 addMoon 时,它现在使用你之前未见过的方法 unshiftmoons 数组的开头添加新的卫星。在调用每个 showPlanet 之前,你创建了三个行星对象并将它们分配给变量 planet1planet2planet3。在这里显示的部分输出中,请特别注意卫星的顺序:

> Jupiter
> Planet 5 – Gas Giant
> Moons: Europa, Io.
列表 9.6. 使用我们的构造函数创建多个行星 (jsbin.com/wewewe/edit?js,console)

planet1 的代码中,你首先添加了 Io,然后是 Europa。但在输出中顺序被反转了。这是因为你现在使用 unshiftmoons 数组的开头添加项目,而不是使用 push 在末尾添加项目。

9.2.3. 使用 instanceof 操作符区分对象

当在一个程序中处理许多不同的对象时,你可能使用多个不同的构造函数创建的对象,有时能够区分一种类型的对象和另一种对象是有用的:item1 是一个行星、玩家、帖子还是位置?JavaScript 的 instanceof 操作符允许你检查特定的构造函数是否参与了对象的创建。假设你已经定义了 Planet 构造函数,以下代码片段将 true 输出到控制台:

var item1 = new Planet("Jupiter", 5, "Gas Giant");

console.log(item1 instanceof Planet);

instanceof 操作符返回 truefalsetruefalse 被称为 布尔值。实际上,它们是唯一的两个布尔值。在 用 JavaScript 编程入门 的其余部分中,你不会使用 instanceof——我在这里提到它,作为程序员可能更喜欢构造函数而不是他们自己的对象创建函数的另一个原因。你将在本书的第 2 和 3 部分看到更多关于 truefalse 的内容;它们对于在满足某些条件时做出决策和运行代码至关重要。

为了更好地理解构造函数,查看一些更多示例是值得的。下一节将这样做,其中包括测验问题和日历事件的构造函数。

9.3. 建立精通——构造函数的两个示例

一次测验可能包含数十个问题,而日历可能包含数百或数千个事件。这些问题和事件很可能具有相似的结构。这两种类型的对象似乎都是构造函数的理想候选者。

在 列表 9.7 中,你使用 QuizQuestion 构造函数创建一个单独的问题,并在控制台上显示它:

> What is the capital of France?
> (1) Bordeaux
> (2) F
> (3) Paris
> (4) Brussels
列表 9.7. 试题构造函数 (jsbin.com/vuyesi/edit?js,console)

在 列表 9.7 中的 showQuestion 函数中,你使用 forEach 方法遍历可能的答案数组 optionsforEach 方法将每个选项及其索引传递给一个函数,该函数显示选项及其编号。

function (option, i) {
    console.log("(" + (i + 1) + ") " + option);
}

第一个选项的索引是 0,但你希望显示的数字从 (1) 开始。你使用 (i + 1) 而不是 i 来将每个索引向上移动一个,以便显示。

> (1) Bordeaux
> (2) F
> (3) Paris
> (4) Brussels

下一个列表是一个简单的日历事件构造函数。showEvent 方法产生以下输出:

> Annual Review
> 3/5/16, from 4.00pm to 5.00pm
列表 9.8. 日历事件构造函数 (jsbin.com/gemiyu/edit?js,console)

showEvent 方法中,你创建并立即 join 一个数组来形成一个包含日期信息的字符串。你在 第八章 中遇到了 join 方法。这是一种从多个部分构建字符串的相当巧妙的方法。JavaScript 程序员过去认为字符串连接,例如使用 +=,是从子字符串构建字符串的一种相对较慢的方法。将数组的元素连接起来是一种常见的替代方法。以下两种构建 dateString 的方法会产生相同的结果:

var dateString = [
    this.startDate,
    ", from ",
    this.startTime,
    " to ",
    this.endTime
].join("");

var dateString = this.startDate;
dateString += ", from ";
dateString += this.startTime;
dateString += " to ";
dateString += this.endTime;

这些天,在现代浏览器中,字符串连接比以前快得多。我包含了一个使用 join 的例子,因为你很可能在野外遇到它。

构造函数提供了一种标准化、简化的方式,通过单个模板创建多个对象。一场伟大的冒险将涉及许多地点;让我们回顾一下 The Crypt,并使用构造函数为你的玩家提供许多可以掠夺的地方。

9.4. The Crypt—提供掠夺地点

现在,你将应用你对构造函数的知识到 The Crypt 上。图 9.3 显示了本节的重点,即通过使用构造函数创建 Place 对象,如何适合我们正在进行的游戏示例的整体结构。

图 9.3. The Crypt 的元素

到目前为止,你一直专注于 The Crypt 中的玩家。是时候构建一些可以探索的 地点 了。每个地点都需要一个标题和描述,一个物品集合,以及通向其他地点的出口集合。它还需要添加物品和出口的方法以及显示其信息的方法。图 9.4 突出了当你将在控制台上显示的 Place 对象时想要看到的元素。

图 9.4. 在控制台上显示 Place 对象时显示的元素。

管理所有这些元素将需要相当多的代码。正如你所见,构造函数是一个很好的方式来组织所有这些代码,并简化多个 Place 对象的创建。这将是你迄今为止最复杂的构造函数,所以让我们分阶段来构建它。

9.4.1. 构建 Place 构造函数—标题和描述

好的,这是一个很好的骨架构造器,让你开始。它只是设置了titledescription属性以及一个基本的getInfo方法,你可以随着添加更多属性来开发它。初始输出看起来像这样:

> The Old Library
> You are in a library. Dusty books line the walls.
列表 9.9. 地点构造器,第一部分 (jsbin.com/pogive/edit?js,console)

目前还没有盒子或边框—只有骨架。你定义一个getInfo方法,该方法返回一个包含地点标题和描述的字符串。记住,"\n"是一个转义序列,用于指定换行符;标题将在一行,描述将在下一行。

好的,所以你可以创建地点。但玩家如何掠夺这些地点?你需要一些宝藏!

9.4.2. 构建地点构造器—为你收藏的物品

当玩家探索他们的环境时,他们期望遇到可以帮助他们解决谜题和克服障碍的物品,如锁着的门、恶臭的僵尸、咆哮的豹子和过于友好的触手。你需要一种方法向你创建的地点添加物品,并将这些物品包含在每个地点显示的信息中。

列表 9.10 扩展了你的骨架构造器,包括所需的项目功能。你通过使用来自第七章的spacer命名空间来升级了信息的显示;它现在看起来像这样:

===================
= The Old Library =
===================
You are in a library. Dusty books line the walls.
Items:
   - a rusty key
========================================

这更像是来自图 9.4 的目标输出!spacer代码在列表中没有显示,但在 JS Bin 上。

列表 9.10. 地点构造器,第二部分 (jsbin.com/qemica/edit?js,console)

你使用spacer命名空间来添加换行符,将标题框起来,并在信息字符串末尾添加边框。spacer方法的工作方式在第七章中有详细描述,所以如果你需要提醒,请查看那里。

你可以在Place构造函数中将items参数作为一个部分。但你想在游戏运行时能够向地点添加物品—例如,当玩家丢弃一个物品时—所以你保持了构造函数的简单性,并包含了一个addItem方法。

那黑暗通道里是什么?是龙穴?是星际飞船的桥?是恐龙遗失的峡谷?

9.4.3. 构建地点构造器—探索出口

一个地点不能构成一场冒险;玩家想要漫游广阔的奇妙世界。你Place构造器的最后一个添加功能是添加通向其他地点的出口。你为每个地点包含一个出口数组和一个向数组添加目的地的方法。你需要显示出口,通向你的目标输出:

===================
= The Old Library =
===================
You are in a library. Dusty books line the walls.

Items:
   - a rusty key

Exits from The Old Library:
   - The Kitchen
   - The Main Hall
========================================

再次,打印的列表中省略了spacer代码。

列表 9.11. 地点构造函数,第三部分 (jsbin.com/parale/edit?js,console)

ch09ex11-0.jpg

ch09ex11-1.jpg

列表相当长,但你之前已经看到了所有这些技术,也有一些重复。一次关注一个方法,并花时间跟随代码。

你首先设置一个快速快捷方式。spacer.newLine方法总是返回换行转义序列"\n",所以你只需调用一次,并将返回值赋给newLine变量。然后你使用newLine而不是spacer.newLine()。这样你就可以少打一些字,同时不影响代码的可读性。你还添加了getExitsgetTitle方法,这些方法构建了它们的部分地点信息字符串。而addExits方法就像addItems方法一样,但——你猜对了——是用来添加出口而不是项目。

exits属性包含一个Place对象的数组。换句话说,每个Place对象都包含一个Place对象的集合。这种在对象内部嵌套对象的能力可以导致复杂的现实世界情况模型。尽管完整的模型可能相当复杂,但每个组件应该相对容易理解。你的游戏《密室》为你提供了足够的组件来欣赏如何从简单的组件构建复杂的程序。让我们回顾一个你很熟悉的组件,并为玩家对象定义一个构造函数。

9.5. 密室——简化玩家创建

图 9.5 显示了Player构造函数函数在我们正在进行的游戏示例的整体结构中的位置。

图 9.5. 《密室》的元素

09fig05_alt.jpg

在第八章中,你向Player对象添加了一个项目数组,并定义了函数来在显示玩家信息时包含这些项目。你仍然手动创建了玩家:

var player1 = {
    name: "Kandra",
    place: "The Dungeon of Doom",
    health: 50,
    items : ["a trusty lamp"]
};

你将玩家显示函数分配给了它们自己的变量——你收集了一大堆!

var getPlayerName = function (player) { ... };
var getPlayerHealth = function (player) { ... };
var getPlayerPlace = function (player) { ... };
var getPlayerItems = function (player) { ... };
var getPlayerInfo = function (player, character) { ... };
var showPlayerInfo = function (player, character) { ... };

显示函数组合在一起,在控制台上显示信息,如图 9.6 所示图 9.6。

图 9.6. 在控制台上显示Player对象时显示的元素。

09fig06_alt.jpg

要向玩家的收藏夹中添加一个项目,你直接将其推送到项目数组中:

player1.items.push("a rusty key");

9.5.1. 组织玩家属性

是时候整理所有这些组件了;你使用你对构造函数的掌握来简化玩家创建,将函数变成方法,并消除日益增长的变量数量。Player构造函数将允许你创建如下Player对象:

var player1 = new Player("Kandra", 50);

构造函数还会添加一个你可以调用的方法来添加项目,例如:

player1.addItem("a rusty key");

代码清单 9.12 展示了Player构造函数的代码。它使用了在第七章中引入的spacer命名空间中的方法。spacer代码包含在 JS Bin 中,但在代码清单中没有显示。为了测试由Player构造函数创建的Player对象,代码清单还使用了第 9.4 节中的Place构造函数。同样,这段代码在这里省略了,但在 JS Bin 中包含了。此外,还有一个新的值类型null,在代码清单之后的章节中进行了解释。

再次强调,这里有很多代码(JS Bin 中还有更多),但不要担心;你之前已经见过类似的代码。检查注释,阅读代码清单下面的解释,并在 JS Bin 上尝试Further Adventures。快速查看第八章中的The Crypt部分也可能很有帮助;这基于前面的内容。

代码清单 9.12. Player构造函数 (jsbin.com/leqahi/edit?js,console)

图片

图片

呼吁!这又是一个很长的代码清单。让我们把它分解一下。

9.5.2. 将函数转换为方法

在第七章和第八章中,你定义了一系列函数来构建玩家信息字符串并显示它,例如,

var getPlayerHealth = function (player) {
    return player.name + " has health " + player.health;
};

你将一个Player对象作为参数传递给函数:

getPlayerHealth(player1);

函数随后使用Player对象的属性来构建并返回一个信息字符串。

你现在已经将函数移动到Player构造函数中,并将它们分配给特殊this对象的属性。

this.getHealth = function () {
    return this.name + " has health " + this.health;
};

当我们将函数作为对象的属性时,我们称它们为方法。你通过使用点符号和括号来调用方法:

player1.getHealth();

你不再需要将Player对象作为参数传递给函数。在函数体中使用this的地方,将用player1来代替。

return this.name + " has health " + this.health;

变为

return player1.name + " has health " + player1.health;

将所有函数转换为Player对象的方法消除了为每个函数单独变量需要的必要性。它还保持了与对象一起的函数定义。真不错!

9.5.3. 为玩家分配地点

你需要知道玩家在The Crypt的位置。在早期章节中,你为每个玩家的place属性分配了一个字符串:

player1.place = "The Old Library";

但地点不仅仅是标题;使用Place构造函数创建的Place对象有标题、描述、物品数组、出口和方法。从现在开始,你将为玩家分配完整的Place对象:

var library = new Place(
    "The Old Library",
    "You are in a library. Dusty books line the walls."
);

var player1 = new Player("Kandra", 50);

player1.place = library;

当玩家探索他们的环境时,你可以更新place属性,为每个新位置分配一个之前构建的Place对象。

现在你将完整的Place对象分配给玩家的地点属性,getPlace方法必须做一些额外的工作来构建其信息字符串:

this.getPlace = function () {
    return this.name + " is in " + this.place.title;
};

之前,this.place保存了玩家当前地点的标题;现在它保存了一个Place对象。标题现在通过this.place.title来访问。

9.5.4. 使用 null 作为对象的占位符

注意,在 列表 9.12 中,你将特殊值 null 分配给构造函数中的 place 属性。这表明你打算在你的程序中使用 place 属性,但目前还没有值;你只能在创建了一些地点之后才能分配玩家的 place 属性。

player.place = null;      // You expect an object to be assigned later

/* other code */

player.place = library;   // An object is assigned, as expected
定义

null 是其自身类型的价值。它不是字符串、数字、布尔值(truefalse)或 undefined。它也不是一个对象。程序员经常像你一样使用它来表明他们尚未将对象分配给变量或属性,但预期在某个时刻会分配一个。

现在你有了创建许多对象的效率方法,你可以使用构造函数构建 The Crypt 中的所有地方。你可以添加物品并将地点链接成一个地图。你可以将地点分配给玩家。你仍然需要找到一种让玩家在地点之间移动的方法。一旦你做到了这一点,你将拥有一个可以探索的游戏环境!

9.6. 摘要

  • 使用构造函数创建具有相似结构的对象。

  • 将构造函数分配给以大写字母开头的变量。以这种方式命名构造函数是一种广泛遵循的约定:

    var Person = function (name) { ... };
    
  • 使用 new 关键字调用构造函数。将返回的对象分配给一个变量:

    var person = new Person("Jahver");
    
  • 使用特殊的 this 变量在构造函数中设置属性。this 会自动从构造函数返回:

    var Person = function (name) {
        this.name = name;
    };
    
    var person = new Person("Jahver");
    
  • 就像访问任何其他对象的属性一样访问返回对象的属性:

    person.name; // Jahver
    
  • 将函数分配给属性以创建方法。使用点符号和括号调用方法:

    var Person = function (name) {
        this.name = name;
        this.sayHello = function () {
            return this.name + " says hi.";
        };
    };
    
    var person = new Person("Jahver");
    person.sayHello();  // Jahver says hi.
    
  • 如果你预期在某个时刻会分配一个对象,但对象目前不可用,则将值 null 分配给变量或属性:

    player1.place = null;
    
    /* other code */
    
    player1.place = library;
    
  • 使用 instanceof 操作符检查构造函数是否参与了对象的创建。操作符返回一个布尔值,truefalse

    var person = new Person("Jahver");
    
    person instanceof Person;    // true
    
    person instanceof Planet;    // false
    

第十章。括号表示法:灵活的属性名称

本章涵盖的内容有

  • 方括号作为点符号的替代

  • 使用方括号设置和获取属性

  • 方括号表示法的灵活性

  • 如何在 The Crypt 中构建一个可工作的游戏

在 第三章 中,你看到了如何使用花括号创建对象,以及如何使用点符号获取和设置属性。你已经使用对象来模拟玩家、地点、行星、帖子以及测验和日历事件。你将函数作为属性添加以创建方法,并将对象作为参数和返回值传递给函数。对象是 JavaScript 世界的中心!

在本章中,你将了解一种新的处理对象属性的方法,这种方法使你在属性名称上拥有更多灵活性,允许你使用变量作为键,并在程序运行时从数据中生成新的属性。

我们还完成了Get Programming with JavaScript的第一部分,通过一个可工作的版本The Crypt,最终给了玩家探索地图和收集宝藏的机会!方括号符号为你提供了更好的方式来管理游戏中地点之间的链接,创建一个Place对象的网络,并为冒险增添一丝神秘感。Ooooo,神秘...

10.1. 使用方括号而不是点

到目前为止,你一直使用点符号来设置和获取对象属性。

question1.question = "What is the capital of France?";

console.log(player1.name);

this.title = title;

属性名,即其,通过一个点与变量名连接。JavaScript 还提供了一种替代方法:你可以通过在方括号中包含属性键作为字符串来设置和获取属性。

question1["question"] = "What is the capital of France?";

console.log(player1["name"]);

this["title"] = title;

这种新方法为你提供了更多灵活性,可以在用作键的字符串中使用,并允许你在程序运行时添加动态属性。比如说,你有一个states对象,它存储了美国各州的缩写作为值,使用州的全名作为键。你可以使用点符号来查找俄亥俄州的缩写

console.log(states.ohio);   // OH

但是,由于那个讨厌的空格,不是用于新罕布什尔州

console.log(states.new hampshire);    // ERROR!

方括号解决了问题(图 10.1):

图 10.1. 使用方括号符号设置属性

console.log(states["new hampshire"]);    // NH

如果你有一个使用states对象的getStateCode函数,方括号符号将允许你使用参数作为键:

var getStateCode = function (stateName) {
    var stateCode = states[stateName];
    return stateCode;
};

使用参数提供键在点符号中是不可能的。表 10.1 展示了更多需要使用方括号符号的情况。

表 10.1. 某些键是否有效的情况
我想 我尝试 成功?

| 在对象字面量中使用 ohio 作为键 | states = { ohio : "OH"

}; | 是 |

| 在对象字面量中使用 new hampshire 作为键 | states = { new hampshire : "NH"

}; | 否 |

| 在对象字面量中使用"new hampshire"作为键 | states = { "new hampshire" : "NH"

}; | 是 |

使用点符号将 maryland 作为键 states.maryland = "MD";
使用点符号将 south carolina 作为键 states.south carolina = "SC";
使用方括号符号将 south carolina 作为键 states["south carolina"] = "SC";

| 使用点符号将参数作为键 | function (stateName) { return states.stateName;

} | 否 |

| 使用方括号符号将参数作为键 | function (stateName) { return states[stateName];

} | 是 |

让我们通过一些更多的例子来调查这些想法。

10.1.1. 方括号的实际应用——人名作为键

假设你需要记录人们的简单年龄记录。你可以使用一个ages对象,其中每个人的名字是键,他们的年龄是对应的值。列表 10.1,10.2,和 10.3 展示了这种方法,将两个年龄记录到控制台:

> 56
> 21

在第一个列表中,你使用方括号符号设置属性,并使用点符号获取它们。

列表 10.1. 对象属性的括号符号 (jsbin.com/kipedu/edit?js,console)

这两种方法对于在列表 10.1 中使用的键是等效的。如果你想在属性名称中包含空格,必须使用括号符号。下一个列表展示了使用完整名称的类似示例。

列表 10.2. 较长的字符串作为键 (jsbin.com/toviya/edit?js,console)
var ages = {};

ages["Kandra Smith"] = 56;
ages["Dax Aniaku"] = 21;

console.log(ages["Kandra Smith"]);
console.log(ages["Dax Aniaku"]);

尝试使用点符号与包含空格的属性名称将使 JavaScript 困惑:ages.Kandra Smith将被解释为ages.Kandra,而Smith将被视为一个单独的变量名,如图 10.2 所示。

图 10.2. 属性名称中的空格与点符号不兼容。

括号符号立即为您提供使用在上下文中可能更有意义的属性名称的灵活性,例如人名,而不是被限制在点符号的单个单词中。

括号符号还允许你在不知道键的情况下动态添加属性。也许用户正在输入信息,或者信息是从文件或数据库中获取的。在下一个列表中,你包括一个addAge函数来向ages对象添加新的人。如果你运行这个列表,你将能够通过控制台使用该函数添加新的人。

列表 10.3. 使用函数添加年龄 (jsbin.com/pipuva/edit?js,console)

在列表 10.3 中,你将addAge函数中的name参数用作ages对象的键。当你调用addAge时,你包含在调用中的第一个参数被分配给name参数。例如,

addAge("Kandra Smith", 56);

"Kandra Smith"分配给name,将56分配给age,因此

ages[name] = age;

变为

ages["Kandra Smith"] = 56

在方括号之间使用变量来设置属性的能力,使你在程序中动态创建和修改对象具有极大的灵活性。你将在 10.1.2 节和 10.2 节中进一步使用这项技术。

如果你想要创建一个已经包含属性的对象,那么你使用大括号语法,用逗号分隔键值对。如果你在键周围加上引号,你可以在键中包含空格。以下列表展示了这个概念。它还介绍了Object.keys方法,该方法返回一个包含对象上设置的所有键的数组。在列表中,你将keys数组打印到控制台。

> ["Kandra Smith", "Dax Aniaku", "Blinky"]
列表 10.4. 使用 Object.keys (jsbin.com/mehuno/edit?js,console)

Object 是一个内置的 JavaScript 对象。它提供了一些方法,其中之一被称为 keys。因为 Object.keys 返回一个数组,所以你可以使用 forEach 将每个键传递给一个函数。列表 10.5 依次将每个键传递给 console.log 函数,该函数只是将键记录到控制台。

> Kandra Smith
> Dax Aniaku
> Blinky
列表 10.5. 使用 forEach 遍历 Object.keys (jsbin.com/seteco/edit?js,console)

图片

让我们看看使用方括号表示法包含复杂属性名的另一个例子。

10.1.2. 充分利用方括号表示法——单词计数

作为你工作场所的社会媒体专家,你被分配了分析推文的任务。你的第一项工作是计算每个单词在一批推文中使用的次数。图 10.3 显示了推文分析程序控制台输出的部分。

图 10.3. 计数推文中使用的单词

图片

程序如下所示(只包含三个推文以节省空间)并使用一个对象 words 来记录单词计数。

列表 10.6. 从推文中计数单词 (jsbin.com/figati/edit?js,console)

图片

程序在几行代码中做了很多事情。首先,它使用 join 数组方法(在第八章中介绍)将所有推文连接成一个长字符串,每对推文之间有一个空格。然后,它使用 split 方法创建一个包含所有单词的数组。split 是一个内置的字符串方法。你可以使用它将字符串拆分成片段,每个片段作为数组的元素。如果你将一个字符串赋值给 message 变量

var message = "I love donuts";

你可以通过在变量上调用 split 方法将字符串拆分成一个包含三个元素的数组。

console.log(message.split(" "));

> ["I", "love", "donuts"]

你传递给 split 方法的参数是一个字符串,该函数使用它来决定在哪里分割文本。上一个例子使用空格作为分隔符,但任何字符串都可以工作。以下是一个使用逗号的例子:

var csv = "Kandra Smith,50,The Dungeon of Doom";
var details = csv.split(",");
console.log(details);

> ["Kandra Smith", "50", "The Dungeon of Doom"]

如果你将空字符串 "" 作为 split 的参数传递,它将创建一个包含文本中使用的所有单个字符的数组:

var message = "I love donuts";
console.log(message.split(""));

> ["I", " ", "l", "o", "v", "e", " ", "d", "o", "n", "u", "t", "s"]

回到你的推文分析器。一旦它使用 split 生成单词数组,列表 10.6 就会两次遍历单词数组。第一次,它使用单词作为键并使用零作为值创建属性。["I", "love", "donuts"] 导致

words["I"] = 0;
words["love"] = 0;
words["donuts"] = 0;

如果一个单词出现多次,它的属性将在每次出现时被分配零——有一点冗余但不是问题。在第二次迭代中,代码每次单词出现时都会将属性值加一。

words["I"] = words["I"] + 1;
words["love"] = words["love"] + 1;
words["donuts"] = words["donuts"] + 1;

或者,你也可以使用 += 运算符为每个单词的计数加一。你已经看到 += 用于连接字符串,但它也可以与数字一起使用,将新数字添加到现有数字上。以下两个语句是等价的:

words[word] = words[word] + 1;
words[word] += 1;

事实上,如果你只想每次添加一个,有一个运算符正好为此目的,++。以下两个语句是等价的:

words[word] += 1;
words[word]++;

但你希望你的代码易于理解,++有点简短(以及还有一些我们不深入探讨的复杂问题)。你可能会在代码世界中遇到它,但直到第四部分你不会在这本书中使用它。

你已经对所有的单词进行了两次迭代。到第二次迭代的结束时,每个属性都有一个单词作为键,单词的计数作为其值。做得好;涨工资!请务必查看 JS Bin 上的工作示例,并添加更多推文或来自其他来源的文本进行分析。

通过简单的调整,你可以执行字母计数而不是单词计数。下一个列表显示了如何操作。

列表 10.7. 从推文中计数字母(jsbin.com/rusufi/edit?js,console)

列表 10.7 与列表 10.6 的工作方式相同,使用joinsplit创建一个数组,然后迭代数组两次进行计数。

在下一节中,你将利用方括号符号处理任意字符串的能力来管理The Crypt中的出口。

10.2. The Crypt——增强出口的兴奋感

现在,你将应用你对方括号符号的知识到The Crypt。图 10.4 显示了本节的重点,通过出口连接地点,如何融入到我们正在进行的游戏示例的整体结构中。

图 10.4. The Crypt的元素

事实上,当你为The Crypt定义的Place构造函数在第九章中列出当前地点的出口时,会泄露一些信息:

===============
= The Kitchen =
===============
You are in a kitchen. There is a disturbing smell.

Items:
   - a piece of cheese

Exits from The Kitchen:
   - The Kitchen Garden
   - The Kitchen Cupboard
   - The Old Library
========================================

它列出了玩家离开当前位置时会发现什么。到本节结束时,你通过只列出可用的方向而不是目的地,增加了一丝神秘感。

Exits from The Kitchen:
   - west
   - east
   - south

玩家只有在到达那里后才会发现角落里有什么。感受紧张气氛!

为了帮助你专注于使用方括号符号的对象来增强出口——毕竟,这正是本章的主要内容——你创建了一个新的、简化的Place构造函数来进行实验。然后,在第 10.2.4 节中,你使用你正在发展的方括号技能来更新你的构造函数,从第九章开始。

10.2.1. 使用对象来保存出口

为了增加未知的一层,你使用出口对象而不是出口数组。对象键是方向,如"north""the trapdoor",值是目的地。列表 10.8 在控制台上显示了方向和目的地。你稍后会隐藏目的地。

> north goes to The Kitchen
> the trapdoor goes to The Dungeon
列表 10.8. 出口对象(jsbin.com/daqato/edit?js,console)

在 列表 10.8 中,你首先定义了一个非常简单的 Place 构造函数。记得从 第九章 中,当你使用 new 关键字调用构造函数时,JavaScript 会自动创建一个空对象并将其分配给特殊的 this 变量。你将 thistitle 属性设置为 title 参数的值。

var Place = function (title) {
    this.title = title;
};

你立即使用构造函数来创建两个 Place 对象:

var kitchen = new Place("The Kitchen");
var dungeon = new Place("The Dungeon");

为了保持简单,你目前使用一个单独的 exits 变量。稍后,你将其作为 Place 构造函数函数的一部分包含在内。因此,你创建一个空对象并将其分配给 exits 变量:

var exits = {};

你有两个 Place 对象,kitchendungeon,并且你想要将它们设置为不同方向的目的地。你使用方向作为键,目的地作为值,在 exits 对象上创建相应的属性:

exits["north"] = kitchen;
exits["the trapdoor"] = dungeon;

最后,你使用 forEach 方法依次将每个键(换句话说,每个方向)传递给指定的函数。

console.log(key + " goes to " + exits[key].title);

键是 "north""the trapdoor",所以代码与

console.log("north" + " goes to " + exits["north"].title);
console.log("the trapdoor" + " goes to " + exits["the trapdoor"].title);

exits["north"]kitchen 对象,而 exits["the trapdoor"]dungeon 对象,所以代码变为

console.log("north" + " goes to " + kitchen.title);
console.log("the trapdoor" + " goes to " + dungeon.title);

导致在列表之前显示的输出。

10.2.2. 创建用于添加和显示出口的函数

好的,所以你成功地使用了方括号表示法来关联方向和目的地。在 列表 10.9 中,你向代码中添加了两个辅助函数,addExitshowExits,以简化出口的添加和显示。输出与 列表 10.8 相同:

> north goes to The Kitchen
> the trapdoor goes to The Dungeon
列表 10.9. 添加和显示出口的函数 (jsbin.com/mibube/edit?js,console)

addExit 函数接受两个参数:一个 direction 字符串和一个 Place 对象。direction 字符串成为 exits 对象上的新键;Place 对象成为相应的值。

addExit("north", kitchen);

执行代码

exits["north"] = kitchen;

showExits 函数遍历 exits 对象的键(换句话说,它遍历方向)并显示每个方向的目的地。

10.2.3. 为每个地点对象设置其自己的出口集合

你已经看到了如何使用 exits 对象来模拟方向和目的地。但每个地点对象都需要其自己的出口集合。你不想把《仙子欢乐园》的出口和《末日地牢》的出口混淆起来。在 列表 10.10 中,你将 exits 对象移动到 Place 构造函数中。为了测试新的 Place 构造函数的功能,你创建了三个地点,librarykitchengarden,并在 kitchen 上添加了一些出口。然后显示 kitchen 的出口:

> Exits from The Kitchen:
> south
> west
列表 10.10. 地点构造函数中的出口对象 (jsbin.com/foboka/edit?js,console)

addExitshowExits函数被设计成与Placeexits对象一起工作,因此将它们捆绑在构造函数内的其他地点代码中是有意义的。然后你可以使用点符号和括号来调用函数:

kitchen.addExit("south", library);
kitchen.showExits();

列表 10.11 使用相同的Place代码构建了一个稍微大一点的地图,链接了四个地点,如图 10.5 所示。

图 10.5. 一个包含四个地点的地图

两个地点的输出显示在控制台上:

> Exits from The Old Library:
> north
> Exits from The Kitchen:
> south
> west
> east
列表 10.11. 一个包含四个地点的地图(jsbin.com/bufico/edit?js,console)

注意,一旦你定义了一个Place构造函数,你就可以用它来创建和链接你需要的任意数量的地点。构造函数代码可以保持不变——你可以将它从一个冒险移动到另一个冒险——你只需要更改你创建的地点和地图数据。

10.2.4. 将exits对象添加到完整的Place构造函数中

到目前为止,在第 10.2 节中,你已经看到了如何将exits对象与方括号符号结合使用来管理The CryptPlace对象之间的链接。为了保持对方括号的关注,你从一个新的简单Place构造函数开始构建代码。现在是时候将增强的出口与你在第九章中构建构造函数所做的出色工作结合起来,以生成图 10.6 中显示的每个地点的输出格式。

图 10.6. 当你调用showInfoPlace对象的控制台输出

列表 10.12 显示了完整的Place构造函数代码。其中大部分内容在第九章中已经讨论过,所以如果你需要刷新记忆,请回到那里。新的出口代码以粗体显示,并附有注释。JS Bin 上的列表包括下一节中讨论的地图信息。

列表 10.12. Place构造函数(jsbin.com/zozule/edit?js,console)

使用这个最新的Place构造函数版本,你可以创建地点对象,管理它们的物品和出口,并在控制台上显示格式化的信息(由spacer提供)。让我们试一试。

10.2.5. 测试Place构造函数

为了测试Place构造函数,你重新创建了列表 10.11 中的地图,该地图链接了四个地点:厨房图书馆花园壁橱。下一个列表显示了地图创建代码。输出显示在图 10.6 中。

列表 10.13. 测试Place构造函数(jsbin.com/zozule/edit?js,console)

你跟随地图创建代码,调用kitchen.showInfo来测试厨房对象及其物品和出口是否已按预期创建。

10.3. The Crypt——游戏开始!

您已经非常接近了!您已经有了构建和显示玩家可以探索的宇宙所需的一切:

  • spacer 命名空间

  • Player 构造函数

  • Place 构造函数

  • 地图创建代码

在玩家可以开始他们的冒险之前,还需要添加一个部分:

  • 游戏控制

图 10.7 展示了本节中您将创建的三个游戏函数以及它们如何融入我们正在进行的示例的整体结构。

图 10.7. 《The Crypt》的元素

您希望玩家能够在控制台提示符下发出命令,命令从地点移动到地点以及捡起他们找到的物品。例如,要向北移动并捡起物品,玩家将输入

> go("north")
> get()

幸运的是,由于您在构造函数中投入了工作,游戏控制代码实际上相当简短。它在这里显示。

列表 10.14. 玩游戏 (jsbin.com/sezayo/edit?js,console)

您创建了三个函数:rendergoget。每个函数将在下面的单独部分中进行讨论。goget 函数都以返回一个空字符串 "" 结束。当您在控制台调用函数时,它将自动显示它们的返回值。返回空字符串可以防止控制台显示返回值 undefined

10.3.1. 更新显示—渲染

在每次游戏开始以及玩家采取行动时,您都希望更新控制台上的显示。Place 对象和 Player 对象都有 showInfo 方法来显示它们当前的状态;例如,

player.showInfo("*");
kitchen.showInfo();

程序将玩家的当前位置分配给他们的 place 属性。因此,要显示当前位置的信息,您需要包含以下代码

player.place.showInfo();

您可以继续向控制台追加文本,但每次玩家采取行动后清除它并从一张白纸开始会更整洁。clear 方法从控制台移除文本。

console.clear();

玩家移动后以及他们捡起物品后,您需要更新控制台。您不是重复显示代码,而是将其包装在 render 函数中,并在需要时调用它。

10.3.2. 探索地图—移动

要移动到新位置,玩家需要调用 go 函数,并指定他们想要移动的方向:

go("south");

您需要找到玩家指定的方向的终点,并将其指定为玩家的新的位置:

player.place = player.place.exits[direction];

在赋值表达式的右侧,您使用方括号表示法来检索指定方向的 Place 对象,即 exits[direction]。您使用点表示法来访问 exits 对象:

player;    // The Player object      

player.place;    // The player's current location
                 // A Place object

player.place.exits;    // The exits from the current location.
                       // An object with directions as keys and
                       // destinations as values.

player.place.exits[direction];    // The place is the specified direction.
                                  // A Place object.

将目标位置的 Place 对象分配给玩家的 place 属性,替换他们的旧位置。他们已经移动了。

10.3.3. 收集所有物品—获取

要捡起物品,玩家调用 get 函数:

get();

你将物品从当前位置移除并添加到玩家的物品数组中。当前位置是 player.place。当前位置的物品存储在 player.place.items 数组中。要移除并返回 items 数组中的最后一个物品,你使用 pop 方法:

var item = player.place.items.pop();

然后将其添加到玩家的收藏中:

player.addItem(item);

就这样!你已经给了玩家从地方到地方大胆前进的能力,收集他们可能找到的任何宝藏。

10.3.4. 设计更大的冒险——贾弗的船

基于厨房的四个位置可能不是你希望中的冒险世界。为了创造神秘领域、复制反乌托邦未来和编织阴谋网,你只需要更改代码中的地图部分。尝试构建你自己的位置,通过出口将它们连接起来,并填充有趣的文物。

为了让你开始,有一个简短的冒险可以探索和扩展在 JS Bin 上,网址为 jsbin.com/yadabo/edit?console。你可能需要点击运行来启动游戏。它设置在一个名为 The Sparrow 的小型太空货船上。有六个物品可以收集,不包括你的激光枪。收集完所有物品后,打开 JavaScript 面板并尝试添加更多位置。祝您狩猎愉快!

10.4. 接下来是什么?

虽然有一个工作良好的游戏很棒,但 The Crypt 的代码并不非常健壮。玩家可以很容易地通过尝试进入一个不存在的方向等方式来破坏它。他们还可以在控制台中访问所有游戏对象——很容易给自己额外的宝藏或传送到新的位置:

> player.addItem("riches beyond my wildest dreams")
> player.place = treasureRoom

游戏当然需要更多的挑战。应该有一些只有使用特定物品才能克服的谜题。

Get Programming with JavaScript 的 第二部分 中,你将通过将代码拆分为模块、防止访问对象、设置代码运行的条件以及将数据与数据的显示分离来解决这些问题。这种组织将帮助你设计和管理更大、更复杂的程序,并为在 第三部分 中与网页一起工作做好准备。

10.5. 摘要

  • 使用方括号中的字符串来指定属性名:

    player["Kandra"] = 56;
    exits["north"] = kitchen;
    words["to"] = words["to"] + 1;
    
  • 特别是,当属性名包含空格或其他不允许使用点表示法的字符时,使用方括号:

    player["Kandra Smith"] = 56;
    exits["a gloomy tunnel"] = dungeon;
    words["!"] = 0;
    
  • 使用函数参数在调用函数时动态分配属性名:

    var scores = {};
    var updateScore = function (playerName, score) {
        scores[playerName] = score;
    };
    updateScore("Dax", 2000);
    console.log(scores["Dax"]);
    
  • 将对象作为参数传递给 Object.keys 方法以创建对象的键数组:

    var scores = {
        Kandra : 100,
        Dax : 60,
        Blinky : 30000
    };
    var keys = Object.keys(scores);    // ["Kandra", "Dax", "Blinky"]
    
  • 使用 split 方法将字符串拆分成数组。将用作确定拆分位置的字符串作为参数传递:

    var query = "page=5&items=10&tag=pluto";
    var options = query.split("&");    // ["page=5", "items=10", "tag=pluto"]
    

第二部分. 组织你的程序

在程序员中共享代码很常见;许多问题已经被其他人解决,你的解决方案可能在你自己的未来项目中,在你的团队中,或者更广泛的开发者社区中非常有用。当代码模块被共享时,通过定义一个清晰的接口,或一组你期望用户使用的属性和函数,来明确其他人应该如何使用它们是很重要的。模块的内部——你如何让它完成工作——应该受到保护。第二部分探讨了使用局部变量而不是全局变量来隐藏模块实现的方法。

随着你的程序的增长,组织的需求增加,你将开始注意到你反复使用的模式和程序。第二部分还探讨了在代码中使用模块的方法来提高灵活性、重用性和可维护性。你的代码的灵活性可以通过仅在满足某些条件时执行其部分来提高,你将看到如何使用if语句来指定这些条件。

你创建的模块通常会在项目中执行特定的、定义良好的任务。三种常见的模块类型是模型,用于表示和处理数据,视图用于在模型中展示数据,以及控制器用于根据用户或系统操作更新模型和视图。

到第二部分结束时,你将把The Crypt分解成模块,并更新游戏以包括挑战:玩家需要克服的谜题。

第十一章. 范围:隐藏信息

本章涵盖

  • 全局变量的危险

  • 局部变量的好处

  • 使用命名空间来减少全局变量

  • 使用函数创建局部变量

  • 返回用户接口

你希望用户能够与你的基于控制台的程序进行交互。但你不想给他们太多的控制!本章探讨了在控制台中隐藏程序的一部分以及明确定义你期望他们使用的属性和方法的方法。例如,对于一个问答程序,用户应该能够提交答案但不能更改他们的分数:

> quiz.submit("Ben Nevis")    // OK

> quiz.score = 1000000        // Don't let the user do this!

通过将公共接口与使程序工作的私有变量和函数分开,你声明了你的意图,说明了用户可以做什么,以及使用你的代码的其他程序员应该做什么,并减少了代码被误用的风险。这对程序、玩家和其他程序员都有好处。

函数是关键。与在函数外部声明的变量可以在程序中的任何地方访问不同,在函数内部声明的变量只能在函数内部访问。本章将向你展示如何从函数中返回对象,只包含你希望用户可以访问的属性和方法。构造函数也是函数,你将看到如何使用它们的特殊this对象来设置公共属性和方法。

最后,你将应用所学知识到The Crypt,移除用户对构造函数、地点和玩家属性的访问权限,并提供显示玩家和地点信息以及导航地图的方法。用户将能够这样做:

> game.go("north")     // Move from place to place
> game.get()           // Pick up an item

用户无法通过这种方式来欺骗游戏:

> player.health = 1000000
> player.place = exit
> player.items.push("The Mega-Sword of Ultimate Annihilation")

让我们先强调一下程序中任何地方都可以看到的变量的陷阱;它们看起来是个不错的想法![剧透:它们并不好。]*

11.1. 全局变量的危险

在第二章中,在你开始 JavaScript 冒险之旅的时候,你发现了如何声明、赋值和使用变量。以下列表展示了显示在控制台上的山脉名称的简单示例过程:

> Ben Nevis
列表 11.1. 声明、赋值和使用变量 (jsbin.com/gujacum/edit?js,console)
var mountain;

mountain = "Ben Nevis";

console.log(mountain);

你在程序的第一行声明了变量mountain。然后你可以在整个代码中使用这个变量;你给它赋值,并将其作为参数传递给console.log

程序中的函数也可以访问mountain变量(图 11.1)。

图 11.1. 函数可以看到函数体外的mountain变量。

下一个列表展示了使用mountainshowMountain函数,其输出与列表 11.1 相同。

列表 11.2. 在函数内部访问变量 (jsbin.com/zojida/edit?js,console)

像在函数外部声明的mountain这样的变量,可以在任何地方访问,被称为全局变量。你可能认为这些全局变量听起来非常有用:一旦声明,就可以在代码的任何地方自由使用。不幸的是,它们有一些严重的缺点,通常被认为是个坏主意。让我们调查一下避免这些淘气的全局变量的原因。

11.1.1. 访问所有区域——偷看和修改

你希望用户能够在控制台与你的程序交互。你不想让他们偷偷查看所有技术变量。下一个列表展示了一个小小的测验。

列表 11.3. 一个小小的测验 (jsbin.com/nubipi/edit?js,console)
var question = "What is the highest mountain in Wales?";
var answer = "Snowdon";

console.log(question);

由于questionanswer是全局变量,它们可以在整个程序和通过控制台访问。在控制台提示符下,一旦用户运行程序,他们可以输入answer并按 Enter 键来显示answer变量的值。偷偷一瞥我们的技术高峰!

> answer
  Snowdon

不仅仅是偷偷查看是个问题;偷偷修改也是个问题!用户可以随意更改全局变量的值,更新分数、银行余额、速度、价格和答案。

> answer = "Tryfan"

11.1.2. 访问所有区域——依赖于实现

不依赖实现的观念很重要,但可能需要时间来完全理解。如果你第一次阅读时没有完全理解,不要担心——你可以在阅读本章中的其他示例之后返回来。

暗中窥视和调整也可能成为程序员的麻烦(参见侧边栏 “谁在使用你的代码?”)。如果你编写的代码被其他程序员使用,你可能不希望他们暗中修改,编写依赖于你代码内部结构的自己的代码。当你发布使用更高效算法的代码的新版本时,依赖于你一些变量的其他代码可能会出错。

谁在使用你的代码?

考虑你在 第七章 中创建的 spacer 函数。自从那时起,你一直在你的代码中使用它们来编写 The Crypt。这是 使用你自己的代码的一个例子。

对于程序员来说,成为 团队 的一员,无论是小团队还是大团队,是很常见的。团队可以共同开发同一个程序的不同方面。没有必要每个程序员都创建他们自己的格式化函数,所以他们都会使用你的 spacer 命名空间。

你的团队成员对你的格式化能力印象深刻,鼓励你更广泛地分享 spacer 命名空间。你将你的代码上传到像 Github.com (github.com) 或 npmjs.com (www.npmjs.com) 这样的 社区 仓库。然后,其他程序员可以下载并使用你的代码,甚至贡献改进和扩展。

spacer 代码被一个由热心的爱好者组成的团队所采用,并进一步发展。它甚至拥有自己的网站!它如此受欢迎,以至于 每个人 都在使用 spacer.js 库。

用户应该能够依赖你的 界面——你公开的功能性——而不是担心你的 实现——你如何编程这些功能。

定义

界面 是一组你希望用户访问的属性和函数。它是他们与你的应用程序交互的方式。例如,自动柜员机的屏幕、按钮和现金分配器。

定义

实现 是你用来使应用程序在幕后完成其工作的代码。它通常对用户不可见。例如,使自动柜员机工作的代码,它是如何与你的银行通信的,以及它是如何计算现金的。

例如,为了在 The Crypt 中的地方之间实现链接,你可能使用一个 exits 数组来表示出口方向,以及一个 destinations 数组来表示出口所指向的地方。

var exits = [];
var destinations = [];

然后,为了管理添加新的出口,你编写一个 addExit 函数。

var addExit = function (direction, destination) {
    exits.push(direction);
    destination.push(destination);
};

你期望与其他程序员一起使用你的 Place 对象的程序员使用 addExit 函数 (图 11.2)。

图 11.2. 用户应使用界面,而不是实现。

11fig02.jpg

您记录了 addExit接口 的一部分的事实,即用户(在这种情况下是程序员)应与之交互的属性和函数集。

addExit("north", kitchen);     // Good—using function

但如果他们可以访问 exitsdestinations 变量,他们可能会选择直接使用这些变量,绕过函数。

exits.push("north");           // Bad—accessing variables directly
destinations.push(kitchen);

似乎一切都很正常,其他程序员的程序运行良好。然后,在审查您的代码后,您决定可以通过使用单个对象来改进使用两个单独数组的方法。

var exits = {};
// No destinations array. No longer needed in new implementation.

要使用您已实现的新的 exits 方法,您需要更新 addExit 函数。

var addExit = function (direction, destination) {
    exits[direction] = destination;
};

使用该接口的程序员将不会在其程序中看到任何变化。但是绕过接口直接访问变量的程序员将看到他们的程序崩溃!(见图 11.3)

图 11.3. 如果实现发生变化,依赖实现可能会导致错误。

图片

addExit("north", kitchen);     // Good. Works just the same.

exits.push("north");           // ERROR! exits is not an array.
destinations.push(kitchen);    // ERROR! destinations doesn't exist.

允许通过使用全局变量访问我们程序的所有区域,模糊了实现和接口之间的界限,给用户提供了窥视和调整的自由,并使程序在实现细节更改时容易失败。

11.1.3. 命名冲突

由于我们的程序可能由不同团队或程序员编写的许多代码片段组成,因此很可能在多个地方使用相同的变量名。如果一个程序员声明了一个变量名,比如 spacer(格式化函数的命名空间),然后在程序稍后另一个程序员声明了相同的变量名,spacer(基于控制台的空间冒险游戏中的犬类角色),第二个将覆盖第一个——冲突!我们真的需要一种方法来保护我们的变量免受此类冲突的影响。(第十三章 详细探讨了冲突。)

11.1.4. 疯狂的虫子

程序可能长达数千行。依赖可能声明在代码较远处的全局变量,并且可能被程序中的函数和指令查看和调整,会导致程序脆弱。如果程序出现任何问题(这很可能会发生),可能很难追踪代码的流程并确定问题的位置。

11.2. 局部变量的好处

在函数体内声明的变量不能在函数外部访问。它们被称为 局部变量;它们仅限于声明它们的函数内(图 11.4)。

图 11.4. 函数内声明的变量是局部变量。

图片

尝试在函数外部访问局部变量将导致错误。列表 11.4 尝试将 mountain 变量的值记录到控制台,但会显示如下信息:

> ReferenceError: Can't find variable: mountain

(您得到的错误可能略有不同。不同的浏览器可能会以不同的方式格式化错误信息。)

列表 11.4. 隐藏在控制台中的变量 (jsbin.com/bobilu/edit?js,console)

所有函数之外的所有变量集合称为 全局作用域。每个函数都创建自己的 局部作用域,即自己的变量集合。在 列表 11.4 中,变量 mountain 不在函数外的全局作用域中,因此在 console.log 语句中使用时报告了错误。

在同一作用域内使用变量不会引起任何麻烦,如下面的列表所示。

> Devils Tower
列表 11.5. 变量在函数内部可见 (jsbin.com/raluqu/edit?js,console)

列表 11.6 结合了全局变量和局部变量。你可以在任何地方访问全局变量,但只能在 show 函数内部使用局部变量 secretMountain

列表 11.6 产生以下输出:

> Ben Nevis
> Devils Tower
> Ben Nevis
> ReferenceError: Can't find variable: secretMountain
列表 11.6. 全局和局部变量 (jsbin.com/riconi/edit?js,console)

11.3. 接口——控制访问和提供功能

你希望用户在控制台上与你的程序交互,但你不想让他们深入到实现中做出你未打算的改变。你提供给用户的属性和动作集合称为 接口。你需要一种方法来提供简单的接口,同时隐藏其他所有内容。

在本节中,你使用一个非常简单的程序,一个计数器,作为示例。在 第 11.4 节 中,你为问答应用程序开发了一个接口,在 第 11.5 节、11.6 节 和 11.7 节 中,你将所学知识应用到 The Crypt

以下列表显示了计数程序的第一版本,使用全局变量来保存当前计数。

列表 11.7. 计数器 (jsbin.com/yagese/edit?js,console)

在控制台提示符下运行程序并遵循以下步骤。(你的操作显示提示符 >,但响应不显示。)

> count()
  1
> count()
  2
> count()
  3

它似乎正在正常工作,那么问题是什么?嗯,因为 counter 是一个全局变量,用户可以随时更改它。

> counter = 99
  99
> count()
  100

你可能不希望用户像那样调整计数变量。接下来,你使用在 第 11.2 节 中学到的关于局部变量的知识来隐藏 counter 变量,使其对用户不可见。

11.3.1. 使用函数隐藏变量

你希望通过你的计数应用程序实现两个结果:

  • counter 变量对用户不可见。

  • count 函数对用户可见。

列表 11.8 展示了一个解决方案。运行程序可以启用以下所需的控制台交互:

> count()
  1
> counter
  Can't find variable: counter

(再次强调,浏览器显示的错误可能略有不同。)

列表 11.8. 隐藏计数变量 (jsbin.com/fuwuvi/edit?js,console)

表 11.1 以一系列问题和解决方案的形式总结了关键思想;图 11.5 阐述了返回的计数函数,即分配给 count 的函数,仍然可以访问局部 counter 变量。

图 11.5. 返回的函数仍然可以访问局部 counter 变量。

表 11.1. 计数器应用程序的问题和解决方案
问题 解决方案
我希望计数器变量对用户不可见。 getCounter 函数内部声明计数器变量。作为一个局部变量,计数器对用户不可访问。
我的函数 countUpBy1,它增加计数器,需要访问计数器变量。 getCounter 函数内部定义 countUpBy1 函数。因为它在 getCounter 内部,所以它将可以访问计数器变量。
我希望用户能够调用计数函数。 getCounter 函数返回计数函数 countUpBy1。然后它可以被分配给全局变量。

通过返回一个函数并将其赋值给 count 变量 (图 11.5),你为用户提供了使用程序的方法;你给了他们一个接口——他们只需调用 getCounter() 来获取计数函数,然后调用 count() 来增加计数。

11.3.2. 使用 getCount 创建多个独立的计数器

在 代码列表 11.8 中,你定义了一个函数 getCounter 来创建计数器。每次调用 getCounter 时,它都会执行以下三个相同的步骤:

1.  声明一个 counter 变量并将其赋值为 0

2.  定义一个用于计数的函数。该函数使用步骤 1 中的 counter 变量。

3.  返回执行计数的函数。

每次调用 getCounter 时,它都会声明一个 counter 变量并定义一个计数函数。如果你多次调用 getCountercounter 变量不会相互干扰吗?不,因为 getCounter 每次运行时都会创建一个新的局部作用域;你将获得多个 counter 变量的副本,每个副本都在其自己的隔离变量集合中,即其自己的局部作用域中。

列表 11.9 更新了 getCounter 代码以创建两个计数器。然后你可以执行以下控制台交互:

> climbCount()
  1
> climbCount()
  2

> climbCount()
  3
> peakCount()
  1
> climbCount()
  4

peakCountclimbCount 不会相互干扰。

列表 11.9. 多个计数器 (jsbin.com/sicoce/edit?js,console)

counter 作为局部变量允许你拥有多个独立的计数器。

11.3.3. 使用构造函数创建多个独立的计数器

如果您将要创建很多计数器,您可以使用构造函数来定义计数器是什么以及它做什么。如 第九章 中详细说明的,构造函数简化了创建具有相似属性和方法的对象的常见过程。您通过使用 new 关键字来调用构造函数。对于计数器,您可能如下所示:

var peaks = new Counter();
var climbs = new Counter();

构造函数将自动创建一个分配给特殊 this 变量的对象,并返回它。您可以将计数函数设置为 this 的属性,使其在构造函数外部可用;参见 listing 11.10。在控制台上,您可能调用计数函数如下:

> peaks.count()
  1
> peaks.count();
  2
> climbs.count();
  1
列表 11.10. 计数器构造函数 (jsbin.com/yidomap/edit?js,console)

比较以下列表 11.9 (listings 11.9) 和 11.10 (11.10)。它们几乎完全相同!唯一的区别在于在 listing 11.10 中,你将计数函数设置为 this 的属性,而不是返回它。构造函数会自动返回 this 对象,因此你不需要这样做。构造函数仍然是一个函数,所以 counter 仍然是一个局部变量。无论是普通函数还是构造函数,都是有效的。它们都允许你返回一个接口,同时隐藏实现细节。

计数器是一个简单、清晰的例子。让我们看看有更多移动部件的东西,即问答应用。不要作弊!

11.4. 创建一个快速问答应用

您想要创建一个简单的问答应用。该应用应该能够做三件事:

1. 在控制台显示一个问题。

2. 在控制台显示当前问题的答案。

3. 将问题库中的下一个问题移动到下一个问题。

控制台典型交互的开始可能看起来像这样:

> quiz.quizMe()
  What is the highest mountain in the world?
> quiz.showMe()
  Everest
> quiz.next()
  Ok
> quiz.quizMe()
  What is the highest mountain in Scotland?

如您所见,有三个函数 quizMeshowMenext 满足应用的三项要求。您还需要一个问题和答案的数组以及一个变量来跟踪当前的问题。在函数外部声明的变量被称为在 全局命名空间 中。为了避免将所有这些问答变量 污染全局命名空间,您可以将其设置为单个对象的属性。正如您在 第七章 中所看到的,当以这种方式使用单个对象收集相关变量时,它通常被称为 命名空间

11.4.1. 使用对象作为命名空间

应用将创建一个全局变量 quiz。然后,您可以将所有变量设置为 quiz 对象的属性,如 listing 11.11 中所示。问题和答案的数组开始如下:

var quiz = {
    questions: [
        {
            question: "What is the highest mountain in the world?",
            answer: "Everest"
        }
    ]
};

然后通过 quiz.questions 访问数组。请记住,数组索引从 0 开始,因此要访问第一个问题-答案对象,请使用 quiz.questions[0]。要从 questions 数组中获取第一个问题和第一个答案,请使用以下代码:

quiz.questions[0].question;
quiz.questions[0].answer;

您使用 qIndex 属性跟踪当前问题。

列表 11.11. 改进测验应用程序 (jsbin.com/tupoto/edit?js,console)

图 ch11ex11-0.jpg

图 ch11ex11-1.jpg

列表 11.11 满足您应用程序的三个要求,并且足够礼貌地只使用一个全局变量,quiz。它所需的一切都在 quiz 对象内部。因为所有属性都是作为 quiz 的属性访问的,所以我们说它们位于 quiz 命名空间中。

不幸的是,您的应用程序没有克服 第 11.1.1 节 中看到的全局变量的 访问所有区域 缺点。quiz 对象的所有属性都是 公共的——用户仍然可以偷看并调整所有值。

11.4.2. 隐藏问题数组

在 列表 11.11 中,玩家可以在控制台中访问 quiz 对象的所有属性。这使他们可以随心所欲地造成破坏,更改属性:

> quiz.qIndex = 300
> quiz.questions[2].answer = "282"

但您希望他们只使用 quiz.quizMequiz.showMequiz.next。让我们通过使用局部变量将 questions 数组和 qIndex 值设为 私有,如图 11.6 所示。

图 11.6. 使用局部变量使 qIndexquestions 成为私有

图 11fig06_alt.jpg

下一个列表使用 getQuiz 函数创建一个局部作用域,用于隐藏 questions 数组和 qIndex 值。

列表 11.12. 隐藏问题和答案 (jsbin.com/qahedu/edit?js,console)

图 ch11ex12-0.jpg

图 ch11ex12-1.jpg

程序返回一个具有三个属性的对象:quizMeshowMenext。最后一行将返回的对象赋值给 quiz 变量。然后您可以使用 quiz 来访问这三个函数:

> quiz.quizMe()
  What is the highest mountain in the world?
> quiz.answer()
  Everest

使测验应用程序工作的代码称为其 实现。其中一些实现通过使用局部变量隐藏在 getQuiz 函数中。该函数返回的对象向用户提供了一个 接口,一种与程序交互的公共方式。用户可以调用 quizMeshowMenext 函数,因为它们是接口对象的成员方法。用户无法访问 qIndexquestions 变量,因为它们是 getQuiz 函数的局部变量。

图 11.7 展示了在 JS Bin 控制台中尝试访问变量 qIndexquestions 时抛出的错误。(您的错误信息可能略有不同。)

图 11.7. 现在尝试从控制台访问 qIndexquestions 会导致错误。

图 11fig07_alt.jpg

11.5. 保密室——隐藏玩家信息

在本节以及第 11.6 节和 11.7 节中,你更新了The Crypt的代码,以隐藏实现中的一部分变量、属性和函数,这些不应该被外界窥视。同时,你考虑了你的接口应该采取什么形式——你将向玩家和程序员提供哪些信息和操作。

11.5.1. 我们当前的玩家构造函数——一切都是公开的

到目前为止,你还没有尝试控制用户对Player对象中数据的访问。以下列表显示了你的Player构造函数的当前形式。

列表 11.13. 玩家构造函数(jsbin.com/dacedu/edit?js,console)

图片

你将所有玩家数据和所有函数都设置为特殊this对象的属性。当你使用new关键字调用构造函数时,它会自动创建this对象。在下面的代码片段中,你调用Player构造函数来创建一个新的玩家。

var player1 = new Player("Jahver", 80);

构造函数会自动返回this对象,然后你将对象分配给player1变量。因为你将所有内容都附加到了构造函数中的this上,并且this被返回并分配给了player1,你现在可以使用player1来访问数据和方法:例如,player1.nameplayer1.itemsplayer1.addItemplayer1.getInfoplayer1.showInfo都是可访问的。

11.5.2. 更新的玩家构造函数——一些变量被隐藏

为了控制用户对玩家数据的访问,你可以使用参数和变量,而不必为this对象的属性赋值。在函数定义的开括号之间,参数的作用就像函数体内声明的变量。它们是函数本地的,可以在函数的任何地方使用,但不能被函数外的代码访问。

var Player = function (name, health) {

    // name and health are local variables.
    // They can be used here.   

    this.getHealthInfo = function () {
        // name and health can be used here.       
    };
};

// name and health can NOT be used here.
// This is outside the scope of the player function.

图 11.8 显示了从旧的Player构造函数(列表 11.13)到新的(列表 11.14)中变量和属性可见性的变化。新的构造函数隐藏了局部作用域中的大多数函数,只将四个函数分配给特殊的this对象作为接口。

图 11.8. 在构造函数的局部作用域中隐藏变量和函数

图片

下一列表显示了代码中的变化;使用var声明的局部变量用于防止直接访问玩家的数据。

列表 11.14. 在构造函数中隐藏玩家信息(jsbin.com/fuyaca/edit?js,console)

图片

图片

你将不想在外部构造函数中看到的属性和函数分配给局部变量。你希望公开的方法作为this对象的属性分配。

var items = [];    // Keep private

this.showInfo = function () { };    // Make public

setPlacegetPlace方法使用户能够访问place变量。如果你只是提供访问它的方法,为什么还要费心使用变量来使place私有?它们提供了一个接口,并允许你隐藏实现细节,同时也让你能够控制访问。当调用setPlace方法时,你可以在将其分配给place变量之前检查destination参数是否是一个有效的位置。当调用getPlace方法时,你可以在返回位置之前检查用户是否有权访问。你还没有实施这些额外的检查,但你的两个方法已经准备好在你需要添加任何条件时使用。

你还简化了showInfo显示的信息,使其不再包括当前位置。Place对象有自己的showInfo方法,因此没有必要重复显示位置详情作为玩家信息的一部分。当你现在在控制台显示玩家信息时,它看起来像这样:

****************************************
* Kandra (50)                          *
****************************************
  Items:
   - The Sword of Doom
****************************************

玩家的健康状态显示在他们名字的括号中。要显示关于玩家当前位置的信息,你可以调用getPlace来检索位置对象,然后调用该位置对象的showInfo方法:

var place = player.getPlace();
place.showInfo();

11.6. 密室——隐藏位置信息

你现在对Place构造函数所做的,与对Player构造函数所做的相同。你隐藏了数据,并提供了按需访问它的方法。下一个列表显示了之前Place构造函数的结构,来自第十章,数据和方法都设置在this对象上。

列表 11.15. 位置构造函数 (jsbin.com/kavane/edit?js,console)

下一个列表显示了你的更新后的Place构造函数版本,使用参数和变量来隐藏数据,一个新的getExit函数来返回指定方向的目的地,以及一个新的getLastItem方法来返回物品数组中的最后一个项目。

列表 11.16. 在构造函数中隐藏位置信息 (jsbin.com/riviga/edit?js,console)

在创建一个小的方法集——你的接口——通过this对象使其可见的同时,你在PlayerPlace构造函数中隐藏了某些属性和函数。你的程序的其他部分使用构造函数(或者可能在他们的冒险游戏程序中使用你的代码的程序员)只能使用接口中的方法。

对于在控制台输入命令的玩家,你将通过防止他们意外地发现并更改重要的游戏值来保留他们对游戏的乐趣。(作弊是一个如此令人讨厌的词。)你将向玩家展示一组小而简单的动作,他们可以用来探索、收集、打击和摧毁——在他们穿越密室的过程中,任何适当的事情。

11.7. 密室——用户交互

在第十章中,您创建了一个版本的《密室》,允许玩家从一个地点移动到另一个地点并捡起他们找到的物品。不幸的是,您将所有变量都污染了全局命名空间。不要为此自责;那时你还年轻且天真。现在您有了一种方法来隐藏实现,不让用户看到。

列表 11.17 展示了当前实现的概要;完整版本在 JS Bin 上。它与第十章的版本几乎相同,但使用了本章构造函数更新中的getPlacesetPlace玩家方法以及getExitgetLastItem地点方法。使用它们的代码已全部展示。

列表 11.17. 游戏中有许多全局变量 (jsbin.com/dateqe/edit?js,console)
// The spacer namespace
var spacer = { /* formatting functions */ };

// Constructors
var Player = function (name, health) { ... };
var Place = function (title, description) { ... };

// Game controls
var render = function () {
    console.clear();
    player.getPlace().showInfo();
    player.showInfo(); 
};

var go = function (direction) {
    var place = player.getPlace();
    var destination = place.getExit(direction);
    player.setPlace(destination);
    render();
    return "";
};

var get = function () {
    var place = player.getPlace();
    var item = place.getLastItem();

    player.addItem(item);
    render();
    return "";
};

// Map
var kitchen = new Place("The Kitchen", "You are in a kitchen...");
var library = new Place("The Old Library", "You are in a library...");

kitchen.addItem("a piece of cheese");
library.addItem("a rusty key");

kitchen.addExit("south", library);
library.addExit("north", kitchen);

// Game initialization
var player = new Player("Kandra", 50);
player.addItem("The Sword of Doom");
player.setPlace(kitchen);

render();

注意所有全局变量——spacer命名空间、PlayerPlace构造函数、游戏控制函数、所有地点以及玩家——都被分配到任何函数之外的外部变量中。这是臭名昭著的全局污染。只有游戏控制函数goget需要对玩家可用。您可以隐藏其余部分。

11.7.1. 接口——前往和获取

目前您希望用户执行的操作只有两个:

game.go("north"); // Move; e.g. to the place north of the current location
game.get();       // Pick up an item from the current location

要隐藏实现,您将游戏代码封装在一个函数中。然后,为了允许用户执行两个期望的操作而不再执行更多操作,您从函数中返回一个接口对象,包含两个方法:

return {
    go: function (direction) {
        // Move to a new place       
    },

    get: function () {
        // Pick up an item       
    }
};

接口方法使用玩家和地点对象的功能来完成它们的工作,这些对象由各自的构造函数创建。

11.7.2. 隐藏实现

为了隐藏代码的其余部分,您将其封装在一个函数中,以创建局部作用域。只返回您的接口对象。下面的列表显示了更新后的代码的亮点。完整的列表可在 JS Bin 上找到。

列表 11.18. 通过接口对象让用户与游戏交互 (jsbin.com/yuporu/edit?js,console)

列表的最后一条语句调用了getGame函数。函数执行后返回具有goget方法的接口对象。该语句将接口对象赋值给game变量。玩家可以通过使用点符号通过game变量访问goget方法:

game.get();
game.go("east");

玩家无法访问游戏中的任何其他变量或函数。您的工作完成了!至少对于这一章来说。

将代码包裹在函数中,然后从该函数返回接口对象的wrap-and-return过程称为模块模式。这是一种常见的将接口与实现分离的方法,有助于打包你的代码以实现可移植性和重用。从现在开始,你将经常看到它,尤其是在第十三章([kindle_split_022.html#ch13])中,当你使用 HTML script标签调查导入模块时。在你到达那里之前,你需要在第十二章([kindle_split_021.html#ch12])中做出一些决定。如果你准备好了,请继续阅读;否则休息一下,放松一下,尝试进一步冒险,并做好准备。

11.8。总结

  • 减少程序中的全局变量数量。全局变量是在所有函数外部声明的变量。它们可以在程序的任何地方访问,但会污染全局命名空间,暴露你的实现,可能造成命名冲突,并可能帮助引入难以发现的错误:

    var myGlobal = "Look at me";
    
    var useGlobal = function () {
        console.log("I can see the global: " + myGlobal);
    };
    
    console.log("I too can see the global: " + myGlobal);
    
  • 通过收集相关变量和函数作为单个对象的属性来减少全局变量的数量。这样的对象通常被称为命名空间:

    var singleGlobal = {
        method1 : function () { ... },
        method2 : function () { ... }
    };
    
  • 将代码包裹在函数中以创建局部作用域,局部变量的集合:

    var getGame = function () {
        var myLocal = "A local scope, for local people.";
        console.log(myLocal);  // Displays message 
    };
    
    console.log(myLocal);  // ERROR! myLocal is not declared here.
                           // It is not in the global scope.
    
  • 将代码包裹在函数中以使变量私有,隐藏你的实现,避免命名冲突,并减少难以发现的错误的风险。

  • 从函数中返回公共接口以清楚地定义用户期望访问的属性和方法(模块模式)。

  • 返回一个单独的函数:

    var getCounter = function () {
        var counter = 0;    // Private
    
        return function () { ... };
    };
    
    var count = getCounter();
    
  • 返回一个对象:

    var getGame = function () {
        // Game implementation
    
        return {
            // Interface methods
        };
    };
    
    var game = getGame();
    
  • 记住,构造函数创建一个局部作用域,就像任何其他函数一样。特殊的this变量会自动返回。它充当公共接口:

    var Counter = function () {
        var counter = 0;    // Private
    
        this.count = function () { ... };
    };
    

第十二章。条件:选择要运行的代码

本章涵盖

  • 使用比较运算符比较值

  • 检查truefalse的条件

  • if语句——仅在满足条件时运行代码

  • else子句——当条件不满足时运行代码

  • 确保用户输入不会破坏你的代码

  • 使用Math.random()生成随机数

到目前为止,你所有的代码都遵循单一路径。当你调用一个函数时,函数体内的每个语句都会被执行。你已经完成了很多工作,并覆盖了 JavaScript 中的许多核心概念,但你的程序缺乏灵活性;它们无法决定是否执行代码块。

在本章中,你将学习如何在满足特定条件时运行代码。突然之间,你的程序可以分支,提供选项、灵活性和丰富性。你可以增加玩家的分数如果他们击中了金桔,如果用户指定了有效的方向,或者如果推文的长度少于 141 个字符。

如果你想了解你的程序如何做出决定,请继续阅读,否则,... 好吧,无论如何都要继续阅读。你真的需要知道这些内容!

12.1。代码的条件执行

首先,创建一个简单的程序,要求用户猜测一个秘密数字。如果他们猜对了,程序会说,“做得好!”控制台交互可能看起来像这样:

> guess(2)
  undefined
> guess(8)
  Well done!
  undefined

undefined的丑陋外观是怎么回事?当你控制台调用一个函数时,它的代码被执行,然后显示其返回值。以下列表中的guess函数没有包含返回语句,因此它自动返回undefined

列表 12.1. 猜数字 jsbin.com/feholi/edit?js,console

guess函数检查用户的数字是否等于秘密数字。它使用严格的相等运算符===,以及一个if 语句,以确保只有在数字匹配时才显示“做得好!”消息。以下几节将更详细地探讨严格的相等运算符和if语句,并介绍else 子句

12.1.1. 严格的相等运算符,===

严格的相等运算符比较两个值。如果它们相等,则返回true;如果不相等,则返回false。你可以在控制台测试它:

> 2 === 8
  false
> 8 === 8
  true
> 8 === "8"
  false
> "8" === "8"
  true

在第三个例子中,你可以看到严格的相等运算符并不认为数字8和字符串"8"相等。这是因为数字和字符串是不同类型的数据。truefalse是第三种类型的数据;它们被称为布尔值。实际上,truefalse是唯一的可能布尔值。布尔值在决定程序下一步应该做什么时很有用;例如,通过使用if语句。

12.1.2. if语句

要在满足指定条件时仅执行代码块,你使用if语句。

if (condition) {
    // Code to execute
}

如果括号中的条件评估为true,则 JavaScript 执行花括号之间的代码块中的语句。如果条件评估为false,则 JavaScript 跳过代码块。注意,在if语句末尾的花括号后面没有分号。

列表 12.1 使用了严格的相等运算符来为条件返回truefalse值。

if (userNumber === secret) {
    console.log("Well done!");
}

代码仅在user-Number的值等于secret的值时将“做得好!”消息记录到控制台。例如,假设secret8,而用户选择了2

if (2 === 8) {                   // The condition is false.
    console.log("Well done!");   // Not executed
}

如果用户选择8,则if语句变为

if (8 === 8) {                   // The condition is true.
    console.log("Well done!");   // This is executed.
}

12.1.3. else子句

有时候我们希望在if语句中的条件评估为false时执行不同的代码。我们可以通过在if语句后附加一个else子句来实现这一点(图 12.1)。

图 12.1. 根据条件值执行代码,使用ifelse

从列表 12.2:

if (userNumber === secret) {
    console.log("Well done!");
} else {
    console.log("Unlucky, try again.");
}

如果 userNumbersecret 相等,JavaScript 显示 “做得好!” 否则,它显示 “不幸,再试一次。” 注意在 else 子句末尾的括号后面没有分号。再次假设 secret8,用户选择 2

if (2 === 8) {                             // The condition is false. 
    console.log("Well done!");             // Not executed.
} else {
    console.log("Unlucky, try again.");    // This is executed.
}

如果用户选择 8,则 if 语句变为

if (8 === 8) {                             // The condition is true.
    console.log("Well done!");             // This is executed. 
} else {
    console.log("Unlucky, try again.");    // Not executed.
}

控制台上的猜数字交互可能现在看起来像这样:

> guess(2)
  Unlucky, try again.
  undefined
> guess(8)
  Well done!
  undefined
列表 12.2. 猜数字——else 子句 (jsbin.com/nakosi/edit?js,console)

接下来,你使用局部变量使 secret 变得神秘。

12.1.4. 在函数内部隐藏秘密数字

在 列表 12.2 中,secretguess 变量都在任何函数外部声明。你在 第十一章 中看到了这样做是如何使它们成为全局变量的,可以在控制台和整个程序中访问。这对 guess 来说很好——你希望用户能够猜测数字——但对 secret 来说却是一场灾难——用户可以随意查看和调整其值。如果你在 列表 12.2 中运行代码,你可以在控制台执行以下操作:

> secret          // You can access secret. It's a global variable.
  8
> guess(8)
  Well done!
  undefined
> secret = 20     // You can reset secret to whatever you want. 
  20
> guess(20)
  Well done!
  undefined

这不是一个很好的猜数字游戏!

第十一章 也讨论了如何在 JavaScript 中使用函数创建局部作用域,一个仅在函数内部可访问的变量集合。列表 12.3 使用 getGuesser 函数来隐藏秘密数字。getGuesser 返回的函数被分配给 guess 变量 (图 12.2)。

图 12.2. getGuesser 返回的函数被分配给 guess 变量

guess 是一个全局变量,在控制台可用:

> guess(2)
  Unlucky, try again
  undefined
列表 12.3. 猜数字——使用局部作用域 (jsbin.com/hotife/edit?js,console)

分配给 getGuesser 的函数创建了一个局部作用域,允许你保护 secret 变量不被用户访问。它返回另一个允许用户猜测数字的函数。该函数被分配给 guess 变量。因为猜测检查函数是在 getGuesser 函数创建的局部作用域中定义的,所以它可以访问 secret 变量并执行其检查。

你有一个猜数字游戏,但总是同一个秘密数字。实际上,这根本不是一个秘密数字!让我们利用 JavaScript 的 Math 命名空间中的几个方法,给我们的猜数字游戏增加一些神秘感。

12.2. 使用 Math.random() 生成随机数

Math 命名空间为你提供了一个 random 方法来生成随机数。它总是返回一个大于或等于 0 且小于 1 的数字。在控制台提示符中试一试:

> Math.random()
  0.7265986735001206
> Math.random()
  0.07281153951771557
> Math.random()
  0.552000432042405

显然,你的数字会不同,因为它们是随机的!除非你真的很喜欢猜数字并且有很多空闲时间,否则这些随机数字可能对你来说有点太复杂了。

要驯服数字,将它们放大到所需的范围内,然后转换为整数。因为它们最初小于 1,乘以 10 会使它们小于 10。以下是一系列使用Math.random的赋值示例:

var number = Math.random();             //  0 <= number < 1

要缩放可能的数字,乘以:

var number = Math.random() * 10;        //  0 <= number < 10

要上下调整可能的数字,加或减:

var number = Math.random() + 1;         //  1 <= number < 2

要缩放并移动,先乘后加:

var number = Math.random() * 10 + 1;    //  1 <= number < 11

注意,在最后一个任务中,数字将在 1 到 11 之间;它们可以等于 1,但将小于 11。<=符号表示小于或等于,而<符号表示小于。不等式0 <= number < 1表示数字在 0 到 1 之间,可以等于 0 但不能等于 1(见第 12.3.1 节)。

好的,所以你放大了随机数,但它们仍然有点棘手。在控制台中,你可以看到你生成的数字类型:

> Math.random() * 10 + 1
  3.2726867394521832
> Math.random() * 10 + 1
  9.840337357949466

最后一步是去除每个数字的小数部分,将数字四舍五入到整数。为此,你使用Math命名空间中的floor方法。

> Math.floor(3.2726867394521832)
  3
> Math.floor(9.840337357949466)
  9

floor方法总是向下取整,无论小数是多少:10.00001、10.2、10.5、10.8 和 10.99999 都向下取整到 10,例如。你使用floor来获取返回随机整数的表达式,范围在 1 到 10(包括 1)之间:

var number = Math.random() * 10 + 1               // 1 <= number < 11
var number = Math.floor(Math.random() * 10 + 1)   // 1 <= number <= 10

此外,还有一个Math.ceil方法总是向上取整,还有一个Math.round方法根据数学四舍五入的常规规则进行四舍五入。有关 JavaScript 的Math对象更多信息,可以在Get Programming with JavaScript网站上找到:www.room51.co.uk/js/math.html

列表 12.4 将Math方法应用于实践。guess函数现在返回字符串而不是记录它们;控制台自动显示返回值,整理了交互:

> guess(2)
  Unlucky, try again.
> guess(8)
  Unlucky, try again.
> guess(7)
  Well done!
列表 12.4. 猜测随机数 (jsbin.com/mezowa/edit?js,console)

使用随机数使你的猜测游戏更有趣。但其中并不涉及太多策略;这只是直接的猜测。游戏可以通过在每次猜测后提供更好的反馈来得到改进。

12.3. 使用 else if 的进一步条件

通过为每次猜测获得更好的反馈,用户可以在与你的猜测游戏对抗时发展出更有效的策略。策略游戏总是比猜测游戏更有趣。如果用户的猜测不正确,告诉他们是否过高或过低。

> guess(2)
  Too low!
> guess(7)
  Too high!
> guess(5)
  Well done!

图 12.3 展示了用于生成用户猜测的三种可能反馈类型的条件。

图 12.3. 嵌套条件可以提供多个选项。

下面的列表展示了如何使用额外的if语句来区分两种类型的错误答案。

列表 12.5. 高于或低于 (jsbin.com/cixeju/edit?js,console)

如果代码块只包含一个语句,JavaScript 允许我们省略大括号;以下三个语句是等价的:

if (userNumber === secret) {
    return "Well done!";
}

if (userNumber === secret)
    return "Well done!";

if (userName === secret) return "Well done!";

ifelse子句变得复杂,并且代码随时间更新时,如果你省略了大括号,有时很难找到哪些语句与哪些子句相对应。许多程序员(包括我)建议你始终为代码块使用大括号(除了嵌套if语句的情况,如稍后所示)。其他人则不那么严格。最终,这可以归结为个人(或团队)的偏好。现在,我会选择你最容易理解的方式。

一个if语句,即使带有else子句,也只算作一个语句。当else子句只包含一个if语句时,通常省略大括号。以下三个代码片段是等价的:

首先,如列表 12.5 中所示。嵌套的if-else语句位于一对大括号内。

else {                              // Curly braces at start
    if (userNumber > secret) {
        return "Too high!";
    } else {
        return "Too low!";
    }
}                                   // Curly braces at end

内层的if-else是一个单独的语句,因此不需要用大括号括起来。

else                                // No curly braces  
    if (userNumber > secret) {
        return "Too high!";

    } else {
        return "Too low!";
    }
                                    // No curly braces

最后,由于 JavaScript 主要忽略空格和制表符,内层的if-else语句可以移动到初始else之后。

else if (userNumber > secret) {     // if moved next to else 
    return "Too high!";
} else {
    return "Too low!";
}

最后一个版本是最常见的格式。下一个列表显示了上下文中的更整洁的else-if块。

列表 12.6. 一个更整洁的 else-if 块 (jsbin.com/cidoru/edit?js,console)
var getGuesser = function () {
    var secret = Math.floor(Math.random() * 10 + 1);

    return function (userNumber) {
        if (userNumber === secret) {
            return "Well done!";
        } else if (userNumber > secret) {
            return "Too high!";
        } else {
            return "Too low!";
        }
    };
};

var guess = getGuesser();

第二个if语句以粗体显示,以便与列表 12.5 进行比较。你移除了第一个else块的大括号,并将第二个if移动到第一个else旁边。列表 12.6 显示了编写else-if块最常见的方式。如果你更喜欢列表 12.5 中的较长的版本,请随意坚持使用它;没有块法官在等待对你进行语法滥用判决。(在此处,作者被叫去处理一场骚乱——他公寓门上非常响亮的撞击声……是法律!

猜测游戏中考虑了所有可能的结果;猜测可能是正确的,也可能太高或太低。如果猜测不正确且不是太高,那么它一定是太低了。

12.3.1. 比较运算符

列表 12.5 和 12.6 都使用了大于运算符 >。它作用于两个值并返回truefalse。它是比较两个值的一组运算符之一。其中一些运算符在表 12.1 中显示。

表 12.1. 比较运算符
运算符 名称 示例 评估结果
> 大于 5 > 3 3 > 10 7 > 7 true false false
>= 大于或等于 5 >= 3 3 >= 10 7 >= 7 true false true
< 小于 5 < 3 3 < 10 7 < 7 false true false
<= 小于或等于 5 <= 3 3 <= 10 7 <= 7 false true true
=== 严格等于 5 === 3 7 === 7 7 === "7" false true false
!== 不严格等于 5 !== 3 7 !== 7 7 !== "7" true false true

因为表 12.1 中的运算符返回truefalse,它们可以用作if语句的条件。你可能想知道严格相等运算符的严格部分——我们将贯穿整本书——以及是否存在非严格版本。是的,存在。对于非严格相等,你可以使用==。参见“宽松相等和强制转换”侧边栏。

宽松相等和强制转换

宽松相等运算符 == 允许将值强制转换为不同类型以便进行比较。

强制转换是将一个值从一种类型转换为另一种类型的过程,例如,从字符串转换为数字。

因此,虽然严格比较7 === "7"的结果是false,因为一个值是数字,另一个是字符串,但宽松比较7 == "7"的结果是true,因为字符串首先被转换为数字,7 == 7true

强制转换的规则超出了本书的范围(尽管可以忽略那些说它们不值得学习的人),我们将坚持使用严格相等比较。

现在,显然,猜测数字是非常有趣的,但你可以从像书中几次提到的基于事实的测验中学到更多。添加检查答案的能力将有助于提升测验应用,使其不仅仅是一个简单的游戏。

12.4. 在测验应用中检查答案

现在你可以在if语句中检查条件了,你终于可以保留用户在测验程序中答对问题的数量了。一个典型的控制台交互可能是这样的:

> quiz.quizMe()
  What is the highest mountain in the world?
> quiz.submit("Everest")
  Correct!
  Your score is 1 out of 1
> quiz.quizMe()
  What is the highest mountain in Scotland?
> quiz.submit("Snowdon")
  No, the answer is Ben Nevis
  You have finished the quiz
  Your score is 1 out of 2

测验程序代码在下一列表中展示。getQuiz函数包含了测验的实现,并返回一个只包含两个方法(quizMesubmit)的接口对象。在列表之后,你可以仔细看看程序是如何工作的。

列表 12.7. 检查测验答案 (jsbin.com/hidogo/edit?js,console)

你新的测验程序有几个动态部分;让我们将其分解成更小的部分。

12.4.1. 使用单个var关键字进行多重声明

到目前为止,你一直在为每个声明的变量使用var关键字:

var score;
var getQuestion;
var next;
var submit;

JavaScript 允许你使用单个var关键字声明一系列变量。变量之间用逗号分隔,列表以分号结束。之前的声明可以用以下更简短的形式重写:

var score,
    getQuestion,
    next,
    submit;

你甚至可以在一行中声明变量:

var score, getQuestion, next, submit;

大多数程序员喜欢每行一个变量。你还可以包括赋值:

var score = 0,
    getQuestion,
    next,
    submit = function (userAnswer) {
        // function body  
    };

目标是确保所有变量都已声明,代码易于阅读和理解。在列表 12.7 中的风格是我倾向于选择的;我发现它稍微容易阅读一些,而且输入的代码也稍微少一些。一些程序员使用var关键字在单独的一行上声明每个变量,就像我们到目前为止在列表中做的那样;如果每个变量都有自己的var关键字,那么剪切和粘贴变量会更容易。这并不值得担心——你可能会随着时间的推移而确定一种风格。

12.4.2. 显示问题

getQuestion函数从questions数组中返回一个问题。它使用qIndex变量从数组中选择当前的问题和答案对象。它返回问题和答案对象的question属性。

return questions[qIndex].question;

但只有当测验仍在进行时,它才会返回问题。否则,它返回一个字符串说明测验已完成:

return "You have finished the quiz.";

程序使用inPlay变量来标记测验正在进行和已完成。当测验进行时,inPlay变量的值为true,当测验完成后为falsegetQuestion函数使用inPlay变量作为if语句中的条件:

if (inPlay) {
  return questions[qIndex].question;

} else {
  return "You have finished the quiz.";
}

inPlaytrue时,返回问题。当inPlayfalse时,返回消息。(记住,当你从控制台提示符调用函数时,控制台会自动显示返回值。)

12.4.3. 转到下一个问题

程序调用next函数从一个问题移动到下一个问题。它通过增加qIndex变量来实现移动。

qIndex = qIndex + 1;

程序将questions数组中当前元素的索引存储在qIndex中。记住,数组索引是从零开始的,所以对于一个长度为 4 的数组,索引可以是 0、1、2 或 3。索引 4 将超出数组的末尾(3 是最后一个索引)。一般来说,如果索引大于或等于数组的长度,你就超出了数组的末尾。所有数组都有一个length属性。在测验中,它代表问题的数量。

next函数检查索引是否超出了最后一个问题:

if (qIndex >= questions.length)

如果索引超出了数组的末尾,那么所有问题都已提出,测验结束,因此inPlay被设置为false

if (qIndex >= questions.length) {
  inPlay = false;
  console.log("You have finished the quiz.");
}

12.4.4. 检查玩家的答案

checkAnswer函数很简单。如果玩家提交的答案等于问题数组中的当前答案,则玩家的分数增加。否则,显示正确答案。

if (userAnswer === questions[qIndex].answer) {
  console.log("Correct!");
  score = score + 1;
} else {
  console.log("No, the answer is " + questions[qIndex].answer);
}

12.4.5. 处理玩家的答案

submit函数协调玩家提交答案时发生的事情。它返回一个包含玩家分数的消息或表示测验结束的消息。

Your score is 1 out of 2      // If inPlay is true 

You have finished the quiz.   // If inPlay is false

如果测验仍在进行中,submit会调用两个其他函数,checkAnswernext。每个函数将依次执行其代码。你正在使用函数按需运行代码。

if (inPlay) {
  checkAnswer(userAnswer);
  next();
  message = "Your score is " + score + " out of " + qIndex;
}

12.4.6. 返回界面对象

你保持了getQuiz返回的接口对象简单。它没有自己的实现代码。你从getQuiz的局部作用域中分配其两个属性函数。

return {
  quizMe: getQuestion,
  submit: submit
};

如第十一章第十一章中所述,接口对象允许你在时间上保持一致的接口,即使getQuiz内部的实现发生了变化。用户将始终调用quiz.quizMe()quiz.submit()。你可以更改分配给接口对象这两个属性的函数以及这些函数的工作方式,但你永远不要删除或重命名这些属性。

注意程序是如何由许多小部分组成,共同构建其功能的。一如既往,你的目标是使代码可读、易懂且易于跟踪。if语句及其else子句帮助你指导程序的流程,在每个阶段采取适当的行动。

是时候将这些新想法应用到The Crypt上了。

12.5. The Crypt—检查用户输入

在第十一章中,你创建了一个getGame函数,它返回了The Crypt的公共接口。玩家可以调用go方法从一个地方移动到另一个地方,以及调用get方法来拾取物品:

return {
    go: function (direction) {
        var place = player.getPlace();
        var destination = place.getExit(direction);
        player.setPlace(destination);

        render();
        return "";
    },

    get: function () {
        var place = player.getPlace();
        var item = place.getLastItem();
        player.addItem(item);
        render();
        return "";
    }
};

12.5.1. 逐步分析go方法

让我们逐步分析go方法的前三行。看看你是否能发现可能出现问题的位置。

获取玩家的位置

你从getPlace方法开始。它返回玩家的当前位置。

var place = player.getPlace();

然后你将位置分配给place变量。如果玩家目前位于厨房,那么代码等同于

var place = kitchen;

程序之前使用setPlace方法分配了玩家的起始位置:

player.setPlace(kitchen);
使用方向找到目的地

现在你有了当前位置,你可以调用它的getExit方法来获取给定方向的目的地。

var destination = place.getExit(direction);

当玩家调用go方法时,参数被分配给direction参数。

> game.go("south")

之前的命令将执行以下等效代码:

var destination = place.getExit("south");

如果图书馆在厨房的南方,那么代码等同于

var destination = library;
将玩家移动到目的地

你已经有了目的地;你只需要更新玩家的位置。

player.setPlace(destination);

太棒了!用户可以决定在游戏中去哪里。那么,你能让他们在你的精心设计的城堡中自由行动吗?不,你看到,用户是邪恶的。纯粹的邪恶!

12.5.2. 永远不要相信用户输入

抱歉,我慌了。当然,用户并不邪恶。但他们确实会犯错误。他们有时会养猫。而且大多数猫都不会打字。每当需要用户为程序提供输入时,我们必须防范错误,无论是打字错误(可能是猫引起的),还是误解(可能是我们的错),或者是出于对程序能做什么的好奇心而进行的探索。

go 方法期望用户输入一个有效的方向作为字符串。它使用该方向来查找目的地,即玩家将被移动的地方。如果用户输入一个不存在的方向,整个游戏就会崩溃!

> game.go("snarf")

图 12.4 展示了在播放 The Crypt 的 第十一章 版本时,我在 JS Bin 中输入上一条命令所发生的情况。jsbin.com/yuporu/edit?js,console

图 12.4. 指定一个不存在的方向会导致 The Crypt 游戏崩溃。

浏览器上的错误消息可能略有不同。即使在错误之后输入一个有效的方向也无法解决问题。从 图 12.4 中的错误来看,似乎 place 变量存在问题。go 方法中的关键语句是使用用户输入的那个:

var destination = place.getExit(direction);

如果指定的方向不是地点的出口之一,那么 getExit 函数将返回 undefined。程序将 undefined 赋值给 destination 变量,并将该值设置为玩家的新位置:

player.setPlace(destination);

因此,玩家的位置现在是 undefined,而不是使用 Place 构造函数构建的地方。undefined 没有提供 showInfogetExit 方法;它没有任何方法!图 12.4 中的错误现在应该更容易理解。

那么如何防止用户(以及他们的猫)犯错误呢?

12.5.3. 安全探索——使用 if 语句避免问题

你可以使用 if 语句在更新玩家位置之前检查是否有有效的目的地:

go: function (direction) {
    var place = player.getPlace();
    var destination = place.getExit(direction);

    if (destination !== undefined) {
        player.setPlace(destination);
        render();
        return "";
    } else {
        return "*** There is no exit in that direction ***";
    }
}

如果当前地点没有指定方向的出口,getExit 方法将返回 undefined。你只需要在调用 setPlace 之前检查目的地是否不是 undefined

if (destination !== undefined) {
    // There is a valid destination.   
}

记住从 表 12.1 中,!== 操作符在两个值不相等时返回 true,在它们相等时返回 false。你可以添加一个 else 子句来捕获目的地是 undefined 的情况。

if (destination !== undefined) {
    // There is a valid destination. 
} else {
    // There is no exit in the direction specified.   
}

列表 12.8 展示了从 getGame 函数返回的 goget 方法的更新版本。在控制台中输入一个不存在的方向现在看起来是这样的:

> game.go("snarf")
  *** You can't go in that direction ***

当没有物品可以拾取时调用 get 的样子如下:

> game.get()
  *** There is no item to get ***

此列表中只显示了部分代码。包含 PlayerPlace 构造函数以及更多地点的完整列表在 JS Bin 上。

列表 12.8. 检查用户输入 (jsbin.com/zoruxu/edit?js,console)

在打印的列表 12.8 中,省略了PlayerPlace构造函数的细节,以便更容易关注对goget方法的变化。在第十三章[kindle_split_022.html#ch13]中,你将每个构造函数移动到自己的文件中,并了解如何在 JS Bin 中导入文件。这种增加的模块化可以帮助你一次关注一件事,并使在多个项目中重用代码更容易。

12.6. 摘要

  • 使用比较运算符比较两个值。运算符返回truefalse,布尔值:

    >  5 === 5    // Strict equality       
       true
    >  10 > 13    // Greater than        
       false
    
  • 使用if语句仅在满足条件时执行代码:

    if (condition) {
        // Execute code if condition evaluates to true
    }
    
  • 使用比较运算符和/或变量设置条件:

    if (userNumber === secret) {
        // Execute code if userNumber and secret are equal 
    }
    
    if (inPlay) {
        // Execute code if inPlay evaluates to true 
    }
    
  • 当条件不满足时,添加else子句以执行代码:

    if (condition) {
        // Execute code if condition evaluates to true
    } else {
        // Execute code if condition evaluates to false
    }
    
  • else子句中包含额外的if语句以覆盖所有可能性:

    if (userNumber === secret) {
        console.log("Well done!");
    } else if (userNumber > secret) {
        console.log("Too high!");
    } else {
        console.log("Too low!");
    }
    
  • 使用Math.random()生成随机数。生成的数字介于 0 和 1 之间。它们可以等于 0 但不能等于 1:

    > Math.random()
      0.552000432042405
    
  • 将随机数扩展到你想要的范围:

    Math.random() * 10                    // 0 <= decimal < 10
    Math.random() * 10 + 1                // 1 <= decimal < 11
    
  • 使用Math.floor()将随机数四舍五入为整数:

    Math.floor(Math.random() * 10 + 1)    // 1 <= integer <= 10
    
  • 永远不要相信用户输入。放置检查以确保任何输入都是有效的。

第十三章。模块:将程序分解成片段

本章涵盖

  • 使用脚本元素将代码导入 JS Bin

  • 避免重复的变量名

  • 不将函数分配给变量而运行它们

  • 使用模块来组织你的(共享)代码库

随着你开发的应用程序变得更大,涉及越来越多的变量、对象、数组和函数,在单个程序文件中高效工作可能会变得越来越困难。好的文本编辑器和开发环境可以帮助,但即使有了它们的工具,很快就会变成一个很好的主意,将代码分散到多个文件中。

例如,在The Crypt中,你有spacer、玩家、地点、地图以及游戏逻辑本身。你可能已经注意到,当所有元素都包含在内时,JS Bin 上的代码列表变得有多长。将每个元素的代码放在自己的文件中可以帮助你一次专注于程序的一个部分,并使不同程序员开发和应用测试应用程序的不同部分更容易。图 13.1 显示了将一个大型程序分解为模块的目标。

图 13.1. 将一个大型程序分解为模块

图片

在不同的文件中拥有离散的功能和数据也促进了代码的重用。与其从一项项目中将有用的函数和 JavaScript 片段剪切粘贴到其他项目中,不如将它们保留在单个库文件中,当需要时将其导入到其他项目中。例如,我们可靠的spacer命名空间来自第七章章节 7,用于在控制台上格式化文本,可以在问答应用和博客应用中使用。与其在每个应用中重复spacer代码,不如将其放在自己的文件中,并在需要时导入它(图 13.2)。

图 13.2. 将spacer移动到模块中允许相同的单个文件在许多项目中使用。

13fig02_alt.jpg

我将松散地称这样的文件为模块。有许多已发布的 JavaScript 项目和模块管理标准,它们将为自己定义模块必须采取的形式,而 JavaScript 的最新版本也在引入原生的模块系统,但我现在很高兴保持简单。

本章将探讨如何使用 HTML script元素将模块导入 JS Bin。你将看到随机数生成函数和spacer命名空间的文本格式化功能如何被纳入其他项目中。当你开始将来自不同模块的代码包含到一个程序中时,你必须密切关注正在使用的变量名;有可能会覆盖你的变量。你还将了解如何通过使用命名空间和利用立即执行函数表达式来最小化这些问题,这是一种运行函数代码而不将其分配给变量的方法。

首先,让我们看看 JS Bin 是如何与文件一起工作的。

13.1. 理解 JS Bin 上的 bins 和文件

在第十二章中,你创建了一个简单的游戏,挑战玩家猜测一个介于 1 到 10 之间的数字。你使用Math.random方法生成这个数字。现在,你希望更新你的测验应用,以显示其问题库中的随机问题。猜数字游戏和测验都需要能够生成两个限制之间的随机整数。它们都可以使用以下类似的功能:

var between = function (lowest, highest) {
    // Return a whole number between lowest and highest inclusive
};

在本节中,你将在 JS Bin 上创建并保存一个包含数字生成器代码的 JavaScript 文件。在第 13.2 节中,你将学习如何加载该文件,在第 13.3 节中,你将如何将其加载到猜数字游戏和测验应用中,如图 13.3 所示。

图 13.3. 测验应用和猜数字游戏导入了数字生成器代码。

13fig03_alt.jpg

在你学习如何导入文件之前,我们需要简要地看看 JS Bin 是如何保存你的工作的。

JS Bin 是一个简单的开发环境,它允许你在单独的面板中工作 HTML、CSS 和 JavaScript 代码。图 13.4 显示了这三个面板同时打开,都包含代码。

图 13.4. JS Bin 上的 HTML、CSS 和 JavaScript 面板

13fig04_alt.jpg

JS Bin 将 HTML、CSS 和 JavaScript 面板中的代码组合起来,生成你的网页,并在输出面板中显示(图 13.5)。你将在第三部分(kindle_split_026.html#part03)中更详细地了解 HTML(以及一点 CSS)。

图 13.5. HTML、CSS 和 JavaScript 代码都被用来生成网页输出。

13fig05_alt.jpg

当您运行代码时出现的任何错误或警告都会显示在控制台面板中。您也可以从 JavaScript 代码中向控制台面板记录消息——这就是您迄今为止显示输出的方式。您专注于 JavaScript,所以您没有关心输出面板中生成的网页。您可以从 图 13.4 和 图 13.5 在 JS Bin 的 jsbin.com/jejunu/edit?output 上看到页面。切换面板以查看代码。

除了提供一个综合环境供您编辑构成网页的不同类型的代码外,JS Bin 还允许您以单独的文件形式访问 HTML、CSS 和 JavaScript 代码。要在 JS Bin 中创建 JavaScript 代码并查看它作为单独的文件,请按照以下步骤操作:

1.  创建一个 bin

2.  在 JavaScript 面板中编写一些代码

3.  记录文件名

4.  查看单个代码文件

13.1.1. 创建一个 bin

在 JS Bin 的文件菜单上点击新建。JS Bin 会为你创建 HTML、CSS 和 JavaScript 文件。它将这些三个文件统称为 bin,并在对应的面板上显示每个文件的内容。HTML 文件包含一些常见的模板代码,适用于大多数新网页;CSS 和 JavaScript 文件为空。

13.1.2. 编写一些代码

将以下代码添加到 JavaScript 面板中。

var between = function (lowest, highest) {
    var range = highest - lowest + 1;
    return lowest + Math.floor(Math.random() * range);
};

between 函数返回 lowesthighest 之间的一个随机整数(包括这两个值)。例如,between(3, 5) 将返回 345

13.1.3. 记录文件名

JS Bin 为每个 bin 分配一个用于编辑 bin 文件和单独访问它们的代码。查看浏览器地址栏中的当前 URL。(您可能需要点击地址栏才能看到完整的地址。)图 13.6 展示了带有 bin 代码和可见面板的高亮 URL。

图 13.6. 分解 JS Bin URL

记录您在 JS Bin 上当前工作的 bin 代码——它将不同于 图 13.6。我的工作 bin 代码是 qezoce

13.1.4. 查看单个代码文件

要访问单个代码文件,您使用不同的 URL 格式,如图 图 13.7 所示。它以 output 为前缀,以 bin 代码和文件扩展名结尾。文件扩展名指定了您想要加载的文件类型。使用 js 用于 JavaScript。

图 13.7. JavaScript 文件的 JS Bin URL

访问 output.jsbin.com/qezoce.js 只会加载 JavaScript 文件,如图 图 13.8 所示。JS Bin 编辑环境中的所有面板、菜单和控制都不会加载;它只是 JavaScript 文件的纯文本。

图 13.8. JS Bin 上的 JavaScript 文件

(输出并不总是格式化得很好,以便人类阅读;可能已经删除了不必要的空格和换行符。)尝试使用你的 JS Bin 中的 bin 代码加载你的文件版本。

太好了!你可以接触到你的纯 JavaScript。但是,你如何让它出现在另一个程序中?

13.2. 导入文件到其他项目中

你将创建一个程序,使用上一节中的数字生成函数between。你需要采取以下步骤:

1. 创建一个 bin

2. 在 JavaScript 面板中编写一些代码

3. 在 HTML 面板中添加一个script元素

4. 刷新页面

5. 运行程序

13.2.1. 创建一个 bin

通过点击文件菜单上的“新建”来在 JS Bin 上创建一个 bin。HTML、CSS 和 JavaScript 面板将被重置。

13.2.2. 编写一些代码

在 JavaScript 面板中输入以下代码:

// requires the Number Generator module
var num = between(3, 7);
console.log(num);

现在运行程序将导致错误——没有声明between变量或定义函数。between函数在单独的文件中。这提出了依赖性的问题;当将代码拆分成模块时,一个模块依赖于另一个模块以正常工作并不罕见。前面的代码依赖于数字生成器模块。更高级的模块系统通常允许你明确记录并自动加载依赖项;目前,你可以添加注释来显示任何所需的模块。

13.2.3. 添加一个script元素

是时候利用 JS Bin 上的 HTML 面板了。HTML 是用于网页结构和内容的代码;它是如何指定标题、段落、列表和链接等的。你将在第十七章中得到适当的介绍,并在第三部分(kindle_split_026.html#part03)中充分利用它。现在,你的重点仍然非常集中在 JavaScript 上,并使用 JS Bin 来帮助你学习和探索。你将只使用一小段 HTML 来帮助你将较长的程序拆分成单独的文件,并在需要时加载它们。

你使用 HTML 的script元素来指定你想要加载的 JavaScript 文件。图 13.9 显示了组成该元素的各个部分。

图 13.9. 组成script元素的各个部分

目前不必太担心所有部分的名称。你可以使用script元素来加载文件,而不需要完全理解 HTML 元素、标签和属性。你将在第十七章中专注于这些内容。

在 JS Bin 上显示 HTML 面板。你会看到一些默认的 HTML 已经就位。对于你的目的来说,你不需要这些——你并不是在构建一个网页;你只是对加载一个 JavaScript 文件感兴趣。用你上一节创建的文件的 bin 代码替换默认的 HTML,以加载 JavaScript。

<script src="http://output.jsbin.com/qezoce.js"></script>

包含的 HTML 是一个带有 src 属性的单个 script 元素。您使用 script 元素来加载由 src 属性指定的 JavaScript 文件。(srcsource 的缩写——文件的地址。)通常,要加载文件,请使用以下格式:

<script src="path/to/someFile.js"></script>

现代浏览器会假设文件包含 JavaScript。对于旧浏览器,您还可以包含一个 type 属性。

<script src="path/to/someFile.js" type="text/javascript"></script>

13.2.4. 刷新页面

JS Bin 并非总是自动加载文件;您可能需要在浏览器中刷新页面,在添加 script 元素后。

13.2.5. 运行程序

点击运行。控制台面板应显示一个介于 3 和 7 之间的数字。继续点击运行以生成更多随机数字。

列表 13.1 和 13.2 重复了您使用的 HTML 和 JavaScript。这两个列表的 JS Bin 链接都指向同一个 bin。运行程序五次会产生类似以下输出(它是随机的!):

> 5
> 7
> 7
> 3
> 4
列表 13.1. 使用 script 标签加载 JavaScript(HTML)(jsbin.com/lifugam/edit?html,js,console
<script src="http://output.jsbin.com/qezoce.js"></script>
列表 13.2. 在 JavaScript 面板中的代码(jsbin.com/lifugam/edit?html,js,console
// requires the Number Generator module
var num = between(3, 7);
console.log(num);

当您运行程序时,JS Bin 将首先加载并运行 script 元素的 src 属性中指定的文件。然后它将运行 JavaScript 面板中的任何代码。加载的文件和 JavaScript 面板代码一起形成以下单个程序:

// From the loaded file
var between = function (lowest, highest) {       

    var range = highest - lowest + 1;            
    return lowest + Math.floor(Math.random() * range);

};                                               

// From the JavaScript panel
var num = between(3, 7);             
console.log(num);

当您在控制台面板中点击运行按钮时,JS Bin 可能需要一段时间来加载 script 标签中指定的文件。一旦代码运行,随机数就会被记录到控制台。

13.3. 导入数字生成器——更多示例

您已经看到了 JS Bin 如何为每个创建的 bin 分配一个代码,以及您如何使用该代码来访问项目中的单个文件。您创建了一个随机数生成器函数 between,并访问了包含代码的 JavaScript 文件。将工作拆分成模块的一个目标是通过导入而不是复制粘贴来在多个项目中使用相同的代码;请参阅图 13.10。

图 13.10. 将数字生成器函数导入两个项目

图片 13fig10_alt.jpg

让我们通过将数字生成器导入到其他两个项目中来验证这个想法:测验应用和猜谜游戏。

13.3.1. 在测验应用中挑选随机问题

是时候随机化您的测验应用了。应用的新版本每次在控制台调用 quizMe 时都会从其题库中显示一个随机问题:

> quiz.quizMe()
  5 x 6
> quiz.submit("30")
  Correct!
> quiz.quizMe()
  7 x 8
> quiz.submit("30")
  No, the answer is 56

列表 13.4 展示了主测验应用的 JavaScript 代码。您可以通过 HTML 面板上的 script 元素导入 between 函数的代码(列表 13.3)。

列表 13.3. 在测验应用中使用数字生成器(HTML)(jsbin.com/ponogi/edit?html,js,console
<script src="http://output.jsbin.com/qezoce.js"></script>
列表 13.4. 在问答应用中使用数字生成器 (jsbin.com/ponogi/edit?html,js,console)

你使用 between 函数从题库中随机选择一个问题。questions 数组中的元素数量由 questions.length 给出(每个数组都有一个 length 属性),问题索引从 0 开始,到长度减 1。如果数组中有四个元素,那么索引从 0 到 3。因此,要选择一个随机索引,你使用

qIndex = between(0, questions.length – 1);

你使用了 第十一章 中的 wrap-and-return 模块模式,将实现(使一切工作的代码)隐藏在 getQuiz 函数内部,并作为对象返回公共接口。

点击列表链接访问 JS Bin 上的游戏并测试你对乘法事实的了解!答案存储为字符串,所以请确保你提交字符串以便程序进行检查:quiz.submit("30"),而不是 quiz.submit(30)

13.3.2. 在你的猜测游戏中使用 between 函数

列表 13.6 展示了你的猜测游戏的 JavaScript 代码。在控制台提示符下,玩家必须猜测一个介于 5 和 10 之间的数字(包括 5 和 10):

> guess(7)
  Too high!
> guess(5)
  Too low!
> guess(6)
  Well done!

应用程序使用 between 函数,因此你需要在 HTML 面板上的 script 元素中导入它(见以下列表)。

列表 13.5. 在猜测游戏中使用数字生成器(HTML)(jsbin.com/tixina/edit?html,js,console)
<script src="http://output.jsbin.com/qezoce.js"></script>
列表 13.6. 在猜测游戏中使用数字生成器 (jsbin.com/tixina/edit?html,js,console)

再次,点击列表链接访问 JS Bin 并开始猜测吧!

问答应用和猜测游戏现在都导入相同的数字生成器文件。数字生成代码在一个地方;这是一个唯一的真相来源。任何更新或修复都可以在这个文件上执行,所有使用它的项目都将加载新版本。

导入一个文件很有用,但你能否加载多个文件呢?

13.4. 导入多个文件

在 第七章 和 第十一章 中,你看到可以使用对象作为命名空间,这是一种组织属性和方法的方式,以便只需要一个变量。作为一个例子,你创建了 spacer,一个用于在控制台上格式化文本的函数命名空间。spacer 命名空间在许多项目中都可能很有用,无论何时你想用边框和框来格式化你的文本输出。与其将 spacer 代码复制粘贴到每个使用它的程序中,不如将其保存到自己的文件中,并在需要时导入。你可以在 jsbin.com/juneqo/edit?js 查看spacer 代码。

让我们立即使用spacer。在列表 13.6 中,你更新了你的猜测游戏,以导入between函数并使用它来生成要猜测的秘密数字。假设你现在想通过将消息包装在框中来格式化你提供给玩家的反馈。这正是spacer的作用!控制台游戏看起来可能像这样:

> guess(10)                  > guess(5)                  > guess(9)
  +++++++++++++                ------------                ==============
  + Too high! +                - Too low! -                = Well done! =
  +++++++++++++                ------------                ==============

图 13.11 显示了猜测游戏应用导入Number Generatorspacer模块。

图 13.11. 导入Number Generatorspacer模块

下面的列表显示了添加到 HTML 面板中的脚本元素,用于导入你正在使用的两个模块。

列表 13.7. 在猜测游戏中使用spacerbetween(HTML)(jsbin.com/foqowa/edit?html,js,console)
<!-- Number Generator -->
<script src="http://output.jsbin.com/qezoce.js"></script>

<!-- spacer -->
<script src="http://output.jsbin.com/juneqo.js"></script>

添加了注释来标记正在导入的模块;JS Bin 地址不太友好,所以清楚地说明你试图加载的内容很有帮助。这些注释是 HTML 注释,所以它们看起来与 Java-Script 注释略有不同。

下一个列表显示了使用两个导入模块betweenspacer的猜测游戏代码。

列表 13.8. 在猜测游戏中使用spacerbetween(jsbin.com/foqowa/edit?html,js,console)

将之前编写和测试过的代码放入这样的单独文件中也有助于你专注于正在工作的新代码;列表 13.8 可以简短而甜蜜,因为你的信任的spacer代码被包装在外部文件中。

当你导入 JavaScript 时,就像所有导入的代码都被合并成一个单独的文件。如果不同的导入文件使用了相同的变量名,后来的代码可能会无意中覆盖早期的代码。嘭——你遇到了变量冲突!

13.5. 冲突——当导入的代码覆盖你的变量时

你认为你的猜测游戏反馈中的框式消息有点过多。你希望反馈看起来像这样:

> guess(10)                        > guess(9)
  + T-o-o- -h-i-g-h-! +              = W-e-l-l- -d-o-n-e-! =

消息占用的空间更少,但字符之间用破折号分隔得很好。幸运的是,你的一个朋友,Kallie,一直在开发她自己的格式化函数,并且已经友好地将它们打包成一个你可以导入的模块。你需要的功能叫做dasher,使用起来非常简单:

dasher("message");     // m-e-s-s-a-g-e   
dasher("Too low!");    // T-o-o- -l-o-w-!

你向猜测游戏的 HTML 面板添加一个脚本元素来导入 Kallie 的代码,如下所示。

列表 13.9. 导入 Kallie 的代码(HTML)(jsbin.com/zusodu/edit?html,js,console)
<!-- Number Generator -->
<script src="http://output.jsbin.com/qezoce.js"></script>

<!-- spacer -->
<script src="http://output.jsbin.com/juneqo.js"></script>

<!-- Kallie's code -->
<script src="http://output.jsbin.com/soxeke.js"></script>

不,我还没有向你展示 Kallie 的代码中的 JavaScript。你知道dasher是界面的一部分,是你预期使用的变量和函数,你也知道dasher的功能;你不需要知道它是如何做到的,即实现。当然,你可能(并且可能应该)对实现感兴趣,但你不需要理解它就能使用界面,dasher函数。

你更新猜谜游戏以使用dasher函数,如下所示。

列表 13.10. 使用 Kallie 的代码 (jsbin.com/zusodu/edit?html,js,console)

图片

对于你的工作感到满意,你运行程序并做出猜测。错误!什么?spacer.wrap去哪里了?图 13.12 显示了在我的浏览器中发生的情况。

图 13.12. 由于某种原因,程序找不到spacer.wrap

图片

spacer.wrap似乎出了问题,它一直是你的spacer命名空间中工作的一部分,从第七章(kindle_split_015.html#ch07)开始。为什么它现在选择崩溃?是时候检查 Kallie 的模块内部了。如果你查看以下列表中的她的代码,你可能会发现问题。

列表 13.11. Kallie 的格式化代码 (jsbin.com/soxeke/edit?js,console)

图片

Kallie 的代码不仅包括dasher函数;它还有一个spacer函数。她使用的三个变量,spreaderspacerdasher,都是全局变量。你的spacer命名空间也使用一个全局变量,spacer。她的spacer已经覆盖了你的spacer,如图 13.13(#ch13fig13)所示。

图 13.13. 两个模块声明了相同的全局变量,导致冲突。

图片

问题不在于函数的实现——它们工作得很好——而在于函数如何在模块内部提供。当变量被添加到全局作用域中,可能是由不同模块中的不同人添加的,总有可能在多个声明中使用相同的名称。

13.5.1. 变量冲突

你的模块通过使用至少一个全局变量来提供其功能。但如果同一个变量被多个模块使用怎么办?嗯,最后一个模块获胜,将其自己的值赋给变量。在猜谜游戏代码中,你加载了两个使用全局spacer变量的模块,然后你试图在程序中稍后使用该变量。以下片段显示了第二个spacer如何覆盖第一个,导致尝试使用spacer.wrap时出错。

// Code in first module
var spacer = {    
    line : ...,     
    wrap : ...,     
    box : ...        
};                 

// Code in second module
var spacer = function (text) { ... };     

// Later in the code
spacer.wrap(msg, msg.length + 4, "=");  // ERROR! There is no spacer.wrap.

当一个声明像这样取代另一个时,这被称为 变量冲突;第二个变量声明覆盖了第一个。你可以理解为什么声明大量全局变量被称为 污染全局命名空间——你声明的变量越多,冲突的可能性就越大。通过使用下一部分讨论的命名空间和立即调用的函数表达式来减少全局变量的数量。

13.5.2. 通过使用命名空间最小化冲突

您的 spacer 命名空间模块表现良好,因为它只使用了一个全局变量。而不是为 linewrapbox 函数分别使用不同的全局变量,它使用一个对象作为命名空间,并将函数分配给对象的属性。然后,这个对象被分配给单个变量,spacer

Kallie 为她的模块造成的污染道歉——她一直很忙——并将模块更新为使用命名空间,如下一清单所示。

清单 13.12. 在命名空间中的 Kallie 格式化代码 (jsbin.com/moheka/edit?js,console)

图片 237

您将猜谜游戏的 HTML 更新为导入更新的模块 (清单 13.13),并将您的 JavaScript 代码更新为在 kalliesCode 命名空间内调用 dasher 函数 (清单 13.14)。

清单 13.13. 导入 Kallie 的命名空间 (HTML) (jsbin.com/seqahi/edit?html,js,console)
<!-- Number Generator -->
<script src="http://output.jsbin.com/qezoce.js"></script>

<!-- spacer -->
<script src="http://output.jsbin.com/juneqo.js"></script>

<!-- Kallie's code -->
<script src="http://output.jsbin.com/moheka.js"></script>
清单 13.14. 使用 Kallie 的代码 (jsbin.com/seqahi/edit?html,js,console)

图片 238

按照清单链接访问 JS Bin,运行程序并玩游戏。使用命名空间已经解决了问题。

= W-e-l-l- -d-o-n-e-! =

避免污染全局命名空间的重要方法之一,这次是通过使用函数,将在下一部分介绍。它有一个很棒的名字...现在就来看看吧!

13.6. 立即调用的函数表达式 (IIFE)

如果这个章节的标题立即在你的脸上引起了古怪的表情,我希望那是一种好奇、兴趣和冒险的感觉,而不是困惑、恐惧或惊恐。立即调用的函数表达式只是你直接调用的函数,甚至都不用使用变量。但你为什么要这样做呢?

在 第 13.5 节 中,您导入了一个由朋友 Kallie 编写的代码模块。(说实话,您已经认识她很多年了。)不幸的是,Kallie 的全局变量与您的冲突了(ouch!)并覆盖了您心爱的 spacer 命名空间。您学到了关于全局变量危险的重要教训,并发誓要尽一切可能减少全局命名空间的污染。您决定回顾一些代码,寻找可以删除的全局变量。

你从本章早些时候的测验应用程序开始复习。以下列表显示了你的代码结构,包括全局变量、局部变量和接口对象。

列表 13.15. 带有两个全局变量的随机测验问题 (jsbin.com/ponogi/edit?html,js,console)

图片

程序声明了两个全局变量:getQuizquiz。在第一行中,你定义了一个函数并将其赋值给getQuiz

var getQuiz = function () { ... };

然后,在最后一行,你立即调用该函数。

var quiz = getQuiz();

因此,你将getQuiz声明为全局变量——这是一种恶臭的污染——然后只使用一次,就让它像恶臭一样悬挂在全局作用域中。真丢脸!记住,全局变量有覆盖其他全局变量的风险;在小段代码中可能看起来不错,但随着项目的增长和模块的创建和导入,不久就会产生一种不安的微弱气息,最终变成绝望的恶臭。

小贴士

避免声明全局变量,污染全局命名空间。通过使用对象作为命名空间来减少全局变量的数量。在函数内声明局部变量。

你可以使用立即调用的函数表达式将全局变量的数量减半。为了理解如何使用 IIFE(立即调用的函数表达式)来提高程序的性能,请考虑以下要点:

  • 识别函数表达式

  • 调用函数

  • 立即调用函数表达式

  • 从 IIFE 返回信息

13.6.1. 识别函数表达式

你从第四章开始就一直在使用函数表达式。你已经将它们赋值给变量和属性,将它们作为参数传递,并从其他函数中返回它们。

var show = function (message) {        //
    console.log(message);              // Assign to a variable
};                                     //

var namespace = {
    show: function (message) {         //
        console.log(message);          // Assign to a property
    }                                  //
};

tweets.forEach(function (message) {    //
    console.log(message);              // Pass as an argument to forEach
});                                    //

var getFunction = function () {
    var localMessage = "Hello Local!";

    return function () {               //
        console.log(localMessage);     // Return from a function
    };                                 //
};

你已经使用函数来创建可以在需要时调用的代码块,以及创建局部作用域,隐藏你希望保持私密的变量,防止用户和程序员的查看和修改。

13.6.2. 调用函数

调用调用一个函数,你使用函数调用操作符(),一对括号。以下是调用上一节中提到的四个示例函数表达式的步骤:

show("Hello World!");                 // Call the show function

namespace.show("Hello World!");       // Call the show method

// Automatically called by forEach

var show = getFunction();             // getFunction returns a function
show();                               // Call the returned function

你在调用操作符的括号内传递参数给函数。函数返回的任何值将替换函数调用。

13.6.3. 立即调用函数表达式

你不需要将函数表达式赋值给变量来调用它。只需将函数表达式放在括号内,并附加函数调用操作符:

(function () {
    console.log("Hello World!");
})();

图 13.14 展示了代码模式的注释版本。

图 13.14. 立即调用的函数表达式

图片

函数体内的代码立即执行。但未使用任何全局变量。函数表达式对全局命名空间没有影响。呼吸无污染、清新可口的空气。

13.6.4. 从 IIFE 返回信息

因此,立即执行函数减少了污染,并且对于隐藏你的私有变量非常有用。但还有更多!就像任何其他函数一样,立即执行函数可以返回值,例如作为接口的对象,这让你可以控制访问函数内部的秘密好东西。(注意:请仔细考虑你的私有变量应该有多少访问权限。)

列表 13.16 展示了如何使用立即执行函数表达式(IIFE)返回一个接口对象,然后将其分配给一个变量。如果你运行程序,你可以在控制台访问 quiz 接口:

> quiz.quizMe()
  12 x 12
> quiz.submit("144")
  Correct!
列表 13.16. 在测验应用中使用 IIFE (jsbin.com/titano/edit?html,js,console)

你定义一个函数,立即调用它,并将返回的对象分配给 quiz 变量。使用立即执行函数表达式可以消除额外的变量。而不是

var getQuiz = function () {
    /* private */
    return interface;
};
var quiz = getQuiz();

你有

var quiz = (function () {
    /* private */
    return interface;
})();

你不需要使用 getQuiz 变量。

《密码学》使用了多个构造函数、函数和对象来构建游戏。你已经看到了模块和立即执行函数表达式的优势;让我们将游戏代码分解成模块。

13.7. 《密码学》——将代码组织成模块

在本章的开头,你看到了随着《密码学》代码量的增加,将程序分解成模块的动机。现在你已经了解了如何做以及为什么这样做,是时候采取行动了。

下面的列表显示了用于加载构成游戏的各个模块的五个 HTML 脚本元素。

列表 13.17. 为《密码学》导入模块(HTML)(jsbin.com/zikuta/edit?html,js,console)
<!-- spacer -->
<script src="http://output.jsbin.com/juneqo.js"></script>

<!-- Player constructor -->
<script src="http://output.jsbin.com/nubijex.js"></script>

<!-- Place constructor -->
<script src="http://output.jsbin.com/dofuci.js"></script>

<!-- Map code -->
<script src="http://output.jsbin.com/dipaxo.js"></script>

<!-- Game initialization -->
<script src="http://output.jsbin.com/fisupe.js"></script>

注意到 spacer 模块的地址与你在本章中一直使用的地址相同。这就是模块的美丽之处!你有一个文件,可以在多个项目中使用。如果你要向 spacer 命名空间添加一个新的格式化函数,它将立即对所有导入该模块的项目可用。

下面的列表显示了启动游戏所需的单行 JavaScript 代码。

列表 13.18. 为《密码学》导入模块 (jsbin.com/zikuta/edit?html,js,console)
var game = theCrypt.getGame();

运行游戏;它应该以模块形式正常工作。所有功能都在模块中定义;启动游戏只需要最少的代码。

除了 spacer 模块外,《密码学》的其他模块都共享相同的命名空间 theCrypt (图 13.15)。你通过调用 theCrypt 命名空间的 getGame 方法来启动游戏,即 theCrypt.getGame()

图 13.15. 游戏导入了五个模块。其中四个需要共享一个命名空间。

你知道使用命名空间是减少全局变量数量和分组相关函数的好方法,但不同的模块文件中的代码如何共享相同的命名空间?

13.7.1. 在模块间共享命名空间

利用本章所学,你想做以下事情:

  • 使用单个全局命名空间 theCrypt 为所有构成 The Crypt 的模块。

  • 只将那些其他模块需要的属性和函数分配给命名空间。

  • 将其他所有内容隐藏在函数的局部作用域中。

模块向命名空间 theCrypt 添加属性,就像它们向任何其他对象添加属性一样:

theCrypt.Player = Player;

但哪个模块首先创建了命名空间?尝试访问未声明的变量将导致错误。但必须按特定顺序加载模块以确保首先声明 theCrypt 的模块将是不方便的。幸运的是,在浏览器中,全局变量会自动分配给特殊的全局对象,window

全局对象,window

window 对象是 JavaScript 在浏览器中处理全局变量的方式之一。你可以在控制台提示符中测试全局变量声明和 window

> var test = "Hi"
  undefined
> test
  "Hi"
> window.test
  "Hi"

你声明一个全局变量 test,它将自动分配为 window 的属性。回到提示符,尝试访问一个不存在的全局变量:

> theCrypt
  "Can't find variable: theCrypt"

会抛出错误(尽管你的浏览器可能显示一个略有不同的错误消息)。另一方面,尝试访问 window 中不存在的属性不会抛出错误;它返回 undefined

> window.theCrypt
  undefined

你可以使用 window 对象检查 theCrypt 是否已声明为命名空间,而不会引发任何错误。

使用 window 检查全局变量

下一个列表展示了 Player 模块。构造函数周围添加了额外的代码以模块化它。

列表 13.19. 将 Player 构造函数作为模块(jsbin.com/nubijex/edit?js

在 列表 13.19 中,Player 模块检查 window 对象以查看 theCrypt 是否已声明为全局变量。如果模块找不到 theCrypt,则它将声明它:

if (window.theCrypt === undefined) {
    window.theCrypt = {};
}

一旦模块确保命名空间存在,它就会将 Player 构造函数作为属性添加:

theCrypt.Player = Player;

Player 构造函数现在可以通过全局变量 theCrypt 供其他需要它的代码使用。

下一个列表展示了使用相同全局命名空间的 Place 模块。

列表 13.20. Place 构造函数作为模块(jsbin.com/dofuci/edit?js

每个模块检查 window 对象上的 theCrypt 作为全局变量意味着不需要任何模块首先加载,因为它是创建命名空间的那个。它们都检查 theCrypt。第一个加载的模块将找不到它,并在使用之前创建它。之后加载的模块将找到 theCrypt 并将其属性分配给它想要共享的属性。

将命名空间属性分配给局部变量

您喜欢命名空间减少全局变量的方式,但您不太喜欢使用共享属性时需要输入更多。例如,地图代码模块需要使用Place构造函数在The Crypt中创建地点:

var kitchen = new Place("The Kitchen", "You are in a kitchen...");

但您已经将Place构造函数移动到了命名空间theCrypt中。您可以在地图代码中更改所有对Place的引用:

var kitchen = new theCrypt.Place("The Kitchen", "You are in a kitchen...");
var library = new theCrypt.Place("The Old Library", "You are in a lib...");

但创建一个局部的Place变量并保留原始代码可能会更容易:

var Place = theCrypt.Place;

var kitchen = new Place("The Kitchen", "You are in a kitchen...");

如果您需要多次使用命名空间属性,这是一个减少输入的好方法。

将代码部分移动到它们自己的模块后,您可以开始仔细思考每个模块的作用。它们可以被分成更小的、具有更具体任务的模块吗?接下来的三个章节将探讨一些常见任务和常见于使用和显示变化数据的程序中的代码模式:模型、视图和控制器。

13.8. 摘要

  • 将程序分解成模块,即可以独立加载的代码部分。

  • 在多个项目中使用相同的模块。

  • 通过使用 HTML 脚本元素在 JS Bin 中加载模块:

    <script src="path/to/module.js"></script>
    
  • 在脚本元素的src属性中指定模块文件的位置。

  • 通过使用命名空间和立即调用的函数表达式来最小化变量冲突,其中一个变量被另一个变量覆盖。

  • 通过将函数定义括在括号中并附加函数调用运算符()来创建立即调用的函数表达式。

    (function () {
        // Code to be executed immediately
    })();
    
  • 从立即调用的函数表达式中返回一个接口。这被称为模块模式:

    var game = (function () {
        // Local variables go here
    
        return {
            // Interface methods           
        };
    })();
    
  • 确保加载其他模块所依赖的任何模块。

第十四章. 模型:与数据一起工作

本章涵盖

  • 从数据构建模型

  • 在多个项目中使用相同的数据

  • 在数据文件之间切换

  • The Crypt中指定映射数据

在第十三章中,您看到了如何使用模块将程序分解成单独的文件。然后您可以独立地工作在模块上,轻松地在模块之间切换,并在多个项目中重用它们。您甚至可以发布您的模块并导入其他人编写的已发布的模块。

本章保持了模块化和重用的精神。您会看到将数据从构造函数和函数中移出。您以简单的方式表示数据,以便多个应用可以使用这些数据,即使这些应用是用不同的编程语言编写的。然后,您考虑如何将数据输入到构造函数和函数中,以构建添加额外功能模型的模型。最后,您为The Crypt定义映射数据,增加挑战,使游戏更具吸引力。

14.1. 构建健身应用—数据和模型

您的开发工作已经引起了人们的注意(The Fruitinator!成为全球热门),您现在是一个团队开发健身应用的一部分。注重健康的使用者跟踪他们的锻炼,记录每次会话的日期和持续时间。

Mahesha
120 minutes on February 5th, 2017
35 minutes on February 6th, 2017

2 hours 35 minutes so far.
Great work!

团队正在使用 Python 编程语言开发应用程序的 Android 版本,使用 Swift 开发 iOS 版本,以及使用 JavaScript 开发的基于网络的版本。所有版本都将使用相同的数据(图 14.1)。

图 14.1. 不同版本的应用程序使用相同的数据。

图片

构建应用程序涉及的任务包括:

1. 以字符串形式检索用户数据

2. 将用户数据转换为用户模型

3. 显示用户数据

4. 为用户提供添加会话的界面

数据以文本形式通过互联网传输。文本的格式是 JSON,你将在第二十章中查看。正如你将看到的,文本很容易转换为 JavaScript 对象。团队要求你专注于第二个任务,即从用户数据构建用户模型。

14.1.1. 定义用户构造函数

你被要求编写 JavaScript 代码来模拟一个健身应用程序的用户。你的模型需要执行以下操作:

1. 存储用户的姓名。

2. 存储用户的锻炼会话列表,每个会话都有一个日期和时长。

3. 包含一个将会话添加到列表中的方法。

4. 包含一个用于检索用户数据的方法。

列表 14.1 显示了你的初始构造函数。你可以在控制台测试它:

> var user = new User("Mahesha")
  undefined
> user.addSession("2017-02-05", 120)
  120
> user.addSession("2017-02-06", 35)
  155
> user.getData().total
  155
列表 14.1. 用户构造函数(jsbin.com/suzala/edit?js,console)

图片

构造函数包含一个 name 参数,一些使用 var 声明的私有变量,以及分配给 this 对象的两个公共方法,addSessiongetDatagetData 方法使用不带参数的 slice 来获取 sessions 数组的副本。(有关 slice 等数组方法的提醒,请参阅第八章章节 8。)提供会话信息的副本可以防止用户在 addSession 方法之外调整 sessions 数组。getData 返回的对象还包括一个 total 属性,用于存储已记录会话的总时长。

当你使用 User 构造函数创建一个 JavaScript 对象时,你创建了一个用户 模型。该模型不仅仅是数据;它包括私有变量和用于管理数据的公共方法,如图 14.2 所示。

图 14.2. 使用 User 构造函数函数创建的用户模型

图片

如果模型不仅仅是数据,那么数据看起来是什么样子?

14.1.2. 感知数据作为 JavaScript 对象

用户的 数据是一个简单的 JavaScript 对象:

var userData = {
    "name" : "Mahesha",
    "sessions" : [
        { "sessionDate" : "2017-02-05", "duration" : 120 },
        { "sessionDate" : "2017-02-06", "duration" : 35 },
        { "sessionDate" : "2017-02-06", "duration" : 45 }
    ]
};

你可以访问其属性,如 userData.name。但它仍然只是数据;它没有使用 User 构造函数构建的模型所具有的额外功能,如 addSessiongetData 方法。数据对象再次在图 14.3 中显示,以与图 14.2 进行比较。

图 14.3. 以简单 JavaScript 对象表示的用户数据

图片

使用简单的 JavaScript 对象作为数据格式非常常见,即使在其他编程语言中也是如此,尤其是在网络上。你的团队开发健身应用非常高兴用户数据以这种广泛支持的形式表示。

为了充分利用用户模型提供的额外方法addSessiongetData,你需要定义一个函数,从基本数据对象构建模型。

14.1.3. 将数据转换为用户模型

在列表 14.3 中,你定义了buildUser函数,该函数接受单个用户的 JavaScript 对象数据,并通过调用 User 构造函数创建一个模型。你通过使用 JavaScript 对象创建一个用户模型来测试buildUser函数。为创建的用户添加一个额外的锻炼课程会在控制台产生以下输出:

> 240

你正在使用列表 14.1 中的 User 构造函数,因此通过向项目中添加 HTML script元素来导入它(见第十三章),如下一个列表所示。

列表 14.2. 从用户数据构建用户模型的功能(HTML)(jsbin.com/zenire/edit?html,js,console)
<!-- User constructor -->
<script src="http://output.jsbin.com/suzala.js"></script>
列表 14.3. 从用户数据构建用户模型的功能(jsbin.com/zenire/edit?html,js,console)

![252fig01_alt.jpg]

通过buildUser函数,你现在可以将存储为简单 JavaScript 对象的普通用户数据升级为增强的用户模型,该模型将数据作为私有变量隐藏,但添加了管理模型状态和访问数据副本的方法。

14.1.4. 健身应用的下一步是什么?

你的团队对你的应用工作感到满意;你已经完成了本章的要求。以下是应用的全部要求:

1. 将用户数据作为字符串检索

2. 将用户数据转换为用户模型

3. 显示用户数据

4. 为用户提供添加会话的接口

你已经完成了第二个要求。你将在本书后面的章节中处理其他要求。现在,让我们回到The Crypt。你能像在健身应用中将用户数据从用户模型中分离出来一样,将地图数据从地点模型中分离出来吗?

14.2. The Crypt—将地图数据从游戏中分离出来

在本节中,你将应用在健身应用中学习到的知识来The Crypt。特别是,你将完成以下任务:

1. 使用基本的 JavaScript 对象在游戏中表示地图数据

2. 将退出挑战添加到地图数据中

3. 更新Place构造函数以包含设置和获取挑战的方法

4. 编写一个从地图数据构建地点模型的功能

目前,你通过在程序中手动调用Place构造函数为每个创建的地点调用方法,然后调用添加项目和出口的方法来构建The Crypt的地图。

// Create two places
var kitchen = new Place(
    "The Kitchen",
    "You are in a kitchen. There is a disturbing smell."
);

var library = new Place(
    "The Old Library",
    "You are in a library. Dusty books line the walls."
);

kitchen.addItem("a piece of cheese");   // Add items separately
library.addItem("a rusty key");         //

kitchen.addExit("south", library);      // Add exits separately
library.addExit("north", kitchen);      //

地图数据(地方、出口和物品的描述)与创建游戏使用的对象的 JavaScript 绑定在一起;你可以在代码中找到有关地图位置的唯一地方是Place构造函数的调用中。

var library = new Place(
    "The Old Library",                                     // Data inside
    "You are in a library. Dusty books line the walls."    // constructor
);

关于地方出口和物品的详细信息与地方本身是分开的。

你在第 14.1 节中看到,当数据以通用格式表示时可以更容易地共享;其他程序和编程语言可以读取格式化为简单 JavaScript 对象的格式,但不知道你的Place对象。如果你将原始地图数据与构造函数以及addItemaddExit等方法分离,将更容易定义新地图、存储它们、切换它们和共享它们(图 14.4)。

在图 14.4 中,你可以看到将地图数据从地图构建器中分离出来使得切换地图变得更加容易。

为了实现数据与游戏代码的这种分离,你必须决定数据将采取什么形式,然后编写一个函数将数据转换为游戏使用的Place模型。

14.2.1. 地图数据

一个具有标题、地点列表和起始地点名称的 JavaScript 对象将代表每个地图。

{
    "title": "The Dark House",
    "firstPlace" : "The Kitchen",
    "places" : [
        // Array of place objects
    ]
};

places数组中的每个地方也将是一个对象。以下片段显示了这样一个地方:

{
    "title" : "The Kitchen",
    "description" : "You are in a kitchen. There is a disturbing smell.",
    "items" : [ "a piece of cheese" ],
    "exits" : [
        { "direction" : "south", "to" : "The Old Library" },
        { "direction" : "west",  "to" : "The Kitchen Garden" },
        { "direction" : "east",  "to" : "The Kitchen Cupboard" }
    ]
};

每个出口都是一个具有方向属性和它所引导的地方名称的对象。数据紧凑且易于阅读,并保持物品和出口与其所属的地方。

下面的列表显示了地图数据的一部分。四个完整的位置在 JS Bin 上。

列表 14.4. 地图数据 (jsbin.com/qonoje/edit?js,console)

地图数据只是每个地方的描述;构建的Place模型将添加程序期望的功能。

14.2.2. 向地图数据添加挑战

如其目前所示,The Crypt让你探索奇异的新世界。你可以在异国他乡找到物品。你甚至可以捡起这些物品并将它们添加到你的收藏中。但还有一样东西缺失。你的冒险更像是一次假期(尽管夹杂着一些随意的盗窃)。你需要挑战!

为了使游戏更有趣,你会在出口处添加挑战。当试图向某个方向行进时,你可能会遇到一个需要解决的问题。游戏玩法可能看起来像这样:

> game.go("south")
  *** A zombie sinks its teeth into your neck. ***

> game.use("a piece of cheese south")
  *** The zombie is strangely resilient. ***

> game.use("holy water south")
  *** The zombie disintegrates into a puddle of putrid goo. ***

> game.go("south")
  The Old Library
  You are in The Old Library...

挑战阻止了你向南行进。为了克服挑战,你必须使用特定物品来应对挑战的方向。

game.use("holy water south")

如果你没有所需的物品,你必须向不同的方向探险,克服其他挑战,直到找到你需要的物品。那么,你如何在游戏中创建挑战呢?

关于《The Crypt》冒险的所有信息都需要在其地图数据中表示。目前,地方的出口很简单:

{ "direction" : "south", "to" : "The Old Library" }

它们有一个方向和它们所引导的地方的标题。要向特定的出口添加一个挑战,包括一个 challenge 属性,如下所示:

{
    "direction" : "south",
    "to" : "The Old Library",
    "challenge" : {
        "message" : "A zombie sinks its teeth into your neck.",
        "success" : "The zombie disintegrates into a puddle of goo.",
        "failure" : "The zombie is strangely resilient.",
        "requires" : "holy water",
        "itemConsumed" : true,
        "damage" : 20
    }
}

表 14.1 列出了挑战对象的属性及其用途和是否必需。

表 14.1. 挑战属性
属性 它是用来做什么的? 必需?
信息 当玩家试图朝向出口的方向移动且挑战尚未克服时,显示给玩家的信息
成功 当玩家使用克服挑战所需的物品时,显示给玩家的信息
失败 当玩家试图使用错误的物品克服挑战时,显示给玩家的信息
需要 克服挑战所需的物品
项目消耗 一旦使用,项目将从玩家物品列表中移除
损伤 当玩家在克服挑战之前试图朝向出口的方向移动时,从玩家健康值中减去的数量

为了适应挑战,你需要更新 Place 构造函数。

14.2.3. 更新 Place 构造函数以包含挑战

你必须更新 Place 模型以允许挑战。它需要一个对象来存储挑战,以及添加和检索指定方向挑战的方法,addChallengegetChallenge。下一个列表显示了 Place 构造函数的更改。

列表 14.5. 带有挑战的 Place 构造函数 (jsbin.com/ruviso/edit?js,console)

你创建一个私有的 challenges 对象来存储任何挑战。就像出口一样,你使用方向作为存储挑战的键。如果玩家在向南移动之前必须克服一个挑战,那么挑战的详细信息将存储在 challenges["south"] 中。

要存储一个挑战,请使用 addChallenge 方法,要获取指定方向的挑战,请使用 getChallenge 方法。

14.2.4. 使用地图数据构建游戏地图

你实现 The Crypt 时使用的模型是通过 Place 构造函数创建的,并通过出口连接。现在,由于你的地图数据不再与游戏逻辑绑定,你需要一种方法将地图转换为一系列的地方模型。

你编写一个名为 buildMap 的函数,该函数接受一个地图数据对象作为参数,并创建通过其出口连接的地方模型。它返回地图上第一个地方的模式,即游戏的起点。

var firstPlace = buildMap(mapData);

图 14.5 展示了 buildMap 函数如何两次使用 forEach:首先创建地方模型,然后通过添加出口将模型连接起来。

图 14.5. 首先,创建所有的地方模型;然后通过它们的出口将它们连接起来。

buildMap 函数在 代码列表 14.6 中展示,并遵循以下步骤:

1.  为每个地方创建一个模型 (buildPlace)

  1. 使用标题和描述调用 Place 构造函数
  2. 向新创建的地点模型添加任何物品
  3. 将地点模型放入地点存储中

2. 添加每个地点的出口和挑战(buildExits

  1. 从地点存储中检索地点模型
  2. 为地点数据中的每个出口的模型添加一个出口
  3. 为地点数据中的每个出口添加一个挑战

3. 返回游戏中的第一个地点的模型

列表 14.6. 地图构建器 (jsbin.com/paqihi/edit?js,console)

图片

图片

buildPlace 函数通过使用 Place 构造函数将单个地点的数据转换为地点模型。记住,游戏模块使用一个全局命名空间 theCrypt(见第十三章),因此构造函数通过 theCrypt.Place 访问。在你可以通过出口将地点模型链接起来之前,所有地点模型都需要存在。你通过使用 forEach 方法遍历 mapData.places 数组为地图数据中的每个地点调用 buildPlace

mapData.places.forEach(buildPlace);

buildPlace 中,你将创建的每个地点添加到 placesStore 对象中。

buildExits 函数将一个地点的数据分配给一个参数,placeData,并从 placesStore 中获取匹配的地点模型。

var here = placesStore[placeData.title];

它将模型分配给 here 变量。因为地点模型有一个 addExit 方法,所以你可以使用 here.addExit 为当前模型添加出口。出口数据看起来像这样:

"exits" : [
    { "direction": "south",
      "to": "The Old Library",
      "challenge" : {
        "message" : "A zombie sinks its teeth into your neck.",
        "success" : "The zombie disintegrates into a puddle of goo.",
        "failure" : "The zombie is strangely resilient.",
        "requires" : "holy water",
        "itemConsumed" : true,
        "damage" : 20
      }
    },
    { "direction": "west",  "to": "The Kitchen Garden" },
    { "direction": "east",  "to": "The Kitchen Cupboard" }
]

因此,buildExits 函数会遍历出口数据,如果存在,并为每个找到的出口调用 addExitadd-Challenge

placeData.exits.forEach(function (exit) {
    var there = placesStore[exit.to];
    here.addExit(exit.direction, there);
    here.addChallenge(exit.direction, exit.challenge);
});

每个出口的 to 属性给出了出口所指向的地点的标题。因此,可以通过使用 exit.to 作为键在 placesStore 中找到出口所指向的地点模型。

你为每个出口添加一个挑战,无论地图数据中是否已经存在挑战。

here.addChallenge(exit.direction, exit.challenge);

如果地图数据中没有为出口设置挑战,那么 exit.challenge 将是 undefined。当你编写与第十六章中的挑战一起工作的代码时,你将在尝试使用挑战之前检查挑战是否为 undefined

哈哈!通过为每个地点调用 buildPlacesbuildExits,你已经创建了一个相互链接的地点模型地图,供勇敢的探险者探索。他们会发现财富和荣耀吗?他们会发现启迪吗?或者他们会遭遇厄运?好吧,他们需要从某个地方开始,这就是为什么 buildMap 函数返回地图上指定的第一个地点模型。

return placesStore[mapData.firstPlace];

你已经成功地将地图数据与游戏实现分离。这些数据以可以跨项目和编程语言重用的形式存在。

如果您点击 JS Bin 链接 列表 14.6,您会看到完整的代码将 buildMap 函数包裹在一个立即调用的函数表达式中,并将 buildMap 赋值给您的全局命名空间 theCrypt。这是您所有模块使用的相同机制,因此在打印的列表中未显示。

14.2.5. 将所有部件组合起来运行游戏

游戏初始化模块需要微调。在这里,您更新对 buildMap 的调用,传递来自新地图数据模块的数据。

列表 14.7. 使用新的地图构建器 (jsbin.com/mogano/edit?js,console)

buildMap 函数返回冒险开始的地方,并将其设置为玩家的位置。

您还需要在 The Crypt 中添加一个 HTML script 元素来导入地图数据模块。下面的列表显示了游戏最新版本的 HTML 面板。

列表 14.8. 使用地图构建器(HTML)(jsbin.com/rulayu/edit?html,console)
<!-- spacer -->
<script src="http://output.jsbin.com/juneqo.js"></script>

<!-- Player constructor -->
<script src="http://output.jsbin.com/nubijex.js"></script>

<!-- Place constructor -->
<script src="http://output.jsbin.com/ruviso.js"></script>

<!-- map data (Kitchen example with challenge) -->
<script src="http://output.jsbin.com/jayici.js"></script>

<!-- map builder -->
<script src="http://output.jsbin.com/paqihi.js"></script>

<!-- Game initialization -->
<script src="http://output.jsbin.com/mogano.js"></script>

尝试玩玩。它应该和之前一样工作,提供两个命令 game.gogame.get。您可能找不到僵尸(尽管它在那里,潜伏在地图数据的阴影中)——您将在第十六章(kindle_split_025.html#ch16)中将挑战融入游戏玩法中。

将地图数据与地图构建代码分离的一个目标是为了更容易地在地图之间切换。因此,由于您做得很好,这里提供了完全相同的游戏代码,但使用了不同的地图文件。(这是一个古老的绝地武士技巧!)

麻雀

地图数据:jsbin.com/woniqo/edit?js

游戏:jsbin.com/dequzi/edit?console

您甚至可以将整个模块化驱动进一步分离,将玩家和地点的显示与模型分离。第十五章 将引导您进入旅程的下一阶段。

14.3. 概述

  • 以一种易于在项目、应用程序和编程语言之间重用的形式表示数据。简单的 JavaScript 对象是网络数据交换的常见格式。

  • 将数据与程序逻辑分离,以便更容易地在数据源之间切换。

  • 定义增强数据的模型,添加功能并隐藏私有变量,以便其他程序部分使用。

  • 定义函数从数据中创建模型。

第十五章. 视图:显示数据

本章涵盖的内容

  • 使用视图展示数据

  • 从构造函数中移除显示代码

  • 将相同的模型传递给多个视图

控制台并不是唯一可以显示用户信息的地方;我非常确信你很想知道如何在网页上输出数据!还有桌面和手机应用、电子邮件和打印文档需要考虑。即使在控制台上,你可能也希望输出有多个不同格式;也许你想要一个简单的文本版本和一个带有框和边框的华丽版本。你不想重写大量程序来改变它们展示信息的方式。

视图是专注于显示信息的代码模块。它们根据数据创建视觉输出。它们可能包括按钮和文本框等控件,但那部分内容留到第三部分再讲。将你的显示代码移入视图可以让你切换所需的输出类型或以多种方式显示数据,而无需更改代码的其他部分,比如模型的构造函数。

在本章中,你将在两个示例的背景下创建视图,即健身应用和The Crypt。对于健身应用,你将构建一个简单的控制台视图,然后使用spacer命名空间进行格式化来创建一个增强版本。对于The Crypt,你将分离出目前位于PlayerPlace构造函数中的显示代码,将其移动到新的玩家和地点视图中。

你在第二部分中追求的所有这些模块化可能看起来像是一场艰难的攀登,但通过切换简单代码块来更新功能所提供的灵活性非常值得努力。而且视图非常出色!

15.1. 构建健身应用——显示最新的用户数据

你和你的团队正在努力编写一个多平台健身应用,让用户记录他们的锻炼。构建应用涉及的任务包括:

1. 以字符串形式检索用户数据。

2. 将用户数据转换为用户模型。

3. 显示用户数据。

4. 为用户提供添加会话的界面。

你已经成功将用户数据从简单的 JavaScript 对象转换为具有附加功能(包括addSessiongetData方法)的用户模型(见第十四章)。现在,你需要着手显示用户数据。

你将创建两个视图,这些代码接受用户模型并显示模型中的数据。图 15.1 展示了不同的视图如何与同一个单一模型协同工作以产生不同的输出。

图 15.1. 同一个用户模型被不同的视图用来产生不同的输出。

你现在将坚持使用控制台视图,但在用 JavaScript 编程入门的第三部分中,你将切换到生成网页输出的视图。

15.1.1. 创建你的第一个健身应用视图

你为健身应用创建的第一个视图简单但有效,在控制台上显示用户信息,看起来像这样:

Mahesha
35 minutes on 2017-02-05
45 minutes on 2017-02-06

80 minutes so far
Well done!

要产生输出,你需要一个视图和一个用户模型:

var user = new User("Mahesha");       // Create a user model
user.addSession("2017-02-05", 35);
user.addSession("2017-02-06", 45);
userView.render(user);         // Pass the model to a view for display

在列表 15.1 中,你定义并立即调用一个函数来创建用户视图,返回一个简单的接口对象。你将接口分配给userView变量。为了测试视图,你调用userView.render,它产生了之前显示的显示效果。

列表 15.1. 简单的用户视图(jsbin.com/goqinep/edit?js,console)

你将视图代码包裹在一个立即调用的函数表达式中,以创建一个局部作用域,隐藏实现细节,无需额外的全局变量。(有关 IIFEs 和全局污染的弊端,请参阅第十三章。)

从第十四章中包含的User构造函数用于用户模型。用户模型提供了一个getData方法,它返回一个简单的 JavaScript 对象:

{
    "name" : "Mahesha",
    "total" : 80,
    "sessions" : [
        { "sessionDate" : "2017-02-05", "duration" : 35 },
        { "sessionDate" : "2017-02-06", "duration" : 45 }
    ]
}

视图的getInfo函数使用该数据对象来构建在列表之前显示的信息字符串。

但为什么只限于一个视图,当你可以有多个视图呢?

15.1.2. 使用模块切换健身应用视图

最终,你将向你的团队展示多个观点,他们将从这些观点中选择最佳的一个用于与健身应用一起投入生产。你添加到你的组合中的第二个视图使用了spacer命名空间中的格式化功能来为视图的输出添加边框和框。它被称为fitnessApp.userViewEnhanced,你可以在 JS Bin 上查看视图的代码作为一个模块:jsbin.com/puculi/edit?js,console。它产生的输出如下:

Mahesha
----------------------------
35 minutes on 2017-02-05
45 minutes on 2017-02-06

80 minutes so far
----------------------------

**************
* Well done! *
**************

(第二个视图的代码与第一个类似。在这里,我更感兴趣的是展示如何使用多个视图,所以我省略了书中第二个视图的代码。)

下一列表显示了用于导入spacerfitnessApp模块的 HTML script元素。注意,导入了两个视图模块。

列表 15.2. 测试两个用户视图(HTML)(jsbin.com/vamuzu/edit?html,js,console)
<!-- spacer -->
<script src="http://output.jsbin.com/juneqo.js"></script>

<!-- fitnessApp.User -->
<script src="http://output.jsbin.com/fasebo.js"></script>

<!-- fitnessApp.userView -->
<script src="http://output.jsbin.com/yapahe.js"></script>

<!-- fitnessApp.userViewEnhanced -->
<script src="http://output.jsbin.com/puculi.js"></script>

以下列表显示了用于测试你创建的两个视图的 JavaScript 代码。

列表 15.3. 测试两个用户视图(jsbin.com/vamuzu/edit?html,js,console)

多么可爱的视图!它们都工作得很好,显示来自相同用户模型的数据,所以使用哪个团队选择将很容易。而且,如果你或团队中的其他人需要创建更多视图(那个网页视图会很棒),你将很容易做到,而无需触摸用户模型代码。

15.1.3. 健身应用下一步是什么?

您已经从数据中创建了用户模型,并使用视图以多种方式显示这些数据。您的下一个任务是让用户能够在控制台中简单地记录他们的锻炼课程,随着数据的变化,让视图重新渲染显示。您将通过在第十六章中创建一个健身应用控制器来实现这一点。

15.2. 密室——将视图代码从 Player 和 Place 移动

在上一节中,您看到了如何从头创建一个视图。您有一个提供数据的用户模型,并且您编写了视图代码来显示数据。在本节中,您使用来自The Crypt的现有模型PlayerPlace。显示模型数据的代码目前与模型本身混合在一起。图 15.2 显示了游戏当前版本和新版本中的所有模块。要切换到新版本,您将显示代码从PlayerPlace模型中分离出来,创建两个视图,playerViewplaceView

图 15.2. 将视图函数从PlayerPlace模型移动到它们自己的模块中

15fig02_alt.jpg

您将首先更新玩家,然后更新地点。

15.2.1. 为玩家创建视图

在图 15.3 的左侧是您在做出任何更改之前在Player模型中定义的函数,目前模型包括六个函数(左上角)用于显示玩家信息。您希望模型只关注玩家数据的管理,而不是其显示。在图 15.3 的右侧显示了更新后的Player模型,其中添加了一些额外的方法,以及一个新的名为playerView的模块,该模块只关注玩家信息的显示。您现在创建这两个模块。

图 15.3. 将显示函数从Player模型移动到它们自己的模块中。

15fig03_alt.jpg

模型

模型不仅仅是数据。它们提供用于管理数据的方法——添加、删除、更新——并且可以防止直接访问数据。要创建玩家模型,您使用new关键字调用Player构造函数。

var player = new Player("Jahver", 80);

列表 15.4 显示了新的Player构造函数。尽管您不希望Player模型担心其数据的显示,但您确实需要让这些数据对任何需要它的视图可用。您仍然想要保护私有数据免受无赖的篡改,因此您创建了一个方法,getData,它返回数据的副本。返回的数据将如下所示:

{
    "name" : "Jahver",
    "health" : 80,
    "items" : [ "a rusty key" ],
    "place" : "The Crypt"
}

为了在第十六章中处理挑战,您在Player构造函数中添加了三个更多的方法:hasItemremoveItemapplyDamage。如果您前往 JS Bin 并运行新构造函数的代码,您可以在控制台提示符中执行以下操作(省略了undefined的响应):

创建一个玩家,添加一些项目,并获取玩家的数据:

> var p = new theCrypt.Player("Dax", 10)
> p.addItem("a key")
> p.addItem("a lamp")
> p.getData()
  [object Object] {
    health: 10,
    items: ["a key", "a lamp"],
    name: "Dax"
  }

使用新的项目方法来检查玩家是否有物品并移除一个物品:

> p.hasItem("a key")
  true
> p.hasItem("a sword")
  false
> p.removeItem("a key")
> p.getData().items
  ["a lamp"]

对玩家施加伤害并检查他们的健康:

> p.applyDamage(2)
> p.getData().health
  8
> p.applyDamage(10)
> p.getData().health
  -2
列表 15.4. Player 构造函数 (jsbin.com/yaneye/edit?js,console)

Player 构造函数不再将其创建的模型分配任何显示方法。从上一个版本中保留的方法,addItemsetPlacegetPlace,纯粹是为了管理模型持有的玩家数据。显示方法已移动到新的视图对象。

hasItemremoveItem 都使用了 indexOf 数组方法。如果指定的项在 items 数组中,indexOf 将返回该项的索引。如果该项不在数组中,indexOf 将返回 -1

["a key", "a lamp"].indexOf("a lamp");    // 1  -> second item in array
["a key", "a lamp"].indexOf("a sword");   // -1 -> not in array

removeItem 方法也使用 spliceitems 数组中移除一个项。

items.splice(itemIndex, 1);

splice 的第一个参数是要开始移除项的数组中的索引。第二个参数是要移除的项数。在 removeItem 中,你想要移除一个指定的项,所以 splice 的第二个参数是 1

视图

你的玩家视图将重新创建以前由玩家对象本身生成的表示。要显示一个玩家,你调用视图的 render 方法,并将玩家模型作为参数传递:

theCrypt.playerView.render(kandra);

render 方法调用 getInfo 函数,该函数构建玩家信息字符串,并在控制台上显示返回的字符串:

****************************************
* Kandra (50)                          *
****************************************
  Items:
   - a rusty key
   - a piece of cheese
****************************************

下一个列表显示了 playerView 的代码。分配给 theCrypt .playerView 的接口只有一个方法,render

列表 15.5. 玩家视图 (jsbin.com/zucifu/edit?js,console)

render 是唯一一个产生用户可以看到的输出的函数,它将生成的玩家信息字符串记录到控制台上。在其中,你将玩家的数据传递给 getInfo,然后它将数据传递给辅助函数,每个函数都构建整体信息字符串的一部分。

你已经将玩家模型从玩家视图中分离出来。你将在第十七章中看到,如何轻松地更改视图,使其在网页上显示玩家信息而不是在控制台上。首先,你遵循为玩家创建模型和视图的步骤,在 The Crypt 中创建地点的模型和视图。

15.2.2. 为地点创建视图

正如你在上一节中对玩家所做的那样,你重新编写了地点的构造函数,以便它创建包含每个地点数据的模型,并提供一些用于操作这些数据的方法。这些模型还将有一个 getData 方法,以便视图可以获取每个地点数据的副本以进行显示。然后你创建一个视图来在控制台上记录地点数据。图 15.4 显示了旧模型代码将如何拆分以形成新的。

图 15.4. 将显示函数从 Place 模型移动到它们自己的模块

模型

列表 15.6 显示了一个新的Place构造函数版本,其中移除了展示代码并添加了getData方法。getData返回的数据将具有以下形式:

{
    "title" : "The Old Library",
    "description" : "You are in a dusty library. Books line the walls.",
    "items" : [ "a rusty key" ],
    "exits" : [ "west", "up" ]
}

其他方法与构造函数的先前版本保持不变。

列表 15.6. 简化的Place构造函数 (jsbin.com/vuwave/edit?js,console)

每个地点模型都链接到目的地。目的地也是地点模型。目的地存储在地点模型的exits对象中。exits对象的键是目的地的方向。例如,exits["south"]可能是一个具有title属性为"The Old Library"的地点模型。Object.keys方法返回一个对象的所有键的数组。因此,Object.keys(exits)返回从当前位置出发的所有出口方向,例如,["south", "east", "west"]

Place构造函数中的getData方法返回关于某个地点的一些数据的副本,包括使用Object .keys(exits)生成的出口方向数组。视图将使用该数组来显示玩家可以选择继续冒险的可用出口。

视图

列表 15.7 显示了地点视图模块的代码。它的工作原理现在应该很熟悉了。它会在控制台上产生如下输出:

===============
= The Kitchen =
===============
You are in a kitchen. There is a disturbing smell.

Items:
   - a piece of cheese

Exits from The Kitchen:
   - east
   - south
========================================
列表 15.7. 地点视图 (jsbin.com/royine/edit?js,console)

就像玩家视图模块一样,地点视图调用了一些辅助函数,在这种情况下是从主显示函数getInfo中调用的getItemsInfogetExitsInfogetTitleInfo,以构建关于模型的信息字符串。render函数是唯一一个产生用户可以看到的输出的函数,它在控制台上显示组装好的地点信息。

现在你已经有了玩家和地点的模型构造函数和视图。数据和数据的展示已经被分离。模型只关注数据的操作,与展示无关。视图只关注展示数据,与更改数据无关。

视图被编写成可以以相同的方式使用。它们都包含一个render方法,该方法传递要显示的模型,如下面的代码片段所示:

// Create some models
var kandra = new Player("Kandra", 50);                   
var library = new Place("The Library", "You are in a dusty library.");

// Use views to display the model info
playerView.render(kandra);         
placeView.render(library);

除了显示玩家和地点的信息外,你还需要在玩家玩游戏时显示消息。他们可能想知道是否被僵尸咬了或者被豹子划伤了!

15.3. 与玩家交流——消息视图

当冒险者在The Crypt中四处走动时,你需要让他们知道发生了什么。你使用玩家视图来更新他们的健康和携带的物品。你使用地点视图来显示每个地点的标题和描述,并列出其物品和出口。你还需要一种方式来显示当玩家尝试无效操作或因受伤而屈服时的反馈。

> game.go("north")
  *** There is no exit in that direction ***
> game.get("a piece of cheese")
  *** That item is not here ***
> game.use("a lamp north")
  *** That doesn't help. The door is still locked. ***

为了处理此类消息,你创建一个消息视图。像你的其他视图一样,它在公共接口中有一个单独的方法,render。你将显示的文本传递给render方法:

theCrypt.messageView.render("That item is not here.");

下一个列表显示了视图的代码。本地的getMessageInfo函数返回一个用于显示的字符串,而render函数将其记录到控制台。

列表 15.8. 消息视图(jsbin.com/jatofe/edit?js,console

图片

你在render函数中使用console.error方法。它与console.log类似,但开发者用它来标记程序中的错误。控制台通常以与标准日志消息不同的方式显示错误。错误通常以红色显示。这符合你在控制台上向玩家显示消息的目的。

在第十四章中,你为The Crypt的地图数据添加了挑战。你将使用消息视图来显示与每个挑战相关的各种成功和失败消息。知道何时显示这些消息需要一些代码来检查用户操作与挑战,根据结果更新玩家和地点模型,并使用视图显示游戏的最新状态。你需要控制器。第十六章将为你提供解决方案。

15.4. 摘要

  • 创建视图以显示模型数据。

  • 保持关注点的分离,模型操作数据,视图显示数据。

  • 创建多个视图以不同的方式显示相同模型数据。

  • 保持所有视图的接口一致。例如,本章中的所有视图都使用render方法作为接口。它们都可以以相同的方式调用:

    fitnessApp.userView.render(user);
    fitnessApp.userViewEnhanced.render(user);
    theCrypt.playerView.render(player);
    theCrypt.placeView.render(place);
    theCrypt.messageView.render(message);
    

第十六章. 控制器:连接模型和视图

本章涵盖

  • 对用户输入采取行动

  • 根据用户操作更新模型

  • 将更新的模型传递给视图进行显示

  • 完成The Crypt的控制台版本

《用 JavaScript 编程入门》的第二部分(Part 2)是关于组织你的代码。随着你的程序增长,这种组织会带来回报,使你更容易关注单个部分,切换模块以改变功能,并在多个项目中重用代码。

将程序分解为模块可以鼓励你为每个模块分配一个特定的任务。第十四章、第十五章和第十六章构成三部曲,每章都探讨一个模块可能执行的共同任务。你在第十四章中遇到了模型,在第十五章中遇到了视图,现在你将它们与控制器联系起来。

要查看控制器的作用以及它们如何对用户输入做出反应来管理模型并更新视图,你将继续在两个项目中工作:健身应用和The Crypt。在健身应用中,用户将记录锻炼会话,而在The Crypt中,玩家将面对在探索危险地方时需要解决的谜题。但他们能在健康值降到零之前逃脱吗?

16.1. 构建健身应用—控制器

你的团队成员一直在向朋友和家人介绍你正在创建的健身应用,他们已经排好了一长串的人来试用它;他们是一群超级热情的人,一直在认真地记录他们的锻炼情况。到目前为止,你的工作进展顺利—接下来还有什么要做?以下是团队设定的要求:

1. 将用户数据作为字符串检索。

2. 将用户数据转换为用户模型。

3. 显示用户数据。

4. 为用户提供添加会话的界面。

你已经完成了任务 2 和 3,在第十四章中构建了模型,在第十五章中构建了视图。任务 1 涉及从互联网检索数据—你将在第三部分中处理这项工作。这留下了任务 4—你需要为那些渴望的健身爱好者提供一个简单的方法来记录他们完成的锻炼会话。

你决定为应用用户提供一个单独的命令来记录他们的活动:

app.log("2017-02-07", 50)

用户调用app.log方法,传入会话的日期和锻炼的分钟数。但是,这个命令是如何到达用户模型的?视图又是如何知道更新显示的?这就是控制器的工作。

16.1.1. 控制器执行什么操作?

控制器协调程序的其他部分,对用户输入做出反应,并更新模型和视图。图 16.1 显示了在 JS Bin 中打开时健身应用将加载的模块。你已经有了数据、User构造函数和来自第十四章和第十五章的视图。你需要创建一个控制器来处理不同部分之间的交互。

图 16.1. 健身应用的四模块

图 16.2 显示了你的控制器如何参与初始化应用,使用User构造函数从数据中构建用户模型。当应用运行时,控制器会响应用户调用app.log,将记录的会话添加到用户模型中,然后将更新的模型传递给视图进行显示。

图 16.2. 控制器在健身应用中执行的任务

你知道控制器需要做什么;你将如何让它做到这一点?

16.1.2. 构建健身应用控制器

下面的代码片段展示了你可能会在控制器中看到的代码类型:

var user = buildUser(userData);      // Convert user data into a user model.

> app.log("2017-02-08", 50)          // When the user logs a session

user.addSession("2017-02-08", 50);   // the controller adds it to the model

fitnessApp.userView.render(user);    // and updates the view.

第一个列表显示了控制器的完整代码。

列表 16.1. 健身应用控制器 (jsbin.com/goniro/edit?js,console)

当控制器模块被加载时,它会将它的 init 函数添加到 fitnessApp 命名空间中。你可以通过调用 init 并传递用户数据来启动应用。

var app = fitnessApp.init(fitnessApp.userData);

init 函数返回一个包含单个方法 log 的接口。通过将返回的接口分配给 app,你可以让用户通过调用 app.log 来记录他们的会话。

16.1.3. 组装组件以创建一个可工作的健身应用

列表 16.2 和 16.3 展示了 JS Bin 上的健身应用的 HTML 和 JavaScript 代码。运行程序让你可以记录像这样的会话:

> app.log("2017-02-08", 55)

控制器将你的记录的会话添加到用户模型中,并将更新后的模型传递给视图,产生以下输出:

Mahesha
35 minutes on 2017-02-05
45 minutes on 2017-02-06
55 minutes on 2017-02-08

135 minutes so far
Well done!

Thanks for logging your session.

下一个列表使用 HTML script 元素来加载你的四个健身应用模块。这些模块通过将它们分配给 fitnessApp 命名空间来共享属性、对象和函数。

列表 16.2. 健身应用(HTML)(jsbin.com/huxuti/edit?html,js,console)
<!-- fitnessApp.userData -->
<script src="http://output.jsbin.com/tenuwis.js"></script>

<!-- fitnessApp.userView -->
<script src="http://output.jsbin.com/yapahe.js"></script>

<!-- fitnessApp.User -->
<script src="http://output.jsbin.com/fasebo.js"></script>

<!-- fitnessApp.controller -->
<script src="http://output.jsbin.com/goniro.js"></script>

下一个列表展示了初始化程序并使 app.log 方法对用户可用的所需单行 JavaScript 代码。

列表 16.3. 健身应用 (jsbin.com/huxuti/edit?js,console)
var app = fitnessApp.init(fitnessApp.userData);

运行程序并尝试记录一些会话。

16.1.4. 健身应用下一步是什么?

你的应用的最后一步是从互联网上获取用户数据作为文本,然后将其转换为 JavaScript 对象,以便传递给 fitnessApp.init。你将在第二十章中返回到你对应用的工作。章节 20。

16.2. 密码库——添加游戏控制器

好的,这是《用 JavaScript 编程入门》第二部分中的 The Crypt 的全部内容。到本章结束时,你将拥有一个带有挑战供玩家克服以及健康降至零的风险的运行控制台应用,结束游戏。为了使游戏完全运行,还需要一个额外的拼图:一个控制器。

你有《密室》的数据、模型和视图。controller是连接一切的模块。它将地图数据传递给地图构建器,并将模型数据传递给视图。它提供了用户访问以玩游戏和响应玩家命令的接口。对于一个游戏模块来说,这确实有很多工作要做,所以花一分钟看看它如何与其他构成《密室》的模块相匹配是值得的。

图 16.3 展示了构成《密室》的所有模块。Controller模块已取代了之前标记为“游戏初始化”的模块。现在你正在使用模型和视图,将模块称为控制器更为合适。但为什么?控制器做什么?

图 16.3. 构成《密室》的模块

图 16.3

16.2.1. 控制器做什么?

你的控制器将在游戏运行时初始化游戏并对用户输入采取行动,根据玩家的动作更新模型和视图。

初始化游戏

当游戏首次加载时,控制器将:

1.  使用Place构造函数从地图数据中构建地点模型。

2.  使用Player构造函数构建玩家模型。

3.  将地图数据中指定的第一个地点分配为玩家的位置。

4.  提供用户界面。

步骤 1 和 2 在图 16.4 中展示。

图 16.4. 当游戏加载时,控制器构建玩家和放置模型。

图 16.4

对用户输入采取行动

在游戏运行期间,控制器将:

1.  检查玩家动作是否有效。

2.  更新玩家和地点模型。

3.  将更新后的模型传递给视图进行显示。

4.  将反馈消息传递给消息视图。

5.  如果玩家的健康值达到零,停止游戏。

步骤 2、3 和 4 在图 16.5 中展示。

图 16.5. 控制器响应用户操作并更新模型和视图。

图 16.5

玩家模型包含一个getPlace方法,控制器使用它来访问玩家的当前位置(注意Place模型如何在图 16.5 中连接到Player模型)。

16.2.2. 接近控制器代码

你已经看到了《密室》中的控制器是如何工作的,并准备好探索使其工作的代码。尽管你之前已经看到了很多代码,但将其作为一段长列表展示可能会让它显得比实际更令人畏惧,所以你的探索被分成了几个部分。

  • (16.3) 控制器的整体结构

  • (16.4) 初始化游戏、监控玩家健康、更新显示和结束游戏

  • (16.5) 处理玩家命令和挑战——getgouse

  • (16.6) 运行游戏

让游戏开始吧!

16.3. 《密室》——控制器代码的结构

正如你所见,控制器在《地牢》中执行许多任务以开始、监控和结束游戏。为了帮助你了解各个部分如何组成整体,列表 16.4 省略了函数体,专注于代码中的变量。你可以通过点击 JS Bin 的链接查看完整的列表,函数体和代码也将在本章后续部分展示。

列表 16.4. 游戏控制器 (jsbin.com/yeqicu/edit?js)

图片

列表 16.4 还包括注释,指导你查看本章中调查缺失函数体的列表。

是时候开始游戏了!停止它!监控玩家健康!更新显示!(抱歉这么兴奋,但完整的游戏即将完成。)

16.4. 地牢——开始和停止游戏

虽然主要游戏动作发生在getgouse函数中,但你还需要轻松地开始和停止游戏,并保持显示最新信息。现在玩家面临着残酷的挑战,这些挑战可能会耗尽他们的健康,你需要检查他们是否仍然足够健康和强壮以继续游戏。

16.4.1. 初始化游戏

要开始游戏,需要通过调用init方法进行初始化。

game.init(map, playerName);

init构建地图,创建玩家模型,将玩家的位置设置为正确的地方,然后显示玩家和地点信息,如以下列表所示。

列表 16.5. init函数 (jsbin.com/yeqicu/edit?js)

图片

你需要在init函数外部声明playerinPlay变量,这样控制器中的其他函数就可以访问它们。buildMap函数返回游戏中的起始位置,然后你将那个位置设置为玩家的当前位置。init函数调用render来显示起始位置和玩家信息。

rendergetgouse函数使用inPlay变量。只有当inPlaytrue时,它们才会执行常规任务。当玩家的健康值降至零时,控制器将inPlay设置为false。为了实现这一点,你必须密切关注玩家的健康值。

16.4.2. 监控玩家健康

每当玩家受到伤害时,你需要检查他们的健康值是否已达到零,因为那意味着“游戏结束,伙计。游戏结束!”控制器的checkGameStatus函数负责检查,如果玩家因遭遇僵尸咬伤、豹子撕裂和讨厌的木刺而屈服,将isPlay变量设置为false,如下一列表所示。

列表 16.6. 检查健康值是否降至零 (jsbin.com/yeqicu/edit?js)

图片

条件使用小于或等于比较运算符<=来检查健康值是否小于或等于零。

player.getData().health <= 0

如果玩家已经死亡,你将停止游戏并显示一条最终消息。如果他们足够健康可以继续,checkGameStatus 将不执行任何操作。

16.4.3. 更新显示—使用视图模块的函数

与直接调用视图模块的渲染方法相比,控制器使用自己的函数。这使得在需要时切换到不同的视图模块变得更容易,因为视图只在一个地方被引用,如下所示。

列表 16.7. 更新显示 (jsbin.com/yeqicu/edit?js)

图片

init 函数负责启动游戏,随后在整个游戏中使用 checkGameStatusrenderrender-Message。主要动作来自 getgouse 函数,其中 gouse 函数会根据物品、出口和挑战做出一些复杂的决策。现在是时候调查玩家命令了——让它变得如此吧!

16.5. The Crypt—发出命令和解决谜题

用户在探索 The Crypt 时只能执行一小套动作。控制器模块将以下接口分配给全局 window 对象:

window.game = {
    get: get,
    go: go,
    use: use,
    init: init
};

用户通过在控制台调用游戏控制器接口方法来在游戏中启动动作(例如 game.get()game.go("south"))。然后,游戏控制器根据用户的指示创建、访问或更新模型,并将更改后的模型传递给视图进行显示。

你已经看过 init 函数;本节(16.5)将逐步介绍 getgouse 函数。get 函数变化不大,但 go 函数现在会检查挑战。而 use 是一个全新的函数。

16.5.1. 使用 game.get 拾取物品

探索 The Crypt 的玩家在从一个地方移动到另一个地方的过程中会发现物品。这些物品将帮助他们克服可能遇到的挑战。get 方法为他们提供了一种捡起所发现物品的方式,如下所示。

列表 16.8. get 函数 (jsbin.com/yeqicu/edit?js)

图片

从地点的物品列表末尾移除一个物品并添加到玩家的物品列表中。如果地点没有物品,getLastItem 将返回 undefined

gouse 函数都涉及到检查挑战。需要快速复习一下。

16.5.2. 列出挑战的属性

你对挑战的想法很感兴趣,想要添加一些冒险元素。玩家从书本一开始就有一个健康属性;你现在准备好在玩家在你的迷宫中谦卑地移动时对他们造成恶劣的伤害了。

你在 第十四章 开发地图数据时第一次看到了挑战,但直到现在你还没有使用它们。两个玩家命令 gouse 会检查指定方向上的挑战,所以值得提醒自己构成挑战的属性:

"challenge" : {
    "message" : "A zombie sinks its teeth into your neck.",
    "success" : "The zombie disintegrates into a puddle of goo.",
    "failure" : "The zombie is strangely resilient.",
    "requires" : "holy water",
    "itemConsumed" : true,
    "damage" : 20
}

表 16.1 列出了挑战对象的属性及其用途以及是否必需。

表 16.1. 挑战对象的属性
属性 它是用来做什么的? 是否必需?
消息 当玩家试图朝出口方向移动且挑战未克服时显示给玩家的消息。
成功 当玩家使用克服挑战所需的物品时显示给玩家的消息。
失败 当玩家试图使用错误的物品克服挑战时显示给玩家的消息。
需要 克服挑战所需的物品。
物品消耗 如果物品在使用后从玩家的物品列表中移除。
损伤 当玩家在克服挑战之前试图朝出口方向移动时,从玩家健康值中减去的数量。
完成 挑战是否已完成。通常在初始数据中缺失,当在游戏中解决挑战时设置为true

要从位置对象中检索挑战,请调用getChallenge,指定一个方向:

place.getChallenge("south");

如果那个方向没有挑战,getChallenge返回undefined。第一个使用挑战的命令是game.go

16.5.3. 使用 game.go 移动

玩家使用控制器的go函数从一个地方移动到另一个地方。但如果锁、跳跃或豹子阻挡了他们的道路,控制器需要通知他们。玩家调用go,指定他们希望旅行的方向:

> game.go("south")

这里显示了完整的go函数。代码的说明如下。

列表 16.9. go 函数 (jsbin.com/yeqicu/edit?js)

函数首先收集它需要处理玩家请求的信息:

var place = player.getPlace();
var destination = place.getExit(direction);
var challenge = place.getChallenge(direction);

可能玩家指定的方向没有出口。你需要检查这一点:

if (destination === undefined) {
    renderMessage("There is no exit in that direction");
} else {
    // Check for challenges
}

好的,假设玩家没有撞墙,并且指定方向有出口。如果没有挑战,或者玩家已经完成了挑战,你可以将他们移动到目的地。

if ((challenge === undefined) || challenge.complete) {
    player.setPlace(destination);
    render();
} else {
    // Mention the leopard
    // and apply any damage.
}

if条件中的||符号是逻辑或运算符。它允许你同时检查由两个表达式组成的条件。如果第一个表达式challenge === undefined或第二个表达式challenge.complete中的任何一个评估为true,则整个条件为true。如果两个表达式都评估为false,则整个条件为false。(JavaScript 还有一个逻辑与运算符&&,如果两个表达式都为true,则评估为true,否则为false。)

如果玩家没有完成挑战,你需要应用挑战造成的任何伤害(碰撞、擦伤、豹子咬伤等)。

if (challenge.damage) {
    player.applyDamage(challenge.damage);
}

最后,更新显示以显示对健康状态的任何更改,显示挑战的初始信息,并检查玩家的健康是否仍然高于零——游戏仍在进行中。

render();
renderMessage(challenge.message);
checkGameStatus();

这样就处理了玩家的探索。但如果路上真的有一只大、饿的猫怎么办?玩家如何擦掉豹子?

16.5.4. 使用 game.use 擦掉豹子

如果锁、跳跃或豹子阻挡了玩家的道路,玩家可以调用控制器的 use 函数来克服挑战。他们指定要使用的物品和使用的方向:

> game.use("a ball of wool", "south")
  *** The leopard chases the ball of wool, purring loudly. ***

完整的 use 函数在 列表 16.10 中显示。它包含多个嵌套的 if 语句,但不要担心:关键决策在 图 16.6 中显示,你可以跟随列表了解其工作原理。

图 16.6. use 函数做出的关键决策

列表 16.10. use 函数(jsbin.com/yeqicu/edit?js)

函数首先收集它需要用来响应玩家请求的信息:

var place = player.getPlace();
var challenge = place.getChallenge(direction);

可能是玩家指定的方向没有挑战。你需要检查这一点:

if ((challenge === undefined) || challenge.complete) {
    renderMessage("You don't need to use that there");
} else {
    // There is a challenge to be overcome.
}

一些玩家很狡猾!你需要检查他们是否拥有他们试图使用的物品。如果没有,给他们发送一条礼貌的信息:

if (player.hasItem(item)) {
    // Check it's the right item
} else {
    renderMessage("You don't have that item");
}

好吧,他们正在使用他们拥有的物品来对抗真正的挑战。这是否是正确的工具?如果它不是,让他们知道:

if (item === challenge.requires) {
    // Complete the challenge
} else {
    renderMessage(challenge.failure);
}

最后,在兔子洞的底部,如果他们通过了所有检查,完成挑战:

renderMessage(challenge.success);
challenge.complete = true;

if (challenge.itemConsumed) {
    player.removeItem(item);
}

干得好;你通过了 getgouse 函数的奇妙世界。这些部分并不太复杂,但当你有深层嵌套的 ifbut 语句时,它们可能会变得相当复杂。给自己举办一个茶会吧!但不要迟到 第 16.6 节——那里才是你的冒险真正开始的地方。

16.6. 《密码》——运行游戏

要运行游戏,你需要包含你创建的所有模块,然后调用 game.init 方法,传递地图数据和玩家的名字。图 16.7 显示了所有涉及的模块。

图 16.7. 《密码》的众多模块

下一个列表显示了用于加载模块的 HTML script 元素。

列表 16.11. 加载游戏模块(HTML)(jsbin.com/fociqo/edit?html,console)

HTML 包含一个与其它不同的初始 script 元素。它没有 src 属性。它不是链接到一个文件来加载,而是在 script 标签之间直接包含 JavaScript。模块文件可能需要一点时间来加载,所以给玩家一些反馈,表明正在发生某些事情是很好的。将代码放在 HTML 中可以让它立即运行;无需等待文件加载。

下一个列表显示了启动所需的所有代码。

列表 16.12. 运行游戏 (jsbin.com/fociqo/edit?js,console)
var playerName = "Jahver";
var map = theCrypt.mapData;

game.init(map, playerName);

隐藏 JavaScript 面板,运行游戏,并开始探索!不过要小心;挑战会消耗你的健康。

16.7. 密室——应用的下一步是什么?

恭喜!你已经创建了一个具有可交换地图和模块化架构(一种说法,意思是它由许多部分组成)的工作控制台冒险游戏。

第二部分的《用 JavaScript 编程入门》主要讲述了如何组织代码以更好地应对更大的程序。你对私有变量、模块、模型、视图和控制器知识的掌握使你能够应对更雄心勃勃的项目。通过坚持使用控制台,你能够专注于 JavaScript 语言的关键概念;现在是时候跳转到 HTML 和网页界面了。正如你将看到的,你构建的有序程序将使你作为网页开发者的第一步变得容易得多。

16.8. 摘要

  • 使用控制器来管理模型和视图。控制器根据用户输入更新模型,并将数据从模型传递到视图进行显示。

第三部分. 浏览器中的 JavaScript

在此之前的用 JavaScript 编程入门中,你一直使用控制台作为与程序交互的方式。你能够专注于 JavaScript 代码。现在,是时候开始使用网页作为用户界面了,使用超文本标记语言(HTML)来指定标题、段落和列表项以进行展示;以及按钮、下拉列表和文本框以供用户输入。你使用模板作为从你的数据中高效生成 HTML 的方法,并使用XMLHttpRequest对象来为你的网页加载额外数据。

第三部分向你展示了如何在你自己的电脑上组织文件,而不是在 JS Bin 上,并在你继续 JavaScript 冒险时提出一些下一步的建议。密码得到了 HTML 的改造,并能够一次加载一个位置,当玩家探索你为他们创建的世界时。

第十七章. HTML:构建网页

本章涵盖的内容

  • 使用 HTML 显示静态内容

  • 标签、元素和属性

  • 常见 HTML 元素

  • 使用 JavaScript 操作网页内容

  • 密码中切换到基于 HTML 的视图

JavaScript 与为网页添加交互性相关:例如,对用户点击按钮和从下拉菜单中选择做出反应,以及用新内容更新页面的一部分。在掌握了语言的基础知识后,你几乎准备好迈出交互性和动态内容的一步了。你的重点仍然是 JavaScript,但你需要学习足够的 HTML,以便了解 JavaScript 如何用于创建和操作网页内容。JS Bin 在你学习的过程中仍然为你提供支持;你可以在输出面板中添加 HTML 并查看生成的网页。在第二十一章中,你将看到如何从 JS Bin 沙盒中走出来,并组织你自己的 HTML、CSS 和 JavaScript 文件。

你花费了大量时间以模块化方式构建密码。在这一章中,当你看到如何轻松地将输出从控制台切换到网页上时,你会看到这项工作的回报。这只需要几行代码——几乎就像魔法一样。

17.1. HTML、CSS、JavaScript——构建网页

你想要构建一个电影评论网页,我的电影评分,该网页显示有关你最喜欢的电影的信息,并允许你对其进行评分(图 17.1)。为了构建这个页面,你使用了三种语言:

图 17.1. CSS 和 JavaScript 通过增强展示和交互性建立在 HTML 内容之上。

  • HTML ——你使用 HTML 来注释页面文本并指定要加载的媒体;标题、段落、列表、图像和按钮都使用 HTML 指定。这是你的基础层,是基本内容,是你希望访客找到并阅读的信息。

  • CSS ——你使用层叠样式表(Cascading Style Sheets)语言来指定页面的外观、颜色、字体、边距、边框等。这是你的展示层,是一种视觉享受,可以增强内容。

  • JavaScript —你使用 JavaScript 语言为页面添加交互性;通过 JavaScript 响应按钮点击、过滤内容、加载额外数据以及弹出消息。这是你的行为层,是用户界面魔法的一丝微妙点缀,它使事情变得顺畅,并可以提高用户体验和效率。

图 17.1 包含了 My Movie Ratings 页面的三个截图,展示了 CSS 和 JavaScript 层如何建立在 HTML 基础之上。

即使没有 CSS 和 JavaScript,在 图 17.1 的左侧,关于电影的关键信息也是可访问的。CSS 添加了视觉样式、身份感以及(希望如此)引导和愉悦访客的设计。最后,在右侧,该图显示了用户点击评分按钮后通过 JavaScript 弹出的消息。

查看你在 JS Bin 上的 My Movie Ratings 页面。这里有基础的 HTML 版本,网址为 jsbin.com/sayayim/edit?output,以及包含 CSS 和 JavaScript 的版本,网址为 jsbin.com/hikuzi/edit?output。(你可能需要运行 JavaScript 来使评分按钮生效。)

17.1.1. 加载层

如果你拥有自己的网站,比如 jahvers.crypt,你将通过在浏览器中指向网页的 URL,例如 jahvers.crypt/movies.html,来加载 My Movie Ratings 网页。浏览器会加载由该地址的服务器发送的 HTML 文档,即 movies.html。movies.html 文档可能包含 CSS 和 JavaScript 代码,分别位于 stylescript 标签之间:

<head>
    <title>My Movie Ratings</title>

    <style>
        /* CSS goes here */
    </style>

    <script>
        /* JavaScript goes here */
    </script>
</head>

<body>
    <!-- Page content for display goes here -->
</body>

目前不必担心所有的 HTML 标签;我们将在 第 17.2 节 中介绍它们。你很清楚模块化的好处(这正是 第二部分 的主题!),所以你会很高兴地知道 HTML 文档可以加载 CSS 文件以及 JavaScript 文件:

<head>
    <title>My Movie Ratings</title>

    <link rel="stylesheet" href="movies.css" />
    <script src="movies.js"></script>
</head>

<body>
    <!-- Page content for display goes here -->
</body>

你之前已经见过用于加载 JavaScript 模块的 script 标签;link 标签则执行类似的任务来加载 CSS。当你的浏览器正在读取你的 HTML 文档,准备显示它时,它会加载由 linkscript 元素指定的 CSS 和 JavaScript 文件。你将在 第二十一章 中详细了解如何组织自己的 HTML、CSS 和 JavaScript 文件。现在,JS Bin 会为你完成这项工作。

17.1.2. 在 JS Bin 中加载层

当你在 JS Bin 上工作于你的项目或代码块时,你可以在三个面板中添加代码:HTML、CSS 和 JavaScript。JS Bin 会自动合并这三个面板的代码,将 CSS 和 JavaScript 嵌入 HTML 中,并在其输出面板上显示生成的网页。

Get Programming with JavaScript 中,你不会花太多时间在 CSS 上,但在示例中使用的地方,你总是可以自己查看 CSS 面板;使用的代码大多是直接的。在简要介绍 HTML 之后,你将在 第 17.3 节 回到 JavaScript。

17.2. HTML——一个非常简短的介绍

对于 My Movie Ratings,您的电影评论网站,您想要标题、演员和导演的列表、可能的评分选择以及一个提交您判决的按钮。您需要一种方法来指定这些文本是标题,但文本是列表项;这里有一个下拉列表,还有一个按钮。你已经不再是控制台了,多萝西——你已经离开了由空格和新行字符组成的单色世界,进入了奇妙的多彩的 HTML 世界。(请放心。本章中没有飞猴。)

您使用 HTML 来注释文本并标识您想要嵌入文档中的媒体。这些注释指定了每个文本部分在文档结构中的作用。媒体可以是图像、视频、音频或某些其他格式。

标记本身采取的是 标签 的形式。在一个文档中,一段文本可能是一个标题、一个段落、一个列表项或一个引文,例如,并且有标签来标记这些文本部分。以下是一个标题和一个段落,每个都使用开标签和闭标签进行标记:

<h1>My Movie Ratings</h1>

<p>Brief info about my favorite movies.</p>

您使用开标签和闭标签包裹每个文本部分。标题有 <h1></h1>,段落有 <p></p>。每个开标签和闭标签对一起指定了一个 元素h1 标签指定一个标题元素;p 标签指定一个段落元素。

当网络浏览器加载一个 HTML 文档时,它会看到标签并在其内存中的页面模型中创建相应的元素。标签指定要创建的元素类型,标签之间的文本形成元素的内容。

17.2.1. 从一个空页面开始

在您开始向页面添加内容之前,看看构成裸骨网页的 HTML。如果您在 JS Bin 上创建一个新的 bin 并查看 HTML 面板,您将看到以下标记。(我稍微进行了格式化。)

<!DOCTYPE html>

<html>
    <head>
        <meta charset="utf-8">
        <title>JS Bin</title>
    </head>

    <body>

    </body>
</html>

第一行的 DOCTYPE 给浏览器提供了有关您正在使用的 HTML 版本的信息,并帮助它决定如何处理和显示页面。HTML 在这些年中已经发展,许多版本和变体都由复杂的文档类型声明指定。幸运的是,<!DOCTYPE html> 是最新版本(目前是 HTML5)的一个简洁的缩写。

在文档类型之后,您将整个文档包裹在 html 标签中。在文档内部,有两个部分,headbodyhead 部分包含有关文档的信息:其标题和字符编码。body 部分是页面主要内容所在的地方——当用户访问网页时将显示给用户的内容。所有您的页面都将使用这里所示的基本结构。

17.2.2. 添加一些内容

图 17.2 展示了在新的 JS Bin 文档的 body 标签之间添加的一些文本,用于 My Movie Ratings。这些文本已经使用适当的 HTML 标签进行标记,以表示标题、副标题和段落。

图 17.2。标题和段落的层次结构

浏览器通过使用不同的字体大小来表示标题的层次结构,h1h2h3。以下列表显示了在网页的body标签之间使用的 HTML。

列表 17.1。我的电影评分——标题和段落(HTML)(jsbin.com/nosiwi/edit?html,output)

17.2.3。标记列表

每部电影都包含一个演员列表和一个导演列表。使用li标签标记列表项元素。一个演员的单个列表项看起来像这样:

<li>Amy Poehler</li>

列表项需要是列表的一部分。如果您关心项目的顺序,可以使用有序列表,如果不关心,则可以使用无序列表。使用ol标签创建有序列表,使用ul标签创建无序列表。图 17.3 显示了演员和导演列表的输出。列表 17.2 显示了新的代码。

图 17.3。浏览器以数字和项目符号的形式渲染有序列表和无序列表。

列表 17.2。有序和无序列表(HTML)(jsbin.com/vegahe/edit?html,output)

如图 17.3 所示,浏览器会自动为有序列表添加数字,为无序列表添加项目符号。

17.2.4。一些常见的 HTML 元素

图 17.4 是包含一些常见 HTML 元素的网页截图。每个元素的描述内容都描述了该元素。您可以在 JS Bin 上访问该页面:jsbin.com/nuriho/edit?html,css,output

图 17.4。具有自我描述 HTML 元素的网页

用于在图 17.4 中创建页面的标签显示在表 17.1 中。还有很多 HTML 标签,但就目前而言,这些标签已经足够您使用了。

表 17.1。用于包装内容的常见 HTML 元素
标签 元素 它是用来做什么的?

标题 文档或文档部分的标题

...

子标题 按重要性递减的子标题

段落 段落
分区 将属于同一文档部分的元素包裹在一起
    有序列表 包裹一组列表项,其中项的顺序很重要(例如,编号列表)
      无序列表 包裹一组列表项,其中项的顺序不重要(例如,项目符号列表)
    • 列表项 将单个项目包裹在有序或无序列表中
      头部 包裹提供文档元信息以及文档所需的额外代码的元素
      主体 包裹页面的主要内容,直接显示在网页上的内容

      17.3。使用 JavaScript 向网页添加内容

      在寻找为网站增添趣味的方法时,你决定用随机问候语欢迎你的 My Movie Ratings 网页访客。为了测试这个想法,你创建了一个最小化的页面。图 17.5 展示了你所追求的效果,页面访问四次产生了四个不同的问候语。

      图 17.5. 来自网页的随机问候语

      刷新页面会生成一个新的随机问候语。在 JS Bin 上尝试一下 output.jsbin.com/mikura.html。接下来的两个列表展示了如何创建页面。运行 JavaScript 代码以显示消息。

      列表 17.3. 使用 JavaScript 向段落添加内容(HTML)(jsbin.com/mikura/edit?html,js,output)
      <p id="greeting">Welcome!</p>
      
      列表 17.4. 使用 JavaScript 向段落添加内容 (jsbin.com/mikura/edit?html,js,output)

      要生成这种效果,你需要遵循以下步骤:

      1. 在 HTML 中给元素分配一个 id

      2. 使用 id 在 JavaScript 中获取元素引用

      3. 使用引用更新元素的内容

      17.3.1. 通过 id 获取元素

      你在段落元素中显示问候语。要设置问候语,你需要获取 JavaScript 中那个段落的引用。你给 HTML 元素赋予一个唯一的 id 属性:

      <p id="greeting">Welcome!</p>
      

      要在 JavaScript 程序中获取元素的引用,你使用 document.get-ElementById 方法,并将元素的 id 作为参数传递:

      var para = document.getElementById("greeting");
      

      网络浏览器使 document 对象对你的 JavaScript 代码可用。document 对象具有属性和方法,允许你与页面上的元素层次结构进行交互。

      你通过使用 document.get-ElementById 获取了段落元素的引用。将引用分配给 para 变量后,你现在可以使用 para 来操作元素。你通过设置元素的 innerHTML 属性来更新段落的内联内容。

      para.innerHTML = "Ay up me duck!";
      

      段落的原始内容被替换,变成了

      <p id="greeting">Ay up me duck!</p>
      

      17.3.2. 函数声明

      你现在在定义用于后续使用的命名函数时,使用的是函数声明而不是函数表达式。

      var sayHello = function () {   //
          console.log("Hello");      // function expression assigned to variable
      };                             //
      
      function sayHello () {         //
          console.log("Hello");      // function declaration
      }                              //
      

      函数声明在第四章 中被提及,作为定义函数的另一种语法。到目前为止,你一直使用表达式来保持与将值、对象、数组、函数分配给变量的赋值一致性。

      var num = 4;
      var movie = {};
      var actors = [];
      var getRating = function () {};
      

      对于命名函数,函数声明更为常见,因此你切换到了 Get Programming with JavaScript 的第三部分 的声明。

      17.3.3. 没有 JavaScript?

      有时,访客可能会在既没有启用 JavaScript 或禁用了 JavaScript 的设备上访问你的网站。或者他们可能在一个网络较慢的环境中,比如在酒店或火车上,加载任何 JavaScript 模块需要更长的时间(长得多!)如果你的页面依赖于 JavaScript 来显示内容,访客可能会看到一个空白的区域。在可能的情况下,考虑在页面中包含初始内容,这样访问就不会是浪费时间。内容应该对所有用户都可用,对于那些设备能够处理 JavaScript 带来的额外灵活性、流畅性和炫酷效果的用户,可以在其之上添加这些内容。

      在你的随机问候测试中,你已经在初始 HTML 中包含了问候语,“欢迎!”。随机问候是一个有趣的元素,但不是必需的。

      在了解了如何向段落添加一些文本之后,你现在可以增加难度,一次性添加一系列元素。

      17.4. 从数组中显示数据

      My Movie Ratings 上的每部电影都有一个标题和一行摘要。图 17.6 展示了网站上三部电影的列表。

      图 17.6. 电影列表

      给定一些电影数据,你可以遍历电影并将它们插入到页面上的现有元素中。电影数据如下所示:

      var moviesData = [
         {
            "title" : "Inside Out",
            "summary" : "An emotional adventure inside the head of a young girl."
         },
         {
            "title" : "Tomorrowland",
            "summary" : "Recreating the hope and wonder of previous generations."
         },
         {
            "title" : "The Wizard of Oz",
            "summary" : "Strangers find friendship and strength on a long walk."
         }
      ];
      

      你在页面上放置了一个具有 idmoviesdiv 元素,如下所示。使用 div 元素作为容器来收集相关元素组。

      列表 17.5. 使用 JavaScript 构建 HTML (HTML) (jsbin.com/jakowat/edit?html,js,output)
      <body>
          <h1>My Movie Ratings</h1>
      
          <div id="movies"></div>
      </body>
      

      每部电影将是一个无序列表中的列表项。浏览器会自动为无序列表中的列表项添加项目符号。下一个列表展示了用于显示电影的 JavaScript 代码。

      列表 17.6. 使用 JavaScript 构建 HTML (jsbin.com/jakowat/edit?html,js,output)

      getMovieHTML 方法通过在适当的开始和结束标签之间嵌入 movie 对象的属性来构建单个电影的 HTML。

      var html = "<h3>" + movie.title + "</h3>";
      html += "<p>" + movie.summary + "</p>";
      

      getMoviesHTML 方法遍历电影数组,在遍历过程中构建所有电影的 HTML。它使用 getMovieHTML 获取每部电影的 HTML,并将返回的字符串包裹在 li 标签中以创建列表项元素。

      movies.forEach(function (movie) {
          html += "<li>" + getMovieHTML(movie) + "</li>";
      });
      

      然后,它将项目列表的 HTML 包裹在开始和结束的 ul 标签中,并返回完整的 HTML 字符串。

      return "<ul>" + html + "</ul>";
      

      getMoviesHTML 函数返回类似以下格式的 HTML(但不含换行符和额外空格):

      <ul>
          <li>
              <h3>Inside Out</h3>
      
              <p>An emotional adventure inside the head of a young girl.</p>
          </li>
          <li>
              <h3>Tomorrowland</h3>
              <p>Recreating the hope and wonder of previous generations.</p>
          </li>
          <li>
              <h3>The Wizard of Oz</h3>
              <p>Strangers find friendship and strength on a long walk.</p>
          </li>
      </ul>
      

      render 方法是改变页面的方法。它获取页面上的目标 div 的引用,并使用 getMoviesHTML 返回的 HTML 字符串设置其 innerHTML 属性。

      var moviesDiv = document.getElementById("movies");
      moviesDiv.innerHTML = getMoviesHTML(movies);
      

      为了减少全局变量的数量,你可以将getMovieHTMLget-MoviesHTMLrender函数包裹在一个立即执行的函数表达式(IIFE——见第十三章)中,该表达式仅在其接口中返回render。列表已被保持简单,以帮助关注 HTML 的生成以及如何在网页上更新元素。

      17.5. 《The Crypt》——使用 Web 视图显示玩家和位置

      你已经将《The Crypt》的程序拆分成了多个模块,这些代码段可以独立加载。模块执行不同的任务:你有数据、模型、视图和控制器(图 17.7)。

      图 17.7. 组成《The Crypt》的模块

      模块的优势在于你可以轻松地切换它们以改变程序的行为。现在是时候兑现这个承诺了。要构建《The Crypt》的第一个 HTML 版本,请按照以下步骤操作:

      1. 更新玩家视图(两行代码)

      2. 更新位置视图(两行代码)

      3. 创建一个 HTML 页面,其中包含script元素以加载所有游戏模块,并为玩家和位置视图提供占位符以填充

      你不会看到输出,直到所有部件都到位。但为了了解你正在构建的内容,图 17.8 显示了你的第一个 HTML 游戏的实际运行情况。网页输出显示了玩家和位置当前的状态。你在控制台输入命令。

      图 17.8. 《The Crypt》在控制台面板上的命令和消息以及输出面板上的更新

      在查看玩家和位置视图的更新版本之前,花点时间考虑一下你模块化方法的强大功能。显示每个视图信息的单个方法是render方法。为了反映其美丽和简单性,它得到了以下整个章节的介绍。

      17.5.1. 更新玩家和位置视图模块——render方法

      在从控制台应用程序迁移到 Web 应用程序的过程中,你需要更新玩家和视图。新版本通过在网页元素中插入文本来显示信息,而不是将信息记录到控制台。这些是你所做的唯一更改(图 17.9),而且这些更改本身很小。

      图 17.9. 你只需要更改视图,就可以切换到《The Crypt》的基于 Web 的版本。

      第十五章中的视图在单个方法render中使用console.log,这是有意为之;你只有一个地方需要做出更改。

      基于控制台玩家的视图的render方法看起来是这样的:

      function render (player) {
          console.log(getInfo(player.getData()));
      };
      

      基于控制台的位置视图的render方法看起来是这样的:

      function render (place) {
          console.log(getInfo(place.getData()));
      };
      

      使用你在本章中学到的关于更新网页元素的知识,新的位置视图的render方法看起来是这样的:

      function render (place) {
          var placeDiv = document.getElementById("place");
          placeDiv.innerHTML = getInfo(place.getData());
      };
      

      新的玩家视图的render方法看起来几乎相同:

      function render (place) {
          var playerDiv = document.getElementById("player");
          playerDiv.innerHTML = getInfo(player.getData());
      };
      

      这些确实是你需要更改的 JavaScript 的唯一内容。新方法使用 document.getElementById 来获取网页上元素的引用。在你创建 列表 17.9 中的网页之前,这些元素将不存在,因此你不能测试你的新视图。请耐心等待;它们的时间会到来。

      17.5.2. 更新玩家和地点视图模块——列表

      列表 17.7 展示了带有更新后的 render 方法的新的玩家视图。其他所有内容自第十五章 chapter 15 以来保持不变,因此省略了函数体。JS Bin 链接在那里,如果你想查看完整的列表,可以检查。在两个视图的列表之后讨论了 "use strict" 的含义。

      列表 17.7. 基于网络的玩家视图 (jsbin.com/cehexi/edit?js,console)

      下一个列表显示了带有更新后的 render 方法的新的地点视图。

      列表 17.8. 基于网络的地点视图 (jsbin.com/cakine/edit?js,console)

      这些对 render 方法的更改就是你需要将玩家和地点信息从控制台移动到网页显示的所有更改。目前,用户仍然通过控制台输入命令。

      17.5.3. 使用 JavaScript 的严格模式

      JavaScript 被用于数百万个网页上。随着 JavaScript 的演变和成熟,这些页面需要继续工作。为了采用更现代的语言使用方式,这些方式会提醒你可能犯的错误,优化语言的工作方式,并为进一步的发展做好准备,你可以在 严格模式 下运行你的代码。为了为一个函数启用严格模式,请在函数顶部添加 "use strict"

      书中的所有代码都可以在严格模式下运行,但我认为在学习基础知识时可能会分散注意力。第三部分 part 3 中的所有模块都将使用严格模式;列表 17.8 是第三部分 part 3 中的第一个模块,这就是为什么它在这个时候被介绍。更多详情请访问 www.room51.co.uk/js/strict-mode.html

      17.5.4. 在 HTML 中加载模块和添加占位符

      下一个列表显示了 The Crypt 的第一个基于网络的完整 HTML 版本。它包括 script 标签来加载所有需要的模块。

      列表 17.9. 基于网络的 The Crypt 游戏 (HTML) (jsbin.com/zaxaje/edit?html,console,output)

      您使用head部分中的title元素设置页面标题。在 JS Bin 编辑环境中工作时,页面标题并不总是可见,但通常由浏览器用于标记显示页面的选项卡和窗口,以及当保存页面为书签或收藏时。加载模块的script标签添加在body标签关闭之前。这确保了程序使用的两个div元素在尝试用玩家和地点信息更新它们之前已经在页面上。

      不幸的是,因为您的视图使用spacer命名空间通过换行符和空格格式化生成的文本,而网页不尊重这些换行符和空格,所以所有玩家和地点信息都连在一起。尽管输出并不完全符合您的期望,但您仍然可以玩游戏,如图 17.10 所示。

      图 17.10. 文本输出连在一起,但游戏仍然可玩。

      17.5.5. 添加 CSS 风格

      在 JS Bin 中,您还有一张底牌!CSS 面板用于指定您希望页面上的元素看起来如何:它们的大小、颜色、边距、边框等。通过几行 CSS,您可以告诉浏览器尊重由视图模块生成的换行符,并使用一个给每个字符相同空间的字体,就像控制台上的字体一样。

      下一个列表显示了要添加到 CSS 面板的代码。

      列表 17.10. 基于网络的《密码游戏》(CSS)(jsbin.com/toyahi/edit?css)
      div {
        white-space: pre;
        font-family: monospace;
      }
      

      第一条规则告诉浏览器保留div元素中文本内容的空白(即空格和换行符)。第二条指定浏览器应使用等宽字体显示div元素中的文本。您的页面使用div元素来显示玩家信息和地点信息。网页上的输出现在格式化得与控制台上的格式相同。请参阅图 17.8 以查看格式良好的输出。

      17.5.6. 游戏玩法

      您仍然可以通过game对象的方法在控制台中发出游戏指令。可用方法有getgouse。在jsbin.com/toyahi/edit?console,output上运行程序,尝试玩游戏,并在控制台输入如下命令:

      > game.get()
      > game.go("south")
      > game.use("a rusty key", "north")
      

      使用换行符和空格来格式化输出并不是最佳方法。更好的方法是使用适当的 HTML 标签(用于标题、段落和列表)来传达输出中不同信息片段的结构。当您在第十九章中研究模板时,您将了解使用 HTML 格式化玩家和地点信息的更好方法。

      当前版本的《密码学》是一个混合体。虽然你使用 HTML 来显示一些信息,但你仍然强迫玩家在控制台输入命令。在第十八章中,你将发现如何使用简单的表单元素,如按钮、下拉列表和文本框,让玩家直接在网页上与游戏互动。为了为完全基于 HTML 的版本做准备,你通过重复对位置和玩家视图所做的更改来完成本章,但这次是对消息视图进行更改。

      17.5.7. 准备消息视图

      为了使 UI 100%基于网页,你需要更新消息视图,使其在网页上显示消息而不是在控制台上。以下列表显示了新的模块代码。

      列表 17.11. 基于网络的邮件视图(jsbin.com/nocosej/edit?js,console)

      320fig01_alt.jpg

      视图需要一个具有idmessages的元素。它还添加了一个clear方法,用于从页面上删除消息。

      17.6. 概述

      • 使用 HTML 注释文本以指定文本在文档结构中的作用。例如,文本是标题、列表项或段落。

      • 在 HTML 文档之前加上其文档类型:

        <!DOCTYPE html>
        
      • 使用html标签包裹整个文档:

        <html> ... </html>
        
      • 文档的head元素包含有关文档的信息,例如,其标题和字符编码:

        <head>
            <meta charset="utf-8">
            <title>My Web Page</title>
        </head>
        
      • 文档的body元素包含要在网页上显示的内容:

        <body> ... </body>
        
      • 使用适合内容的相关标签。在许多其他标签中,有标题<h1><h2>...<h6>;段落<p>;列表项<li>;以及列表<ol><ul>

      • 在打开标签中添加id属性以唯一标识页面上的元素。

        <p id="message"></p>
        
      • 从 JavaScript 中,通过使用document .getElementById方法获取 HTML 元素的引用:

        var para = document.getElementById("message");
        
      • 通过设置元素的innerHTML属性来更改元素的内容:

        para.innerHTML = "New text for the paragraph.";
        

      第十八章. 控件:获取用户输入

      本章涵盖

      • 使用按钮来启动操作

      • 使用文本框和下拉列表收集用户输入

      • 当按钮被点击时自动调用函数

      我们喜欢按钮!无论是我们在亚马逊上购买书籍、喜欢一条推文,还是发送深夜醉酒的电子邮件,我们都很难抗拒点击那些诱人、多彩的按钮。好吧,现在是时候你发出自己的行动号召,并开始在页面上添加按钮了。而且,当你在做这件事的时候,你还需要为文本框和下拉列表腾出空间。

      在第十七章中,你跳到了 HTML 并使用 JavaScript 向网页添加内容。然而,为了获取用户输入,你仍然坚持使用控制台。在这些充满活力的网络应用的日子里,你希望用户仅通过网页进行交互;他们不应该需要知道 JavaScript 并寻找控制台来使用你为他们编写的程序。

      本章介绍了 HTML 中的inputselectbutton元素,使用户能够将信息输入到文本框中,从下拉列表中选择,并通过点击按钮来启动操作。您将了解如何设置在按钮被点击时自动调用的函数。

      点击按钮很有趣,所以让我们从这里开始。点击!

      18.1. 使用按钮

      您正在构建一个“我的电影评分”网站(见第十七章),并且一直在测试当访客到达网站时显示的随机问候语。您希望访客能够查看更多问候语而无需重新加载整个页面。您决定在测试页面上添加一个按钮,该按钮会使用随机问候语更新显示(图 18.1)。

      图 18.1. 点击“Say Hi”按钮会显示一个随机问候语。

      图 18.1

      点击“Say Hi”按钮会显示问候语。但这是如何实现的?您如何让您的 JavaScript 程序响应用户点击按钮?您需要做三件事:

      1.  在页面上添加一个button HTML 元素

      2.  编写一个更新问候语的函数

      3.  让按钮在点击时调用函数

      18.1.1. 在页面上添加一个按钮

      好吧,首先,您需要在页面上有一个按钮。HTML 包括一个button元素。将按钮文本放在标签之间(图 18.2):

      图 18.2. 三个按钮的 HTML 和生成的输出

      图 18.2

      <button>Button 1</button>
      

      要在 JavaScript 中处理按钮,您需要在 HTML 中给它一个唯一的id属性。

      <button id="btnGreeting">Say Hi</button>
      

      以下列表显示了“我的电影评分”测试页面的关键 HTML。具有id"greeting"的段落是您稍后插入随机问候语的地方。

      列表 18.1. 带按钮的“我的电影评分”问候语(HTML)(jsbin.com/josoqik/edit?html,output)
      <button id="btnGreeting">Say Hi</button>
      
      <p id="greeting">Welcome!</p>
      

      输出结果显示在图 18.1 中。如果您对如何应用样式感兴趣,请随意查看 JS Bin 上的 CSS 面板。

      18.1.2. 编写更新问候语的函数

      在第十七章中,您编写了两个函数getGreetingupdateGreeting,它们选择一个随机的欢迎信息并更新显示。

      function getGreeting () {
          // Return random greeting
      };
      
      function updateGreeting () {
          para.innerHTML = getGreeting();
      };
      
      var para = document.getElementById("greeting");
      updateGreeting();
      

      updateGreeting函数通过设置段落元素parainnerHTML属性来更新显示。

      因此,您已经在 HTML 中放置了按钮和段落,并且您有一个更新段落的函数。现在,您想让按钮在用户点击时调用updateGreeting函数。

      18.1.3. 监听点击事件

      点击—“Wassup!” ... 点击—“Hola!” ... 点击—“Ay up me duck!”

      “亲爱的按钮,请在我点击你的时候调用我的updateGreeting函数,”这是您想要发送的指令。要给按钮这样的指令,您需要在 JavaScript 中获取按钮的引用。

      var btn = document.getElementById("btnGreeting");
      

      一旦您有了按钮的引用btn,您就可以告诉按钮在按钮被点击时调用一个函数:

      btn.addEventListener("click", updateGreeting);
      

      addEventListener 方法告诉按钮(或从其调用的任何元素)在指定的事件发生时调用指定的函数——在这种情况下,当按钮被点击时。这就像 updateGreeting 函数正在等待,或者 监听 按钮被点击。

      列表 18.2 显示了随机问候测试页面的最终列表。它与第十七章中的列表相同,但增加了两行按钮代码。代码被包裹在一个立即调用的函数表达式中,以避免污染全局命名空间。它还包括一个 use strict 语句,指示浏览器使用严格模式(见第 17.5.3 节)。

      列表 18.2. My Movie Ratings 欢迎信息与按钮 (jsbin.com/josoqik/edit?js,output)

      图片

      回顾第十二章,以提醒如何使用 Math.floorMath .random 生成一个用于数组的随机索引。

      事件种类繁多

      点击不是函数可以监听的唯一事件;鼠标移动、页面滚动、键盘按键和图片加载也是一些其他事件,而触摸设备和移动设备出现了一些新事件,如点击、滑动、强按和摇晃。

      目前我们会保持简单,只关注按钮点击,但如果你对可能的事件种类感兴趣,可以查看 Mozilla 开发者网络上的事件参考developer.mozilla.org/en-US/docs/Web/Events

      18.2. 使用选择元素选择一个选项

      My Movie Ratings 网站允许用户对电影进行评分。他们从下拉菜单中选择一个评分,然后点击评分按钮。页面随后显示他们的评分信息(图 18.3)。

      图 18.3. 点击评分按钮会弹出带有你评分的消息。

      图片

      要实现评分系统,你需要做四件事:

      1. 在页面上添加一个带有每个评分 option 元素的 select HTML 元素

      2. 在页面上添加一个带有“评分”文本的 button HTML 元素

      3. 编写一个函数以弹出带有评分的消息

      4. 告诉按钮在点击时调用函数

      18.2.1. 在页面上添加一个选择元素

      网络浏览器将 select 元素渲染为下拉列表。你使用 option 元素来指定列表中的选项(图 18.4)。

      图 18.4. HTML 中的两个选择元素,在网页上显示,并且正在选择中

      图片

      <select id="movies">
          <option>Inside Out</option>
          <option>Tomorrowland</option>
          <option>The Wizard of Oz</option>
      </select>
      

      在 JS Bin 上测试它。创建一个新的 bin,并打开 HTML、控制台和输出面板。将 HTML 面板的内容替换为上一个片段中的 select 代码。你应该在输出面板上看到下拉列表出现。在控制台提示符下,输入以下命令:

      > var dd = document.getElementById("movies")
        undefined
      > dd.value
        Inside Out
      

      在输出面板的下拉列表中选择不同的电影。然后使用 JavaScript 检查其新值:

      > dd.value
        The Wizard of Oz
      

      你也可以从 JavaScript 更新下拉列表的选中值:

      > dd.value = "Tomorrowland"
        Tomorrowland
      

      在 JavaScript 中设置的值在输出面板中被选中。

      图 18.4 显示了两个select元素的 HTML 以及它们在网页上的渲染方式。

      下一个列表显示了 My Movie Ratings 主页体的 HTML,其中包含一个用于评分电影的select元素和按钮。

      列表 18.3.带有下拉列表的 My Movie Ratings(HTML)(jsbin.com/hikuzi/edit?html,output

      图片

      图片

      注意,你可以添加一个selected属性,以便在网页上显示下拉列表时选择该选项。

      18.2.2. 评分电影的功能和调用它的按钮

      要对电影进行评分,你需要一个函数,该函数从下拉列表中获取用户的选择,从select元素中获取选中选项的值,并弹出包含该值的消息,如列表 18.4 所示。

      列表 18.4.带有下拉列表的 My Movie Ratings(jsbin.com/hikuzi/edit?js,output

      图片

      下拉列表的value属性提供了用户的评分。你调用浏览器提供的alert函数,在弹出对话框中显示评分。在将代码包装在rateMovie函数中后,你使用addEventListener告诉评分按钮在点击时调用rateMovie

      您的用户可以从列表中选择一个评分。但如果你希望给他们更多的自由来表达自己呢?

      18.3. 使用文本框读取用户输入

      恭喜,My Movie Ratings 网站现在允许用户对电影进行评分!你的成功让你开始思考——关于评论呢?为什么不鼓励用户在评分时添加简短的评论呢?图 18.5 显示了添加了两个评论并有一个评论正在进行中的网站。

      图 18.5。电影有一个文本框来添加评论,还有一个用于显示它们的区域。

      图片

      用户将评论输入到文本框中,从下拉列表中选择一个评分,然后点击评分按钮。现在,你不再弹出消息,而是将他们的评论和评分添加到电影的评论部分。为了向网站添加评论,你需要做五件事:

      1. 在页面上添加一个文本框

      2. 在 HTML 中添加一个无序列表作为评论的放置位置

      3. 在 JavaScript 中获取文本框的引用并访问其值

      4. 在 JavaScript 中获取评论列表的引用

      5. 更新rateMovie函数以将评论追加到评论列表

      18.3.1. 在页面上添加文本框

      要在页面上添加一个文本框,你使用一个inputHTML 元素,将其type属性设置为"text"。包括一个id属性,以便从 JavaScript 中引用该元素。

      <input type="text" id="txtComment" />
      

      input 元素在网页上显示表单控件。它不包装任何其他内容,因此写作一个 自闭合 标签。没有开闭标签对。注意标签末尾的斜杠;它表明这个标签不需要闭合标签。文本框位于 "controls" div 中,与评分下拉列表和评分按钮一起。

      <div class="controls">
      
          <input type="text" id="txtComment" />
      
          <select id="rating"> <!-- options for ratings --> </select>
          <button id="rate">Rate</button>
      
      </div>
      

      type 属性设置为 "text"input 元素在页面上显示为文本框。其他常见的 type 属性包括 passwordsubmitcheckboxradio。微软、苹果、谷歌、Mozilla 和 Opera 等浏览器制造商正在努力改进对新类型(如颜色选择器、日期选择器和滑块)的支持。要了解更多关于输入元素类型的信息,您可以访问 Mozilla 开发者网络 developer.mozilla.org/en/docs/Web/HTML/Element/Input

      18.3.2. 向显示评论的列表中添加无序列表

      您使用一个 ul 元素作为评论列表,在其前面添加一个标题。您给列表一个 id 属性,这样您就可以使用 JavaScript 添加列表项。

      <h4>Comments</h4>
      <ul id="comments"></ul>
      

      您需要在演员和导演列表之后添加评论列表。

      18.3.3. 获取新元素的引用

      为了读取文本框中输入的评论并添加列表项到评论列表中,您需要在 JavaScript 代码中获取这两个元素的引用。

      var commentsList = document.getElementById("comments");  // The list
      var commentBox = document.getElementById("txtComment");  // The text box
      

      用户在文本框中输入的文本可以通过其 value 属性访问。要查看 value 属性的实际操作,您可以在控制台中访问和更新它。访问 My Movie Ratings 页面 jsbin.com/nevaxit/edit?console,output,在文本框中输入“Great Movie!”,然后在控制台提示符中输入以下命令:

      > var txt = document.getElementById("txtComment")
        undefined
      > txt.value
        Great Movie!
      > txt.value = "Rubbish!"
        Rubbish!
      

      最后一个命令将更新网页上文本框的内容。

      18.3.4. 更新 rateMovie 函数

      下一个列表显示了所有组装好的部分。来自文本框的评论和来自下拉列表的评分作为列表项附加到现有的评论列表中。

      列表 18.5. 电影、评论和随机的问候语 (jsbin.com/nevaxit/edit?js,output)

      在列表中,您需要获取五个 HTML 元素的引用。而不是每次都输入 document.getElementById(id),您可以创建一个执行相同任务但名称更短的功能,即 getById

      列表 18.6 显示了更新的 HTML,包括文本框、评论列表和一个用于随机问候语的 span 元素。一个 span 在段落内 内联 包裹文本,让您可以描述它所包裹的文本的目的,在这个例子中,使用 id"greeting",可以不同地样式化(改变颜色、加粗、改变大小),并通过 JavaScript 访问(例如,添加一个随机问候语)。

      这里显示了 body 元素的内容,一些项目已被省略或压缩,因为它们与 列表 18.3 中的内容没有变化。所有内容都在 JS Bin 上。

      列表 18.6. 电影、评论和随机问候语 (HTML) (jsbin.com/nevaxit/edit?html,output)

      output.jsbin.com/nevaxit 上尝试网页。添加一些评论和评分,并将它们添加到列表中。并查看随机问候语。

      18.3.5. 使用 CSS 设计示例样式

      Get Programming with JavaScript 的第三部分 [kindle_split_026.html#part03] 中的许多示例都使用了 CSS 规则设置了颜色、字体、边距、边框等。虽然这本书没有直接教授 CSS,但请查看 CSS 规则。逐个来看,大多数都很容易理解。尝试更改一些值或删除一些或所有规则。虽然它们可能会使页面看起来更美观,但所有示例在没有它们的情况下都应该运行良好。

      18.4. 通过文本框的 The Crypt 玩家命令

      探索 The Crypt 的玩家一直在控制台提示符中输入 getgouse 命令。现在,您可以通过在文本框中输入命令来将用户输入移动到游戏网页上,如图 18.6 所示。

      图 18.6. 命令现在输入在页面底部的文本框中。

      为了创建一组基于网页的用户控件,您需要做三件事:

      1.  向页面添加文本框和按钮。

      2.  编写一个将文本框中的文本转换为游戏命令的函数。

      3.  编写一个在按钮被点击时调用的函数。

      您可以更新控制器模块中的代码,包括两个新函数。但控制器模块已经运行良好,设置了游戏,并与玩家、地点和消息的模型和视图一起工作(见 第十六章)。更好的计划是添加一个单独的模块来处理文本框中输入的命令,根据需要调用现有的控制器来执行 getgouse 方法。图 18.7 显示了 The Crypt 使用的模块,包括新的 Commands 模块。

      图 18.7. The Crypt 的模块,包括用于通过文本框执行命令的命令模块

      从添加文本框和按钮到网页的 HTML 开始。

      18.4.1. 向页面添加控件

      您需要一种让用户输入命令字符串并提交的方法。以下列表显示了添加到 The Crypt 网页中的 HTML,包括文本框和按钮。玩家、地点和消息的 div 元素最初是空的,这就是为什么它们在输出面板上不可见。

      列表 18.7. 向 The Crypt (HTML) 添加控件 (jsbin.com/rijage/edit?html,output)

      你的控制,文本框和按钮,现在都在页面上,并且它们已经设置了 id 属性,以便可以使用 JavaScript 访问。

      18.4.2. 将文本框输入映射到游戏命令

      表 18.1 显示了输入到文本框中的命令如何与在第十六章中创建的控制器模块的方法相匹配。

      表 18.1. 比较文本框命令与控制器模块的方法
      文本框命令 控制器方法
      get game.get()
      go north game.go("north")
      use a rusty key north game.use("a rusty key", "north")

      正如你所见,文本框命令应该使玩家更容易玩游戏。那么,你如何将文本框输入翻译成控制器能理解的命令呢?

      18.4.3. 使用 split、join、pop 和 shift 发出命令

      用户将在文本框中输入一个命令。你需要将这个命令转换成程序将采取的操作,如表 18.1 所示。要根据文本框中输入的命令调用正确的控制器方法(getgouse),你首先用 JavaScript 对象表示该命令。该对象将有一个 type 属性,与你要调用的控制器方法相匹配。表 18.2 显示了命令及其应生成的命令对象。

      表 18.2. 命令对象
      文本框命令 命令对象

      | get | { type: "get"

      } |

      | go north | { type: "go",

      方向: "north"

      } |

      | use a rusty key north | { type: "use",

      项目: "一把生锈的钥匙",

      方向: "north"

      } |

      每个文本框命令的第一个单词给出了命令对象的 type。要获取命令的单独单词,你使用 split 将字符串转换为单词数组。

      var commandWords = commandString.split(" ");
      

      例如,"get a rusty key" 变成 ["get", "a", "rusty", "key"]shift 数组方法移除并返回数组中的第一个元素。这对于获取命令对象的 type 完美。

      var commandWords = commandString.split(" ");
      var command = {
          type: commandWords.shift();
      };
      

      命令词不再在数组中。["get", "a", "rusty", "key"] 变成 ["a", "rusty", "key"]

      对于 gouse,你通过使用 pop 数组方法获取 commandWords 数组中的最后一个元素,即 direction

      command.direction = commandString.pop();
      

      如果 commandWords 数组中还有任何元素,你将它们重新连接起来形成物品的名称。

      command.item = commandWords.join(" ");
      

      例如,["a", "rusty", "key"] 被连接起来成为 "a rusty key"

      下一个列表显示了一个函数,该函数使用刚刚讨论的想法将命令字符串转换为命令对象。

      列表 18.8. 将命令字符串转换为命令对象 (jsbin.com/repebe/edit?js,console)

      在手头有了命令对象后,你现在可以调用游戏控制器的匹配方法。为了组织不同的可能命令类型,使用专门为决定多个选项而设计的控制结构:switch 块。

      18.4.4. 使用 switch 判断选项

      你希望根据发出的命令采取不同的操作。你可以使用一系列的 if-else 块。但 switch 块是程序员认为更整洁的替代方案。switch 允许你定义一系列代码块,并根据变量或属性的值执行某些代码块。以下是一个使用 command.type 作为 switch 变量的示例,比较两种方法:

      switch (command.type) {             |
                                          |
        case "get":                       |    if (command.type === "get") {
          game.get();                     |      game.get();
          break;                          |    }
                                          |
        case "go":                        |    else if (command.type === "go") {
          game.go(command.direction);     |      game.go(command.direction);
          break;                          |    }
                                          |
        case "use":                       |    else if (command.type === "use") {
          game.use(command.item,          |      game.use(command.item,
            command.direction);           |        command.direction);
          break;                          |    }
                                          |
        default:                          |    else {
          game.renderMessage(             |      game.renderMessage(
            "I can't do that");           |        "I can't do that");
      }                                   |    }
      

      如果 command.type 的值为 "get",则执行第一个 case 块中的代码。如果 command.type 的值为 "go",则执行第二个 case 中的代码。如果没有 break 语句,switch 块将继续执行第一个匹配后的所有 case 块。你可以包含一个 default 情况,其中包含在没有任何其他条件匹配时执行的代码。

      两种方法之间没有太大的区别;在 switch 块中读取条件稍微容易一些,但你需要额外的 break 语句。再一次,这取决于个人喜好:如果你认为 switch 块更整洁,就使用它;只是不要忘记 break 语句。

      列表 18.9 展示了在 doAction 函数上下文中 The Cryptswitch 块。

      18.4.5. 使其生效——监听按钮点击

      UI 问题的最后一部分是将 JavaScript 链接到 HTML。你需要按钮在点击时调用 doAction 函数,如 列表 18.9 所示:

      var commandButton = document.getElementById("btnCommand");
      
      commandButton.addEventListener("click", doAction);
      

      doAction 函数从文本框中检索文本:

      var txtCommand = document.getElementById("txtCommand");
      var commandString = txtCommand.value;
      

      doAction 函数随后解析命令字符串以创建命令对象。它使用 switch 块来调用匹配的控制器方法。列表 18.9 展示了命令模块的各个部分是如何组合在一起的。它使用了来自 列表 18.8 的 parseCommand 函数,该函数可以在 JS Bin 上看到。请注意,JS Bin 中的列表仅供参考;该模块不能在 JS Bin 中独立运行,将会抛出错误。

      列表 18.9. 命令模块 (jsbin.com/qedubi/edit?js,console)

      你希望为玩家平滑路径,因此,在 switch 块之后,你通过将 value 属性设置为空字符串来清除用户从文本框中的命令。而且因为你非常关心,你通过调用文本框的 focus 方法将光标放在文本框中,准备接收他们的下一个命令。在 switch 块之前,你也使用了消息视图的 clear 方法,该方法在 第十七章 的末尾添加,以清理任何旧消息。

      命令模块的代码被包裹在一个立即执行的函数表达式中。一旦网页加载脚本,它就会执行,并将事件监听器添加到按钮元素上。

      18.4.6. 进入加密室

      你没有修改现有的、正在工作的控制器模块代码,而是创建了一个新的模块,即命令模块,该模块解析玩家的命令并调用控制器的公共方法,getgouse。这很棒!控制器与玩家界面独立;它与基于控制台的游戏中使用的相同,并且可以与新的界面一起工作——你能添加一个按钮来获取房间里的物品吗?

      图 18.8 展示了正在进行中的 The Crypt 游戏,玩家通过消息视图查看消息。

      图 18.8。The Crypt 游戏进行中,显示给玩家的消息

      列表 18.7 包含了游戏的最新版本的 HTML,但你需要更改消息视图的 script 元素并添加一个新的命令模块:

      <!-- message view -->
      <script src="http://output.jsbin.com/nocosej.js"></script>
      
      <!-- Web Page Controls -->
      <script src="http://output.jsbin.com/qedubi.js"></script>
      

      output.jsbin.com/depijo 上玩游戏,并查看 jsbin.com/depijo/edit?html,javascript 中的 HTML 和 JavaScript。小心不要踩到僵尸!

      18.5。总结

      • 要使用按钮、下拉列表和文本框等控件,你需要一个用于控件的 HTML 元素、元素上的 id 属性以及在 JavaScript 中的元素引用。然后你可以访问文本框或下拉列表的 value 属性,并指定在按钮被点击时调用的函数。

      • 使用 button 元素在页面上显示按钮。在打开和关闭标签之间包含要显示在按钮上的文本:

        <button>Click Me!</button>
        
      • 在打开标签中包含一个 id 属性,以便可以从 JavaScript 代码中访问按钮:

        <button id="messageButton">Click Me!</button>
        
      • 使用按钮的 id 在 JavaScript 中获取按钮的引用:

        var btn = document.getElementById("messageButton");
        
      • 定义一个可以在按钮被点击时调用的函数:

        var showMessage = function () {
            var messageDiv = document.getElementById("message");
            messageDiv.innerHTML = "You clicked the button!";
        };
        
      • 为按钮添加事件监听器,以便在按钮被点击时调用函数:

        btn.addEventListener("click", showMessage);
        
      • 使用 input 元素并设置其 type 属性为 text 在页面上显示文本框。input 元素没有关闭标签:

        <input type="text" id="userMessage" />
        
      • 通过使用文本框的 value 属性获取或设置文本框中的文本:

        var txtBox = document.getElementById("userMessage");
        var message = txtBox.value;
        
      • 使用 select 元素和 option 元素来显示下拉列表:

        <select>
            <option>Choice 1</option>
            <option>Choice 2</option>
            <option>Choice 3</option>
        </select>
        
      • 使用 switch 块根据变量的值或属性执行代码:

        switch (command.type) {
            case "go":
                // Execute code when command.type === "go"
                break;
        
            case "get":
                // Execute code when command.type === "get"
                break;
        
            default:
                // Execute code if no other cases match
        }
        

      第十九章。模板:用数据填充占位符

      本章涵盖的内容

      • 用一个字符串替换另一个字符串

      • 使用 while 循环重复代码

      • 使用模板将 HTML 与 JavaScript 分离

      • 在网页中嵌入模板

      • 使用 map 将一个数组转换为另一个数组

      你希望你的网站易于导航,使用起来愉快。它们的设计应考虑每个页面应包含哪些内容,其可访问性和可用性,外观和感觉,以及整体用户体验。你的团队成员,在构建和维护网站时,将拥有不同的优势;即使是一个人的团队,在不同的时间关注网站的不同方面也是有意义的。

      用 JavaScript 编程入门一直青睐模块化开发应用的方法,您已经看到,尽管可能需要管理更多的部分,但每个部分的管理更加简单、灵活和专注。但是,从第二部分的纯 JavaScript 到第三部分的 JavaScript 和 HTML,出现了不受欢迎的流交叉。现在,您有视图将数据、JavaScript 和 HTML 混合在一起以产生输出(参见第十七章——或者更好的是,继续阅读):

      var html = "<h3>" + movie.title + "</h3>";
      html += "<p>" + movie.summary + "</p>";
      

      本章向您展示如何通过使用模板将 HTML 从数据和 JavaScript 中分离出来。您的团队中的设计师可以专注于他们的 HTML,避免复杂的 JavaScript 语法。而且,像模块化方法一样,隔离部分可以提高灵活性、可交换性和可维护性。不要交叉流!

      19.1. 构建新闻页面——最新消息

      在第十四章、第十五章和第十六章中,您一直在开发一个健身应用,让用户记录他们的锻炼会话;您是开发团队的一员,负责为不同的设备和平台开发应用。嗯,健身应用受到了很多关注;测试者的早期报告是积极的,社交媒体上也有很多讨论。您决定为团队创建一个新闻页面,以便让开发者、测试人员和其他感兴趣的人了解您正在做的工作。

      图 19.1 显示了包含两个新闻条目的新闻页面。所有团队成员都为新闻页面做出贡献,将他们的条目添加到中央内容管理系统(CMS)中。其他人管理 CMS,并为您提供作为 JavaScript 数据的新闻条目。您的任务是把这些数据转换成新闻页面的 HTML。

      图 19.1. 健身应用新闻,包含两个新闻条目

      19.1.1. 比较新闻条目数据和 HTML

      团队成员都很积极,并定期在内容管理系统上更新新闻。CMS 以这种形式为您提供数据:

      var posts = [
          {
            title: "Fitness App v1.0 Live!",
            body: "Yes, version 1 is here...",
            posted: "October 3rd, 2016",
            author: "Oskar",
            social: "@appOskar51"
          },
          {
            title: "Improved Formatting",
            body: "The app's looking better than ever...",
            posted: "October 8th, 2016",
            author: "Kallie",
            social: "@kal5tar"
          }
      ];
      

      团队中的设计师编写单个新闻条目的 HTML:

      <div class="newsItem">
          <h3>Fitness App v1.0 Live!<span> - by Oskar</span></h3>
          <p>Yes, version 1 is here...</p>
          <p class="posted"><em>Posted: October 3rd, 2016</em></p>
          <p class="follow">Follow Oskar @appOskar51</p>
      </div>
      

      每个条目都被包裹在一个div元素中,由一个标题和三个段落组成。标题包含一个用于帖子作者的span元素。(您使用span通过 CSS 将作者与其他标题部分区分开来。)您如何从数据中构建完成的 HTML 新闻条目?

      19.1.2. 通过字符串连接构建 HTML

      到目前为止,您一直是通过字符串连接逐个构建用于显示的字符串。对于一个新闻条目,您可能这样做:

      var item = '<div class="newsItem"><h3>' + post.title;
      item += '<span> - ' + post.author + '</span></h3>';
      item += '<p>' + post.body + '</p>';
      item += '<p class="posted"><em>Posted: ' + post.posted + '</em></p>';
      item += '<p class="follow">Follow ' + post.author + ' ';
      item += post.social + '</p></div>';
      

      这种方法的缺点是它将 JavaScript 和数据与 HTML 混合在一起。你的团队中有一些出色的设计师,他们对 HTML 了如指掌,但对 JavaScript 却不太自信。他们会很高兴地用 HTML 拼凑新闻条目的结构,但所有的var+=和点符号都是谜。即使他们能够接受 JavaScript,更新代码也不是一件轻松的事情;我看着你,引号!有更好的方法:让 HTML 专家专注于他们最擅长的领域。

      19.1.3. 使用 HTML 模板进行设计

      你希望设计师为通用新闻条目提供一些优雅、结构良好的 HTML,然后你将用最新的数据填充它。你希望他们为你提供一个新闻条目的模板

      <div class="newsItem">
          <h3>{{title}}<span> - {{author}}</span></h3>
          <p>{{body}}</p>
          <p class="posted"><em>Posted: {{posted}}</em></p>
          <p class="follow">Follow {{author}} {{social}}</p>
      </div>
      

      这比所有那些字符串连接要整洁得多,对吧?没有单引号和双引号之间的潜在混淆,数据的不同字段(标题、正文、作者、发布、社交)被清楚地标识为带有双大括号的占位符

      但如果模板与 HTML 一起包含,它不会出现在网页上吗?不会,如果你用script标签包裹它。

      19.1.4. 使用脚本标签作为模板

      HTML 模板与网页的其余 HTML 内容一起保留,放置在script标签内。为script元素使用非标准type属性,然后当浏览器加载页面时,它不会识别type并忽略模板。script元素的内容不会作为输出的一部分出现——可见的网页——也不会作为 JavaScript 运行。

      <script type="text/template" id="newsItemTemplate">
          <div class="newsItem">
              <h3>{{title}}<span> - {{author}}</span></h3>
              <p>{{body}}</p>
              <p class="posted"><em>Posted: {{posted}}</em></p>
              <p class="follow">Follow {{author}} {{social}}</p>
          </div>
      </script>
      

      如果script元素具有"text/javascript"类型的type属性,或者type属性缺失,浏览器将尝试将其内容作为 JavaScript 代码执行。但如果是"text/template"类型的type,浏览器将简单地跳过其内容。

      虽然在渲染页面时浏览器会忽略模板,但你仍然可以通过其id属性从 JavaScript 中访问它。

      var templateScript = document.getElementById("newsItemTemplate");
      var templateString = templateScript.innerHTML;
      

      第一个列表显示了新闻页面的body元素内容。有一个标题,一个用于新闻条目的div,以及包裹在script标签中的模板。

      列表 19.1. 健身应用新闻(HTML)(jsbin.com/viyuyo/edit?html,output)

      图片描述

      你只需要找到一种方法用实际数据替换占位符,你就可以准备好待发布的新闻条目。你需要学习如何用一个字符串,一个占位符,替换成另一个字符串,一些数据。

      19.2. 替换一个字符串为另一个字符串

      要替换一个字符串为另一个字符串,你使用replace字符串方法,将需要查找的字符串作为第一个参数,将用于替换的字符串作为第二个参数。该方法返回一个新的字符串。以下列表显示了如何将字符串"{{title}}"替换为字符串"Fitness App v1.0 Live!"。它会在控制台产生以下输出:

      > <h3>{{title}}</h3>
      > <h3>Fitness App v1.0 Live!</h3>
      
      列表 19.2. 用一个字符串替换另一个字符串 (jsbin.com/jeyohu/edit?js,console)

      图片

      replace 方法搜索它附加到的字符串,返回一个新的字符串。

      before.replace(string1, string2);
      

      这个片段搜索存储在变量 before 中的字符串。

      19.2.1. 连续调用 replace

      replace 方法作用于字符串。它也返回一个字符串。这意味着 replace 可以在其自己的返回值上调用,允许你进行如下链式调用:

      template
          .replace("{{title}}", "Fitness App v1.0 Live!")
          .replace("{{author}}", "Oskar");
      

      如果 template 是字符串 "<h3>{{title}}<span> - by {{author}}</span></h3>",那么前面的代码片段通过以下步骤工作。首先,替换 {{title}} 占位符:

      "<h3>{{title}}<span> - by {{author}}</span></h3>"
          .replace("{{title}}", "Fitness App v1.0 Live!")
          .replace("{{author}}", "Oskar");
      

      然后替换了 {{author}} 占位符:

      "<h3>Fitness App v1.0 Live!<span> - by {{author}}</span></h3>"
          .replace("{{author}}", "Oskar");
      

      最终结果是

      "<h3>Fitness App v1.0 Live!<span> - by Oskar</span></h3>";
      

      列表 19.3 展示了连续调用 replace 的一个示例。它在控制台上产生了以下输出:

      > Follow {{author}} {{social}}
      > Follow Oskar @appOskar51
      
      列表 19.3. 连续调用 replace (jsbin.com/rebugu/edit?js,console)

      图片

      在 列表 19.3 中,replace 的调用被写在了多行中。这对程序没有影响,但使阅读代码的人更容易挑出单独的调用。当方法可以通过点符号链式调用,如 replace,它们被称为具有 流畅接口。程序员经常设计一系列对象和方法以使用流畅接口,以便它们更容易使用、阅读和理解。

      19.3. 当循环——多次替换字符串

      了解如何使用 replace 来交换一个字符串为另一个字符串,你可以编写代码来测试它在新闻页面上的效果。图 19.2 展示了在替换占位符之前新闻条目模板的外观。

      图 19.2. 显示所有占位符的新闻条目

      图片

      新闻条目的数据是一个具有五个属性(titlebodyauthorpostedsocial)的 JavaScript 对象:

      var data = {
          title: "Fitness App v1.0 Live!",
          body: "Yes, version 1 is here...",
          posted: "October 3rd, 2016",
          author: "Oskar",
          social: "@appOskar51"
      };
      

      你为每个属性调用 replace,一开始你认为一切顺利。图 19.3 展示了渲染的新闻条目。

      图 19.3. 一个新闻条目——其中一个 {{author}} 占位符尚未被替换(右下角)。

      图片

      很接近,但不是普利策奖!其中一个占位符尚未被替换。新闻条目的右下角仍然有一个顽固的 {{author}}。下面的列表显示了您使用的代码。出了什么问题?

      列表 19.4. 为每个属性调用一次 replace (jsbin.com/quroha/edit?js,html,output)

      图片

      问题在于,replace 方法只替换它试图匹配的字符串的第一个出现。新闻条目模板有两个 {{author}} 占位符,而你只替换了其中一个。尝试自己解决这个问题,通过为同一个占位符调用 replace 两次。

      模板和replace代码都过于紧密耦合。如果设计者将模板修改为第三次包含{{author}}占位符(可能是用于电子邮件链接或简短的个人简介),你将不得不再次深入 JavaScript 代码并更新代码以添加对replace的另一个调用。

      你想要代码能够自动调用replace,直到所有占位符都被填充。你如何正确地调用replace指定次数?

      19.3.1. 当条件满足时重复代码

      你新闻条目模板中的所有占位符都需要替换为相应的数据。例如,{{author}}的两个实例都应该填充为Oskar,如图 19.4 所示(右上角和右下角)。

      图 19.4。两个作者占位符都已替换为 Oskar(右上角和右下角)。

      你的代码应该在找到每个属性的占位符时持续调用replace。在伪代码(假设代码)中,它应该看起来像这样:

      while there is a placeholder {
          replace the placeholder with data
      }
      

      大括号之间的代码块应该一直重复,直到找到占位符。一旦没有找到占位符,代码块可以被跳过。以下是在 JavaScript 中实现该目标的方法:

      while (filled.indexOf(placeholder) !== -1) {
          filled = filled.replace(placeholder, data.property);
      }
      

      你很快就会深入了解细节,所以如果你现在还没有完全理解,不要过于担心。但你应该能够理解代码的基本功能。让我们看看while循环在一般情况下是如何工作的。

      19.3.2. while循环

      while循环在filled字符串中找到占位符时持续调用replace。当没有找到占位符时,执行将继续到代码块之后。一般来说,while循环允许你在条件为true时重复执行代码块。while循环的结构如下所示:

      while (condition) {
          // Code to execute if condition is true.
      }
      

      循环首先评估条件。如果评估结果为true,则执行代码块,就像一个if语句。与if语句不同,一旦代码块被执行,while循环将再次评估条件。如果条件仍然评估为true,则再次执行代码块。循环将一直执行代码块,直到条件为true。如果条件为false,则跳过代码块,程序继续执行块之后的语句。

      下一个列表展示了如何使用while循环显示从 1 到 10 的整数。

      列表 19.5。使用while循环进行计数(jsbin.com/quroga/edit?js,console)

      count 变量达到 11 时,条件将评估为 falsecount 的值将不会记录到控制台。代码块应该始终更改在条件中使用的变量的值。否则,如果条件的值最初为 true,它将永远不会变为 false,循环将变成一个 无限循环。在 列表 19.5 中,你使用增量运算符 ++ 将 1 添加到 count 的值。这里展示了三种将 1 添加到 count 变量值的不同方法:

      count = count + 1;
      count += 1;
      count++;
      

      19.3.3. 在找到字符串时替换字符串

      你可以使用 while 循环来替换多个占位符的实例。首先,使用 indexOf 方法检查你想要替换的占位符是否存在。如果字符串找不到,indexOf 返回 -1。如果占位符被找到,则 indexOf 不会返回 -1。在 while 循环的条件中使用这个事实:

      while (filled.indexOf(placeholder) !== -1) {
          // Make changes to filled
      }
      

      列表 19.6 使用 while 循环不断替换字符串,直到它不再被找到。它在控制台上产生以下输出:

      > Starting replacement...
      > {{title}} by Oskar. Follow {{author}} {{social}}
      > {{title}} by Oskar. Follow Oskar {{social}}
      > ...replacement finished.
      
      列表 19.6. 使用 while 循环进行替换 (jsbin.com/cabaju/edit?js,console)

      代码首先记录开始替换过程。然后,while 循环执行其代码块两次,先替换一个 {{author}} 实例,然后替换另一个。由于没有更多的 {{author}} 实例要查找,filled.indexOf("{{author}}") 返回 -1while 循环结束。循环之后继续执行,代码通过记录替换过程已完成来结束。

      19.3.4. 使用正则表达式替换字符串

      有一种替换字符串的替代方法。它使用 正则表达式,这是一种指定你想要匹配和替换的字符模式的强大但通常复杂的方法。正则表达式略超出了本书的范围,但你可以在 www.room51.co.uk/js/regexp.htmlGet Programming with JavaScript 网站上调查许多示例。

      19.4. 自动化模板占位符替换

      健身应用使用用户锻炼会话的数据,其新闻页面使用新闻条目的数据。测验应用使用问题和答案的集合数据,The Crypt 使用其地图的数据。使用 HTML 模板来显示你可能在项目中找到的所有类型的数据将非常棒。但你不希望每次需要用数据填充占位符时都要重新发明 while。那么,自动化模板使用的关键是什么?

      19.4.1. 匹配模板占位符与对象属性

      图 19.5 再次显示了健身应用新闻页面上的一个新闻条目。占位符尚未用数据填充。

      图 19.5. 一个未填充占位符的新闻条目

      将填充占位符的数据看起来是这样的:

      var data = {
          title: "Fitness App v1.0 Live!",
          body: "Yes, version 1 is here...",
          posted: "October 3rd, 2016",
          author: "Oskar",
          social: "@appOskar51"
      };
      

      新闻项目数据对象的属性名,即其 ,与模板中占位符的名称相匹配。对于每个键,你希望继续替换其匹配的占位符,直到它们可以被找到。

      1. 从 title 开始,并继续用 Fitness App v1.0 Live! 替换 {{title}},直到没有更多的 {{title}} 占位符需要替换。

      2. 继续到 body,并继续用 Yes, version 1 is here... 替换 {{body}},直到没有更多的 {{body}} 占位符需要替换。

      3. 对每个键重复此过程,直到所有键的所有占位符都已替换。

      你可以通过使用 Object .keys 方法来获取所有新闻项目属性名的数组。

      var keys = Object.keys(data);
      

      对于新闻条目,keys = ["title", "body", "posted", "author", "social"]

      带着键,你可以轻松地在模板中创建占位符并用值替换它们。记住,你可以使用方括号表示法来检索键的值:

      data["title"];    // "Fitness App v1.0 Live!"
      data["author"];   // "Oskar"
      

      如果你有一个属性的键,你还可以构建模板中需要匹配的占位符:

      var placeholder = "{{" + key + "}}";
      

      属性的 命名得很好;你使用它来解锁属性的值及其占位符(表 19.1)。

      表 19.1. 属性的键用于访问其值并构建其占位符
      占位符
      title data["title"] {{title}}
      body data["body"] {{body}}
      posted data["posted"] {{posted}}

      19.4.2. 为每个键填充所有占位符

      是时候将所有部分组合起来,编写一些代码,这些代码不仅将为健身应用新闻页面填充数据,而且还将与任何数据和匹配的模板一起工作。列表 19.7 展示了一个 fill 函数,它使用了上一节中的想法,遍历数据对象的键,并用值替换占位符。

      列表 19.7. 填充模板数据的函数(jsbin.com/bazika/edit?js,output)

      列表 19.7 还包括测试 fill 函数的代码。在最后一行,它设置了由函数返回的 HTML 的 div 元素内容。

      19.4.3. 使用模板构建项目列表

      你已经笑了。是的,健身应用新闻页面有一个新闻条目列表。是的,你的模板 fill 函数只使用一个新闻条目就能工作。但你知道 forEach 在处理列表时的强大功能。编写一个 fillList 函数非常简单,如下所示。

      列表 19.8. 使用模板构建列表(jsbin.com/hilecu/edit?js,output)

      在列表 19.8 中的fillList函数中,你使用forEach方法遍历数据对象数组dataArray,将每个对象传递给fill函数,并将返回的填充模板追加到listString中。

      现在你有了所需的两个模板函数,你渴望了解完成后的健身应用新闻页面。

      19.5. 建立新闻页面——新闻即时更新

      是时候模块化了。你知道健身应用团队是如何运作的;他们喜欢分享代码,但讨厌污染。他们希望看到一个具有独立数据模板模块的新闻页面。图 19.6显示了设置。

      图 19.6。页面包括新闻条目模板和 JavaScript 代码,并导入两个模块。

      图片

      JS Bin 上的新闻页面将新闻条目模板放在一个script元素中,并在 JavaScript 面板中使用模块和模板来显示新闻条目。显示两个新闻条目的完成网页在图 19.1 中显示。

      要完成新闻页面的工作,首先创建模块,然后将它们导入到网页中。在接下来的两个部分中详细了解。

      19.5.1. 创建模板和数据模块

      将模板函数放入一个模块中,将新闻数据放入另一个模块中。使用命名空间以避免创建过多的局部变量。

      模板函数

      下一个列表显示了如何将列表 19.7 和 19.8 中的两个模板函数打包到一个模板模块中。

      列表 19.9。模板模块(jsbin.com/pugase/edit?js,console

      图片

      你使用了一个新的命名空间,gpwj(来自《用 JavaScript 编程》);模板模块在许多项目中都会很有用,因此将其作为通用工具命名空间的一部分而不是与健身应用或《密码学》或其他任何使用的地方一起使用是值得的。要调用函数,请包含命名空间:

      var newsItemHTML = gpwj.templates.fill(newsItemTemplate, data);
      
      新闻数据

      对于现实世界的新闻页面,数据将来自中央内容管理系统。你通过创建一个提供模拟数据的模块来模拟 CMS 新闻源。你可以在稍后替换模块,以使用具有实时 CMS 连接的模块。

      下一个列表包括数据和获取其分配到fitnessApp.news命名空间的功能。

      列表 19.10。新闻页面数据模块(jsbin.com/fupiki/edit?js,console

      图片

      要获取新闻条目,调用getItems,指定你想要的条目数量。例如,要获取三个新闻条目,你会使用以下代码:

      var itemsData = fitnessApp.news.getItems(3);
      

      实际的新闻数据模块将从 CMS 检索项目,它将使用与列表 19.10 相同的接口。换句话说,其新闻也将通过调用其getItems方法来访问。通过使用相同的接口,你可以轻松地将模块从静态版本交换到动态 CMS 版本。

      19.5.2. 导入模块

      为了向读者提供健身应用团队发展的最新头条新闻,你创建了这里所示的简单新闻页面 HTML。

      列表 19.11. 一个模块化新闻页面(HTML)(jsbin.com/vemufa/edit?html,output)

      图片

      为了将所有这些部分组合在一起,你添加了 JavaScript 代码,从页面中检索模板,用新闻条目数据填充它,并使用生成的 HTML 更新新闻 div,如下面的列表所示。

      列表 19.12. 一个模块化新闻页面(jsbin.com/vemufa/edit?js,output)

      图片

      使用模板是从应用程序数据生成 HTML 的常见方式。有许多 JavaScript 模板库可供免费使用;Handlebars、Moustache 和 Pug 是三个流行的例子。

      停止 presses!你的团队成员非常喜欢你所采用的整洁、模块化、可重用的方法,并誓言每天都会添加新的新闻条目。其中一些人已经开始将 gpwj.templates 函数整合到他们自己的应用程序中。你决定也这样做,当你回到 The Crypt 时。

      19.6. The Crypt—改进视图

      在 第十七章 中,你为玩家和地点创建了一些基于 Web 的视图。它们显示的信息,原本是为控制台设计的,使用了空格、换行和来自 spacer 命名空间的框和边框进行格式化,而不是 HTML。现在你已经看到了如何使用模板来分离标记和 JavaScript,是时候改进视图,以便在 The Crypt 中使用适当的 HTML 标签来包装数据了。

      图 19.7 展示了经过修改后 The Crypt 的样子。从纯文本切换到 HTML 使得使用 CSS 来样式化输出成为可能,从而为视觉上有趣的设计提供了更大的潜力。图 19.8 展示了项目中的模块,其中新模块和更新模块被突出显示。

      图 19.7. The Crypt 的最新版本使用 HTML 模板来构建页面元素。

      图片

      图 19.8. The Crypt 中的模块,新模块或更新模块被突出显示

      图片

      你在本章的早期部分创建了模板模块。现在你创建模板并更新视图以使用它们。

      19.6.1. 为所有视图创建 HTML 模板

      下面的列表展示了嵌入在 HTML script 标签内的所有 The Crypt 模板。它们是 JS Bin 上的一个完整网页的一部分。

      列表 19.13. 带有模板的 The Crypt (jsbin.com/yapiyic/edit?html,output)

      图片

      模板中没有混合 JavaScript。一个习惯于使用 HTML 工作的网页设计师可以使用他们的知识来更新模板,而无需从任何其他代码中拆分 HTML。显然,这种关注点的分离使模板更整洁、更易于阅读,也更容易为任何参与构建应用程序的人使用。

      19.6.2. 更新视图以使用新的模板

      现在,将The Crypt的模板字符串嵌入到页面上的 HTML 中,您需要更新视图模块以获取模板字符串,并用消息或玩家和地点模型的数据填充它们。列表 19.14,19.15,和 19.16 显示了新的视图代码。注意:视图是为控制器设计的,单独使用时不会工作。包含到 JS Bin 的链接,以便您可以复制、克隆或更改代码(如果需要的话)。

      消息

      最简单的视图是消息视图。以下是更新的代码。

      列表 19.14. 使用模板的消息视图(jsbin.com/jojeyo/edit?js,console

      传递给消息视图的render方法的只是字符串。template.fill方法期望对象(它使用对象键来创建占位符并查找值),所以render从消息创建一个对象data,用作参数。消息"hello"变为{ message: "hello" }。然后fill方法将{{message}}占位符替换为值"hello"

      玩家

      玩家视图比消息视图稍微复杂一些,因为玩家有物品。除了有一个模板字符串来显示玩家的姓名和健康状态外,玩家视图还需要另一个用于物品的模板。

      下一个列表显示了新的玩家视图。与物品相关的代码以粗体显示。

      列表 19.15. 使用模板的玩家视图(jsbin.com/suyona/edit?js,console

      玩家的物品数据是一个字符串数组。因为fillList期望一个对象数组,所以您使用map将数组从字符串转换为对象。您将一个函数传递给map方法,然后map构建一个新的数组,其中每个新元素都是由该函数创建的。数组["a lamp", "a key"]变为[{ item: "a lamp" }, { item: "a key" }]

      使用fillList生成的物品 HTML 不能添加到页面中,直到玩家 HTML 已经添加,因为玩家 HTML 包括将包含物品的ol元素。

      //Add the filled player template to the page.              
      playerDiv.innerHTML = gpwj.templates.fill(playerTemplate, data);
      
      // Get a reference to the playerItems ol that's just been added to the page
      itemsDiv = document.getElementById("playerItems");
      
      // Add the HTML for the list of items to the playerItems ol element
      itemsDiv.innerHTML = gpwj.templates.fillList(itemTemplate, items);
      
      地点

      地点视图是最复杂的,因为地点有物品和出口。但是显示出口的方法与物品相同,所以额外的复杂性只是轻微的重复。出口数据是一个字符串数组,就像物品数据一样。可以使用相同的模板。

      以下列表显示了更新的地点视图。退出的代码以粗体显示。

      列表 19.16. 使用模板的地点视图 (jsbin.com/yoquna/edit?js,console)

      再次,map 用于将字符串数组转换为对象数组,这次用于物品和出口。

      19.6.3. 进入 The Crypt

      列表 19.13 在 JS Bin 上是使用模板和三个新视图的 The Crypt 的一个工作示例。这里显示了导入新模块的脚本:

      <!-- gpwj.templates -->
      <script src="http://output.jsbin.com/pugase.js"></script>
      
      <!-- player view -->
      <script src="http://output.jsbin.com/suyona.js"></script>
      <!-- place view -->
      <script src="http://output.jsbin.com/yoquna.js"></script>
      <!-- message view -->
      <script src="http://output.jsbin.com/jojeyo.js"></script>
      

      前往 http://output.jsbin.com/yapiyic 并进行尝试。

      19.7. 概述

      • 使用 replace 字符串方法用另一个字符串替换一个字符串:

        "One too three".replace("too", "two");      // Returns "One two three"
        
      • 使用 replace 的链式调用交换多个字符串:

        "One too three".replace("One", "Far").replace("three", "long");   
        // Returns "Far too long"
        
      • 使用 while 循环在条件保持 true 时执行一段代码。

        var count = 10;
        
        while (count > 5) {        // Displays 10 9 8 7 6 on separate lines
            console.log(count);
            count = count – 1;
        }
        
      • 使用 map 数组方法根据现有数组的元素创建一个新数组。传递给 map 的函数作为参数返回一个基于旧值的新值:

        var planets = ["Mercury", "Venus"];
        
        var bigPlanets = planets.map(function (oldValue) {
            return oldValue + " becomes " + oldValue.toUpperCase();
        });
        
        // bigPlanets === ["Mercury becomes MERCURY", "Venus becomes VENUS"]
        
      • 使用带有占位符的模板字符串来避免将 JavaScript 与显示字符串混合:

        var templateString = "<h3>{{title}}</h3><p>{{body}}</p>";
        
      • 通过使用具有非标准 type 属性的 script 标签将模板字符串嵌入到 HTML 中。

        <script type="text/template" id="postTemplate">
            <h3>{{title}}</h3><p>{{body}}</p>
        </script>
        
      • 通过 script 标签的 id 属性和元素的 innerHTML 属性从 JavaScript 访问模板:

        var templateString = document.getElementById("postTemplate").innerHTML;
        
      • 使用对象中的数据填充模板。它返回一个字符串,其中的占位符被对象的属性替换:

        var data = {title: "Out of Office", body: "I'm going on an adventure!"};
        var template = "<h3>{{title}}</h3><p>{{body}}</p>";
        gpwj.templates.fill(template, data);  
        // Returns "<h3>Out of Office</h3><p>I'm going on an adventure!</p>"
        

      第二十章. XHR:加载数据

      本章涵盖

      • 使用 XMLHttpRequest 对象加载数据

      • 数据加载后调用函数

      • 使用加载的数据更新视图

      • JavaScript 对象表示法 (JSON)

      • 将 JSON 文本转换为 JavaScript 对象

      日历或电影数据库或冒险游戏可能需要大量数据。新闻网站可能有最新的更新,包括突发新闻和体育比分。并不是总是希望一次性加载所有数据,或者让访客不断刷新网页以获取最新信息。如果页面能够仅访问它需要以保持新鲜的数据片段,那就太好了,即使它已经加载了。股价、推文、评论、比分,以及是的,僵尸的健康状况都可以独立更新,而无需完全重新加载页面。

      本章向您展示如何在运行时通过网络获取数据,用于您的应用。特别是,当您在健身应用中切换用户时,您会加载锻炼数据,当玩家解决 The Crypt 的谜题时,您会逐个加载位置数据。

      20.1. 构建健身应用—检索用户数据

      您和您的团队一直在构建一个健身应用,允许用户跟踪他们的锻炼(参见第十四章 chapters 14 到第十六章 16)。该应用可以将 JavaScript 数据转换为用户模型,使用可选的视图在控制台显示用户信息,并在控制台提示中接受用户输入。您在项目中分配的任务如下:

      1. 以字符串形式检索用户数据。

      2. 将用户数据转换为用户模型。

      3. 显示用户数据。

      4. 为用户提供添加会话的界面。

      您已经完成了任务 2、3 和 4,现在是时候通过互联网检索用户数据了。您希望在应用程序中使用时能够切换用户,如图 20.1 所示。

      图 20.1. 在使用健身应用程序时切换用户

      图 20.1 显示了调用app.loadUser方法以加载第二和第三用户的数据。在您能够获取数据之前,您需要一种方式来指定每个用户数据的位置。

      20.1.1. 定位用户数据

      不同的团队成员正在为不同的平台和设备开发健身应用程序的不同版本。但所有版本都将使用由中央健身应用程序服务器提供的相同数据,如图 20.2 所示。

      图 20.2. 所有应用程序都使用相同的数据。

      到目前为止,在开发应用程序的过程中,您一直使用一个静态数据文件来处理单个用户。您希望能够在不重新加载整个应用程序的情况下,在不一次性加载所有用户数据的情况下(您希望有很多用户!),在用户之间进行切换。您决定定义一个loadUser方法,该方法将为指定的用户 ID 加载用户数据,并在控制台上显示:

      > app.loadUser("qiwizo")
        Loading user details...
      
        Mahesha
        120 minutes on 2017-02-05
        35 minutes on 2017-02-06
        45 minutes on 2017-02-06
      
        200 minutes so far
        Well done!
      

      您的健身应用程序的 ID 对应于 JS Bin 上的文件;在实际应用程序中,它们可能是数据库中的 ID 或可能是唯一的用户名。每个文件只包含一个用户的数据。例如,以下是 Mahesha 的数据,位于output.jsbin.com/qiwizo.json

      {
        "name" : "Mahesha",
        "sessions" : [
          {"sessionDate": "2017-02-05", "duration": 120},
          {"sessionDate": "2017-02-06", "duration": 35},
          {"sessionDate": "2017-02-06", "duration": 45}
        ]
      }
      

      那么,如何在程序运行时加载玩家的数据呢?秘密在于奇怪命名的XMLHttpRequest对象。但在您揭开其秘密之前,了解与远程数据交互的步骤将非常有价值。

      20.1.2. 加载用户数据——概述

      您需要为健身应用程序加载用户数据。每当用户调用loadUser方法时,您的应用程序需要通过网络找到数据,检索它,然后使用它。为了完成所有这些,应用程序将需要以下内容:

      • 一个用户 ID

      • 一个用于查找数据的 URL

      • 一个在数据检索后调用的函数

      第三个要求值得进一步考虑。数据可能跨越互联网,甚至可能跨越世界,在另一台机器上。找到并检索数据需要时间(希望是毫秒,但可能是秒)。在程序能够使用这些数据创建新的 User 对象并更新显示之前,它需要这些数据。你定义了一个函数,称为 回调函数,当数据加载完成时会被调用。你之前已经遇到过回调函数:在 第十八章 中处理按钮时,你请求一个在按钮被点击时调用的函数。这就像函数正在监听点击事件的发生。对于加载数据用户,你的回调函数正在监听加载事件的发生。你加载数据并使用它来更新应用程序的代码可能如下所示:

      function updateUser (userData) {
          // Create a new User from the data
          // Use a view to update the display
      }
      
      function loadData (id, callback) {
          // Build a URL using the id
      
          // Let the app know to run the callback function when the data loads
      
          // Tell the app to go and get the data from the URL
      }
      
      loadData("qiwizo", updateUser);
      

      你将 loadData 函数的用户 idqiwizo)和一个在数据加载完成后要调用的函数 updateUser 传递给它。

      你已经对获取所需数据的步骤有了概念。现在是时候研究具体细节了。

      20.1.3. 加载用户数据——XMLHttpRequest 构造函数

      是的,XMLHttpRequest 构造函数有一个有趣的名字,尽管请求部分是合理的。你想要从世界上的某个地方(一个 服务器)请求信息。你可以将研究 XML 和 HTTP 作为家庭作业。从现在起,除了在代码中,我还会称之为 XHR 构造函数。简短、简洁、可爱?

      XHR 构造函数由浏览器提供。你使用它来创建包含从互联网请求资源的方法的 XHR 对象。你向 XHR 对象传递一个 URL 和一个要调用的函数,当它加载数据时,它会调用你的函数。图 20.3 展示了当使用 XHR 对象加载数据健身应用程序中的用户数据时检索到的数据。

      图 20.3. XHR 对象返回的数据字符串

      列表 20.1 包含了执行数据请求所需的这五个步骤的代码。

      1.  使用 XHR 构造函数创建一个 XHR 对象

      2.  声明或构建 URL

      3.  为 XHR 对象提供一个在数据加载完成后调用的函数

      4.  调用 open 方法,并传入 URL

      5.  调用 send 方法以启动请求

      列表 20.1. 使用 XHR 对象加载数据用户 (jsbin.com/qakofo/edit?js,console)

      你使用 new 关键字调用 XHR 构造函数,它会创建并返回一个 XHR 对象。你调用对象的方法来设置和启动请求。你提供一个函数,一旦数据加载完成就调用它;在 列表 20.1 中,你使用函数表达式作为 addEventListener 的第二个参数,但你也可以使用之前定义的函数的名称。一旦加载完成,数据会自动分配给 XHR 对象的 responseText 属性,你的回调函数可以通过该属性访问数据。open 函数的第一个参数 GET 是请求的 HTTP 方法动词。在本章中,你坚持使用 GET,因为你正在获取数据,但还有其他动词,如 POSTPUTDELETE

      你可以在 图 20.3 中看到数据以文本形式返回。文本显示在双引号之间(第一个字符和最后一个字符)。数据是以称为 JavaScript 对象表示法(JSON)的格式,这在 第 20.2 节 中有更详细的讨论。因为它是一种文本,一个字符串,你不能像访问 JavaScript 对象那样访问数据,例如使用 data.namedata["sessions"]

      幸运的是,有一种简单的方法可以将 JSON 文本转换为 JavaScript 对象。

      20.1.4. 加载用户数据——使用 JSON.parse 解析 XHR 响应

      当 XHR 请求加载了健身应用用户的 数据后,它会运行回调函数。数据作为一个字符串,会自动分配给 XHR 对象的 responseText 属性。为了将字符串转换为你可以访问的 JavaScript 对象,例如 namesessions,你使用 JSON.parse JavaScript 方法。

      var dataAsObject = JSON.parse(dataAsString);
      

      图 20.4 展示了将用户数据解析并记录到控制台后的样子。它现在是一个 对象,具有属性(比较 图 20.3 和 20.4)。

      图 20.4. 一旦

      以下列表更新了 列表 20.1,在将数据记录到控制台之前,解析加载的文本以获取 Java-Script 对象。

      列表 20.2. 将 JSON 文本解析为 JavaScript 对象 (jsbin.com/rexolo/edit?js,console)

      你应该面带微笑。只需几行代码,你就可以访问世界各地的数据。(好吧,从托管你网页的服务器。但那可以是任何地方。)大大的拥抱。

      Get Programming with JavaScript 中开发的全部应用程序都可以从一种简单的方法中受益,以便访问 JS Bin 上的数据;它生成的 bin 代码,如 qiwizo,有点麻烦。在下一节中,你将 XHR 代码打包到一个 load 函数中,这使得获取数据变得更加容易。

      20.1.5. 加载 JS Bin 数据——一个实用的函数

      测验应用程序、我的电影评分页面、健身应用程序新闻页面以及The Crypt都可以在运行时加载数据,就像健身应用程序本身一样。为了简化从 JS Bin 加载数据的过程,你创建了一个模块,Bin Data

      作为一项通用实用函数,你将load函数添加到gpwj命名空间中,该命名空间已经是模板函数的家园(参见第十九章)。要加载 JS Bin 文件中的数据,你使用以下形式的代码:

      function doSomethingWithData (data) {
          // Do something amazing with the data
      }
      
      gpwj.data.load("qiwizo", doSomethingWithData);
      

      下一个列表显示了该函数的实际操作。它生成了图 20.4 中的输出。

      列表 20.3. 加载 JS Bin 数据的函数(jsbin.com/suxoge/edit?js,console)

      你使用load函数在第 20.3 节中的The Crypt中获取位置数据。书中其他项目的数据加载版本可在Get Programming with JavaScript网站上找到,网址为www.room51.co.uk/books/getprogramming/projects/。现在,健身应用程序得到了升级。

      20.1.6. 构建健身应用程序

      健身应用程序团队很高兴看到基于控制台程序的工作原型。还有三个任务待完成:

      1. 更新控制器模块以使用新的load函数来处理用户数据

      2. 使用script元素加载应用程序使用的模块

      3. 添加一行 JavaScript 代码以初始化应用程序

      这里展示了新的控制器。它有一个loadUser函数,该函数调用gpwj.data.load方法。loadUserlog都作为接口从init方法返回。

      列表 20.4. 健身应用程序控制器(jsbin.com/nudezo/edit?js,console)

      因为应用程序在运行时加载用户数据,所以有可能用户在数据加载完成之前尝试记录锻炼会话(健身应用程序的用户非常热衷)。log方法确保在允许记录之前用户已被设置,user !== null。记住,null是 JavaScript 中的一个特殊值,通常用来表示预期一个对象但该对象缺失或尚未分配。

      在所有部件就绪后,只需将它们组合在一起并发射起跑枪。四个健身应用程序模块在图 20.5 中显示,其中 Bin Data 模块取代了提供静态数据的模块。

      图 20.5. 构成健身应用程序的模块

      你不需要修改User构造函数或视图。下一个列表显示了用于加载四个模块的script元素。

      列表 20.5. 健身应用程序(HTML)(jsbin.com/mikigo/edit?html,console)
      <!-- fitnessApp.User -->
      <script src="http://output.jsbin.com/fasebo.js"></script>
      
      <!-- fitnessApp.userView -->
      <script src="http://output.jsbin.com/yapahe.js"></script>
      
      <!-- fitnessApp.controller -->
      <script src="http://output.jsbin.com/nudezo.js"></script>
      
      <!-- gpwj.data -->
      <script src="http://output.jsbin.com/guzula.json"></script>
      

      以下列表展示了启动应用程序的 JavaScript 代码,调用fitnessApp.init并使用 JS Bin 代码为初始用户。

      列表 20.6. 健身应用程序(jsbin.com/mikigo/edit?js,console)
      var app = fitnessApp.init("qiwizo");
      

      在 JS Bin 上查看应用程序的实际运行情况是最好的地方,但本章开头图 20.1(figure 20.1)展示了用户正在加载和记录会话。

      你对运行中的应用程序感到非常兴奋,开发团队也是如此。但还有一些东西缺失(除了实际上能够将数据保存到某个地方之外)。难道《用 JavaScript 编程入门》的第三部分 part 3 不是全部关于基于 HTML 的应用程序吗?为什么你又回到了控制台?

      20.1.7. 健身应用程序——接下来是什么?

      图 20.6 展示了你真正想要的东西——一个基于 HTML 的健身应用程序,具有下拉列表、文本框和按钮。你可以从列表中选择用户,加载他们的详细信息,并记录新的会话。凭借你在书中所学的一切,你能构建这个应用程序吗?没有什么比亲自尝试构建一些东西来帮助你学习更好了。没有压力,没有比赛,也没有失败;要勇敢和好奇,犯错误,并寻求帮助。即使你没有到达终点线,你肯定会在旅途中学到很多东西,得到一次很好的 JavaScript 锻炼,并锻炼你的编码肌肉。而且一旦你亲自尝试了,你就可以偷偷地看看jsbin.com/vayogu/edit?output,看看它是如何拼凑起来的。(别忘了运行程序。)

      图 20.6. 使用基于 Web 的视图的健身应用程序

      如果你想要的是一个基于 HTML 的数据驱动应用程序,那么在The Crypt中看到它运行的效果再好不过了。

      但首先,如承诺的那样,这里是对 JSON 数据格式的一个非常简短的介绍。

      20.2. JSON——一种简单的数据格式

      JSON 是一种易于人类阅读和编写、也易于计算机解析和生成的数据格式。它已成为在网络上交换数据的一种非常流行的格式。以下是一个用 JSON 编写的日历事件:

      {
          "title" : "Cooking with Cheese",
          "date" : "Wed 20 June",
          "location" : "The Kitchen"
      }
      

      格式应该非常熟悉;它基于 JavaScript 的一个子集。属性名和字符串必须用双引号括起来。属性值可以是数组或嵌套对象。以下是 6 月份的一些日历数据:

      {
        "calEvents" : [
          {
            "title" : "Sword Sharpening",
            "date" : "Mon 3 June",
            "location" : "The Crypt"
          },
          {
            "title" : "Team Work Session",
            "date" : "Mon 17 June",
            "location" : "The Crypt"
          },
          {
            "title" : "Cooking with Cheese",
            "date" : "Wed 20 June",
            "location" : "The Kitchen"
          }
        ]
      }
      

      值也可以是数字、布尔值、nullundefined。这就足够了。(老实说,JSON 并没有多少。完整的规范在json.org。你可能会对它的简短感到惊讶——你不得不爱那些效率极高的火车轨道图!)

      JSON 数据以文本形式传输。那么,你如何将其转换为 JavaScript 程序中可以使用的对象和数组呢?

      20.2.1. 使用 JSON.parse 将 JSON 转换为对象和数组

      您可以通过传递给JSON.parse方法将 JSON 文本转换为 JavaScript 对象。然后,您可以使用点或方括号符号访问数据中的属性或元素。如果您已经使用 XHR 对象以 JSON 格式发送请求以获取单个日历事件,您可以像这样将响应转换为 JavaScript 对象:

      var calEvent = JSON.parse(xhr.responseText);
      
      calEvent.title;    // Cooking with Cheese
      calEvent.date;     // Wed 20 June
      calEvent.location; // The Kitchen
      

      这只是对 JSON 的简要介绍,但鉴于整本书都是关于 JavaScript 的,这应该足够了。

      是时候加载地图数据了,神秘房间一个接一个,当你返回地窖时。

      20.3. 地窖——按需加载地图

      地窖中的冒险包括探索古墓、广阔的太空船和神秘的森林。每一次冒险可能覆盖数十个地点,而且很难在一次游戏中完成。与其在游戏开始时加载整个地图,不如在玩家访问时才加载地点。游戏可以在 JS Bin 上为每个地点存储 JSON 数据,并在需要时使用XMLHttpRequest来加载文件。

      图 20.7 只显示了游戏开始时加载的第一个地点。加载的数据包括该地点出口的 JS Bin 文件代码。例如,厨房南边的地点的数据文件代码是kacaluy

      图 20.7。在游戏开始时,只加载了初始地点的数据。

      20fig07_alt.jpg

      地图上每个地点的数据只在玩家移动到该地点时加载。图 20.8 显示了玩家从厨房向南移动后的同一张地图。现在已经加载了老图书馆的数据。

      图 20.8。由于玩家移动到了那里,老图书馆的数据已经被加载。

      20fig08_alt.jpg

      要允许这种逐步加载,您必须在地图数据中包含每个地点出口的 JS Bin 文件代码。

      20.3.1. 使用 JS Bin 文件代码指定出口

      假设一个在地窖中的游戏从厨房开始。一开始,游戏只会加载厨房的数据,而不会加载地图上的其他任何地点的数据。这样在游戏开始时加载的数据就少多了。下面的列表显示了该单个地点的数据。(挑战已被省略,以便您能专注于id。)

      列表 20.7.厨房的 JSON 数据(output.jsbin.com/qulude.json)

      379fig01_alt.jpg

      每个地点都有一个唯一的id,它与存储其数据的文件 JS Bin 代码相对应。您使用id来构造地点的 URL,如下所示:

      var url = "http://output.jsbin.com/" + id + ".json";
      

      玩家将在地图上往返移动,解决谜题,收集宝藏,溶解僵尸,舔舐豹子。他们可能会多次访问某些地点,您希望避免重复加载相同的数据。

      20.3.2. 使用缓存——只加载每个地点一次

      每次从加载的数据中创建一个新的地点时,你都会使用地点的 id 作为键将其存储在缓存中。对于地点的进一步请求可以给出从缓存中存储的地点,从而避免反复从 JS Bin 加载相同的数据。

      var placeStore = {};    // Set up an object to store the loaded places
      
      placesStore[placeData.id] = place;    // Use the place's id as the key
      

      20.3.3. 用地图管理器替换地图数据和地图构建器模块

      The Crypt 的地图构建器曾经接受整个地图的数据,创建所有地点模型,然后将物品、出口和挑战添加到地点。一旦它为整个地图完成了这些操作,它就会返回第一个地点,游戏就可以开始玩了。你现在一次加载一个地点;你需要一个函数来加载单个地点数据,以及一个函数从加载的数据中构建单个地点模型。

      代码列表 20.8 展示了一个新的地图管理器模块的代码,该模块将替换你在 The Crypt 的早期版本中使用的地图构建代码(图 20.9)。地图管理器模块在其接口中提供了一个单独的方法,loadPlace。使用之前定义的回调函数或函数表达式:

      图 20.9. The Crypt 的模块,突出显示新的或更新的模块

      // Use a previously defined function as the callback   
      theCrypt.map.loadPlace("qiwizo", doSomethingWithPlace); 
      
      // Use a function expression as the callback
      theCrypt.map.loadPlace("qiwizo", function (place) {
          // Use the place model
      });
      
      代码列表 20.8. 地图管理器 (jsbin.com/xesoxu/edit?js)

      地图管理器由两个函数组成。让我们依次探索它们。

      将地点添加到存储中

      addPlace 函数使用 Place 构造函数从数据中创建一个模型;添加项目、出口和挑战;将地点存储在缓存中;并返回它。

      function addPlace (placeData) {
          // Create a place model
      
          // Store place in cache
      
          // Add items
          // Add exits and challenges
      
          // Return the new place model
      }
      

      对于这个函数,没有更多要说的了;除了缓存之外,你之前已经看到了所有内容。

      加载单个地点的数据

      当你调用 loadPlace 函数时,你给它你想要的位置的 id 和一个回调函数。你的回调将使用 id 的地点模型作为参数调用。

      callback(place);
      

      但是,loadPlace 函数从哪里获取地点模型呢?首先,它尝试缓存。如果地点不在那里,它将从 JS Bin 加载地点数据。

      var place = placesStore[id];    // Try to get the place from the cache
      
      if (place === undefined) {
          // The place is not in the cache, so
          // load the place data from JS Bin and
          // pass the place to the callback
          callback(place);
      } else {
          // The place was in the cache, so 
          // pass it to the callback
          callback(place);   
      }
      

      如果 loadPlace 函数需要加载数据,它将使用 Bin 数据模块的 load 函数从 JS Bin 获取它。地点不在缓存中,因此代码将地点数据传递给 addPlace,以创建和存储模型,然后再将新的地点模型传递给回调。

      gpwj.data.load(id, function (placeData) {
          var place = addPlace(placeData);    // Create and store a place model
          callback(place);     // Pass the new place model to the callback
      });
      

      现在你已经知道了数据加载的工作原理,你可以在游戏控制器中使用它,用于加载初始位置并将玩家移动到新位置。

      20.3.4. 更新游戏控制器以使用地图管理器

      你需要更新两个控制器方法来使用新的地图管理器。init 方法曾经调用 buildMap 来为一次冒险构建整个地图。现在它将调用 loadPlace 来仅加载游戏中的第一个地点。由于地点不再预先加载,go 方法将需要调用 loadPlace 来从缓存中检索地点或从 JS Bin 加载它。

      下一个列表显示了在控制器上下文中的两个更新方法。

      列表 20.9. 使用地图管理器的游戏控制器 (jsbin.com/vezaza/edit?js)

      init 方法必须在加载起始位置之后才能完成大部分工作。因此,其大部分代码都在传递给 loadPlace 的回调函数中。同样,go 方法在将位置设置为玩家的新位置之前,使用 loadPlace 获取位置。

      20.3.5. 构建游戏页面

      剩下的就是导入模块,并添加几行 JavaScript 代码来初始化游戏。

      下一个列表显示了游戏的完整 HTML。消息 div 和命令文本框已移动到位置和玩家部分上方。

      列表 20.10. 密室 (HTML) (jsbin.com/cujibok/edit?html,output)
      <!DOCTYPE html>
      <html>
      <head>
        <meta charset="utf-8">
        <title>The Crypt</title>
      </head>
      <body>
      
        <h1>The Crypt</h1>
      
        <div id="messages" class="hidden"></div>
      
        <div id="controls">
          <input type="text" id="txtCommand" />
          <input type="button" id="btnCommand" value="Make it so" />
        </div>
      
        <div id="views">
          <div id="place"></div>
          <div id="player"></div>
        </div>
      
        <!-- Templates -->
      
        <script type="text/x-template" id="itemTemplate">
            <li>{{item}}</li>
        </script>
      
        <script type="text/x-template" id="playerTemplate">
            <h3>{{name}}</h3>
            <p>{{health}}</p>
            <ol id="playerItems"></ol>
        </script>
      
        <script type="text/x-template" id="placeTemplate">
            <h3>{{title}}</h3>
            <p>{{description}}</p>
      
            <div class="placePanel">
              <h4>Items</h4>
              <ol id="placeItems"></ol>
            </div>
      
            <div class="placePanel">
              <h4>Exits</h4>
              <ol id="placeExits"></ol>
            </div>
        </script>
      
        <script type="text/x-template" id="messageTemplate">
            <p>*** {{message}} ***</p>
        </script>
      
        <!-- Modules -->
      
        <!-- gpwj.templates -->
        <script src="http://output.jsbin.com/pugase.js"></script>
        <!-- gpwj.data -->
        <script src="http://output.jsbin.com/guzula.js"></script>
      
        <!-- Place constructor -->
        <script src="http://output.jsbin.com/vuwave.js"></script>
        <!-- Player constructor -->
        <script src="http://output.jsbin.com/nonari.js"></script>
      
        <!-- player view -->
        <script src="http://output.jsbin.com/suyona.js"></script>
        <!-- place view -->
        <script src="http://output.jsbin.com/yoquna.js"></script>
        <!-- message view -->
        <script src="http://output.jsbin.com/jojeyo.js"></script>
      
        <!-- map manager -->
        <script src="http://output.jsbin.com/xesoxu.js"></script>
      
        <!-- game controller -->
        <script src="http://output.jsbin.com/vezaza.js"></script>
        <!-- Web Page Controls -->
        <script src="http://output.jsbin.com/xoyasi.js"></script>
      </body>
      </html>
      

      下一个列表显示了 JavaScript 初始化代码。它现在指定了游戏中第一个位置的数据文件的 JS Bin 代码。

      列表 20.11. 密室 (jsbin.com/cujibok/edit?js,output)

      20.3.6. 进入密室

      太棒了!游戏模板与 HTML 中的其余部分组织在一起;如果需要更改设计,模板很容易找到,而且没有 JavaScript、数据和 HTML 的纠缠。更简单的维护,更少的错误,让设计师更快乐。双赢,双赢,双赢!

      现在,你只需要在 The Crypt 中生存下来。测试你的力量、技能和耐力,并在 JS Bin 上玩游戏 output.jsbin.com/cujibok。祝你好运!

      20.4. 总结

      • 使用 XMLHttpRequest 对象加载网页的资源,而无需重新加载整个页面。

      • 使用轻量级、易读的基于 JavaScript 的数据交换格式 JSON 传输您的应用程序数据。

      • 要加载数据,创建一个 XMLHttpRequest 对象,为 load 事件设置事件监听器,使用资源的 URL 调用 open 方法,最后调用 send 方法:

        var xhr = new XMLHttpRequest();
        xhr.addEventListener("load", function () {
           // Use xhr.responseText
        });
        xhr.open("GET", url);
        xhr.send();
        
      • load 事件通过 XHR 对象的 responseText 属性触发时访问 JSON 数据。

      • 通过传递给 JSON.parse 方法将 JSON 数据字符串转换为 JavaScript 对象:

        var data = JSON.parse(xhr.responseText);
        
      • 使用 JavaScript 对象作为缓存。将加载的数据或模型放入缓存,并在使用 XHR 之前检查缓存。

      第二十一章.结论:开始用 JavaScript 编程

      本章涵盖

      • 继续你的好工作

      • 本地使用文件工作

      • 书籍和资源

      那么,你是如何开始用 JavaScript 编程的呢?

      好吧,你只是开始用 JavaScript 编程。

      如果你真的想理解编程,你必须跳进去编写程序。阅读书籍和思考代码可以很有趣,也很富有信息量,但真正能提升你的技能、经验和韧性的还是自己解决问题和编写代码。但你不孤单。在本章中,你将了解在遇到项目难题或只是对事物的工作原理感到好奇时,你可以如何获得帮助。首先,你将研究在自己的电脑上保存网页而不是在 JS Bin 中使用 JavaScript。

      21.1. 本地文件工作

      JS Bin 和其他在线编码网站非常适合尝试你的想法并检查你的代码是否工作。但你想跳出沙盒,创建自己的网站。本节将探讨在本地(在自己的电脑上)编写和保存文件,并在浏览器中打开它们。

      21.1.1. 编写代码

      JavaScript 和 HTML 文件只是文本文件。你可以在任何文本编辑器中编写它们。Windows 上的记事本和 OS X 上的 TextEdit 都可以完成这项工作,尽管它们非常基础。更高级的文本编辑器会进行语法高亮,使用颜色区分关键字、变量、参数、字符串等,并执行代码补全,建议你可能正在尝试输入的内容,并允许你快速插入建议。一些流行的编辑器包括 Sublime Text、BBEdit、Notepad++、Atom 和 Emacs。

      此外,还有集成开发环境(IDEs),它们提供了额外的工具,让你可以管理项目、协作、跟踪版本、合并文件、压缩和缩小文件等等。例如,Visual Studio、Dreamweaver、Eclipse 和 WebStorm。图 21.1显示了具有不同项目支持级别的三个编辑器。

      ![图 21.1]. Notepad、Notepad++和 WebStorm 提供不同级别的项目支持。

      图片

      21.1.2. 保存文件

      以有组织的方式保存你的文件,使用合理的文件夹将它们分成类型或子项目。图 21.2显示了The Crypt可能的文件结构。有单独的文件夹用于样式表、JavaScript 文件和游戏地图。主要 HTML 文件位于项目的根目录中。Get Programming with JavaScript的各种项目文件夹可在其 GitHub 仓库中找到:github.com/jrlarsen/GetProgramming

      ![图 21.2]. The Crypt可能的文件夹和文件结构

      图片

      文件名代表了它们的目的。尽管 JS Bin 会给文件分配随机的名称,但这并不意味着你应该这样做。以下列表显示了游戏的 HTML 文件,其中使用了script元素来加载 JavaScript。在head部分还有一个link元素,用于加载样式化页面的 CSS 文件。模板已被省略,因为它们没有变化。

      列表 21.1. Crypt.html 中的script元素

      图片

      图片

      用于在src属性中指定浏览器查找要加载的文件的路径是相对于 crypt.html 的。例如,maps/TheDarkHouse.js 意味着“从 crypt.html 相同的文件夹开始,找到 maps 文件夹,然后在那个文件夹中找到名为 TheDarkHouse.js 的文件。”

      21.1.3. 在浏览器中打开你的页面

      在你的浏览器菜单中,选择文件 > 打开,浏览你的文件系统以找到 crypt.html。页面应该在浏览器中打开。

      21.1.4. 合并和压缩文件

      如果你将你的项目托管在互联网上供用户访问的 Web 服务器上,将所有的 JavaScript 模块放入一个单独的文件会更好。对于每个script元素,浏览器将向服务器发送请求,以获取由src属性指定的文件。如果所有文件都需要使应用程序工作,那么加载一个大型文件比加载许多小文件要快。

      你可以在 JS Bin 上看到The Crypt的一个版本,所有的 JavaScript 都在一个 bin 中:jsbin.com/xewozi/edit?js,output

      在开发应用程序时,使用单独的文件有助于集中注意力、重用和灵活性。但是,当发布应用程序的时候,合并文件将有助于代码更快地加载。你可以手动将代码复制到一个单独的文件中,但也有一些工具可以帮助你。这些工具可能对初学者来说有点高级,但如果你感兴趣,可以关注 Grunt、Gulp、Browserify 和 CodeKit。

      工具还会将你的代码压缩,将其压缩成更小的文件。当浏览器运行你的 JavaScript 时,它不需要那些帮助你阅读代码的空格和换行符。它也不需要命名良好的变量;abc可以像loadDatacheckGameStatusrenderMessage一样完成工作。工具会删除空格并重命名变量,以创建一个更小的文件,它执行的工作完全相同。接下来的两个列表显示了相同的代码,来自The Crypt控制器模块的use函数。

      列表 21.2. 使用函数(压缩版)
      function r(e,a){if(s){var o=l.getPlace(),i=o.getChallenge(a);void
      0===i||i.complete===!0?t("You don't need to use that
      there"):l.hasItem(e)?e===i.requires?(t(i.success),i.complete=!0,i.itemConsume
      d&&l.removeItem(e)):t(i.failure):t("You don't have that item")}else t("The
      game is over!")}
      
      列表 21.3. 使用函数
      function use (item, direction) {
          if (inPlay) {
              var place = player.getPlace();
              var challenge = place.getChallenge(direction);
      
              if (challenge === undefined || challenge.complete === true) {
                  renderMessage("You don't need to use that there");
              } else if (player.hasItem(item)) {
      
                  if (item === challenge.requires) {
                      renderMessage(challenge.success);
                      challenge.complete = true;
                      if (challenge.itemConsumed) {
                          player.removeItem(item);
                      }
                  } else {
                      renderMessage(challenge.failure);
                  }
      
              } else {
                  renderMessage("You don't have that item");
              }
          } else {
              renderMessage("The game is over!");
          }
      }
      

      你可能能够理解文件大小的节省可以非常显著。

      21.2. 寻求帮助

      你阅读这本书的事实表明你欣赏有指导、有组织的指导。一旦你开始运行,能够随时查阅资源、查找信息并就特定问题寻求帮助是非常好的。作为参考,一个很棒的网站是 Mozilla 开发者网络(developer.mozilla.org),它提供了 HTML、CSS、JavaScript 等文档和示例。对于特定问题的答案,可以加入 stackoverflow(stackoverflow.com)的论坛。社区成员通常会快速回答问题。JavaScript 是第一个达到一百万个提问问题的语言!

      21.3. 接下来是什么?

      《用 JavaScript 编程》 被编写为一本编程入门书籍。虽然 第一部分 探讨了 JavaScript 语言的构建块,第二部分 和 第三部分 更侧重于组织大型项目和体验即将到来的事物。本书并非作为完整的参考书籍,而是作为编程的实际体验。那么接下来呢?

      21.3.1. 伴侣网站

      文章、教程、视频、资源、示例,以及指向其他支持和参考网站的链接将继续添加到 《用 JavaScript 编程》 网站 www.room51.co.uk/books/getProgramming/index.html。书中未涉及的一些主题,如原型、继承、使用 this、Node.js 和 JavaScript 的最新添加功能,将在那里介绍,同时还有改进书中示例(如测验应用、健身应用、健身应用新闻和我的电影评分)的指南。

      21.3.2. 书籍

      关于 JavaScript 的书籍有很多。以下是我阅读并喜欢的几本书。第一本与这本书很好地衔接,第二本则更深入地探讨了该语言:

      • 《JavaScript:现代编程导论》(No Starch Press;第 2 版,2014 年)作者:马里恩·哈弗贝克

      • 《专业 JavaScript:网络开发者指南》(Wrox;第 3 版,2012 年)作者:尼古拉斯·C·扎卡斯

      21.3.3. 网站

      有许多网站将视频教程与交互式练习相结合。我特别喜欢的是 Code School,codeschool.com。Code School 的人们还在 javascript.com 建立了一个很好的 JavaScript 入门网站。

      21.3.4. 熟能生巧

      继续练习。登录 JS Bin 尝试新事物。本书网站上有一些建议的项目。如果您遇到困难,请随时在本书论坛 Manning.com 上的 forums.manning.com/forums/get-programming-with-javascript 发帖提问,或者加入 stackoverflow.com 的社区。

      我希望您在阅读本书、摆弄所有代码、提出假设,并在 JS Bin 上测试您的想法时感到愉快。保持好奇心,用 JavaScript 编程!

      21.4. 摘要

      • 使用文本编辑器编写和保存 JavaScript 和 HTML 文件。

      • 使用文件夹和适当的文件名组织项目文件。

      • 在您的浏览器中打开 HTML 文件。

      • 在 Mozilla 开发者网络和 stackoverflow.com 获取更多信息并获得帮助。

      • 充分利用本书网站上的资源。

      • 练习,练习,再练习。

      • 保持好奇心、冒险精神、弹性和耐心。祝您冒险成功!

      附录. The Crypt:一个运行示例

      在整本书中,你将开发一个基于文本的冒险游戏,名为 The Crypt。玩家可以在地图上探索位置,从一个地方移动到另一个地方,并收集物品以帮助他们解决挑战和克服障碍。每个章节的最后部分将使用你所学到的知识来进一步开发游戏。你将看到编程概念如何帮助你构建组合成大型程序的各个部分。

      游戏元素 任务 JavaScript 章节
      玩家 决定你需要了解每个玩家的哪些信息 变量 2
      在一个地方收集玩家信息 对象 3
      在控制台上显示玩家信息 函数 4–7
      创建每个玩家收集的物品列表 数组 8
      组织玩家创建代码 构造函数 9
      地点 创建许多具有相似结构的可探索地点 构造函数 9
      使用方括号符号连接带有出口的地方 方括号符号 10
      游戏 添加简单的用于移动、收集物品和显示信息的函数 方括号符号 10
      地图 使用方括号符号连接带有出口的地方 方括号符号 10

      附录。密码:一个运行示例

      在本书的第一部分中,当学习核心 JavaScript 概念时,你编写代码来表示游戏中的玩家和地点,并让玩家从一个地点移动到另一个地点并拾取物品。以下图显示了您创建的组件;每个章节中类似的图都会突出整个游戏上下文中正在讨论的思想。

      图片

      书籍的第二部分为玩家增加了挑战:阻止出口直到玩家解决谜题。重点是组织你的代码,隐藏其工作方式,检查用户输入,并构建模块,你可以重用和交换以使项目更具灵活性。

      在第三部分中,你更新了显示以使用 HTML 模板,修改游戏使其在运行时加载数据,用玩家和地点信息填充模板,并添加文本框和按钮,以便玩家可以通过网页输入命令。

      图片

      posted @ 2025-11-14 20:41  绝不原创的飞龙  阅读(3)  评论(0)    收藏  举报