UI 伪类 - 学习 Web 开发

UI 伪类
=====

* 上一页
* Overview: Forms
* 下一页

在以前的文章中，我们以一般的方式介绍了各种表单控件的样式。这包括一些伪类的使用，例如，使用 `:checked` 来选择一个选中的复选框。在这篇文章中，我们将详细探讨在现代浏览器中可用的不同 UI 伪类，以便在不同状态下对表单添加样式。

|  |  |
| --- | --- |
| 前提： | 基本的计算机知识，对 HTML 和 CSS 的基本了解，包括对伪类和伪元素的基本认识。 |
| 目标： | 了解表单的哪些部分难以添加样式，以及为什么难以添加样式；了解如何对这些部分添加样式。 |

都有什么样的伪类？
---------

我们可以使用的（截至 CSS 2.1）与表单相关的原始伪类是：

* `:hover`：只在鼠标指针悬停在一个元素上时选择该元素。
* `:focus`：只在元素被聚焦时选择该元素（也就是说，通过键盘上的 tab 键选中该元素）。
* `:active`：只在元素被激活时选择该元素（也就是说，通过点击或键盘上的 `Return` / `Enter` 键选中该元素）。

这些基本的伪类现在对你来说应该很熟悉了。最近，CSS Selector Level 3 和 CSS Basic UI Level 3 增加了更多与 HTML 表单有关的伪类，提供了其他几个有用的定位条件以便于利用。我们将在下面的章节中更详细地讨论这些内容，但简单地说，我们要看的主要内容是：

* `:required` 和 `:optional`：针对必需的或可选的表单控件。
* `:valid` 和 `:invalid`、`:in-range` 和 `:out-of-range`：针对表单控件，根据对其设置的表单验证约束，或范围内/范围外，是有效/无效的。
* `:enabled` 和 `:disabled`、`:read-only` 和 `:read-write`：针对启用或禁用的表单控件（例如，设置了 `disabled` HTML 属性），以及读写或只读的表单控件（例如，设置了 `readonly` HTML 属性）。
* `:checked`、`:indeterminate` 和 `:default`：分别针对被选中的复选框和单选按钮，处于不确定的状态（既不被选中也不被选中），以及页面加载时默认选择的选项（例如，一个设置了 `checked` 属性的 `<input type="checkbox">`，或者一个设置了 `selected` 属性的 `<option>` 元素）。

还有很多其他的，但上面列出的是最明显有用的。其中一些旨在解决非常具体的小众问题，或者在浏览器中还没有得到很好的支持。上面列出的那些都有相当好的浏览器支持，但当然，你应该仔细测试你的表单实现，以确保它们对你的目标受众有效。

**备注：** 这里讨论的许多伪类都是关于根据表单控件的验证状态（它们的数据是否有效？）你会在我们的下一篇文章客户端表单验证中学习到更多关于设置和控制验证约束的知识，但现在我们将保持关于表单验证的简单内容，以便不会混淆。

根据必需与否为输入控件添加样式
---------------

关于客户端表单验证的一个最基本的概念是，一个表单输入是必需的（在提交表单之前必须填写）还是可选的。

`<input>`、`<select>` 和 `<textarea>` 元素都有一个 `required` 属性可用，设置后意味着你必须在表单成功提交前填入该控件。比如说：

```html
<form>
  <fieldset>
    <legend>Feedback form</legend>
    <div>
      <label for="fname">First name: </label>
      <input id="fname" name="fname" type="text" required />
    </div>
    <div>
      <label for="lname">Last name: </label>
      <input id="lname" name="lname" type="text" required />
    </div>
    <div>
      <label for="email">
        Email address (include if you want a response):
      </label>
      <input id="email" name="email" type="email" />
    </div>
    <div><button>Submit</button></div>
  </fieldset>
</form>
```

在这里，first name 和 last name 是必需的，但电子邮件地址是可选的。

你可以使用 `:required` 和 `:optional` 伪类来匹配这两种状态。例如，如果我们将下列 CSS 应用于上述 HTML 代码中：

```css
input:required {
  border: 1px solid black;
}

input:optional {
  border: 1px solid silver;
}
```

必需的表单控件会有一个黑色的边框，可选的表单控件会有一个银色的边框，像这样：

你也可以尝试在不填写表单的情况下提交，观察浏览器默认给你的客户端验证错误信息。

上述形式并不坏，但也不是很好。首先，我们只用颜色来表示必填和可选的状态，这对色盲的人来说不是很好。其次，网络上关于必填状态的标准惯例是一个星号（\*），或者将“必需”这个词与相关的控件联系起来。

在下一节，我们将看一个更好的例子，使用 `:required` 表示必填字段，这也是对使用生成内容的挖掘。

**备注：** 你可能不会发现自己经常使用 `:optional` 这个伪类。表单控件默认是可选的，所以你可以将可选状态的样式当作默认样式，然后在必需的组件上添加样式。

**备注：** 如果一组同名的单选按钮中的一个单选按钮具有 `required` 属性，那么所有的单选按钮都将无效，直到有一个被选中，但只有分配了该属性的单选按钮才会真正匹配 `:required`。

使用伪类生成内容
--------

在之前的文章中，我们已经看到了生成内容的用法，现在是一个更详细地谈论它的好时机。

我们可以使用 `::before` 和 `::after` 伪元素以及 `content` 属性来使一大块内容出现在受影响元素的前面或后面。这块内容没有被添加到 DOM 中，所以对屏幕阅读器来说是看不见的；它是文档样式的一部分。因为它是一个伪元素，所以它可以像任何实际的 DOM 节点一样被作为目标样式。

当你想给一个元素添加一个视觉指示器，比如一个标签或图标，但又不想让它被辅助技术发现时，这就非常有用。例如，在我们的自定义单选按钮示例中，我们使用生成的内容来处理单选按钮被选中时内圈的位置和动画：

```css
input[type="radio"]::before {
  display: block;
  content: " ";
  width: 10px;
  height: 10px;
  border-radius: 6px;
  background-color: red;
  font-size: 1.2em;
  transform: translate(3px, 3px) scale(0);
  transform-origin: center;
  transition: all 0.3s ease-in;
}

input[type="radio"]:checked::before {
  transform: translate(3px, 3px) scale(1);
  transition: all 0.3s cubic-bezier(0.25, 0.25, 0.56, 2);
}
```

这真的很有用——屏幕阅读器已经让它们的用户知道单选按钮或复选框被选中了，所以你不希望它们读出另一个表示选择的 DOM 元素——那会让人困惑。有一个纯粹的视觉指示器可以解决这个问题。

**备注：** 这个示例也说明了如何将伪类和伪元素结合使用。

回到我们之前的必填/可选的例子，这次我们不会改变输入本身的外观——我们将使用生成的内容来添加一个指示标签（在这里看到它的实时演示，并在这里看到源代码）。

首先，我们要在表单的顶部添加一段话，说明你在寻找什么：

```html
<p>Required fields are labelled with "required".</p>
```

当屏幕阅读器用户到达每个需要的输入时，他们会得到“required”这个额外的信息，而视力正常的用户会看到我们的标签。

由于表单输入不直接支持将生成的内容放在上面（这是因为生成的内容是相对于元素的格式化框放置的，但表单输入的工作方式更像被替换的元素，因此没有格式化框），我们将添加一个空的 `<span>` 元素来存放生成的内容：

```html
<div>
  <label for="fname">First name: </label>
  <input id="fname" name="fname" type="text" required />
  <span></span>
</div>
```

一个直接的问题是，由于 input 和 label 都设置了 `width: 100%`，span 会沉到输入框下一行中。为了修复这一点，我们令父 `<div>` 为弹性容器，同时令它如果内容变得太长，就把它的内容换行：

```css
fieldset > div {
  margin-bottom: 20px;
  display: flex;
  flex-flow: row wrap;
}
```

这样做的效果是，标签和输入是分开的，因为它们都是 `width: 100%`，但 `<span>` 的宽度是 0，所以它可以和 input 位于同一行。

现在来看看生成的内容。我们使用这个 CSS 代码创建它：

```css
input + span {
  position: relative;
}

input:required + span::after {
  font-size: 0.7rem;
  position: absolute;
  content: "required";
  color: white;
  background-color: black;
  padding: 5px 10px;
  top: -26px;
  left: -70px;
}
```

我们将 `<span>` 设置为 `position: relative`，这样我们就可以将生成的内容设置为 `position: absolute`，并将其相对于 `<span>` 而不是 `<body>` 定位（出于定位需求，生成的内容就像它是生成元素的一个子节点）。

然后我们给生成的内容加上“required”，也就是我们想要的标签内容，并按照我们想要的样式和位置排布。结果见下文。

根据数据是否有效为控件添加样式
---------------

表单验证中另一个真正重要的基本概念是表单控件的数据是否有效（在数值数据的情况下，我们也可以谈论范围内和范围外的数据）。具有约束限制的表单控件可以根据这些状态来进行定位。

### :valid 和 :invalid

你可以使用 `:valid` 和 `:invalid` 伪类来定位表单控件。这里有一些值得注意的地方：

* 没有约束验证的表单控件永远是有效的，因此永远与 `:valid` 匹配。
* 设定了 `required`，且没有任何值的表单控件是无效的——它们与 `:invalid` 和 `:required` 匹配。
* 具有内置验证功能的控件，如 `<input type="email">` 或 `<input type="url">`，当输入的数据与它们所需的模式不匹配时，会被（与）`:invalid` 匹配（但当它们为空时是有效的）。
* 当前值超出 `min` 和 `max` 属性所指定的范围限制的控件，会被（与）`:invalid` 匹配，但也会被 `:out-of-range` 匹配，后面还会看到。
* 还有一些其他的方法可以使元素被 `:valid`/`:invalid` 匹配，你会在客户端表单验证文章中看到。但目前还没有必要介绍得太复杂。

让我们看看一个简单的 `:valid`/`:invalid` 的示例（访问 valid-invalid.html 以查看实时演示，也可以查看源代码）。

和前面的示例一样，我们有额外的 `<span>` 来生成内容，我们将用它来提供有效/无效数据的指示：

```html
<div>
  <label for="fname">First name *: </label>
  <input id="fname" name="fname" type="text" required />
  <span></span>
</div>
```

为了提供这些指示，我们使用了这些 CSS 样式：

```css
input + span {
  position: relative;
}

input + span::before {
  position: absolute;
  right: -20px;
  top: 5px;
}

input:invalid {
  border: 2px solid red;
}

input:invalid + span::before {
  content: "✖";
  color: red;
}

input:valid + span::before {
  content: "✓";
  color: green;
}
```

和之前一样，我们将 `<span>` 设置为 `position: relative`，这样我们就可以将生成的内容相对于它们定位。然后，我们根据表单的数据是有效还是无效，绝对定位不同的生成内容——分别是绿色复选框或红色叉号。为了给无效数据增加一点额外的紧迫感，我们还在无效时给输入的数据加上了厚厚的红边。

**备注：** 我们使用 `::before` 来添加这些标签，因为我们已经使用 `::after` 来添加“required”标签。

你可以在下方尝试：

注意，必需的文本 input 在空的时候是无效的，但当它们有东西填入时是有效的。另一方面，电子邮件 input 在空的时候是有效的，因为它不是必需的，但当它包含一些不是正确的电子邮件地址时，就无效了。

### 在范围内和不在范围内的数据

正如我们上面所提示的内容一样，还有两个相关的伪类需要考虑——`:in-range` 和 `:out-of-range`。这些与数字输入相匹配，其中范围限制由 `min` 和 `max` 指定，分别供其数据在指定范围之内或之外所使用。

**备注：** 数值输入类型包括 `date`、`month`、`week`、`time`、`datetime-local`、`number` 和 `range`。

值得注意的是，数据在范围内的输入也会被 `:valid` 伪类匹配，而数据在范围外的输入也会被 `:invalid` 伪类匹配。那么，为什么要同时拥有这两个类呢？这个问题实际上是一个语义问题——超出范围是一种更具体的无效通信类型，所以你可能想为超出范围的输入提供一个不同的消息，这将比只说“无效”对用户更有帮助。你甚至可能想同时提供这两种信息。

让我们看看一个正是这样做的例子。我们的 out-of-range.html 演示（也可参见源代码）建立在前一个示例的基础上，为数字输入提供超出范围的信息，并说明它们是否是必需的。

数值输入看起来像这样：

```html
<div>
  <label for="age">Age (must be 12+): </label>
  <input id="age" name="age" type="number" min="12" max="120" required />
  <span></span>
</div>
```

CSS 样式看起来像这样：

```css
input + span {
  position: relative;
}

input + span::after {
  font-size: 0.7rem;
  position: absolute;
  padding: 5px 10px;
  top: -26px;
}

input:required + span::after {
  color: white;
  background-color: black;
  content: "Required";
  left: -70px;
}

input:out-of-range + span::after {
  color: white;
  background-color: red;
  width: 155px;
  content: "Outside allowable value range";
  left: -182px;
}
```

这和我们之前在 `:required` 示例中的情况类似，只是在这里我们把适用于任何 `::after` 内容的声明分割成一个单独的规则，并给 `:after` 内容的 `:required` 和 `:out-of-range` 状态设置它们自己的内容和样式。你可以在这里试试：

数字输入有可能同时是必需的和超出范围的，那么会发生什么呢？因为 `:out-of-range` 规则在源代码中出现的时间比 `:required` 规则晚，层叠规则开始发挥作用，并且显示“超出范围”信息。

这一点做得很好——当页面第一次加载时，会显示“required”，还有一个红叉和边界。当你输入了一个有效的年龄（即在 12-120 的范围内），输入就会变成有效。然而，如果你将年龄输入改为超出范围，则会弹出“Outside allowable value range”的信息，取代了原先的“required”。

**备注：** 要输入一个无效的/超出范围的值，你必须实际聚焦于表格，用键盘输入该值。控件提供的按钮不会让你在允许的范围之外增加/减少数值。

为启用或禁用、只读或可读写输入控件添加样式
---------------------

一个启用的元素是一个可以被激活的元素；它可以被选择、点击、输入等等。另一方面，一个禁用的元素不能以任何方式进行互动，它的数据甚至不会被发送到服务器上。

这两种状态可以用 `:enabled` 和 `:disabled` 来定位。为什么禁用的输入是有用的？有时候如果某些数据不适用于某个用户，你甚至可能不想在他们提交表单时提交这些数据。一个典型的例子是运输表单，通常你会被问到是否要使用相同的地址付款或运输；如果是这样，你可以只发送一个地址到服务器，也可以直接禁用账单地址字段。

让我们来看看一个这样做的示例。首先，其 HTML 代码是一个简单的表单，包含文本输入，加上一个复选框来切换禁用账单地址的开关。账单地址字段默认是禁用的。

```html
<form>
  <fieldset id="shipping">
    <legend>Shipping address</legend>
    <div>
      <label for="name1">Name: </label>
      <input id="name1" name="name1" type="text" required />
    </div>
    <div>
      <label for="address1">Address: </label>
      <input id="address1" name="address1" type="text" required />
    </div>
    <div>
      <label for="pcode1">Zip/postal code: </label>
      <input id="pcode1" name="pcode1" type="text" required />
    </div>
  </fieldset>
  <fieldset id="billing">
    <legend>Billing address</legend>
    <div>
      <label for="billing-checkbox">Same as shipping address:</label>
      <input type="checkbox" id="billing-checkbox" checked />
    </div>
    <div>
      <label for="name" class="billing-label disabled-label">Name: </label>
      <input id="name" name="name" type="text" disabled required />
    </div>
    <div>
      <label for="address2" class="billing-label disabled-label">
        Address:
      </label>
      <input id="address2" name="address2" type="text" disabled required />
    </div>
    <div>
      <label for="pcode2" class="billing-label disabled-label">
        Zip/postal code:
      </label>
      <input id="pcode2" name="pcode2" type="text" disabled required />
    </div>
  </fieldset>

  <div><button>Submit</button></div>
</form>
```

现在来看看 CSS 部分。这个示例中最相关的部分如下：

```css
input[type="text"]:disabled {
  background: #eee;
  border: 1px solid #ccc;
}

.disabled-label {
  color: #aaa;
}
```

我们用 `input[type="text"]:disabled` 直接选择了我们要禁用的输入控件，但我们也想把相应的文本标签弄成灰色。这些并不那么容易选择，所以我们用一个类来为它们提供这种风格。

最后我们用一些 JavaScript 代码来切换账单地址字段的禁用状态：

```js
// 等待页面完成加载
document.addEventListener(
  "DOMContentLoaded",
  () => {
    // 向复选框附加 `change` 事件
    document
      .getElementById("billing-checkbox")
      .addEventListener("change", toggleBilling);
  },
  false,
);

function toggleBilling() {
  // 选择账单文本字段
  const billingItems = document.querySelectorAll('#billing input[type="text"]');
  // 选择账单文本标签
  const billingLabels = document.querySelectorAll(".billing-label");

  // 切换账单文本字段和标签
  for (let i = 0; i < billingItems.length; i++) {
    billingItems[i].disabled = !billingItems[i].disabled;

    if (
      billingLabels[i].getAttribute("class") === "billing-label disabled-label"
    ) {
      billingLabels[i].setAttribute("class", "billing-label");
    } else {
      billingLabels[i].setAttribute("class", "billing-label disabled-label");
    }
  }
}
```

它使用 `change` 事件来让用户启用/禁用账单字段，并切换相关标签的样式。

你可以在下面看到这个示例（也可以在这里看到它的运行版本，并看到源代码）：

### 只读或可读写

与 `:disabled` 和 `:enabled` 类似，`:read-only` 和 `:read-write` 伪类针对表单输入的两种状态进行了切换。只读输入的值提交给服务器，但用户不能编辑它们，而可读写输入意味着它们可以被编辑——这是它们的默认状态。

使用 `readonly` 属性可将一个输入设置为只读。举个例子，设想一个确认页面，开发者将之前页面上填写的细节发送到这个页面，目的是让用户在一个地方检查所有细节，添加任何需要的最终数据，然后通过提交确认订单。在这一点上，所有最终的表单数据都可以一次性发送到服务器上。

让我们来看看一个表单可能是什么样子的（见运行实例 readonly-confirmation.html，其源代码在这里）。

HTML 的一个片段如下，注意其中的 readonly 属性：

```html
<div>
  <label for="name">Name: </label>
  <input id="name" name="name" type="text" value="Mr Soft" readonly />
</div>
```

如果你尝试了运行实例，你会发现最上面的一组表单元素是不可聚焦的，然而，当表单被提交时，其值会被提交。我们使用 `:read-only` 和 `:read-write` 伪类为表单控件添加了样式，就像这样：

```css
:is(
 input:read-only,
 input:-moz-read-only,
 textarea:-moz-read-only,
 textarea:read-only
 ) {
  border: 0;
  box-shadow: none;
  background-color: white;
}

:is(textarea:-moz-read-write, textarea:read-write) {
  box-shadow: inset 1px 1px 3px #ccc;
  border-radius: 5px;
}
```

Firefox 在版本 78 之前只支持带前缀的伪类，从这个版本以后才开始支持无前缀的版本。完整的示例看起来是这样的：

**备注：** `:enabled` 和 `:read-write` 是另外两个可能很少使用的伪类，它们描述了输入元素的默认状态。

单选和复选按钮状态——选中、默认和中间状态
---------------------

正如我们在本模块前面的文章中所看到的，单选按钮和复选框可以被选中或不被选中。但也有一些其他的状态需要考虑：

* `:default`：匹配在页面加载时默认选中的单选钮/复选框（即通过设置 `checked` 属性），这些匹配 `:default` 伪类，即使用户取消选中。
* `:indeterminate`：当单选钮/复选框既没有被选中也没有被取消时，它们是*中间状态*，并将与 `:indeterminate` 伪类匹配。下文将阐述其细节。
### :checked

当单选钮或复选框被选中时，它们将被 `:checked` 伪类所匹配。

最常见的用途是在复选框或单选按钮被选中时添加不同的样式，在这种情况下，已经使用 `appearance: none;` 删除了系统默认的样式，想自己重新建立样式。我们在上一篇文章中看到了这样的示例，当时我们谈到了在单选钮/复选框上使用 `appearance: none`。

回顾一下，我们的有样式的单选钮示例中的 `:checked` 代码看起来像这样：

```css
input[type="radio"]::before {
  display: block;
  content: " ";
  width: 10px;
  height: 10px;
  border-radius: 6px;
  background-color: red;
  font-size: 1.2em;
  transform: translate(3px, 3px) scale(0);
  transform-origin: center;
  transition: all 0.3s ease-in;
}

input[type="radio"]:checked::before {
  transform: translate(3px, 3px) scale(1);
  transition: all 0.3s cubic-bezier(0.25, 0.25, 0.56, 2);
}
```

你可以在这里试试：

基本上，我们使用 `::before` 伪元素建立了单选按钮“内圈”的样式，但在它上面设置了一个 `scale(0)` 的 `transform` 值。然后我们使用一个 `transition` 来使它在被选择时能有一个很好的动画效果。使用变换而不是过渡 `width`/`height` 的好处是，你可以使用 `transform-origin` 来使它从圆的中心生长，而不是让它看起来从圆的角落生长。

### :default 和 :indeterminate

如前文所述，`:default` 伪类可以匹配在页面加载时默认勾选的单选钮或复选框，即使未勾选也是如此。这对于在选项列表中添加一个指示器很有用，可以提醒用户默认值（或起始选项）是什么，以防他们想重设他们的选择。

另外，上面提到的单选钮或复选框在处于既没有选中也没有取消选中的状态时，会被 `:indeterminate` 伪类所匹配。但这是什么意思呢？不确定的元素包括：

* `<input/radio>` 输入，当同名组中的所有单选按钮都取消勾选时
* `<input/checkbox>` 输入，其 `indeterminate` 属性通过 JavaScript 代码设置为 `true`。
* 没有值的 `<progress>` 元素。

这不是你可能会经常使用的东西。一个可能的用例是一个指示器，告诉用户他们真的需要在继续前进之前选择一个单选按钮。

让我们看看前面示例的几个修改版本，它们提醒用户默认选项是什么，并在不确定的情况下对单选按钮进行样式设计。这两个示例的输入都有以下的 HTML 结构：

```html
<p>
  <input type="radio" name="fruit" value="cherry" id="cherry" />
  <label for="cherry">Cherry</label>
  <span></span>
</p>
```

对于 `:default` 示例，我们给中间的单选按钮输入添加了 `checked` 属性，所以它在加载时将被默认选择。然后我们用下面的 CSS 来设计这个样式：

```css
input ~ span {
  position: relative;
}

input:default ~ span::after {
  font-size: 0.7rem;
  position: absolute;
  content: "Default";
  color: white;
  background-color: black;
  padding: 5px 10px;
  right: -65px;
  top: -3px;
}
```

这为页面加载时最初选择的项目提供了一个小小的“default”标签。注意这里我们使用的是通用兄弟组合器（`~`），而不是相邻兄弟组合器（`+`）——我们需要这样做，因为在源代码中，`<span>` 并不在 `<input>` 之后。

参见下方的实时结果：

**备注：** 你也可以在 GitHub 的 radios-checked-default.html 中找到这个示例的实时演示（也可以看看源代码）。

对于 `:indeterminate` 示例，我们没有默认的选定的单选按钮——这很重要——如果有的话，就不会有 indeterminate 状态的样式。我们用下面的 CSS 来设计不确定的单选按钮：

```css
input[type="radio"]:indeterminate {
  border: 2px solid red;
  animation: 0.4s linear infinite alternate border-pulse;
}

@keyframes border-pulse {
  from {
    border: 2px solid red;
  }

  to {
    border: 6px solid red;
  }
}
```

这将在单选按钮上创建一个有趣的动画边框，希望它能表明你需要选择其中的一个！

参见下方的实时结果：

**备注：** 你也可以在 GitHub 的 radios-checked-indeterminate.html 上找到这个示例的运行实例，也可以参见源代码。

**备注：** 你可以在 `<input type="checkbox">` 参考页上找到一个涉及 `indeterminate` 状态的有趣示例。

更多伪类
----

还有一些其他有趣的伪类，我们没有空间在这里详细地写它们。让我们再谈一谈你应该花时间研究的几个。

以下这些伪类在现代浏览器中得到了相当好的支持：

* `:focus-within` 伪类匹配一个已经收到焦点的元素或*包含*一个已经收到焦点的元素。如果你想让整个表单在其内部的输入被聚焦时以某种方式突出显示，这很有用。
* `:focus-visible` 伪类匹配通过键盘交互（而不是触摸或鼠标）获得焦点的元素。如果你想对键盘焦点与鼠标（或其他）焦点显示不同的样式，这很有用。
* `:placeholder-shown` 伪类匹配占位符（即 `placeholder` 属性的内容）正在显示的 `<input>` 和 `<textarea>` 元素，因为该元素的值为空。

下面这些也很有意思，但在浏览器中还没有得到很好的支持：

* `:blank` 伪类可以选择空表单控件。`:empty` 也匹配没有子元素的元素，如 `<input>`，但它更普遍——它也匹配其他空元素，如 `<br>` 和 `<hr>`。`:empty` 有合理的浏览器支持；`:blank` 伪类的规范还没有完成，所以它还不被任何浏览器支持。
* `:user-invalid` 伪类，如果支持，将类似于 `:invalid`，但有更好的用户体验。如果输入收到焦点时值是有效的，当用户输入数据时，如果值暂时无效，该元素可能会匹配 `:invalid`，但只有当该元素失去焦点时才会匹配 `:user-invalid`。如果该值最初是无效的，它将在整个焦点持续期间同时匹配 `:invalid` 和 `:user-invalid`。与 `:invalid` 类似，如果该值确实变得有效，它将停止匹配 `:user-invalid`。
技能测试！
-----

你已经看到了本文的结尾，但你还记得本文的绝大多数重要的信息吗？在继续学习新的内容之前，你可以找一些进一步的测验来验证你已经掌握了这些信息——参见技能测试：进阶样式 (en-US).

总结
--

这就完成了我们对与表单输入有关的 UI 伪类的研究。继续使用它们，创造一些有趣的表单样式吧。接下来，我们将继续研究一些不同的东西——客户端表单验证。

* 上一页
* Overview: Forms
* 下一页
### 进阶内容

* 如何构建自定义表单组件
* 通过 JavaScript 发送表单
* 表单组件的属性兼容性表格


如何构造 HTML 表单 - 学习 Web 开发

如何构造 HTML 表单
============

* 上一页
* Overview: Forms
* 下一页

有了基础知识，我们现在更详细地了解了用于为表单的不同部分提供结构和意义的元素。

|  |  |
| --- | --- |
| 前提： |
 基本的计算机能力，和基本的
 对 HTML 的理解。
  |
| 目标： | 要理解如何构造 HTML 表单并赋予它们语义，以便它们是可用的和可访问的。 |

HTML 表单的灵活性使它们成为 HTML 中最复杂的结构之一;你可以使用专用的表单元素和属性构建任何类型的基本表单。在构建 HTML 表单时使用正确的结构将有助于确保表单可用性和无障碍。

<form> 元素
---------

`<form>` 元素按照一定的格式定义了表单和确定表单行为的属性。当你想要创建一个 HTML 表单时，都必须从这个元素开始，然后把所有内容都放在里面。许多辅助技术或浏览器插件可以发现`<form>`元素并实现特殊的钩子，使它们更易于使用。

我们早在之前的文章中就遇见过它了。

**备注：** 严格禁止在一个表单内嵌套另一个表单。嵌套会使表单的行为不可预知，而这取决于正在使用的浏览器。

请注意，在`<form>`元素之外使用表单小部件是可以的，但是如果你这样做了，那么表单小部件与任何表单都没有任何关系。这样的小部件可以在表单之外使用，但是你应该对于这些小部件有特别的计划，因为它们自己什么也不做。你将不得不使用 JavaScript 定制他们的行为。

**备注：** HTML5 在 HTML 表单元素中引入`form`属性。它让你显式地将元素与表单绑定在一起，即使元素不在`<form>`中。不幸的是，就目前而言，跨浏览器对这个特性的实现还不足以使用。

<fieldset> 和 <legend> 元素
------------------------

`<fieldset>`元素是一种方便的用于创建具有相同目的的小部件组的方式，出于样式和语义目的。你可以在`<fieldset>`开口标签后加上一个 `<legend>`元素来给`<fieldset>` 标上标签。 `<legend>`的文本内容正式地描述了`<fieldset>`里所含有部件的用途。

许多辅助技术将使用`<legend>` 元素，就好像它是相应的 `<fieldset>` 元素里每个部件的标签的一部分。例如，在说出每个小部件的标签之前，像Jaws或NVDA这样的屏幕阅读器会朗读出 legend 的内容。

这里有一个小例子：

```html
<form>
  <fieldset>
    <legend>Fruit juice size</legend>
    <p>
      <input type="radio" name="size" id="size\_1" value="small" />
      <label for="size\_1">Small</label>
    </p>
    <p>
      <input type="radio" name="size" id="size\_2" value="medium" />
      <label for="size\_2">Medium</label>
    </p>
    <p>
      <input type="radio" name="size" id="size\_3" value="large" />
      <label for="size\_3">Large</label>
    </p>
  </fieldset>
</form>
```

**备注：** 你可以在 fieldset-legend.html (你也可以看预览版) 看到该例。

当阅读上述表格时，屏幕阅读器将会读第一个小部件“Fruit juice size small”，“Fruit juice size medium”为第二个，“Fruit juice size large”为第三个。

本例中的用例是最重要的。每当你有一组单选按钮时，你应该将它们嵌套在`<fieldset>`元素中。还有其他用例，一般来说，`<fieldset>`元素也可以用来对表单进行分段。理想情况下，长表单应该在拆分为多个页面，但是如果表单很长，却必须在单个页面上，那么将以不同的关联关系划分的分段，分别放在不同的 fieldset 里，可以提高可用性。

因为它对辅助技术的影响， `<fieldset>` 元素是构建可访问表单的关键元素之一。无论如何，你有责任不去滥用它。如果可能，每次构建表单时，尝试侦听屏幕阅读器如何解释它。如果听起来很奇怪，试着改进表单结构。

<label> 元素
----------

正如我们在前一篇文章中看到的， `<label>` 元素是为 HTML 表单小部件定义标签的正式方法。如果你想构建可访问的表单，这是最重要的元素——当实现的恰当时，屏幕阅读器会连同有关的说明和表单元素的标签一起朗读。以我们在上一篇文章中看到的例子为例：

```html
<label for="name">Name:</label> <input type="text" id="name" name="user\_name" />
```

`<label>` 标签与 `<input>` 通过他们各自的`for` 属性和 `id` 属性正确相关联（label 的 for 属性和它对应的小部件的 id 属性），这样，屏幕阅读器会读出诸如“Name, edit text”之类的东西。

如果标签没有正确设置，屏幕阅读器只会读出“Edit text blank”之类的东西，这样会没什么帮助。

注意，一个小部件可以嵌套在它的`<label>`元素中，就像这样：

```html
<label for="name">
  Name: <input type="text" id="name" name="user\_name" />
</label>
```

尽管可以这样做，但人们认为设置`for`属性才是最好的做法，因为一些辅助技术不理解标签和小部件之间的隐式关系。

### 标签也可点击！

正确设置标签的另一个好处是可以在所有浏览器中单击标签来激活相应的小部件。这对于像文本输入这样的例子很有用，这样你可以通过点击标签，和点击输入区效果一样，来聚焦于它，这对于单选按钮和复选框尤其有用——这种控件的可点击区域可能非常小，设置标签来使它们可点击区域变大是非常有用的。

举个例子：

```html
<form>
  <p>
    <input type="checkbox" id="taste\_1" name="taste\_cherry" value="1" />
    <label for="taste\_1">I like cherry</label>
  </p>
  <p>
    <input type="checkbox" id="taste\_2" name="taste\_banana" value="2" />
    <label for="taste\_2">I like banana</label>
  </p>
</form>
```

**备注：** 你可以在 checkbox-label.html (你也可以看预览版) 看到该例。

### 多个标签

严格地说，你可以在一个小部件上放置多个标签，但是这不是一个好主意，因为一些辅助技术可能难以处理它们。在多个标签的情况下，你应该将一个小部件和它的标签嵌套在一个`<label>`元素中。

让我们考虑下面这个例子：

```html
<p>Required fields are followed by <abbr title="required">*</abbr>.</p>

<!--这样写：-->
<div>
  <label for="username">Name:</label>
  <input type="text" name="username" />
  <label for="username"><abbr title="required">*</abbr></label>
</div>

<!--但是这样写会更好：-->
<div>
  <label for="username">
    <span>Name:</span>
    <input id="username" type="text" name="username" />
    <abbr title="required">*</abbr>
  </label>
</div>

<!--但最好的可能是这样：-->
<div>
  <label for="username">Name: <abbr title="required">*</abbr></label>
  <input id="username" type="text" name="username" />
</div>
```

顶部的段落定义了所需元素的规则。它必须在开始时确保像屏幕阅读器这样的辅助技术在用户找到必需的元素之前显示或念出它们。这样，他们就知道星号表达的是什么意思了。根据屏幕阅读器的设置，屏幕阅读器会把星号读为“star”或“required”，取决于屏幕阅读器的设置——不管怎样，要念出来的都会在第一段清楚的呈现出来。

* 在第一个例子中，标签根本没有和`input`一起被念出来——读出来的只是“edit the blank”，和单独被念出的标签。多个`<label>`元素会使屏幕阅读器迷惑。
* 在第二个例子中，事情变得清晰一点了——标签和输入一起，读出的是“name star name edit text”，但标签仍然是单独读出的。这还是有点令人困惑，但这次还是稍微好一点了，因为`input`和`label`联系起来了。
* 第三个例子是最好的——标签是一起读出的，标签和输入读出的是“name star edit text”。

**备注：** 你可能会得到一些不同的结果，这取决于你的屏幕阅读器。这是在 VoiceOver 上测试的（NVDA 的行为也类似）。我们也乐于听听你的试验结果。

**备注：** 你可以在 GitHub 上看到 required-labels.html（你也可以看预览版）。不要运行 2 个或 3 个未注释版本的示例——如果你有多个标签和多个输入相同的 ID，那么屏幕阅读器肯定会感到困惑！

用于表单的通用 HTML 结构
---------------

除了特定于 HTML 表单的结构之外，还应该记住表单同样是 HTML。这意味着你可以使用 HTML 的所有强大功能来构造一个 HTML 表单。

正如你在示例中可以看到的，用`<div>`元素包装标签和它的小部件是很常见的做法。`<p>`元素也经常被使用，HTML 列表也是如此（后者在构造多个复选框或单选按钮时最为常见）。

除了`<fieldset>`元素之外，使用 HTML 标题（例如，`<h1>` (en-US)、`<h2>` (en-US)）和分段（如`<section>`）来构造一个复杂的表单也是一种常见的做法。

最重要的是，你要找到一种你觉得很舒服的风格去码代码，而且它也能带来可访问的、可用的形式。

它包含了从功能上划分开并分别包含在`<section>`元素中的部分，以及一个`<fieldset>`来包含单选按钮。

### 自主学习：构建一个表单结构

让我们把这些想法付诸实践，建立一个稍微复杂一点的表单结构——一个支付表单。这个表单将包含许多你可能还不了解的小部件类型—现在不要担心这个；在下一篇文章（原生表单小部件）中，你将了解它们是如何工作的。现在，当你遵循下面的指令时，请仔细阅读这些描述，并开始理解我们使用的包装器元素是如何构造表单的，以及为什么这么做。

1. 在开始之前，在计算机上的一个新目录中，创建一个空白模板文件和我们的支付表单的 CSS 样式的本地副本。
2. 首先，通过添加下面这行代码到你的 HTML`<head>`使你的 HTML 应用 CSS。

```html
<link href="payment-form.css" rel="stylesheet" />
```

3. 接下来，通过添加外部`<form>`元素来开始一张表单：

```html
<form></form>
```

4. 在 `<form>` 标签内，以添加一个标题和段落开始，告诉用户必需的字段是如何标记的：

```html
<h1>Payment form</h1>
<p>
  Required fields are followed by
  <strong><abbr title="required">*</abbr></strong
 >.
</p>
```

5. 接下来，我们将在表单中添加一个更大的代码段，在我们之前的代码下面。在这里，你将看到，我们正在将联系人信息字段包装在一个单独的`<section>`元素中。此外，我们有一组两个单选按钮，每个单选按钮都放在自己的列表中 (`<li>`)) 元素。最后，我们有两个标准文本`<input>`和它们相关的`<label>`元素，每个元素包含在`<p>`中，加上输入密码的密码输入。现在将这些代码添加到你的表单中：

```html
<section>
  <h2>Contact information</h2>
  <fieldset>
    <legend>Title</legend>
    <ul>
      <li>
        <label for="title\_1">
          <input type="radio" id="title\_1" name="title" value="K" />
          King
        </label>
      </li>
      <li>
        <label for="title\_2">
          <input type="radio" id="title\_2" name="title" value="Q" />
          Queen
        </label>
      </li>
      <li>
        <label for="title\_3">
          <input type="radio" id="title\_3" name="title" value="J" />
          Joker
        </label>
      </li>
    </ul>
  </fieldset>
  <p>
    <label for="name">
      <span>Name: </span>
      <strong><abbr title="required">*</abbr></strong>
    </label>
    <input type="text" id="name" name="username" />
  </p>
  <p>
    <label for="mail">
      <span>E-mail: </span>
      <strong><abbr title="required">*</abbr></strong>
    </label>
    <input type="email" id="mail" name="usermail" />
  </p>
  <p>
    <label for="pwd">
      <span>Password: </span>
      <strong><abbr title="required">*</abbr></strong>
    </label>
    <input type="password" id="pwd" name="password" />
  </p>
</section>
```

6. 现在，我们将转到表单的第二个`<section>`——支付信息。在这里，我们有三个不同的小部件以及它们的标签，每个都包含在一个`<p>`中。第一个是选择信用卡类型的下拉菜单 (`<select>`)。第二个是输入一个信用卡号的类型编号的 `<input>` 元素。最后一个是输入`date`类型的`<input>` 元素，用来输入卡片的过期日期（这将在支持的浏览器中出现一个日期选择器小部件，并在非支持的浏览器中回退到普通的文本输入）。同样，在之前的代码后面输入以下内容：

```html
<section>
  <h2>Payment information</h2>
  <p>
    <label for="card">
      <span>Card type:</span>
    </label>
    <select id="card" name="usercard">
      <option value="visa">Visa</option>
      <option value="mc">Mastercard</option>
      <option value="amex">American Express</option>
    </select>
  </p>
  <p>
    <label for="number">
      <span>Card number:</span>
      <strong><abbr title="required">*</abbr></strong>
    </label>
    <input type="number" id="number" name="cardnumber" />
  </p>
  <p>
    <label for="date">
      <span>Expiration date:</span>
      <strong><abbr title="required">*</abbr></strong>
      <em>formatted as mm/yy</em>
    </label>
    <input type="date" id="date" name="expiration" />
  </p>
</section>
```

7. 我们要添加的最后一个部分要简单得多，它只包含了一个`submit`类型的 `<button>` ，用于提交表单数据。现在把这个添加到你的表单的底部：

```html
<p><button type="submit">Validate the payment</button></p>
```

你可以在下面看到已完成的表单 (你可以在 Github 上看到源码和预览版）：

总结
--

现在，你已经具备了正确地构造 HTML 表单所需的所有知识;下一篇文章将深入介绍各种不同类型的表单小部件，你将希望从用户那里收集信息。

参见
--

* A List Apart: *Sensible Forms: A Form Usability Checklist*
* 上一页
* Overview: Forms
* 下一页


使用 JavaScript 发送表单 - 学习 Web 开发

使用 JavaScript 发送表单
==================

HTML 表单可以声明式地发送一个 HTTP 请求。但也可以通过 JavaScript 来为表单准备用于发送的 HTTP 请求（例如通过 `XMLHttpRequest`）。本文对这些方法进行了探讨。

表单不总是表单
-------

在渐进式 Web 应用、单页应用和基于框架的应用中，通常会使用 HTML 表单来发送数据，而不会在收到响应数据时加载新文档。让我们先来谈谈为什么这需要一种不同的方法。

### 获得整体界面的控制

如前一篇文章所述，标准 HTML 表单提交会加载发送数据的 URL，这意味着浏览器窗口会以全页面加载的方式进行导航。避免全页面加载可以避免网络延迟和可能出现的视觉问题（如闪烁），从而提供更流畅的体验。

许多现代用户界面只使用 HTML 表单来收集用户输入，而不是用于数据提交。当用户尝试发送数据时，应用程序会控制并在后台异步传输数据，只更新用户界面中需要更改的部分。

### 表单提交和 AJAX 请求之间的区别？

`XMLHttpRequest`（XHR）DOM 对象可以构建 HTTP 请求、发送请求并获取结果。从历史上看，`XMLHttpRequest` 是为获取和发送 XML 作为交换格式而设计的，后来这种格式被 JSON 所取代。但是，XML 和 JSON 都不适合表单数据请求编码。表单数据（`application/x-www-form-urlencoded`）由键/值对的 URL 编码列表组成。为了传输二进制数据，HTTP 请求被重塑为 `multipart/form-data`。

**备注：** 如今 Fetch API 常用于取代 XHR——它是 XHR 的更现代、更新的版本，工作原理类似，但有一些优点。你在本文中看到的大部分 XHR 代码都可以换成 Fetch。

如果你控制了前端（在浏览器中执行的代码）和后端（在服务器上执行的代码），就可以发送 JSON/XML，并随心所欲地处理它们。

但如果要使用第三方服务，就需要按照服务要求的格式发送数据。

那么我们应该如何发送这些数据呢？下面将介绍所需要的不同技术。

发送表单数据
------

一共有三种方式来发送表单数据：

* 手工构建 `XMLHttpRequest`。
* 使用独立的 `FormData` 对象。
* 使用绑定到 `<form>` 元素的 `FormData`。

让我们仔细看一下。

### 手工构建 XMLHttpRequest

`XMLHttpRequest` 是进行 HTTP 请求的最安全可靠的方式。要使用 `XMLHttpRequest` 发送表单数据，请通过 URL 编码准备数据，并遵守表单数据请求的具体规定。

让我们看个示例：

```html
<button>点我！</button>
```

这是 JavaScript 代码部分：

```js
const btn = document.querySelector("button");

function sendData(data) {
  console.log("Sending data");

  const XHR = new XMLHttpRequest();

  const urlEncodedDataPairs = [];

  // 将数据对象转换为 URL 编码的键/值对数组。
  for (const [name, value] of Object.entries(data)) {
    urlEncodedDataPairs.push(
      `${encodeURIComponent(name)}=${encodeURIComponent(value)}`,
    );
  }

  // 将配对合并为单个字符串，并将所有 % 编码的空格替换为
  // “+”字符；匹配浏览器表单提交的行为。
  const urlEncodedData = urlEncodedDataPairs.join("&").replace(/%20/g, "+");

  // 定义成功数据提交时发生的情况
  XHR.addEventListener("load", (event) => {
    alert("耶！已发送数据并加载响应。");
  });

  // 定义错误提示
  XHR.addEventListener("error", (event) => {
    alert("哎呀！出问题了。");
  });

  // 建立我们的请求
  XHR.open("POST", "https://example.com/cors.php");

  // 为表单数据 POST 请求添加所需的 HTTP 头
  XHR.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

  // 最后，发送我们的数据。
  XHR.send(urlEncodedData);
}

btn.addEventListener("click", () => {
  sendData({ test: "ok" });
});
```

这里是实时演示效果：

**备注：** 当你想要往第三方网站传输数据时，使用 `XMLHttpRequest` 会受到同源策略的影响。如果你需要执行跨源请求，你需要熟悉一下 CORS 和 HTTP 访问控制。

### 使用 XMLHttpRequest 和 FormData 对象

手动建立一个 HTTP 请求非常困难。幸运的是，XMLHttpRequest 规范提供了一种方便简单的方法——利用 `FormData` 对象来处理表单数据请求。

`FormData` 对象可以用来构建用于传输的表单数据，或是获取表单元素中的数据来管理它的发送方式。

该对象的使用详见使用 FormData 对象，下面是两个示例：

#### 使用一个独立的 FormData 对象

```html
<button>点我！</button>
```

你应该会觉得那个 HTML 示例很熟悉，现在来展示 JavaScript 代码：

```js
const btn = document.querySelector("button");

function sendData(data) {
  const XHR = new XMLHttpRequest();
  const FD = new FormData();

  // 把我们的数据添加到这个 FormData 对象中
  for (const [name, value] of Object.entries(data)) {
    FD.append(name, value);
  }

  // 定义数据成功发送并返回后执行的操作
  XHR.addEventListener("load", (event) => {
    alert("耶！已发送数据并加载响应。");
  });

  // 定义发生错误时执行的操作
  XHR.addEventListener("error", (event) => {
    alert("Oops! 出错了。");
  });

  // 设置请求地址和方法
  XHR.open("POST", "https://example.com/cors.php");

  // 发送这个 formData 对象，HTTP 请求头会自动设置
  XHR.send(FD);
}

btn.addEventListener("click", () => {
  sendData({ test: "ok" });
});
```

这里是实时演示效果：

#### 使用绑定到表单元素上的 FormData

你也可以把一个 `FormData` 对象绑定到一个 `<form>` 元素上。这会创建一个 `FormData` 对象，表示表单中包含的数据。

这段 HTML 是典型的情况：

```html
<form id="myForm">
  <label for="myName">告诉我你的名字：</label>
  <input id="myName" name="name" value="小明" />
  <input type="submit" value="提交" />
</form>
```

但是 JavaScript 接管了这个表单：

```js
window.addEventListener("load", () => {
  function sendData() {
    const XHR = new XMLHttpRequest();

    // 我们把这个 FormData 和表单元素绑定在一起。
    const FD = new FormData(form);

    // 我们定义了数据成功发送时会发生的事件
    XHR.addEventListener("load", (event) => {
      alert(event.target.responseText);
    });

    // 我们定义了失败的情形下会发生的事件
    XHR.addEventListener("error", (event) => {
      alert("哎呀！出了一些问题。");
    });

    // 我们设置了我们的请求
    XHR.open("POST", "https://example.com/cors.php");

    // 发送的数据是由用户在表单中提供的
    XHR.send(FD);
  }

  // 获取表单元素
  const form = document.getElementById("myForm");

  // 接管表单的 submit 事件
  form.addEventListener("submit", (event) => {
    event.preventDefault();

    sendData();
  });
});
```

这里是实时演示效果：

你甚至可以通过使用表单的 `elements` 属性来更多的参与此过程，来得到一个包含表单里所有数据元素的列表，并且逐一手动管理它们。想了解更多，请参见示例访问表单控件。

处理二进制数据
-------

如果你使用一个含有 `<input type="file">` 组件的 `FormData` 表单对象，数据会被自动处理。但是要手动发送二进制数据的话，还有额外的工作要做。

在现代网络上，二进制数据有很多来源：例如 `FileReader`、`Canvas`、WebRTC，等等。不幸的是，一些过时的浏览器无法访问二进制数据，或是需要非常复杂的工作环境。这些遗留问题已经超出了本文的涵盖范围。如果你想了解更多关于 `FileReader` API 的知识，参见如何在 web 应用程序中使用文件。

发送二进制数据最简单的方法是使用 `FormData` 的 `append()` 方法，如上图所示。如果必须手工操作，就比较麻烦了。

在下面的例子中，我们使用了`FileReader` API 来访问二进制数据，然后手动构建多部分表单数据请求：

```html
<form id="myForm">
  <p>
    <label for="theText">文本数据：</label>
    <input id="theText" name="myText" value="一些文本数据" type="text" />
  </p>
  <p>
    <label for="theFile">文件数据：</label>
    <input id="theFile" name="myFile" type="file" />
  </p>
  <button>提交！</button>
</form>
```

如你所见，这个 HTML 只是一个标准的 `<form>`，没有什么神奇的事情。“魔法”都在 JavaScript 里：

```js
// 因为我们想获取 DOM 节点，
// 我们在页面加载时初始化我们的脚本。
window.addEventListener("load", () => {
  // 这些变量用于存储表单数据
  const text = document.getElementById("theText");
  const file = {
    dom: document.getElementById("theFile"),
    binary: null,
  };

  // 使用 FileReader API 获取文件内容
  const reader = new FileReader();

  // 因为 FileReader 是异步的，会在完成读取文件时存储结果
  reader.addEventListener("load", () => {
    file.binary = reader.result;
  });

  // 页面加载时，如果一个文件已经被选择，那么读取该文件。
  if (file.dom.files[0]) {
    reader.readAsBinaryString(file.dom.files[0]);
  }

  // 如果没有被选择，一旦用户选择了它，就读取文件。
  file.dom.addEventListener("change", () => {
    if (reader.readyState === FileReader.LOADING) {
      reader.abort();
    }

    reader.readAsBinaryString(file.dom.files[0]);
  });

  // 发送数据是我们需要的主要功能
  function sendData() {
    // 如果存在被选择的文件，等待它读取完成
    // 如果没有，延迟函数的执行
    if (!file.binary && file.dom.files.length > 0) {
      setTimeout(sendData, 10);
      return;
    }

    // 要构建我们的多部分表单数据请求，
    // 我们需要一个 XMLHttpRequest 实例
    const XHR = new XMLHttpRequest();

    // 我们需要一个分隔符来定义请求的每一部分。
    const boundary = "blob";

    // 将我们的主体请求存储于一个字符串中
    let data = "";

    // 所以，如果用户已经选择了一个文件
    if (file.dom.files[0]) {
      // 在请求体中开始新的一部分
      data += `--${boundary}\r\n`;

      // 把它描述成表单数据
      data +=
        "content-disposition: form-data; " +
        // 定义表单数据的名称
        `name="${file.dom.name}"; ` +
        // 提供文件的真实名字
        `filename="${file.dom.files[0].name}"\r\n`;
      // 和文件的 MIME 类型
      data += `Content-Type: ${file.dom.files[0].type}\r\n`;

      // 元数据和数据之间有一条空行。
      data += "\r\n";

      // 将二进制数据添加到主体请求中
      data += file.binary + "\r\n";
    }

    // 文本数据更简单一些
    // 在主体请求中开始一个新的部分
    data += `--${boundary}\r\n`;

    // 假设这是表单数据，并命名它
    data += `content-disposition: form-data; name="${text.name}"\r\n`;
    // 元数据和数据之间有一条空行。
    data += "\r\n";

    // 添加文本数据到主体请求中
    data += text.value + "\r\n";

    // 一旦完成，“关闭”主体请求
    data += `--${boundary}--`;

    // 定义成功提交数据执行的语句
    XHR.addEventListener("load", (event) => {
      alert("耶！已发送数据并加载响应。");
    });

    // 定义发生错误时做的事
    XHR.addEventListener("error", function (event) {
      alert("哎呀！出现了一些问题。");
    });

    // 建立请求
    XHR.open("POST", "https://example.com/cors.php");

    // 添加需要的 HTTP 头部来处理多部分表单数据 POST 请求
    XHR.setRequestHeader(
      "Content-Type",
      `multipart/form-data; boundary=${boundary}`,
    );

    // 最后，发送数据。
    XHR.send(data);
  }

  // 获取表单元素
  const form = document.getElementById("theForm");

  // 添加 submit 事件处理器
  form.addEventListener("submit", (event) => {
    event.preventDefault();
    sendData();
  });
});
```

这里是实时演示效果：

总结
--

取决于不同的浏览器和正在处理数据的类型，通过 JavaScript 发送数据可能会很简单，也可能会很困难。`FormData` 对象是通用的答案，所以请毫不犹豫地在旧浏览器上通过 polyfill 使用它：

参见
--

### 学习路径

* 你的第一个 HTML 表单
* 如何构建 HTML 表单
* 原生表单控件
* HTML5 输入（input）类型
* 其他表单控件
* UI 伪类
* 样式化 HTML 表单
* 表单数据校验
* 发送表单数据
### 更进一步

* **使用 JavaScript 发送表单**
* 如何构建自定义表单控件
* 旧式浏览器的 HTML 表单
* 进一步为 HTML 表单添加样式
* 表单控件兼容性列表


旧式浏览器中的 HTML 表单 - 学习 Web 开发

旧式浏览器中的 HTML 表单
===============

所有 web 开发者很快就会（有时候是痛苦地）发现 Web 是一个令人不快的地方。我们碰到的最恶毒的诅咒是旧式浏览器。好吧，让我们承认吧，当我们提到“旧式浏览器”时，脑海中出现就是 Safari 和 Internet Explorer……但是，这远远不是全部。那么，在移动世界呢？当浏览器和 OS（操作系统）都不能更新时？是的，有非常多老版本的 Android 手机或 iPhone 没有更新到最新的浏览器。它们同样是旧式浏览器。

可悲的是，处理这些传统浏览器的问题是工作的一部分。幸运的是，有一些技巧可以帮助你解决旧式浏览器导致的大多数问题。如果浏览器不支持某种 HTML `<input>` 类型，它不会停止工作，而只是回退为默认的 `type=text` 类型。

了解这些问题
------

实际上，最重要的事情是阅读那些浏览器的文档，并尝试理解通用的（解决）模式。如果你在 MDN 上阅读了本篇文档，这将是个良好的开始，只需要检查你想用的元素或 DOM 接口的支持情况即可。对于大部分 HTML 中可用的元素、属性或 API，MDN 提供了兼容性表格以供查询。

由于 HTML 表单包含了复杂的交互，有一个非常重要的规则：保持简单化，也常称做“KISS 原则”。有很多情况下，我们希望表单“更漂亮”或“具有高级功能”，但构建高效的 HTML 表单并不是一个设计或技术问题，而是一个简单直观且便利的用户互动问题。记得花时间读一下这篇文章 forms usability on UX For The Masses。

### 优雅降级 (Graceful degradation) 是 web 开发者最好的朋友

优雅降级和渐进增强是一种开发模式，它允许你通过同时支持多种浏览器来构建优秀内容。当你为现代浏览器构建内容时，你想确保它能在旧式浏览器中以某种方式工作，这就是优雅降级（graceful degradation）。

让我们看一些关于 HTML 表单的例子：

#### HTML input 类型

HTML5 引入的新 input 类型十分酷，因为他们的降级 (degrade) 是高度可预测的。如果一个浏览器不能理解 `<input>`元素的 `type` 属性，它将会后退到`text`一样的行为。

```html
<label for="myColor">
  Pick a color
  <input type="color" id="myColor" name="color" />
</label>
```

| 支持 | 不受支持 |
| --- | --- |
| Screen shot of the color input on Chrome for Mac OSX | Screen shot of the color input on Firefox for Mac OSX |

#### 表单按钮

有两种在 HTML 表单中定义按钮的方式：

* `type` 属性值为 `button`、`submit`、`reset` 或 `image` 的 `<input>` 元素
* `<button>` 元素

##### `<input>`

`<input>` 元素可能使得通过元素选择器应用 CSS 变得有些困难：

```html
<input type="button" value="click me" />
```

如果我们把所有 input 的外框移除，我们能否只恢复 input 按钮的默认外观？

```css
input {
  /\* 这条规则关闭了含有外边框的 input 类型的默认渲染效果，包括了使用 input 元素定义的按钮 \*/
  border: 1px solid #ccc;
}
input[type="button"] {
  /\* 这条规则并不能恢复默认渲染行为！ \*/
  border: none;
}
input[type="button"] {
  /\* 这条也不行！实际上在任何浏览器上都没有标准的方案 \*/
  border: auto;
  border: initial;
}
input[type="button"] {
  /\* 这条是恢复默认渲染最接近的答案，只要浏览器支持就可以这么做 \*/
  border: revert;
}
```

请查看全局的 CSS `revert` (en-US) 值以获得更多信息。

##### `<button>`

`<button>` 元素曾经受到两个问题的影响，现在已经得到解决：

* 在旧版本的 Internet Explorer 中，存在一个错误，当点击 `<button>` 元素时，会发送该元素开始和结束标签之间的 HTML 内容，而不是 `value` 属性中的值。仅在需要发送该值时才会出现问题，例如当数据处理取决于用户点击的按钮时。
* 某些非常旧的浏览器不使用 `submit` 作为 `type` 属性的默认值。即使在现代浏览器中得到了解决，也推荐总是设置 `<button>` 元素的 `type` 属性。

```html
<!-- 某些情形下，点击按钮将发送 "<em>Do A</em>" 而不是值"A" -->
<button type="submit" name="IWantTo" value="A">
  <em>Do A</em>
</button>
```

选择哪种解决方案由项目的限制条件决定。

### 摆脱 CSS

HTML 表单的一个大问题是用 CSS 对表单小部件进行样式化。表单控件的外观是针对浏览器和操作系统的。例如，颜色类型的输入在 Safari、Chrome 和 Firefox 浏览器中看起来是不同的，但颜色选择器小部件在设备上的所有浏览器中都是一样的，因为它打开了操作系统的本地颜色选择器。

不改变表单控件的默认外观往往是个好主意，因为改变某个 CSS 属性值可以改变一些 input 类型，却不能改变另一些类型。例如，如果你声明了 `input { font-size: 2rem; }`，它会影响到 `number`、`date` 和 `text`，却不会影响 `color` 和 `range`。如果你改变了一个属性，会在不可预知的情况下影响到组件的外观。例如，`[value] { background-color: #ccc; }` 可能会用于改变所有含有 `value` 属性的 `<input>` 元素，但改变 `<meter>` 的 border radius 值可能会在不同浏览器中引起无法预期的显示。你可以声明 `appearance: none;` 来移除浏览器默认样式，但这通常会违背目的：因为你失去了所有的样式，删除了你的访问者所习惯的默认外观和感受。

总而言之，当涉及到表单控件小部件的样式时，用 CSS 为它们设计样式的副作用可能是不可预测的。所以不要这样做。正如从这篇表单控件的 CSS 属性兼容性表格 (en-US)中所看到的复杂性那样，它非常的困难。即使仍然可以对文本元素（如大小、字体颜色等）进行一些调整，但那样做会有副作用。最好的办法还是不要美化 HTML 表单小组件。但你仍然可以将样式应用到表单周围的项目上。如果你是一个专业人士，并且你的客户需要那么做，在这种情况下，你可以研究一些硬技能，如使用 JavaScript 重建组件。但在那种情况下，最好还是毫不犹豫的让客户收回这些愚蠢的决定。

特性检测和 polyfill
--------------

CSS 和 JavaScript 是了不起的技术，但重要的是确保不会破坏传统的浏览器。在使用你的目标浏览器不完全支持的特性之前，你应该进行特性检测。

### CSS 特性检测

在设计一个表单控件的样式之前，你可以使用 `@supports` 检查一下浏览器是否支持你打算使用的特性：

```css
@supports (appearance: none) {
  input[type="search"] {
    appearance: none;
    /\* 重新为 search input 添加样式 \*/
  }
}
```

`appearance` 属性可用于以平台原生的方式显示元素的样式，如果指定为 `none`，则移除默认基于不同平台的样式。

### 非侵入式 JavaScript

API 的兼容性是最大的问题。由于这个原因，与“非侵入式（unobtrusive）”JavaScript 一起工作被认为是最佳实践（译者注：此处意思是说没有或忽略 JavaScript 或 JavaScript 出了问题也能工作）。这个开发模式定义了两个需求：

* 结构和行为之间的严格隔离
* 如果代码出错，内容和基本功能必须保持可访问和可用状态

The principles of unobtrusive JavaScript（最早是由 Peter-Paul Koch 为 Dev.Opera.com 所撰写）这篇文章同样阐述了上述观点。

### Modernizr 库

有很多情形，好的”polyfill“能通过提供缺少的 API 以提供帮助。一个 polyfill 是一些用于填补旧式浏览器中缺失功能的 JavaScript 脚本。虽然它们可以用来改善对任何功能的支持，但对 JavaScript 使用它们比对 CSS 或 HTML 使用风险要小；有很多情况下 JavaScript 会崩溃（网络问题、脚本冲突等）。但是对于 JavaScript 来说，如果你在工作中考虑到非侵入式 JavaScript，如果 polyfill 缺失，那也没什么大不了的。

最好的 polyfill 缺失 API 的方式是使用 Modernizr 库以及它的子项目 YepNope。Modernizr 库允许你测试功能可用性，以便采取相应的行动。YepNope 是一个条件加载库。

下面是一个例子：

```js
Modernizr.load({
  // 这会测试你的浏览器是否支持 HTML5 表单验证 API
  test: Modernizr.formvalidation,

  // 如果浏览器不支持它，则会加载以下 polyfill
  nope: form - validation - API - polyfill.js,

  // 无论如何，你的核心 App 文件依赖于该 API 被加载
  both: app.js,

  // 一旦加载了这两个文件，就会调用该函数来初始化应用程序
  complete: function () {
    app.init();
  },
});
```

Modernizr 团队按照惯例维护着一个优秀的 polyfill 列表，按需使用即可。

**备注：** Modernizr 还有其他很棒的功能可以帮助你处理非侵入式 JavaScript 和优雅降级的技术。请阅读 Modernizr 文档。

### 注意性能

尽管像 Modernizr 这样的脚本非常注重性能，但加载 200 KB 的 polyfill 仍然会影响你的应用程序的性能。这对旧式浏览器来说尤其重要，这些浏览器的 JavaScript 引擎处理速度很慢，让 polyfills 的执行对于用户来说变得很痛苦。性能本身就是一个主题，但旧式浏览器对它非常敏感：基本上，它们速度慢，需要的 polyfill 越多，它们需要处理的 JavaScript 越多。与现代浏览器相比，它们承受双重负担。使用旧版浏览器测试你的代码，了解它们的实际表现。有时，放弃某些功能会带来更好的用户体验，而不是在所有浏览器中具有完全相同的功能。作为最后提醒，总是优先考虑用户。

总结
--

正如你所看到的，处理旧式浏览器不仅仅是表单问题。而是一整套技术；但是掌握所有这些技术超出了本文的范围。基本的前提是，在开始挑战之前，要考虑改变默认的实现方式是否值得做这些工作。

如果你阅读了 HTML 表单指南中的所有文章，你应该可以放心的使用表单了。如果你想探索新技术，请帮助我们提升指南的写作水平。

### 进阶内容

* 通过 JavaScript 发送表单
* 如何构建自定义表单控件
* 旧版浏览器中的 HTML 表单
* 高级表单样式
* 表单控件的属性兼容性列表


Example 4 - 学习 Web 开发

Example 4
=========

这是解释如何构建自定义表单控件的第四个示例。

改变状态
----

### HTML

```html
<form class="no-widget">
  <select name="myFruit">
    <option>Cherry</option>
    <option>Lemon</option>
    <option>Banana</option>
    <option>Strawberry</option>
    <option>Apple</option>
  </select>

  <div class="select">
    <span class="value">Cherry</span>
    <ul class="optList hidden">
      <li class="option">Cherry</li>
      <li class="option">Lemon</li>
      <li class="option">Banana</li>
      <li class="option">Strawberry</li>
      <li class="option">Apple</li>
    </ul>
  </div>
</form>
```

### CSS

```css
.widget select,
.no-widget .select {
  position: absolute;
  left: -5000em;
  height: 0;
  overflow: hidden;
}

/\*--------- \*/
/\* 所需的样式 \*/
/\* -------- \*/

.select {
  position: relative;
  display: inline-block;
}

.select.active,
.select:focus {
  box-shadow: 0 0 3px 1px #227755;
  outline: none;
}

.select .optList {
  position: absolute;
  top: 100%;
  left: 0;
}

.select .optList.hidden {
  max-height: 0;
  visibility: hidden;
}

/\* ------- \*/
/\* 美化样式 \*/
/\* ------- \*/

.select {
  font-size: 0.625em; /\* 10px \*/
  font-family: Verdana, Arial, sans-serif;

  box-sizing: border-box;

  padding: 0.1em 2.5em 0.2em 0.5em; /\* 1px 25px 2px 5px \*/
  width: 10em; /\* 100px \*/

  border: 0.2em solid #000; /\* 2px \*/
  border-radius: 0.4em; /\* 4px \*/

  box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45); /\* 0 1px 2px \*/

  background: #f0f0f0;
  background: linear-gradient(0deg, #e3e3e3, #fcfcfc 50%, #f0f0f0);
}

.select .value {
  display: inline-block;
  width: 100%;
  overflow: hidden;

  white-space: nowrap;
  text-overflow: ellipsis;
  vertical-align: top;
}

.select::after {
  content: "▼";
  position: absolute;
  z-index: 1;
  height: 100%;
  width: 2em; /\* 20px \*/
  top: 0;
  right: 0;

  padding-top: 0.1em;

  box-sizing: border-box;

  text-align: center;

  border-left: 0.2em solid #000;
  border-radius: 0 0.1em 0.1em 0;

  background-color: #000;
  color: #fff;
}

.select .optList {
  z-index: 2;

  list-style: none;
  margin: 0;
  padding: 0;

  background: #f0f0f0;
  border: 0.2em solid #000;
  border-top-width: 0.1em;
  border-radius: 0 0 0.4em 0.4em;

  box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4);

  box-sizing: border-box;

  min-width: 100%;
  max-height: 10em; /\* 100px \*/
  overflow-y: auto;
  overflow-x: hidden;
}

.select .option {
  padding: 0.2em 0.3em;
}

.select .highlight {
  background: #000;
  color: #ffffff;
}
```

### JavaScript

```js
// ------- //
// 函数定义 //
// ------- //

function deactivateSelect(select) {
  if (!select.classList.contains("active")) return;

  const optList = select.querySelector(".optList");

  optList.classList.add("hidden");
  select.classList.remove("active");
}

function activeSelect(select, selectList) {
  if (select.classList.contains("active")) return;

  selectList.forEach(deactivateSelect);
  select.classList.add("active");
}

function toggleOptList(select, show) {
  const optList = select.querySelector(".optList");

  optList.classList.toggle("hidden");
}

function highlightOption(select, option) {
  const optionList = select.querySelectorAll(".option");

  optionList.forEach((other) => {
    other.classList.remove("highlight");
  });

  option.classList.add("highlight");
}

function updateValue(select, index) {
  const nativeWidget = select.previousElementSibling;
  const value = select.querySelector(".value");
  const optionList = select.querySelectorAll(".option");

  nativeWidget.selectedIndex = index;
  value.innerHTML = optionList[index].innerHTML;
  highlightOption(select, optionList[index]);
}

function getIndex(select) {
  const nativeWidget = select.previousElementSibling;

  return nativeWidget.selectedIndex;
}

// ------------- //
// 事件绑定 //
// ------------- //

window.addEventListener("load", () => {
  const form = document.querySelector("form");

  form.classList.remove("no-widget");
  form.classList.add("widget");
});

window.addEventListener("load", () => {
  const selectList = document.querySelectorAll(".select");

  selectList.forEach((select) => {
    const optionList = select.querySelectorAll(".option");

    optionList.forEach((option) => {
      option.addEventListener("mouseover", () => {
        highlightOption(select, option);
      });
    });

    select.addEventListener("click", (event) => {
      toggleOptList(select);
    });

    select.addEventListener("focus", (event) => {
      activeSelect(select, selectList);
    });

    select.addEventListener("blur", (event) => {
      deactivateSelect(select);
    });
  });
});

window.addEventListener("load", () => {
  const selectList = document.querySelectorAll(".select");

  selectList.forEach((select) => {
    const optionList = select.querySelectorAll(".option");
    const selectedIndex = getIndex(select);

    select.tabIndex = 0;
    select.previousElementSibling.tabIndex = -1;

    updateValue(select, selectedIndex);

    optionList.forEach((option, index) => {
      option.addEventListener("click", (event) => {
        updateValue(select, index);
      });
    });

    select.addEventListener("keyup", (event) => {
      let index = getIndex(select);

      if (event.key === "Escape") {
        deactivateSelect(select);
      }
      if (event.key === "ArrowDown" && index < optionList.length - 1) {
        index++;
      }
      if (event.key === "ArrowUp" && index > 0) {
        index--;
      }

      updateValue(select, index);
    });
  });
});
```

### 结果


示例 1 - 学习 Web 开发

示例 1
====

这是第一个如何构建自定义表单控件的代码解释事例。

基本状态
----

### HTML

```html
<div class="select">
  <span class="value">Cherry</span>
  <ul class="optList hidden">
    <li class="option">Cherry</li>
    <li class="option">Lemon</li>
    <li class="option">Banana</li>
    <li class="option">Strawberry</li>
    <li class="option">Apple</li>
  </ul>
</div>
```

### CSS

```css
/\*--------- \*/
/\* 所需的样式 \*/
/\* -------- \*/

.select {
  position: relative;
  display: inline-block;
}

.select.active,
.select:focus {
  box-shadow: 0 0 3px 1px #227755;
  outline: none;
}

.select .optList {
  position: absolute;
  top: 100%;
  left: 0;
}

.select .optList.hidden {
  max-height: 0;
  visibility: hidden;
}

/\* ------- \*/
/\* 美化样式 \*/
/\* ------- \*/

.select {
  font-size: 0.625em; /\* 10px \*/
  font-family: Verdana, Arial, sans-serif;

  box-sizing: border-box;

  padding: 0.1em 2.5em 0.2em 0.5em; /\* 1px 25px 2px 5px \*/
  width: 10em; /\* 100px \*/

  border: 0.2em solid #000; /\* 2px \*/
  border-radius: 0.4em; /\* 4px \*/

  box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45); /\* 0 1px 2px \*/

  background: #f0f0f0;
  background: linear-gradient(0deg, #e3e3e3, #fcfcfc 50%, #f0f0f0);
}

.select .value {
  display: inline-block;
  width: 100%;
  overflow: hidden;

  white-space: nowrap;
  text-overflow: ellipsis;
  vertical-align: top;
}

.select::after {
  content: "▼";
  position: absolute;
  z-index: 1;
  height: 100%;
  width: 2em; /\* 20px \*/
  top: 0;
  right: 0;

  padding-top: 0.1em;

  box-sizing: border-box;

  text-align: center;

  border-left: 0.2em solid #000;
  border-radius: 0 0.1em 0.1em 0;

  background-color: #000;
  color: #fff;
}

.select .optList {
  z-index: 2;

  list-style: none;
  margin: 0;
  padding: 0;

  background: #f0f0f0;
  border: 0.2em solid #000;
  border-top-width: 0.1em;
  border-radius: 0 0 0.4em 0.4em;

  box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4);

  box-sizing: border-box;

  min-width: 100%;
  max-height: 10em; /\* 100px \*/
  overflow-y: auto;
  overflow-x: hidden;
}

.select .option {
  padding: 0.2em 0.3em;
}

.select .highlight {
  background: #000;
  color: #ffffff;
}
```

### 基本状态结果

活动状态
----

### HTML

```html
<div class="select active">
  <span class="value">Cherry</span>
  <ul class="optList hidden">
    <li class="option">Cherry</li>
    <li class="option">Lemon</li>
    <li class="option">Banana</li>
    <li class="option">Strawberry</li>
    <li class="option">Apple</li>
  </ul>
</div>
```

### CSS

```css
/\*--------- \*/
/\* 所需的样式 \*/
/\* -------- \*/

.select {
  position: relative;
  display: inline-block;
}

.select.active,
.select:focus {
  box-shadow: 0 0 3px 1px #227755;
  outline: none;
}

.select .optList {
  position: absolute;
  top: 100%;
  left: 0;
}

.select .optList.hidden {
  max-height: 0;
  visibility: hidden;
}

/\* ------- \*/
/\* 美化样式 \*/
/\* ------- \*/

.select {
  font-size: 0.625em; /\* 10px \*/
  font-family: Verdana, Arial, sans-serif;

  box-sizing: border-box;

  padding: 0.1em 2.5em 0.2em 0.5em; /\* 1px 25px 2px 5px \*/
  width: 10em; /\* 100px \*/

  border: 0.2em solid #000; /\* 2px \*/
  border-radius: 0.4em; /\* 4px \*/

  box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45); /\* 0 1px 2px \*/

  background: #f0f0f0;
  background: linear-gradient(0deg, #e3e3e3, #fcfcfc 50%, #f0f0f0);
}

.select .value {
  display: inline-block;
  width: 100%;
  overflow: hidden;

  white-space: nowrap;
  text-overflow: ellipsis;
  vertical-align: top;
}

.select::after {
  content: "▼";
  position: absolute;
  z-index: 1;
  height: 100%;
  width: 2em; /\* 20px \*/
  top: 0;
  right: 0;

  padding-top: 0.1em;

  box-sizing: border-box;

  text-align: center;

  border-left: 0.2em solid #000;
  border-radius: 0 0.1em 0.1em 0;

  background-color: #000;
  color: #fff;
}

.select .optList {
  z-index: 2;

  list-style: none;
  margin: 0;
  padding: 0;

  background: #f0f0f0;
  border: 0.2em solid #000;
  border-top-width: 0.1em;
  border-radius: 0 0 0.4em 0.4em;

  box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4);

  box-sizing: border-box;

  min-width: 100%;
  max-height: 10em; /\* 100px \*/
  overflow-y: auto;
  overflow-x: hidden;
}

.select .option {
  padding: 0.2em 0.3em;
}

.select .highlight {
  background: #000;
  color: #ffffff;
}
```

### 活动状态结果

展开状态
----

### HTML

```html
<div class="select active">
  <span class="value">Cherry</span>
  <ul class="optList">
    <li class="option highlight">Cherry</li>
    <li class="option">Lemon</li>
    <li class="option">Banana</li>
    <li class="option">Strawberry</li>
    <li class="option">Apple</li>
  </ul>
</div>
```

### CSS

```css
/\*--------- \*/
/\* 所需的样式 \*/
/\* -------- \*/

.select {
  position: relative;
  display: inline-block;
}

.select.active,
.select:focus {
  box-shadow: 0 0 3px 1px #227755;
  outline: none;
}

.select .optList {
  position: absolute;
  top: 100%;
  left: 0;
}

.select .optList.hidden {
  max-height: 0;
  visibility: hidden;
}

/\* ------- \*/
/\* 美化样式 \*/
/\* ------- \*/

.select {
  font-size: 0.625em; /\* 10px \*/
  font-family: Verdana, Arial, sans-serif;

  box-sizing: border-box;

  padding: 0.1em 2.5em 0.2em 0.5em; /\* 1px 25px 2px 5px \*/
  width: 10em; /\* 100px \*/

  border: 0.2em solid #000; /\* 2px \*/
  border-radius: 0.4em; /\* 4px \*/

  box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45); /\* 0 1px 2px \*/

  background: #f0f0f0;
  background: linear-gradient(0deg, #e3e3e3, #fcfcfc 50%, #f0f0f0);
}

.select .value {
  display: inline-block;
  width: 100%;
  overflow: hidden;

  white-space: nowrap;
  text-overflow: ellipsis;
  vertical-align: top;
}

.select::after {
  content: "▼";
  position: absolute;
  z-index: 1;
  height: 100%;
  width: 2em; /\* 20px \*/
  top: 0;
  right: 0;

  padding-top: 0.1em;

  box-sizing: border-box;

  text-align: center;

  border-left: 0.2em solid #000;
  border-radius: 0 0.1em 0.1em 0;

  background-color: #000;
  color: #fff;
}

.select .optList {
  z-index: 2;

  list-style: none;
  margin: 0;
  padding: 0;

  background: #f0f0f0;
  border: 0.2em solid #000;
  border-top-width: 0.1em;
  border-radius: 0 0 0.4em 0.4em;

  box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4);

  box-sizing: border-box;

  min-width: 100%;
  max-height: 10em; /\* 100px \*/
  overflow-y: auto;
  overflow-x: hidden;
}

.select .option {
  padding: 0.2em 0.3em;
}

.select .highlight {
  background: #000;
  color: #fff;
}
```

### 展开状态结果


示例 5 - 学习 Web 开发

示例 5
====

这是解释如何构建自定义表单控件的最后一个示例。

改变状态
----

### HTML

```html
<form class="no-widget">
  <select name="myFruit">
    <option>Cherry</option>
    <option>Lemon</option>
    <option>Banana</option>
    <option>Strawberry</option>
    <option>Apple</option>
  </select>

  <div class="select" role="listbox">
    <span class="value">Cherry</span>
    <ul class="optList hidden" role="presentation">
      <li class="option" role="option" aria-selected="true">Cherry</li>
      <li class="option" role="option">Lemon</li>
      <li class="option" role="option">Banana</li>
      <li class="option" role="option">Strawberry</li>
      <li class="option" role="option">Apple</li>
    </ul>
  </div>
</form>
```

### CSS

```css
.widget select,
.no-widget .select {
  position: absolute;
  left: -5000em;
  height: 0;
  overflow: hidden;
}

/\*--------- \*/
/\* 所需的样式 \*/
/\* -------- \*/

.select {
  position: relative;
  display: inline-block;
}

.select.active,
.select:focus {
  box-shadow: 0 0 3px 1px #227755;
  outline: none;
}

.select .optList {
  position: absolute;
  top: 100%;
  left: 0;
}

.select .optList.hidden {
  max-height: 0;
  visibility: hidden;
}

/\* ------- \*/
/\* 美化样式 \*/
/\* ------- \*/

.select {
  font-size: 0.625em; /\* 10px \*/
  font-family: Verdana, Arial, sans-serif;

  box-sizing: border-box;

  padding: 0.1em 2.5em 0.2em 0.5em; /\* 1px 25px 2px 5px \*/
  width: 10em; /\* 100px \*/

  border: 0.2em solid #000; /\* 2px \*/
  border-radius: 0.4em; /\* 4px \*/

  box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45); /\* 0 1px 2px \*/

  background: #f0f0f0;
  background: linear-gradient(0deg, #e3e3e3, #fcfcfc 50%, #f0f0f0);
}

.select .value {
  display: inline-block;
  width: 100%;
  overflow: hidden;

  white-space: nowrap;
  text-overflow: ellipsis;
  vertical-align: top;
}

.select::after {
  content: "▼";
  position: absolute;
  z-index: 1;
  height: 100%;
  width: 2em; /\* 20px \*/
  top: 0;
  right: 0;

  padding-top: 0.1em;

  box-sizing: border-box;

  text-align: center;

  border-left: 0.2em solid #000;
  border-radius: 0 0.1em 0.1em 0;

  background-color: #000;
  color: #fff;
}

.select .optList {
  z-index: 2;

  list-style: none;
  margin: 0;
  padding: 0;

  background: #f0f0f0;
  border: 0.2em solid #000;
  border-top-width: 0.1em;
  border-radius: 0 0 0.4em 0.4em;

  box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4);

  box-sizing: border-box;

  min-width: 100%;
  max-height: 10em; /\* 100px \*/
  overflow-y: auto;
  overflow-x: hidden;
}

.select .option {
  padding: 0.2em 0.3em;
}

.select .highlight {
  background: #000;
  color: #ffffff;
}
```

### JavaScript

```js
// ------- //
// 函数定义 //
// ------- //

function deactivateSelect(select) {
  if (!select.classList.contains("active")) return;

  const optList = select.querySelector(".optList");

  optList.classList.add("hidden");
  select.classList.remove("active");
}

function activeSelect(select, selectList) {
  if (select.classList.contains("active")) return;

  selectList.forEach(deactivateSelect);
  select.classList.add("active");
}

function toggleOptList(select, show) {
  const optList = select.querySelector(".optList");

  optList.classList.toggle("hidden");
}

function highlightOption(select, option) {
  const optionList = select.querySelectorAll(".option");

  optionList.forEach((other) => {
    other.classList.remove("highlight");
  });

  option.classList.add("highlight");
}

function updateValue(select, index) {
  const nativeWidget = select.previousElementSibling;
  const value = select.querySelector(".value");
  const optionList = select.querySelectorAll(".option");

  optionList.forEach((other) => {
    other.setAttribute("aria-selected", "false");
  });

  optionList[index].setAttribute("aria-selected", "true");

  nativeWidget.selectedIndex = index;
  value.innerHTML = optionList[index].innerHTML;
  highlightOption(select, optionList[index]);
}

function getIndex(select) {
  const nativeWidget = select.previousElementSibling;

  return nativeWidget.selectedIndex;
}

// ------------- //
// 事件绑定 //
// ------------- //

window.addEventListener("load", () => {
  const form = document.querySelector("form");

  form.classList.remove("no-widget");
  form.classList.add("widget");
});

window.addEventListener("load", () => {
  const selectList = document.querySelectorAll(".select");

  selectList.forEach((select) => {
    const optionList = select.querySelectorAll(".option");
    const selectedIndex = getIndex(select);

    select.tabIndex = 0;
    select.previousElementSibling.tabIndex = -1;

    updateValue(select, selectedIndex);

    optionList.forEach((option, index) => {
      option.addEventListener("mouseover", () => {
        highlightOption(select, option);
      });

      option.addEventListener("click", (event) => {
        updateValue(select, index);
      });
    });

    select.addEventListener("click", (event) => {
      toggleOptList(select);
    });

    select.addEventListener("focus", (event) => {
      activeSelect(select, selectList);
    });

    select.addEventListener("blur", (event) => {
      deactivateSelect(select);
    });

    select.addEventListener("keyup", (event) => {
      let index = getIndex(select);

      if (event.key === "Escape") {
        deactivateSelect(select);
      }
      if (event.key === "ArrowDown" && index < optionList.length - 1) {
        index++;
      }
      if (event.key === "ArrowUp" && index > 0) {
        index--;
      }

      updateValue(select, index);
    });
  });
});
```

### 结果


Example 3 - 学习 Web 开发

Example 3
=========

这是解释如何构建自定义表单控件的第三个示例。

改变状态
----

### HTML

```html
<form class="no-widget">
  <select name="myFruit" tabindex="-1">
    <option>Cherry</option>
    <option>Lemon</option>
    <option>Banana</option>
    <option>Strawberry</option>
    <option>Apple</option>
  </select>

  <div class="select" tabindex="0">
    <span class="value">Cherry</span>
    <ul class="optList hidden">
      <li class="option">Cherry</li>
      <li class="option">Lemon</li>
      <li class="option">Banana</li>
      <li class="option">Strawberry</li>
      <li class="option">Apple</li>
    </ul>
  </div>
</form>
```

### CSS

```css
.widget select,
.no-widget .select {
  position: absolute;
  left: -5000em;
  height: 0;
  overflow: hidden;
}

/\*--------- \*/
/\* 所需的样式 \*/
/\* -------- \*/

.select {
  position: relative;
  display: inline-block;
}

.select.active,
.select:focus {
  box-shadow: 0 0 3px 1px #227755;
  outline: none;
}

.select .optList {
  position: absolute;
  top: 100%;
  left: 0;
}

.select .optList.hidden {
  max-height: 0;
  visibility: hidden;
}

/\* ------- \*/
/\* 美化样式 \*/
/\* ------- \*/

.select {
  font-size: 0.625em; /\* 10px \*/
  font-family: Verdana, Arial, sans-serif;

  box-sizing: border-box;

  padding: 0.1em 2.5em 0.2em 0.5em; /\* 1px 25px 2px 5px \*/
  width: 10em; /\* 100px \*/

  border: 0.2em solid #000; /\* 2px \*/
  border-radius: 0.4em; /\* 4px \*/

  box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45); /\* 0 1px 2px \*/

  background: #f0f0f0;
  background: linear-gradient(0deg, #e3e3e3, #fcfcfc 50%, #f0f0f0);
}

.select .value {
  display: inline-block;
  width: 100%;
  overflow: hidden;

  white-space: nowrap;
  text-overflow: ellipsis;
  vertical-align: top;
}

.select::after {
  content: "▼";
  position: absolute;
  z-index: 1;
  height: 100%;
  width: 2em; /\* 20px \*/
  top: 0;
  right: 0;

  padding-top: 0.1em;

  box-sizing: border-box;

  text-align: center;

  border-left: 0.2em solid #000;
  border-radius: 0 0.1em 0.1em 0;

  background-color: #000;
  color: #fff;
}

.select .optList {
  z-index: 2;

  list-style: none;
  margin: 0;
  padding: 0;

  background: #f0f0f0;
  border: 0.2em solid #000;
  border-top-width: 0.1em;
  border-radius: 0 0 0.4em 0.4em;

  box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4);

  box-sizing: border-box;

  min-width: 100%;
  max-height: 10em; /\* 100px \*/
  overflow-y: auto;
  overflow-x: hidden;
}

.select .option {
  padding: 0.2em 0.3em;
}

.select .highlight {
  background: #000;
  color: #ffffff;
}
```

### JavaScript

```js
// ------- //
// 函数定义 //
// ------- //

function deactivateSelect(select) {
  if (!select.classList.contains("active")) return;

  const optList = select.querySelector(".optList");

  optList.classList.add("hidden");
  select.classList.remove("active");
}

function activeSelect(select, selectList) {
  if (select.classList.contains("active")) return;

  selectList.forEach(deactivateSelect);
  select.classList.add("active");
}

function toggleOptList(select, show) {
  const optList = select.querySelector(".optList");

  optList.classList.toggle("hidden");
}

function highlightOption(select, option) {
  const optionList = select.querySelectorAll(".option");

  optionList.forEach((other) => {
    other.classList.remove("highlight");
  });

  option.classList.add("highlight");
}

// ------- //
// 事件绑定 //
// ------- //

window.addEventListener("load", () => {
  const form = document.querySelector("form");

  form.classList.remove("no-widget");
  form.classList.add("widget");
});

window.addEventListener("load", () => {
  const selectList = document.querySelectorAll(".select");

  selectList.forEach((select) => {
    const optionList = select.querySelectorAll(".option");

    optionList.forEach((option) => {
      option.addEventListener("mouseover", () => {
        highlightOption(select, option);
      });
    });

    select.addEventListener(
      "click",
      (event) => {
        toggleOptList(select);
      },
      false,
    );

    select.addEventListener("focus", (event) => {
      activeSelect(select, selectList);
    });

    select.addEventListener("blur", (event) => {
      deactivateSelect(select);
    });

    select.addEventListener("keyup", (event) => {
      if (event.key === "Escape") {
        deactivateSelect(select);
      }
    });
  });
});
```

### 结果


示例 2 - 学习 Web 开发

示例 2
====

这是解释如何构建自定义表单控件的第二个示例。

使用 JS
-----

### HTML

```html
<form class="no-widget">
  <select name="myFruit">
    <option>Cherry</option>
    <option>Lemon</option>
    <option>Banana</option>
    <option>Strawberry</option>
    <option>Apple</option>
  </select>

  <div class="select">
    <span class="value">Cherry</span>
    <ul class="optList hidden">
      <li class="option">Cherry</li>
      <li class="option">Lemon</li>
      <li class="option">Banana</li>
      <li class="option">Strawberry</li>
      <li class="option">Apple</li>
    </ul>
  </div>
</form>
```

### CSS

```css
.widget select,
.no-widget .select {
  position: absolute;
  left: -5000em;
  height: 0;
  overflow: hidden;
}

/\*--------- \*/
/\* 所需的样式 \*/
/\* -------- \*/

.select {
  position: relative;
  display: inline-block;
}

.select.active,
.select:focus {
  box-shadow: 0 0 3px 1px #227755;
  outline: none;
}

.select .optList {
  position: absolute;
  top: 100%;
  left: 0;
}

.select .optList.hidden {
  max-height: 0;
  visibility: hidden;
}

/\* ------- \*/
/\* 美化样式 \*/
/\* ------- \*/

.select {
  font-size: 0.625em; /\* 10px \*/
  font-family: Verdana, Arial, sans-serif;

  box-sizing: border-box;

  padding: 0.1em 2.5em 0.2em 0.5em; /\* 1px 25px 2px 5px \*/
  width: 10em; /\* 100px \*/

  border: 0.2em solid #000; /\* 2px \*/
  border-radius: 0.4em; /\* 4px \*/

  box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45); /\* 0 1px 2px \*/

  background: #f0f0f0;
  background: linear-gradient(0deg, #e3e3e3, #fcfcfc 50%, #f0f0f0);
}

.select .value {
  display: inline-block;
  width: 100%;
  overflow: hidden;

  white-space: nowrap;
  text-overflow: ellipsis;
  vertical-align: top;
}

.select::after {
  content: "▼";
  position: absolute;
  z-index: 1;
  height: 100%;
  width: 2em; /\* 20px \*/
  top: 0;
  right: 0;

  padding-top: 0.1em;

  box-sizing: border-box;

  text-align: center;

  border-left: 0.2em solid #000;
  border-radius: 0 0.1em 0.1em 0;

  background-color: #000;
  color: #fff;
}

.select .optList {
  z-index: 2;

  list-style: none;
  margin: 0;
  padding: 0;

  background: #f0f0f0;
  border: 0.2em solid #000;
  border-top-width: 0.1em;
  border-radius: 0 0 0.4em 0.4em;

  box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4);

  box-sizing: border-box;

  min-width: 100%;
  max-height: 10em; /\* 100px \*/
  overflow-y: auto;
  overflow-x: hidden;
}

.select .option {
  padding: 0.2em 0.3em;
}

.select .highlight {
  background: #000;
  color: #ffffff;
}
```

### JavaScript

```js
window.addEventListener("load", () => {
  const form = document.querySelector("form");

  form.classList.remove("no-widget");
  form.classList.add("widget");
});
```

### 结果

不使用 JS
------

### HTML

```html
<form class="no-widget">
  <select name="myFruit">
    <option>Cherry</option>
    <option>Lemon</option>
    <option>Banana</option>
    <option>Strawberry</option>
    <option>Apple</option>
  </select>

  <div class="select">
    <span class="value">Cherry</span>
    <ul class="optList hidden">
      <li class="option">Cherry</li>
      <li class="option">Lemon</li>
      <li class="option">Banana</li>
      <li class="option">Strawberry</li>
      <li class="option">Apple</li>
    </ul>
  </div>
</form>
```

### CSS

```css
.widget select,
.no-widget .select {
  position: absolute;
  left: -5000em;
  height: 0;
  overflow: hidden;
}
```

### 结果


JavaScript 第一步 - 学习 Web 开发

JavaScript 第一步
==============

在第一个 JavaScript 板块，带领各位体验编写 JavaScript 程序前，首先回答一些基本问题：「什么是 JavaScript？」，「它看上去是什么样的？」，「它能做什么？」。此后，我们将详细讨论一些关键构件，例如变量、字符串、数值和数组。

事前准备
----

学习这个板块前，你不需要预先了解任何 JavaScript 知识，但你应当对 HTML 和 CSS 有所熟悉。我们建议学习 JavaScript 前，先完成以下板块阅读：

* 开始使用 Web（包括 JavaScript 基础简介）。
* HTML 简介。
* CSS 简介。

**备注：** 如果你无法在你使用的电脑/平板/其他设备上创建自己的文件，尝试使用在线编程应用来运行（大部分）代码示例，例如 JSBin 和 Glitch。

学习指南
----

什么是 JavaScript？

欢迎来到 MDN JavaScript 新手课程！第一篇文章，我们将从较高层次审视 JavaScript，解答诸如「它是什么？」、「它在做些什么？」的问题，确保你理解 JavaScript 的用途。

JavaScript 初体验

你已经学习了一些 JavaScript 理论，了解了可以用它做什么，接下来我们将通过一个非常实用的教程，带给你一节 JavaScript 基本特性速成课。在这里你将一步一步构建出一个「猜数字」游戏。

哪里出错了？JavaScript 疑难解答

构建上篇文章中的「猜数字」游戏时，你可能发现它没法运行。不要害怕——这篇文章来解救正为问题挠头的你，它为你提供一些从 JavaScript 程序中发现并解决问题的思路。

存储你需要的信息——变量

读过最近几篇文章，你应当了解 JavaScript 是什么，它能为你做什么，如何搭配其他 web 技术使用，以及从较高层次来看，它的主要特性是什么样的。这篇文章，我们将深入真正的基础，看看如何使用 JavaScript 的基本构件——变量。

JavaScript 中的基本运算——数值和运算符

这一课我们讨论 JavaScript 的运算——如何将运算符与其他特性结合使用，完美操作数值来实现需求。

文字处理——JavaScript 字符串

接下来将目光转向字符串——编程世界中文字片段被称为字符串。本文涉及你在学习 JavaScript 时应当了解的有关字符串的所有常见情形，例如生成字符串、字符串中的引用以及合并字符串。

实用字符串方法

我们已经了解字符串的基本内容，接下来再上一个台阶，思考利用字符串内建方法实现的实用操作，例如获取字符串长度、字符串合并与分割、替换字符串中特定字符，等等。

数组

这个版块最后一篇文章，我们来看数组——干净整洁地把一连串数据项存储在同一个变量名下。这里我们要理解为什么说它实用，探索如何创建数组，如何获取、添加、删除数组中的项目等等内容。

自我评估
----

下面的评估会检验你对上述学习指南中 JavaScript 基础的理解。

笑话生成器

这项评估中，你将运用你在这个版块的文章中学到的知识，构建一个生成随机笑话的好玩 app。玩得开心！


客户端 Web API - 学习 Web 开发

客户端 Web API
===========

当你给网页或者网页应用编写客户端的 JavaScript 时，你很快会遇上应用程序接口（API）——这些编程特性可用来操控网站所基于的浏览器与操作系统的不同方面，或是操控由其他网站或服务端传来的数据。在这个单元里，我们将一同探索什么是 API，以及如何使用一些在你开发中将经常遇见的 API。

预备知识
----

若想深入理解这个单元的内容，你必须能够以自己的能力较好地学完之前的几个章节 (JavaScript 第一步, JavaScript基础要件, 和JavaScript对象介绍). 这几部分涉及到了许多简单的 API 的使用，如果没有它们我们将很难做一些实际的事情。在这个教程中，我们会认为你懂得 JavaScript 的核心知识，而且我们将更深入地探索常见的网页 API。

若你知道 HTML 和 CSS 的基本知识，也会对理解这个单元的内容大有裨益。

**备注：** 如果你正在使用一台无法创建你自身文件的电脑、平板或其他设备，你可以尝试使用一些在线网页编辑器如JSBin或者Glitch，来尝试编辑一些代码示例。

向导
--

Web API 简介

首先，我们将从一个更高的角度来看这些 API —它们是什么，它们怎么起作用的，你该怎么在自己的代码中使用它们以及他们是怎么构成的？我们依旧会再来看一看这些 API 有哪些主要的种类和他们会有哪些用处。

操作文档

当你在制作 WEB 页面和 APP 时，一个你最经常想要做的事就是通过一些方法来操作 WEB 文档。这其中最常见的方法就是使用文档对象模型 Document Object Model (DOM)，它是一系列大量使用了 `Document` object 的 API 来控制 HTML 和样式信息。通过这篇文章，我们来看看使用 DOM 方面的一些细节，以及其他一些有趣的 API 能够通过一些有趣的方式改变你的环境。

从服务器获取数据

在现代网页及其 APP 中另外一个很常见的任务就是与服务器进行数据交互时不再刷新整个页面，这看起来微不足道，但却对一个网页的展现和交互上起到了很大的作用，在这篇文章里，我们将阐述这个概念，然后来了解实现这个功能的技术，例如 `XMLHttpRequest` 和 Fetch API.（抓取 API）。

第三方 API

到目前为止我们所涉及的 API 都是浏览器内置的，但并不代表所有。许多大网站如 Google Maps, Twitter, Facebook, PayPal 等，都提供他们的 API 给开发者们去使用他们的数据（比如在你的博客里展示你分享的推特内容）或者服务（如在你的网页里展示定制的谷歌地图或接入 Facebook 登录功能）。这篇文章介绍了浏览器 API 和第三方 API 的差别以及一些最新的典型应用。

绘制图形

浏览器包含多种强大的图形编程工具，从可缩放矢量图形语言 Scalable Vector Graphics (SVG) language，到 HTML 绘制元素 `<canvas>` 元素 (The Canvas API and WebGL). 这篇文章提供了部分 canvas 的简介，以及让你更深入学习的资源。

视频和音频 API

HTML5 能够通过元素标签嵌入富媒体——`<video>` and `<audio>`——而将有自己的 API 来控制回放，搜索等功能。本文向你展示了如何创建自定义播放控制等常见的任务。

客户端存储

现代 web 浏览器拥有很多不同的技术，能够让你存储与网站相关的数据，并在需要时调用它们，能够让你长期保存数据、保存离线网站及其他实现其他功能。本文解释了这些功能的基本原理。


异步 JavaScript - 学习 Web 开发

异步 JavaScript
=============

在这个模块，我们将查看异步 JavaScript，异步为什么很重要，以及怎样使用异步来有效处理潜在的阻塞操作，比如从服务器上获取资源。

#### 想成为一名前端工程师？

我们汇总了一门课程，其中包含你实现目标所需要的所有基本信息。

**点击开始**

预备知识
----

异步 JavaScript 是一个相当高级的话题，建议你先完成 JavaScript 第一步和创建 JavaScript 代码块 两个模块的学习后再来学习。

**备注：** 如果你工作在一个无权创建自己文件的电脑/平板/其他设备上，你需要在一个在线编程工具上试验（大多数）代码示例，如 JSBin 或者 Glitch.

指南
--

异步 JavaScript 简介

在这篇文章中我们将介绍 **同步编程** 和 **异步编程** 是什么，为什么我们总是需要异步编程技术，异步函数过去是怎样在 JavaScript 上实现的，以及这些方式的问题有哪些。

如何使用 Promise

我们将在这里介绍 Promise 并向你展示如何使用基于 Promise 的 API。我们也会介绍 `async` 和 `await` 关键字。

应用一个基于 Promise 的 API

本文概述了如何实现一个我们自己的基于 Promise 的 API。

Worker 简介

Worker 使你能够在独立的线程中运行任务来保持你的主要代码的可响应性。在这篇文章中我们将把一个长时间运行的同步函数重写为使用 Worker 的示例。

测验
--

序列动画

这个测验要求你使用 Promise 来播放一组特定序列的动画。

参见
--

* 由 Marijn Haverbeke 编写的电子书籍 Eloquent JavaScript 中的 异步编程。


解决 JavaSctript 代码的常见问题 - 学习 Web 开发

解决 JavaSctript 代码的常见问题
======================

以下链接指向撰写 JavaScript 代码时可能遇见的问题。

初学者常见的错误
--------

### 正确的拼写和使用

如果你的代码不工作或浏览器抱怨某些东西是未定义的，请检查你是否正确拼写了所有的变量名称，函数名称等。

导致问题的一些常见的内置浏览器函数有：

| 正确 | 错误 |
| --- | --- |
| `getElementsByTagName()` | `getElementbyTagName()` |
| `getElementsByName()` | `getElementByName()` |
| `getElementsByClassName()` | `getElementByClassName()` |
| `getElementById()` | `getElementsById()` |

### 分号的位置

必须确保没有错误地放置分号，例如：

| 正确 | 错误 |
| --- | --- |
| `elem.style.color = 'red';` | `elem.style.color = 'red;'` |

### 函数

函数有很多容易出错的地方。

最常见的错误之一是函数被声明了却没有被调用。例如：

```js
function myFunction() {
  alert("This is my function.");
}
```

这个函数不会执行，除非你调用它，例如：

```js
myFunction();
```

#### 函数作用域

记住，函数拥有自己的作用域——你不能从函数外部访问一个函数内的变量值，除非你在全局声明了该变量（即不在任何函数内），或者从函数外部获得它的返回值。

#### 在 return 语句之后运行代码

还要记住，当你向一个函数外部返回一个值时，JavaScript 解释器会退出这个函数——在 return 语句之后，不会执行任何代码。

事实上，如果你在返回语句之后有代码，某些浏览器（如 Firefox）会在开发人员控制台中显示出一条错误消息。Firefox 在返回语句后会给你“无法运行到的代码”的提示。

### 对象标记法与正常赋值

当你在 JavaScript 中正常赋值时，使用等号：

```js
const myNumber = 0;
```

但是在对象中，需要小心地使用正确的语法标签。你需要使用花括号来包裹一个对象，冒号来分隔成员名称和值，并用逗号来分隔每个成员，例如：

```js
const myObject = {
  name: "Chris",
  age: 38,
};
```

基本定义
----

* JavaScript 是什么？
* 变量是什么？
* 字符串是什么？
* 数组是什么？
* 循环是什么？
* 函数是什么？
* 事件是什么？
* 对象是什么？
* JSON 是什么？
* web API 是什么？
* DOM 是什么？
基本用例
----

### 常见

* 怎么在页面中添加 JavaScript?
* 怎么在 JavaScript 中添加注释？
### 变量

* 如何声明变量？
* 如何初始化变量的值？
* 如何更新变量的值？（参见 赋值运算符）
* JavaScript 中有哪些数据类型？
* '弱类型'是什么意思？
### 数学

* 在 web 开发中需要处理什么样的数字值？
* JavaScript 如何处理基本的数学运算？
* 什么是运算符的优先级，如何在 JavaScript 中处理优先级问题？
* 如何在 JavaScript 中增加或减少值？
* 如何在 JavaScript 中比较值的大小？（例如，查看哪个值更大，或检查一个值是否与另外一个值相等）
### 字符串

* 如何在 JavaScript 中创建字符串？
* 我需要使用单引号，还是双引号？
* 我如何转义字符串中的字符？
* 我如何将字符串拼接在一起？
* 可以将字符串和数字拼接在一起吗？
* 如何得到字符串的长度？
* 如何得到字符串某个特定位置的字符？
* 如何从给定字符串中找到并提取特定子字符串？
* 如何改变字符串的大小写？
* 如何将特定子字符串替换为另外一个？
### 数组

* 怎么创建数组？
* 如何访问和修改数组中的元素？（包括多维数组）
* 怎么获取数组的长度？
* 怎么添加和移除数组中的元素？
* 如何将一个字符串拆分为数组，或将数组拼接成一个字符串？
### JavaScript 调试

* 什么是错误的基本类型？
* 什么是浏览器开发工具，如何使用它？
* 怎么在 JavaScript 控制台打印值？
* 怎么使用断点和其他 JavaScript 调试工具？

有关 JavaScript 调试的更多内容，请查阅处理 JavaScript 常见问题页面，也可以查看其他常见错误的描述。

### 在代码中作出判断

* 如何根据变量的值或其他条件执行不同代码块？
* 如何使用 if ...else 语句？
* 如何将决策的代码块嵌套在其他代码块之中？
* 如何在 JavaScript 中使用与、或、非运算符？
* 如何方便地处理单一条件产生的多重选择？
* 如何使用三元运算符快速作出基于真假的二重选择？
### 循环/迭代

* 如何重复运行相同的代码？
* 如何在满足某些条件下退出循环？
* 如何在满足某些条件下跳转至下一次循环迭代？
* 如何使用 while 和 do ... while 循环？
中级用例
----

### 函数

* 如何找到浏览器中的内置函数？
* 函数和方法有什么不同？
* 如何创建自己的函数？
* 如何运行（或调用）函数？
* 什么是匿名函数？
* 如何在调用函数时指定参数？
* 什么是函数作用域？
* 什么是函数返回值，如何使用它们？
### 对象

* 如何创建对象？
* 什么是点表示法？
* 什么是括号表示法？
* 如何获取和设置对象的方法和属性？
* 对象中的 `this` 是什么？
* 什么是面向对象编程？
* 什么是构造器和实例，如何创建它们？
* 在 JavaScript 中创建对象的方法都有哪些？
### JSON

* 如何构建 JSON 数据，并使用 JavaScript 读取它？
* 如何将 JSON 文件加载至页面上？
* 如何将 JSON 对象转换为字符串，或者再转换回来？
### 事件

* 什么是事件处理器，怎样使用它们？
* 什么是内联事件处理器？
* `addEventListener()` 函数的作用，如何使用它？
* 我该使用什么样的机制将事件代码添加到我的网页上？
* 什么是事件对象，如何使用它？
* 如何防止默认事件行为？
* 事件如何在嵌套元素中触发？（事件传递及其相关内容——事件冒泡及捕获）
* 什么是事件委托，它如何工作？
### 面向对象的 JavaScript

* 什么是对象原型？
* 什么是构造器属性，如何使用它？
* 如何向构造器中添加方法？
* 如何创建一个新的构造函数，从父构造函数中继承其成员？
* 什么时候应当在 JavaScript 中使用继承？
### Web API

* 如何使用 JavaScript 操作 DOM 模型（如添加或删除元素）？


创建 JavaScript 代码块 - 学习 Web 开发

创建 JavaScript 代码块
=================

在这个模块中，我们将继续介绍 JavaScript 的关键基本特性，在这一章中我们将关注条件控制语句、循环语句、函数模块、事件等通用代码块。你可能在之前的的课程中见过这些模块，但仅仅是见过—在这篇模块中我们将明确讨论这些模块。

预备知识
----

在开始这部分模块之前，你应该熟悉基本的 HTML 和 CSS, 并且已经看完我们之前的模块：JavaScript 第一步。

**备注：** 如果你在使用无法创建自己文件的电脑/平板/其他设备，你可以试试在线编辑器，例如 JSBin 或 Glitch.

指南
--

在代码中做决定 — 条件

在任何程序语言中，程序需要根据不同的输入数据作出相应的选择并执行相关的操作。例如，在游戏中，如果玩家的生命值是 0，那么游戏就结束了。在天气应用中，如果在早上打开应用，则显示一个太阳升起的图片，如果在晚上打开，则显示星星和月亮。在这篇文章里我们将探索如何在 JS 中使用条件结构。

循环语句

有时候你需要在一个行中重复执行某一个任务。例如，查看一整列的名字。在程序中，循环能非常好的处理好这个问题。在本章中我们将介绍 JavaScript 的循环语句。

函数 — 可重用的代码块

在编码中的另一个基本概念是函数 (functions)。 **函数** 允许你在定义的区块内存储一段代码用来执行一个单独的任务，然后调用该段代码时，你需要使用一个简短的命令，而不用重复编写多次该段代码。在这篇文章中我们将探讨函数的基本概念，如语法、如何调用定义的函数、作用域和参数。

打造自己的函数

本文结合前几篇文章中所涉及的基本理论，提供了一个实践经验。在这里你会得到一些实践，并且编写自己的自定义函数。随后，我们也将进一步解释一些与函数相关的有用的细节。

函数返回值

在这个课程中，我们要讨论的最后一个基本概念是返回值（通过返回值结束我们的函数）。有些函数在完成后不返回任何值，而有些函数返回。重要的是了解返回的值是什么，和如何在你的代码中使用他们，以及如何使自定义的函数返回需要的值。

事件介绍

事件是你正在编写的系统中发生的动作或事件，系统告诉你的是这些动作或事件，如果需要的话，你可以以某种方式对它们做出反应。例如，如果用户单击网页上的按钮，你可能希望通过显示信息框来响应该操作。在这最后一篇文章中，我们将重点讨论一些围绕事件有关的概念，看看他们如何在浏览器中工作。

评估
--

下面的评估将测试你对 JavaScript 基础知识的理解。

图片画廊

现在我们已经学习了构建 JavaScript 的基本代码块，我们会通过构建一个在很多网站上相当常见的项目——一个由 JavaScript 驱动的相册，来测试你循环、函数、条件语句和事件方面的知识。


JavaScript 对象入门 - 学习 Web 开发

JavaScript 对象入门
===============

在 JavaScript 中，大多数事物都是对象，从作为核心功能的字符串和数组，到建立在 JavaScript 之上的浏览器 API 。你甚至可以自己创建对象，将相关的函数和变量高效地封装打包成便捷的数据容器。对于进一步学习 JavaScript 语言知识而言，理解这种面向对象（object-oriented, OO）的特性是必不可少的，所以，我们提供了这个模块来帮助你了解这一切。这里我们会先详细介绍对象的理论和语法，再介绍如何创建对象。

预备知识
----

开始这个模块之前，你应当已经对 HTML 和 CSS 有所了解。我们建议你通读 HTML 入门和 CSS 入门模块，再开始了解 JavaScript。

详细了解 JavaScript 对象之前，你应当已经对 JavaScript 基础有所熟悉。尝试这个模块之前，请通读 JavaScript 第一步 和 JavaScript 基础要件

**备注：** 如果你无法在当前使用的电脑/平板/其他设备上创建自己的文件，可以使用在线编程网站如 JSBin 或 Glitch，来试验文章中的（大多数）代码。

指南
--

对象基础

在了解 JavaScript 对象的第一篇文章中，我们将介绍 JavaScript 对象的语法，并回顾先前课程中讲过的某些 JavaScript 功能。你会发现，你已经在使用的很多功能本质上都是对象。

适合初学者的面向对象 JavaScript

了解基础后，我们将关注面向对象 JavaScript (OOJS)。本文将介绍面向对象编程 (OOP) 的基本理论，然后讲解 JavaScript 如何通过构造器 (constructor) 函数模拟对象类别 (class)、如何创建对象实例 (instance)。

对象原型

通过原型 (prototype) 这种机制，JavaScript 中的对象从其他对象继承功能特性；这种继承机制与经典的面向对象编程语言不同。本文将探讨这些差别，解释原型链如何工作，并了解如何通过 `prototype` 属性向已有的构造器添加方法。

JavaScript 中的继承

了解了 OOJS 的大多数细节之后，本文将介绍如何创建“子”对象类别（构造器）并从“父”类别中继承功能。此外，我们还会针对何时何处使用 OOJS 给出建议。

使用 JSON 数据

JavaScript Object Notation (JSON) 是一种将结构化数据表达为 JavaScript 对象的标准格式，其常用于在网站上表达或传输数据（比如：从服务器向客户端发送数据，使之显示在网页上）。你会经常遇到它，因此本文将告诉你如何在 JavaScript 中使用 JSON 数据，包括访问 JSON 对象中的数据条目、编写自己的 JSON 数据等等。

构建对象实战

在前面的文章中我们了解了 JavaScript 对象基本理论和语法，为你打下坚实的基础。本文中你需要进行实战练习，通过构建自定义 JavaScript 对象的实践过程，编写一个有趣而又多彩的程序——“彩色弹跳球”。

学习评估
----

向“弹跳球”演示程序添加新功能

在这个评估中，你需要以上一篇文章中的“弹跳球”演示为起点，向这个演示程序新增一些有趣的功能。


查找并解决 JavaScript 代码的错误 - 学习 Web 开发

查找并解决 JavaScript 代码的错误
======================

* 上一页
* Overview: First steps
* 下一页

上一节中你创建了“猜数字”游戏，但它可能没有正常工作。别担心，本节将为你提供一些简单的提示，来帮助你查找并修复 JavaScript 程序中的错误，从而让你远离困扰。

|  |  |
| --- | --- |
| 前提： | 计算机基础知识，初步理解 HTML 和 CSS，了解 JavaScript。 |
| 目标： | 获得独立修复简单问题的能力和信心。 |

错误类型
----

一般来说，代码错误主要分为两种：

* **语法错误**：代码中存在拼写错误，将导致程序完全或部分不能运行，通常你会收到一些出错信息。只要熟悉语言并了解出错信息的含义，你就能够顺利修复它们。
* **逻辑错误**：有些代码语法虽正确，但执行结果和预期相悖，这里便存在着逻辑错误。这意味着程序虽能运行，但会给出错误的结果。由于一般你不会收到来自这些错误的提示，它们通常比语法错误更难修复。

事情远没有你想的那么简单，随着探究的深入，会有更多差异因素浮出水面。但在编程生涯的初级阶段上述分类方法已足矣。下面我们将依次分析。

一个出错的示例
-------

让我们重回猜数字游戏，这次我们将故意引入一些错误。请到 Github 下载一份 number-game-errors.html （或 在线运行）。

1. 请分别在你的文本编辑器和浏览器中打开刚下载的文件。
2. 先试玩游戏，你会发现在点击“确定”按钮时，游戏并没有响应。

**备注：** 你可能还在为修复你自己版本的游戏头疼，但我们仍然希望你先用我们的版本来完成这一节，这样你才能学到本节中的技术。然后再去修复自己的游戏也不晚。

首先查看开发者控制台，看是否存在语法错误，然后尝试修复。详见下文。

修复语法错误
------

以前的课程中，你学会了在 开发工具 JavaScript 控制台 中输入一些简单的 JavaScript 命令。（如果你忘记了如何在浏览器中打开它，可以直接打开上面的链接）。更实用的是，当 JavaScript 代码进入浏览器的 JavaScript 引擎时，如果存在语法错误，控制台会提供出错信息。现在我们去看一看。

1. 打开 `number-game-errors.html` 所在的标签页，然后打开 JavaScript 控制台。你将看到以下出错信息：

 ![不是函数](/zh-CN/docs/Learn/JavaScript/First_steps/What_went_wrong/%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7_2018-10-17_20.27.08.png)
2. 这个错误很容易跟踪，浏览器为你提供了几条有用的信息（截图来自 Firefox，其他浏览器也提供类似信息）。从左到右依次为：
    * 红色“!”表示这是一个错误。
    * 一条出错信息，表示问题出在哪儿：“TypeError：**guessSubmit**.addeventListener is not a function”（类型错误：**guessSubmit**.addeventListener 不是函数）
    * 点击 [详细了解] 将跳转到一个 MDN 页面，其中包含了此类错误超详细的解释。
    * JavaScript 文件名，点击将跳转到开发者工具的“调试器”标签页。如果你按照这个链接，你会看到错误突出显示的确切行。
    * 出错的行，以及导致错误的首个字符号。这里错误来自 86 行，第 3 个字符。
3. 我们在代码编辑器中找到第 86 行：

```js
guessSubmit.addeventListener("click", checkGuess);
```

4. 出错信息显示“guessSubmit.addeventListener 不是一个函数”，说明这里可能存在拼写错误。如果你不确定某语法的拼写是否正确，可以到 MDN 上去查找，目前最简便的方法就是去你喜欢的搜索引擎搜索“MDN + 语言*特性”。就本文当前内容你可以点击*：`addEventListener()`。
5. 因此这里错误显然是我们把函数名写错造成的。请记住，JavaScript 区分大小写，所以任何轻微的不同或大小写问题都会导致出错。将 `addeventListener` 改为 `addEventListener` 便可解决。

**备注：** 更多信息请参考 类型错误：“x”不是一个函数。

### 语法错误：第二轮

1. 保存页面并刷新，可以看到出错信息不见了。
2. 现在，如果尝试输入一个数字并按确定按钮，你会看到...另一个错误！
 ![](/zh-CN/docs/Learn/JavaScript/First_steps/What_went_wrong/%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7_2018-10-17_21.56.00.png)
3. 此次出错信息为“TypeError：lowOrHi is null”（“类型错误：lowOrHi 为 null”），在第 78 行。

**备注：** `Null`是一个特殊值，意思是“什么也没有”，或者“没有值”。这表示 `lowOrHi` 已声明并初始化，但没有任何有意义的值，可以说：它没有类型没有值。

**备注：** 这条错误没有在页面加载时立即发生，是因为它发生在函数内部（`checkGuess() { ... }`块中）。函数内部的代码运行于一个外部代码相互独立的域内，后面函数的文章中将更详细地讲解。此时此刻，只有当代码运行至 86 行并调用 `checkGuess()` 函数时，代码才会抛出出错信息。
4. 请观察第 78 行代码：

```js
lowOrHi.textContent = "你猜高了！";
```

5. 该行试图将 `lowOrHi` 变量中的 `textContent` 属性设置为一个字符串，但是失败了，这是因为 `lowOrHi` 并不包含预期的内容。为了一探究竟，你可以在代码中查找一下该变量的的其他实例。`lowOrHi` 最早出现于第 48 行：

```js
const lowOrHi = document.querySelector("lowOrHi");
```

6. 此处，我们试图让该变量包含一个指向文档 HTML 中特定元素的引用。我们来检查一下在该行代码执行后变量的值是否为 `null`。在第 49 行添加以下代码：

```js
console.log(lowOrHi);
```

**备注：** `console.log()` 是一个非常实用的调试功能，它可以把值打印到控制台。因此我们将其置于代码第 48 行时，它会将 `lowOrHi` 的值打印至控制台。
7. 保存并刷新，你将在控制台看到 `console.log()` 的执行结果：
 ![](/zh-CN/docs/Learn/JavaScript/First_steps/What_went_wrong/%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7_2018-10-18_16.43.40.png) 显然，此处 `lowOrHi` 的值为 `null`，所以第 48 行肯定有问题。
8. 我们来思考问题有哪些可能。第 48 行使用 `document.querySelector()` 方法和一个 CSS 选择器来取得一个元素的引用。进一步查看我们的文件，我们可以找到有问题的段落：

```js
<p class="lowOrHi"></p>
```

9. 这里我们需要一个类选择器，它应以一个点开头（`.`），但被传递到第 48 行的`querySelector()`方法中的选择器没有点。这可能是问题所在！尝试将第 48 行中的 `lowOrHi` 改成 `.lowOrHi`。
10. 再次保存并刷新，此时 `console.log()` 语句应该返回我们想要的 `<p>` 元素。终于把错误搞定了！此时你可以把 `console.log()` 一行删除，或保留它以便随后参考。选择权在你。

**备注：** 此错误的更多详细信息请参阅：类型错误：“x”（不）是“y”。

### 语法错误：第三轮

1. 现在，如果你再次试玩，你离成功更进了一步。游戏过程按部就班，直到猜测正确或机会用完，游戏结束。
2. 此时如果点击“开始新游戏”，游戏将再次出错，抛出与开始时同样的错误——“TypeError：resetButton.addeventListener is not a function”！这次它来自第 94 行。
3. 查看第 94 行，很容易看到我们犯了同样的错误。我们只需要再次将 `addeventListener` 改为 `addEventListener`。现在就改吧。
逻辑错误
----

此时，游戏应该可以顺利进行了。但经过几次试玩后你一定会注意到要猜的随机数不是 0 就是 1。这可不是我们期望的！

游戏的逻辑肯定是哪里出现了问题，因为游戏并没有返回错误，只是不能正确运行。

1. 寻找 `randomNumber` 变量和首次设定随机数的代码。保存着游戏开始时玩家要猜的随机数的实例大约在 44 行：

```js
let randomNumber = Math.floor(Math.random()) + 1;
```

重新开始游戏产生随机数的设定语句大约在 113 行：

```js
randomNumber = Math.floor(Math.random()) + 1;
```

2. 为了检查问题是否来自这两行，我们要再次转到我们的朋友 - 控制台：在两行代码之后各插入下面的代码：

```js
console.log(randomNumber);
```

3. 保存并刷新，然后试玩，你会看到在控制台显示的随机数总是等于 1。
### 修正逻辑错误

为了解决这个问题，让我们来思考这行代码如何工作。首先，我们调用 `Math.random()`,它生成一个在 0 和 1 之间的十进制随机数，例如 0.5675493843。

```js
Math.random();
```

接下来，我们把调用 `Math.random()` 的结果作为参数传递给 `Math.floor()`，它会舍弃小数部分返回与之最接近的整数。然后我们给这个结果加上 1：

```js
Math.floor(Math.random()) + 1;
```

由于将一个 0 和 1 之间的随机小数的小数部分舍弃，返回的整数一定为 0，因此在此基础上加 1 之后返回值一定为 1。要在舍弃小数部分之前将它乘以 100。便可得到 0 到 99 之间的随机数：

```js
Math.floor(Math.random() \* 100);
```

然后再加 1，便可得到一个 100 以内随机的自然数：

```js
Math.floor(Math.random() \* 100) + 1;
```

将上述两行内容替换为此，然后保存刷新，游戏终于如期运行了！

其他常见错误
------

代码中还会遇到其他常见错误。本节将指出其中的大部分。

### SyntaxError: missing ; before statement（语法错误：语句缺少分号）

这个错误通常意味着你漏写了一行代码最后的分号，但是此类错误有时候会更加隐蔽。例如如果我们把 `checkGuess()` 函数中的这一行 :

```js
let userGuess = Number(guessField.value);
```

改成

```js
let userGuess === Number(guessField.value);
```

将抛出一个错误。因为系统认为你在做其他事情。请不要把赋值运算符（`=`，为一个变量赋值）和严格等于运算符（`===`，比较两个值是否相等，返回 `true`/`false`）弄混淆。

**备注：** 此错误的更多详细信息请参考 SyntaxError: missing ; before statement 。

### 不管输入什么程序总说“你猜对了！”

这是混淆赋值和严格等于运算符的又一症状。例如我们把 `checkGuess()` 里的：

```js
if (userGuess === randomNumber) {
```

改成

```js
if (userGuess = randomNumber) {
```

因为条件永远返回 `true`，使得程序报告你猜对了。小心哦！

### SyntaxError: missing ) after argument list（语法错误：参数表末尾缺少括号）

这个很简单。通常意味着函数/方法调用后的结束括号忘写了。

**备注：** 有关此错误的更多详细信息请参考： SyntaxError: missing ) after argument list。

### SyntaxError: missing : after property id（语法错误：属性 ID 后缺少冒号）

JavaScript 对象的形式有错时通常会导致此类错误，如果把

```js
function checkGuess() {
```

写成了

```js
function checkGuess( {
```

浏览器会认为我们试图将函数的内容当作参数传回函数。写圆括号时要小心！

### SystaxError: missing } after function body（语法错误：函数体末尾缺少花括号）

这个简单。通常意味着函数或条件结构中丢失了一个花括号。如果我们将 `checkGuess()` 函数末尾的花括号删除，就会得到这个错误。

### SyntaxError: expected expression, got '*string*'（语法错误：得到一个 '*string*' 而非表达式）

或者

### SyntaxError: unterminated string literal（语法错误：字符串字面量未正常结束）

这个错误通常意味着字符串两端的引号漏写了一个。如果你漏写了字符串开始的引号，将得到第一条出错信息，这里的 '*string'* 将被替换为浏览器发现的意外字符。如果漏写了末尾的引号将得到第二条。

对于所有的这些错误，想想我们在实例中是如何逐步解决的。错误出现时，转到错误所在的行观察是否能发现问题所在。记住，错误不一定在那一行，错误的原因也可能和我们在上面所说的不同！

**备注：** 有关这些错误的更多详细信息请参考：SyntaxError: Unexpected token 以及 SyntaxError: unterminated string literal。

小结
--

我们有了能够在简单的 JavaScript 程序中除错的基础知识。解决代码中的错误并不总是那么简单，但至少本节内容可以为刚刚踏上学习之路的你节省出几个小时来补觉，同时让问题更快速得到解决。

参见
--

* 许多错误不能一一的在这里列出来，我们正在编写一个参考文档来详细说明它们的含义。请参阅 JavaScript 出错信息参考.
* 如果你在阅读了本文之后遇到了一些错误但不知如何解决，你能够得到别人的帮助！可以到 学习区 或者 Mozilla IRC 的 #mdn 聊天室来提问。告诉我们你遇到的错误是什么，我们会尽量帮助你。附加一段你的代码也是很有用的。
* 上一页
* Overview: First steps
* 下一页


JavaScript 中的基础数学 — 数字和操作符 - 学习 Web 开发

JavaScript 中的基础数学 — 数字和操作符
==========================

* 上一页
* Overview: First steps
* 下一页

在本次课程中，我们讨论 JavaScript 中的数学 — 我们如何使用 运算符 和其他功能来成功地操作数字以完成我们的请求。

|  |  |
| --- | --- |
| 前提： | 基本的计算机知识，对 HTML 和 CSS 初步了解，知道 JavaScript 是什么。 |
| 目标： | 熟悉 JavaScript 中 Math 的基础知识。 |

人人都爱数学
------

好吧，可能不是。有些人喜欢数学，有些人可能从在学校必须学习乘法表和长除法时就讨厌数学，还有人介于两者之间。但我们都不能否认，数学是生活的基本组成部分，我们离不了它。尤其如此，当我们学习编写 JavaScript 程序（或任何其他语言），我们所做的很多事情都依赖于处理数值数据，计算新值等。你将不会惊讶地认识到 JavaScript 有一套可用的全功能的数学功能。

本文仅讨论你现在需要了解的基本部分。

### 数字类型

在编程中，即使是人人熟知的最普遍的十进制数，也比你想象的要复杂的多。我们使用不同的术语来描述不同类型的十进制数，例如：

* **整数** 就是整数，例如 10, 400, 或者 -5.
* **浮点数** (浮点) 有小数点或小数位，例如 12.5，和 56.7786543。
* **双精度**双精度是一种特定类型的浮点数，它们具有比标准浮点数更高的精度（这意味着它们精确到更大的小数位数）。

我们甚至有不同类型的数字系统！十进制是基数 10（意味着它在每列使用 0-9），但是我们也有这样的东西：

* **二进制**——计算机的最基础语言——0 和 1
* **八进制**——基数 8，每列使用 0-7
* **十六进制**——基数 16，每列使用 0-9，然后使用 a-f。在 CSS 中设置颜色时，可能会遇到这些数字。

在你开始担心你的大脑混乱之前，先停下来吧！一开始，我们将在本课程中坚持使用十进制数; 你很少会遇到需要开始考虑其他类型的情况，如果有的话。

第二个好消息是，与其他一些编程语言不同，JavaScript 只有一个数据类型用来表示数字（包括 integers 和 decimals），你猜对了，`Number`。这意味着，你在 JavaScript 中处理的任何类型的数字，都以完全相同的方式处理它们。

### 这是我们的全部数字

让我们快点玩一些数字，以重新定义我们所需要的基本语法。在你的开发工具 JavaScript 控制台中输入下面列出的命令。

**在新窗口中打开**

1. 首先，我们先声明一对变量，并分别用一个整数和一个浮点数来初始化它们，然后再输入变量名来检查是否正确：

```js
let myInt = 5;
let myFloat = 6.667;
myInt;
myFloat;
```

2. 数值键入不带引号——在继续之前尝试声明和初始化更多包含数字的变量。
3. 现在我们来看看我们的原始变量是否是相同的数据类型。在 JavaScript 中有一个称为`typeof` 的运算符。输入如下所示的两行：

```js
typeof myInt;
typeof myFloat;
```

在这两种情况下，都应该返回 `"number"`——这使得事情变得更简单，因为若是不同的数字具有不同的数据类型，那么我们还须以不同的方式处理它们。呦！
算术运算符
-----

算术运算符是我们用来做和的基本运算符：

| 运算符 | 名称 | 作用 | 示例 |
| --- | --- | --- | --- |
| `+` | 加法 | 两个数相加。 | `6 + 9` |
| `-` | 减法 | 从左边减去右边的数。 | `20 - 15` |
| `*` | 乘法 | 两个数相乘。 | `3 * 7` |
| `/` | 除法 | 用右边的数除左边的数 | `10 / 5` |
| `%` | 求余 (有时候也叫取模) | 在你将左边的数分成同右边数字相同的若干整数部分后，返回剩下的余数 | `8 % 3` (返回 2，8 除以 3 的倍数，余下 2。) |
| `**` | 幂 |
 取底数的指数次方，即指数所指定的底数相乘。它在 EcmaScript 2016
 中首次引入。
  | `5 ** 5` (返回 3125，相当于
 `5 * 5 * 5 * 5 * 5` 。)
  |

**备注：** 你以后有时候会看到参与算术计算的数字被称为 操作数 (operands (en-US))。

**备注：** 有时你可能会看到使用较旧的 `Math.pow()` 方法表达的指数，该方法的工作方式非常相似。例如，在 `Math.pow(7, 3)` 中，`7` 是基数，`3` 是指数，因此表达式的结果是 `343`。 `Math.pow(7, 3)` 相当于 `7 ** 3`。

我们可能不需要教你如何做基础数学，但我们想测试你对所涉及的语法的理解。尝试将下面的示例输入到开发者工具 JavaScript 控制台中。

1. 首先尝试输入一些简单的例子，例如

```js
10 + 7;
9 \* 8;
60 % 3;
```

2. 你还可以尝试声明变量并用数字初始化变量，然后尝试使用这些变量来求和 - 求和中变量的行为与直接用其持有的数来求和完全一样。例如：

```js
let num1 = 10;
let num2 = 50;
9 \* num1;
num1 \*\* 3;
num2 / num1;
```

3. 最后在本节中，尝试输入一些更复杂的表达式，如：

```js
5 + 10 \* 3;
(num2 % 9) \* num1;
num2 + num1 / 8 + 2;
```

这最后的一组计算中可能没有给出你期望的结果; 下面的部分也许能告诉你为什么。

### 运算符优先级

我们来看看上面的最后一个例子，假设 num2 的值为 50，num1 的值为 10（如上所述）：

```js
num2 + num1 / 8 + 2;
```

一般人，你会将它看作“50 加 10 等于 60”，然后“8 加 2 等于 10”，最后“60 除以 10 等于 6”。

但浏览器的“10 除以 8 等于 1.25”，那么“50 加 1.25 加 2 等于 53.25”。

这是因为**运算符优先级**——一些运算符将在计算算式（在编程中称为表达式）的结果时先于其他运算符被执行。JavaScript 中的运算符优先级与学校的数学课程相同——乘法和除法总是先完成，然后是加法和减法（总是从左到右进行计算）。

如果想要改变计算优先级，可以把想要优先计算的部分用括号围住。所以要得到结果为 6，我们可以这样做：

```js
(num2 + num1) / (8 + 2);
```

尝试看看。

**备注：** 注意：可以在表达式和运算符中找到所有 JavaScript 运算符的完整列表及其优先级。

自增和自减运算符
--------

有时候，你需要反复把一个变量加 1 或减 1。这可以方便地使用增量（`++`）和递减（ `--` ）运算符来完成。我们在JavaScript 初体验文章的“猜数字”游戏中使用了++，当我们添加 1 到我们的 guessCount 变量来跟踪用户在每次转动之后剩下的猜测时。

```js
guessCount++;
```

**备注：** 它们最常用于循环中，你将在以后的课程中了解。例如，假设你想循环查看价格表，并为每个价格增加销售税。你可以使用循环依次查看每个值，并在每种情况下进行必要的计算，以添加销售税。当需要时，增量器用于移动到下一个值。我们实际上提供了一个简单的例子，显示了如何完成——在线查看效果，并查看源代码，看看是否可以发现增量器！我们稍后将会详细介绍循环。

我们来试试看你们的控制台。首先，请注意，你不能将这些直接应用于一个数字，这可能看起来很奇怪，但是我们为变量赋值一个新的更新值，而不是对该值进行操作。以下将返回错误：

```js
3++;
```

所以，你只能增加一个现有的变量。尝试这个：

```js
let num1 = 4;
num1++;
```

好的，第二个奇怪的东西！执行此操作时，你会看到返回值为 4，这是因为浏览器返回当前值，然后增加变量。如果你再次返回变量值，则可以看到它已经递增：

```js
num1;
```

递减 `--` 同样如此，尝试以下操作：

```js
let num2 = 6;
num2--;
num2;
```

**备注：** 你可以使浏览器以其他方式进行操作 - 递增/递减变量，然后返回值 - 将操作符放在变量的开头，而不是结束。再次尝试上面的例子，但这次使用 `++num1` 和 `--num2`。

赋值运算符
-----

赋值运算符是向变量分配值的运算符。我们已经使用了最基本的一个很多次了：`=`，它只是将右边的值赋给左边的变量，即：

```js
let x = 3; // x 的值是 3
let y = 4; // y 的值是 4
x = y; // x 和 y 有相同的值，4
```

但是还有一些更复杂的类型，它们提供了有用的快捷方式，可以使你的代码更加清洁和高效。最常见的如下：

| 运算符 | 名称 | 作用 | 示例 | 等价于 |
| --- | --- | --- | --- | --- |
| `+=` | 加法赋值 | 右边的数值加上左边的变量，然后再返回新的变量。 | `x = 3; x += 4;` | `x = 3; x = x + 4;` |
| `-=` | 减法赋值 | 左边的变量减去右边的数值，然后再返回新的变量。 | `x = 6; x -= 3;` | `x = 6; x = x - 3;` |
| `*=` | 乘法赋值 | 左边的变量乘以右边的数值，然后再返回新的变量。 | `x = 2; x *= 3;` | `x = 2; x = x * 3;` |
| `/=` | 除法赋值 | 左边的变量除以右边的数值，然后再返回新的变量。 | `x = 10; x /= 5;` | `x = 10; x = x / 5;` |

尝试在你的控制台中输入上面的一些示例，以了解它们的工作原理。在每种情况下，你是否可以猜出在输入第二行之前的值。

请注意，你可以愉快地使用每个表达式右侧的其他变量，例如：

```js
let x = 3; // x 包含值 3
let y = 4; // y 包含值 4
x \*= y; // x 现在包含值 12
```

**备注：** 虽然有很多可用的赋值运算符，但是这些是你现在应该学习的基本的一类。

主动学习：调整画布框的大小
-------------

在这个练习中，我们将让你填写一些数字和操作符来操纵一个框的大小。该框使用称为 Canvas API 的浏览器 API 绘制。没有必要担心这是如何工作的——现在只关注数学。盒子的宽度和高度（以像素为单位）由变量 `x` 和 `y` 定义，变量 `x` 和 `y` 最初都被赋值为 50。

**新窗口打开**

在上面的可编辑代码框中，有两行标有清晰的注释，我们希望你更新以使框增长/缩小到某些大小，在每种情况下使用某些操作符和/或值。我们希望你回答以下问题：

* 更改计算 x 的行，使框仍然是 50px 宽，并且使用数字 43 和 7 以及算术运算符计算 50。
* 更改计算 y 的行，使框为高 75 像素，使用数字 25 和 3 计算 75，以及算术运算符。
* 更改计算 x 的行，使框为 250px 宽，250 是使用两个数字和余数（模）运算符计算的。
* 更改计算 y 的行，使框为 150px 高，150 是使用三个数字计算的，以及减法和除数运算符。
* 更改计算 x 的行，因此该框为 200px 宽，并且使用数字 4 和赋值运算符计算 200。
* 更改计算 y 的行，使框为 200px 高，使用数字 50 和 3，乘法运算符和加法运算符计算 200。

如果你完全混淆了代码，别担心。你可以随时按“重置”按钮，使事情恢复正常。在你正确回答了上述所有问题后，可以自由地使用代码或创建自己的挑战。

比较运算符
-----

有时，我们将要运行真/假测试，然后根据该测试的结果进行相应的操作 - 为此，我们使用比较运算符。

| 运算符 | 名称 | 作用 | 示例 |
| --- | --- | --- | --- |
| `===` | 严格等于 | 测试左右值是否相同 | `5 === 2 + 4` |
| `!==` | 严格不等于 | 测试左右值是否**不**相同 | `5 !== 2 + 3` |
| `<` | 小于 | 测试左值是否小于右值。 | `10 < 6` |
| `>` | 大于 | 测试左值是否大于右值 | `10 > 20` |
| <= | 小于或等于 | 测试左值是否小于或等于右值。 | `3 <= 2` |
| >= | 大于或等于 | 测试左值是否大于或等于正确值。 | `5 >= 4` |

**备注：** 你可能会看到有些人在他们的代码中使用`==`和`!=`来判断相等和不相等，这些都是 JavaScript 中的有效运算符，但它们与`===`/`!==`不同，前者测试值是否相同，但是数据类型可能不同，而后者的严格版本测试值和数据类型是否相同。严格的版本往往导致更少的错误，所以我们建议你使用这些严格的版本。

如果你尝试在控制台中输入这些值，你将看到它们都返回 `true`/`false` 值 - 我们在上一篇文章中提到的那些布尔值。这些是非常有用的，因为它们允许我们在我们的代码中做出决定 - 每次我们想要选择某种类型时，都会使用这些代码，例如：

* 根据功能是打开还是关闭，在按钮上显示正确的文本标签。
* 如果游戏结束，则显示游戏消息，或者如果游戏已经获胜，则显示胜利消息。
* 显示正确的季节性问候，取决于假期季节。
* 根据选择的缩放级别缩小或放大地图。

当我们在以后的的文章中查看条件语句时，我们将介绍如何编写这样的逻辑。现在，我们来看一个简单的例子：

```html
<button>Start machine</button>
<p>The machine is stopped.</p>
```

```js
const btn = document.querySelector("button");
const txt = document.querySelector("p");

btn.addEventListener("click", updateBtn);

function updateBtn() {
  if (btn.textContent === "Start machine") {
    btn.textContent = "Stop machine";
    txt.textContent = "The machine has started!";
  } else {
    btn.textContent = "Start machine";
    txt.textContent = "The machine is stopped.";
  }
}
```

**新窗口打开**

你可以在 updateBtn（）函数内看到正在使用的等号运算符。在这种情况下，我们不会测试两个数学表达式是否具有相同的值 - 我们正在测试一个按钮的文本内容是否包含某个字符串 - 但它仍然是工作原理。如果按钮当前为“启动机器（start machine）”，则将其标签更改为“停止机器（stop machine）”，并根据需要更新标签。如果按钮上写着“停机”时被按下，我们将显示回原来的内容。

**备注：** 这种在两个状态之间来回交换的行为通常被称为**切换**。它在一个状态和另一个状态之间切换 - 点亮，熄灭等

总结
--

在本文中，我们已经介绍了现在需要了解 JavaScript 中数字的基本信息。你会发现，在你学习 JavaScript 过程中，num 类型的数据一直都在被使用，所以熟练的掌握它是一个不错的选择。如果你是那些不喜欢数学的人之一，你应该庆幸这一章内容很简短。

在下一篇文章中，我们将探讨文本，以及 JavaScript 如何让我们操纵它。

**备注：** 如果你喜欢数学，并希望阅读更多关于它如何在 JavaScript 中实现的，那么你可以在 MDN's main JavaScript 部分读到更多关于它的内容。对于学习数字与日期和表达式与运算符来说，那是一个不错的地方。

* 上一页
* Overview: First steps
* 下一页


JavaScript 初体验 - 学习 Web 开发

JavaScript 初体验
==============

* 上一页
* Overview: First steps
* 下一页

现在，你已经学到了一些 JavaScript 的理论知识，以及用 JavaScript 能够做些什么。下面我们会提供一个创建简单的 Javascript 程序的实践的教程——循序渐进地构建一个简易版“猜数字”游戏。

|  |  |
| --- | --- |
| 前提： | 计算机基础知识，初步理解 HTML 和 CSS，了解 JavaScript。 |
| 目标： | 获得编写 JavaScript 的初步经验，初步了解编写 JavaScript 时会涉及哪些内容。 |

我们并不要求你立刻完整理解所有代码：你不需要借此学会 JavaScript，甚至不需要理解我们要求你编写的全部代码。当前只是概括地介绍一些抽象的概念，让你了解 JavaScript 的特性是如何协同工作的，以及获得编写 JavaScript 的一些感受。所有具体特性将在后续文章中详细介绍，如果你没有很快地全部理解它们，请不要担心！

**备注：** 可以看到，JavaScript 中许多代码特性和其他编程语言是一致的（函数、循环，等等）。尽管代码语法不尽相同，但概念基本类似。

像程序员一样思考
--------

学习编程，语法本身并不难，真正困难的是如何应用它来解决现实世界的问题。你要开始像程序员那样思考。一般来讲，这种思考包括了解你程序运行的目的，为达到该目的应选定的代码类型，以及如何使这些代码协同运行。

为达成这一点，我们需要努力编程，获取语法经验，注重实践，再加一点创造力，几项缺一不可。代码写的越多，就会完成得越优秀。虽然我们不能保证你在 5 分钟内拥有“程序员大脑”，但是整个课程中你将得到大量机会来训练程序员思维。

请牢记这一点，然后开始观察本文的示例，体会一下将其分解为可操作任务的大体过程。

示例——猜数字游戏
---------

本文将向你演示如何构建下面的小游戏：

先玩上几盘，在继续之前先熟悉一下这个游戏。

假设你的老板给你布置了以下游戏设计任务要求：

>
> 我想让你开发一个猜数字游戏。游戏应随机选择一个 1 到 100 之间的自然数，然后邀请玩家在 10 轮以内猜出这个数字。每轮后都应告知玩家的答案正确与否，如果出错了，则告诉他数字是低了还是高了。并且应显示出玩家前一轮所猜的数字。一旦玩家猜对，或者用尽所有机会，游戏将结束。游戏结束后，可以让玩家选择重新开始。
>
>
>

看到这个要求，首先我们要做的是将其分解成简单的可操作的任务，尽可能从程序员的思维去思考：

1. 随机生成一个 1 到 100 之间的自然数。
2. 记录玩家当前的轮数。从 1 开始。
3. 为玩家提供一种猜测数字的方法。
4. 一旦有结果提交，先将其记录下来，以便用户可以看到他们先前的猜测。
5. 然后检查它是否正确。
6. 如果正确：
    1. 显示祝贺消息。
    2. 阻止玩家继续猜测（这会使游戏混乱）。
    3. 显示控件允许玩家重新开始游戏。
7. 如果出错，并且玩家有剩余轮次：
    1. 告诉玩家他们错了。
    2. 允许他们输入另一个猜测。
    3. 轮数加 1。
8. 如果出错，并且玩家没有剩余轮次：
    1. 告诉玩家游戏结束。
    2. 阻止玩家继续猜测（这会使游戏混乱）。
    3. 显示控件允许玩家重新开始游戏。
9. 一旦游戏重启，确保游戏的逻辑和 UI 完全重置，然后返回步骤 1。

让我们继续，看看我们如何将这些步骤转换为代码，构建这个示例，从而探索 JavaScript 的特性。

### 初始设置

本教程开始前，请将 number-guessing-game-start.html 文件（查看在线演示）保存下来。同时在文本编辑器和 Web 浏览器中将其打开，可以看到一个简单的标题、一段游戏说明和一个用于输入猜测的表单，此时表单不会执行任何操作。

我们将在 HTML 底部的 `<script>` 元素中添加新的代码：

```html
<script>
 // Your JavaScript goes here
</script>
```

### 添加变量以保存数据

让我们开始吧。首先，在 `<script>` 元素中添加以下代码：

```js
let randomNumber = Math.floor(Math.random() \* 100) + 1;

const guesses = document.querySelector(".guesses");
const lastResult = document.querySelector(".lastResult");
const lowOrHi = document.querySelector(".lowOrHi");

const guessSubmit = document.querySelector(".guessSubmit");
const guessField = document.querySelector(".guessField");

let guessCount = 1;
let resetButton;
```

这段代码设置了存储数据的变量和常量以供程序使用。

变量本质上是值（例如数字或字符串）的名称。你可以使用关键字 `let` 和一个名字来创建变量。

常量也用于对值进行命名，但其不像变量，在创建后讲无法修改这个值。本例中用常量来保存对用户界面元素的引用。界面元素的文字可能会改变，但引用是不变的。你可以使用关键字 `const` 和一个名字来创建常量。

可以使用等号（=）和一个值来为变量或常量赋值。

在我们的示例中：

* 我们用数学算法得出一个 1 到 100 之间的随机数，并赋值给第一个变量（`randomNumber`）。
* 接下来的三个常量均存储着一个引用，分别指向 HTML 结果段落中某个元素（注意它们是如何放置在 `<div>` 元素内的），用于在代码后面段落中插入值：

```html
<div class="resultParas">
  <p class="guesses"></p>
  <p class="lastResult"></p>
  <p class="lowOrHi"></p>
</div>
```

* 接下来的两个常量存储对表单文本输入和提交按钮的引用，并用于控制以后提交猜测：

```html
<label for="guessField">Enter a guess: </label>
<input type="number" id="guessField" class="guessField" />
<input type="submit" value="Submit guess" class="guessSubmit" />
```

* 倒数第二个变量存储一个计数器并初始化为 1（用于跟踪玩家猜测的次数），最后一个变量存储对重置按钮的引用，这个按钮尚不存在（但稍后就有了）。

**备注：** 稍后将讲解更多关于变量和常量的信息。参见下一篇文章。

### 函数（Function）

下面，在之前的代码中添加以下内容：

```js
function checkGuess() {
  alert("I am a placeholder");
}
```

函数是可复用的代码块，可以一次编写，反复运行，从而节省了大量的重复代码。它们真的很有用。定义函数的方法很多，但现在我们先集中考虑当前这个简单的方式。这里我们使用关键字 `function` 、一个函数名、一对小括号定义了一个函数。随后是一对花括号（`{ }`）。花括号内部是调用函数时要运行的所有代码。

要运行一个函数代码时，可以输入函数名加一对小括号。

 让我们尝试一下。保存你的代码并刷新浏览器页面。然后进入
 开发者工具 JavaScript 控制台，并输入以下代码：

```js
checkGuess();
```

在按下 `Return`/`Enter` 之后，你应该会看到一个告警窗口，显示 `I am a placeholder`；我们在代码中定义了一个函数，当我们调用它时，其都会创建一个告警窗口。

**备注：** 后续课程将讲解更多有关函数的知识。

### 运算符（Operator）

JavaScript 运算符允许我们执行比较、做数学运算、连接字符串，以及其他类似的事情。

请保存代码以免丢失，然后刷新浏览器页面，打开 开发者工具 JavaScript 控制台。然后我们就可以尝试下文中的示例了：把下表中“示例”一列中的每一项都原封不动输入进来，每次输入完毕后都按下 `Return`/`Enter` ，可以看到返回的结果。

首先让我们来看看算术运算符，例如：

| 运算符 | 名称 | 示例 |
| --- | --- | --- |
| `+` | 加 | `6 + 9` |
| `-` | 减 | `20 - 15` |
| `*` | 乘 | `3 * 7` |
| `/` | 除 | `10 / 5` |

你也可以使用 `+` 运算符将文本字符串连接在一起（术语“串联”（*concatenation*））。尝试依次输入以下几行：

```js
const name = "Bingo";
name;
const hello = " says hello!";
hello;
const greeting = name + hello;
greeting;
```

还有一些快捷操作符可用，称为复合赋值运算符。例如，如果你只希望在现有字符串末尾添加一个新串，可以这样做：

```js
let name1 = "Bingo";
name1 += " says hello!";
```

这等价于：

```js
let name2 = "Bingo";
name2 = name2 + " says hello!";
```

在执行真/假比较时（例如在条件语句中，见下表），我们使用比较运算符，例如：

| 运算符 | 名称 | 示例 |
| --- | --- | --- |
| `===` | 全等（它们是否完全一样？） |
```js
5 === 2 + 4 // false
'Chris' === 'Bob' // false
5 === 2 + 3 // true
2 === '2' // false；数字与字符串不相等
```

 |
| `!==` | 不相等（它们是否不一样？） |
```js
5 !== 2 + 4 // true
'Chris' !== 'Bob' // true
5 !== 2 + 3 // false
2 !== '2' // true；数字与字符串不相等
```

 |
| `<` | 小于 |
```js
6 < 10 // true
20 < 10 // false
```
 |
| `>` | 大于 |
```js
6 > 10 // false
20 > 10 // true
```
 |

### 条件语句（Conditional）

回到我们的 `checkGuess()` 函数，我们希望它不仅能够给出一个占位符消息，同时还能检查玩家是否猜对，并做出适当的反应。

现在，将当前的 `checkGuess()` 函数替换为此版本：

```js
function checkGuess() {
  const userGuess = Number(guessField.value);
  if (guessCount === 1) {
    guesses.textContent = "Previous guesses: ";
  }
  guesses.textContent += `${userGuess} `;

  if (userGuess === randomNumber) {
    lastResult.textContent = "Congratulations! You got it right!";
    lastResult.style.backgroundColor = "green";
    lowOrHi.textContent = "";
    setGameOver();
  } else if (guessCount === 10) {
    lastResult.textContent = "!!!GAME OVER!!!";
    lowOrHi.textContent = "";
    setGameOver();
  } else {
    lastResult.textContent = "Wrong!";
    lastResult.style.backgroundColor = "red";
    if (userGuess < randomNumber) {
      lowOrHi.textContent = "Last guess was too low!";
    } else if (userGuess > randomNumber) {
      lowOrHi.textContent = "Last guess was too high!";
    }
  }

  guessCount++;
  guessField.value = "";
  guessField.focus();
}
```

呀——好多的代码！让我们来逐段探究。

* 第一行声明了一个名为 `userGuess` 的变量，并将其设置为在文本字段中输入的值。我们还对这个值应用了内置的 `Number()` 方法，只是为了确保该值是一个数字。由于我们没有更改此变量，因此我们使用 `const` 声明。
* 接下来，我们遇到我们的第一个条件代码块。条件代码块让你能够根据某个条件的真假来选择性地运行代码。虽然看起来有点像一个函数，但它不是。条件块的最简单形式是从关键字 `if` 开始，然后是一些括号，然后是一些花括号。括号内包含一个比较。如果比较结果为 `true`，就会执行花括号内的代码。反之，花括号中的代码就会被跳过，从而执行下面的代码。本文的示例中，比较测试的是 `guessCount` 变量是否等于 `1`，即玩家是不是第一次猜数字：

```js
guessCount === 1;
```

如果是，我们让 `guesses` 段落的文本内容等于 `Previous guesses:`。如果不是就不用了。
* 第 6 行将当前 `userGuess` 值附加到 `guesses` 段落的末尾，并加上一个空格，以使每两个猜测值之间有一个空格。
* 下一个代码块中做了几个检查：
    + 第一个 `if(){ }` 检查用户的猜测是否等于在代码顶端设置的 `randomNumber` 值。如果是，则玩家猜对了，游戏胜利，我们将向玩家显示一个漂亮的绿色的祝贺信息，并清除“高了 / 低了”信息框的内容，调用 `setGameOver()` 方法。
    + 紧接着是一个 `else if(){ }` 结构。它会检查这个回合是否是玩家的最后一个回合。如果是，程序将做与前一个程序块相同的事情，只是这次它显示的是 Game Over 而不是祝贺消息。
    + 最后的一个块是 `else { }`，前两个比较都不返回 `true` 时（也就是玩家尚未猜对，但是还有机会）才会执行这里的代码。在这个情况下，我们会告诉玩家他们猜错了，并执行另一个条件测试，判断并告诉玩家猜测的数字是高了还是低了。
* 函数最后三行（26 - 28 行）是为下次猜测值提交做准备的。我们把 `guessCount` 变量的值加 1，以使玩家消耗一次机会（`++` 是自增操作符，为自身加 1），然后我们把表单中文本域的值清空，重新聚焦于此，准备下一轮游戏。
### 事件（Event）

现在，我们有一个实现比较不错的 `checkGuess()` 函数了，但它现在什么事情也做不了，因为我们还没有调用它。理想中，我们希望在点击“Submit guess”按钮时调用它，为此，我们需要使用事件。事件就是浏览器中发生的事儿，比如点击按钮、加载页面、播放视频，等等，我们可以通过调用代码来响应事件。侦听事件发生的结构称为**事件监听器**（Event Listener），响应事件触发而运行的代码块被称为**事件处理器**（Event Handler）。

在 `checkGuess()` 函数后添加以下代码：

```js
guessSubmit.addEventListener("click", checkGuess);
```

这里为 `guessSubmit` 按钮添加了一个事件监听器。`addEventListener()` 方法包含两个可输入值（称为“*参数*”（argument）），监听事件的类型（本例中为 `click`），和当事件发生时我们想要执行的代码（本例中为 `checkGuess()` 函数）。注意，`addEventListener()` 中作为参数的函数名不加括号。

现在，保存代码并刷新页面，示例应该能够工作了，但还不够完善。现在唯一的问题是，如果玩家猜对或游戏次数用完，游戏将出错，因为我们尚未定义游戏结束时应运行的 `setGameOver()` 函数。现在，让我们补全所缺代码，并完善示例功能。

### 补全游戏功能

在代码最后添加一个 `setGameOver()` 函数，然后我们一起来看看它：

```js
function setGameOver() {
  guessField.disabled = true;
  guessSubmit.disabled = true;
  resetButton = document.createElement("button");
  resetButton.textContent = "Start new game";
  document.body.append(resetButton);
  resetButton.addEventListener("click", resetGame);
}
```

* 前两行通过将 `disable` 属性设置为 `true` 来禁用表单文本输入和按钮。这样做是必须的，否则用户就可以在游戏结束后提交更多的猜测，游戏的规则将遭到破坏。
* 接下来的三行创建一个新的 `<button>` 元素，设置它的文本为“Start new game”，并把它添加到当前 HTML 的底部。
* 最后一行在新按钮上设置了一个事件监听器，当它被点击时，一个名为 `resetGame()` 的函数被将被调用。

现在我们需要定义 `resetGame()` 这个函数，依然放到代码底部：

```js
function resetGame() {
  guessCount = 1;

  const resetParas = document.querySelectorAll(".resultParas p");
  for (const resetPara of resetParas) {
    resetPara.textContent = "";
  }

  resetButton.parentNode.removeChild(resetButton);

  guessField.disabled = false;
  guessSubmit.disabled = false;
  guessField.value = "";
  guessField.focus();

  lastResult.style.backgroundColor = "white";

  randomNumber = Math.floor(Math.random() \* 100) + 1;
}
```

这段较长的代码将游戏中的一切重置为初始状态，然后玩家就可以开始新一轮的游戏了。此段代码：

* 将 `guessCount` 重置为 1。
* 清除所有信息段落。这里，我们选择 `<div class="resultParas"></div>` 内的所有段落，然后通过循环迭代，将它们的 `textContent` 设置为 `''`（一个空字符串）。
* 删除重置按钮。
* 启用表单元素，清空文本域并聚焦于此，准备接受新猜测的数字。
* 删除 `lastResult` 段落的背景颜色。
* 生成一个新的随机数，这样就可以猜测新的数字了！

**此刻一个能功能完善的（简易版）游戏就完成了。恭喜！**

我们现在来讨论下其他很重要的代码功能，你可能已经看到过，但是你可能没有意识到这一点。

### 循环（Loop）

上面代码中有一部分需要我们仔细研读，那就是 for...of 循环。循环是一个非常重要的编程概念，它让你能够重复运行一段代码，直到满足某个条件为止。

首先，请再次转到 浏览器开发工具 JavaScript 控制台 然后输入以下内容：

```js
const fruits = ["apples", "bananas", "cherries"];
for (const fruit of fruits) {
  console.log(fruit);
}
```

发生了什么？控制台中打印出了字符串 `'apples'、'bananas'、'cherries'`。

这正是循环所为。`const fruits = ['apples', 'bananas', 'cherries'];` 这一行创建了一个数组。我们在本章稍后的完整的数组指南中会作深入探究。就目前而言，数组是元素（本例中为字符串）的集合。

`for...of` 循环为你提供了一种获取数组中的每一个元素的方法，并在元素的基础上运行 JavaScript 代码。`for (const fruit of fruits)` 这一行的意思是：

1. 获取 `fruits` 中的第一个元素。
2. 将 `fruit` 变量设置为这个元素，然后运行花括号 `{}` 间的代码。
3. 获取 `fruits` 中的下一个元素，然后重复步骤 2，直至到达 `fruits` 的末尾。

现在让我们来看一下猜数字游戏中的循环——`resetGame()` 函数中可以找到以下内容：

```js
const resetParas = document.querySelectorAll(".resultParas p");
for (const resetPara of resetParas) {
  resetPara.textContent = "";
}
```

这段代码通过 `querySelectorAll()` 方法创建了一个包含 `<div class="resultParas">` 内所有段落的变量，然后通过循环迭代，删除每个段落的文本内容。

请注意，即使 `resetParas` 是一个常量，我们也可以更改其内部属性，例如 `textContent`。

### 浅谈对象（Object）

在讨论前最后再改进一波。在 `let resetButton;`（脚本顶端部分）下方添加下面一行内容，然后保存文件：

```js
guessField.focus();
```

这一行通过 `focus()` (en-US) 方法让光标在页面加载完毕时自动放置于 `<input>` 输入框内，这意味着玩家可以马上开始第一次猜测，而无需点击输入框。这只是一个小的改进，却提高了可用性——为使用户能投入游戏提供一个良好的视觉线索。

深入分析一下。JavaScript 中一切都是对象。对象是存储在单个分组中的相关功能的集合。可以创建自己的对象，但这是较高阶的知识，我们今后才会谈及。现在，仅需简要讨论浏览器内置的对象，它们已经能够做许多有用的事情。

在本示例的特定情况下，我们首先创建一个 `guessField` 常量来存储对 HTML 中的文本输入表单域的引用，在文档顶部的声明区域中可以找到以下行：

```js
const guessField = document.querySelector(".guessField");
```

使用 `document` 对象的 `querySelector()` 方法可以获得这个引用。`querySelector()` 需要一个信息——用一个 CSS 选择器 可以选中需要引用的元素。

因为 `guessField` 现在包含一个指向 `<input>` 元素的引用，它现在就能够访问一系列的属性（存储于对象内部的基础变量，其中一些的值无法改变）和方法（存储在对象内部的基础函数）。`focus()` 是 `input` 元素可用方法之一，因此我们可以使用这行代码将光标聚焦于此文本框上︰

```js
guessField.focus();
```

不包含对表单元素引用的变量不提供 `focus()` 方法。例如，引用 `<p>` 元素的 `guesses` 常量，包含一个数字的 `guessCount` 变量。

### 操作浏览器对象

浏览器对象如何使用呢，下面我们来小试牛刀。

1. 首先在浏览器中打开你的程序。
2. 接下来打开浏览器开发者工具，并且切换到 JavaScript 控制台的标签页。
3. 输入 `guessField`，控制台将会显示此变量包含一个 `<input>` 元素。同时控制台还能自动补全运行环境中对象的名字，包括你的变量！
4. 现在输入下面的代码：

```js
guessField.value = 2;
```

`value` 属性表示当前文本区域中输入的值。在输入这条指令后，你将看到文本域区中的文本被我们修改了！
5. 现在试试输入 `guesses` 然后回车。控制台会显示一个包含 `<p>` 元素的变量。
6. 现在试试输入下面这一行：

```js
guesses.value;
```

浏览器会返回 `undefined`，因为段落中并不存在 `value` 属性。
7. 为了改变段落中的文本内容，你需要用 `textContent` 属性来代替 `value`。试试这个：

```js
guesses.textContent = "Where is my paragraph?";
```

8. 下面是一些有趣的东西。尝试依次输入下面几行：

```js
guesses.style.backgroundColor = "yellow";
guesses.style.fontSize = "200%";
guesses.style.padding = "10px";
guesses.style.boxShadow = "3px 3px 6px black";
```

页面上的每个元素都有一个 `style` 属性，它本身包含一个对象，其属性包含应用于该元素的所有内联 CSS 样式。让我们可以使用 JavaScript 在元素上动态设置新的 CSS 样式。
大功告成
----

这个示例已经构建完毕，做得好！来尝试运行一下最终的代码，或者看看我们的最终版本。如果你的版本无法正常工作，请对照源代码进行检查。

* 上一页
* Overview: First steps
* 下一页