实践对象构造 - 学习 Web 开发

实践对象构造
======

* 上一页
* Overview: Objects
* 下一页

在前面的文章中，我们学习了 JavaScript 的面向对象理论和基本的语法知识，为之后的学习建立了良好的基础。这篇文章中我们将进行一次实战演练，通过构造 JavaScript 对象得到生动有趣的成果！

|  |  |
| --- | --- |
| 前提： |
 基本的计算机知识，了解 HTML 与 CSS 的基本概念，熟悉 JavaScript
 基本知识（请参阅
 入门 和
 构建块结构）和面向对象的 JavaScript（OOJS）基础（请参阅
 对象基础）。
  |
| 目标： | 练习使用对象，在真实环境中应用面向对象开发技术。 |

弹跳吧！小彩球！
--------

本文通过编写一个弹球 demo 来展示 JavaScript 中对象的重要性。我们的小球会在屏幕上弹跳，当它们碰到彼此时会变色。最终会像这样：
```

<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8" />
    <style>
 body {
 margin: 0;
 overflow: hidden;
 font-family: "PingFangSC-Regular", "微软雅黑", sans-serif;
 height: 100%;
 }
 h1 {
 font-size: 2rem;
 letter-spacing: -1px;
 position: absolute;
 margin: 0;
 top: -4px;
 right: 5px;
 color: transparent;
 text-shadow: 0 0 4px white;
 }
 </style>
  </head>

  <body>
    <h1>弹球</h1>
    <canvas></canvas>

    <script>
 const canvas = document.querySelector("canvas");
 const ctx = canvas.getContext("2d");

 const width = (canvas.width = window.innerWidth);
 const height = (canvas.height = window.innerHeight);

 function random(min, max) {
 const num = Math.floor(Math.random() \* (max - min)) + min;
 return num;
 }

 function randomColor() {
 const color =
 "rgb(" +
 random(0, 255) +
 "," +
 random(0, 255) +
 "," +
 random(0, 255) +
 ")";
 return color;
 }

 function Ball(x, y, velX, velY, color, size) {
 this.x = x;
 this.y = y;
 this.velX = velX;
 this.velY = velY;
 this.color = color;
 this.size = size;
 }

 Ball.prototype.draw = function () {
 ctx.beginPath();
 ctx.fillStyle = this.color;
 ctx.arc(this.x, this.y, this.size, 0, 2 \* Math.PI);
 ctx.fill();
 };

 Ball.prototype.update = function () {
 if (this.x + this.size >= width) {
 this.velX = -this.velX;
 }

 if (this.x - this.size <= 0) {
 this.velX = -this.velX;
 }

 if (this.y + this.size >= height) {
 this.velY = -this.velY;
 }

 if (this.y - this.size <= 0) {
 this.velY = -this.velY;
 }

 this.x += this.velX;
 this.y += this.velY;
 };

 Ball.prototype.collisionDetect = function () {
 for (let j = 0; j < balls.length; j++) {
 if (this !== balls[j]) {
 const dx = this.x - balls[j].x;
 const dy = this.y - balls[j].y;
 const distance = Math.sqrt(dx \* dx + dy \* dy);

 if (distance < this.size + balls[j].size) {
 balls[j].color = this.color = randomColor();
 }
 }
 }
 };

 let balls = [];

 while (balls.length < 25) {
 const size = random(10, 20);
 let ball = new Ball(
 random(0 + size, width - size),
 random(0 + size, height - size),
 random(-7, 7),
 random(-7, 7),
 randomColor(),
 size,
 );
 balls.push(ball);
 }

 function loop() {
 ctx.fillStyle = "rgba(0,0,0,0.25)";
 ctx.fillRect(0, 0, width, height);

 for (let i = 0; i < balls.length; i++) {
 balls[i].draw();
 balls[i].update();
 balls[i].collisionDetect();
 }

 requestAnimationFrame(loop);
 }

 loop();
 </script>
  </body>
</html>
```

这个实例将会利用 Canvas API 来在屏幕上画小球，还会用到 requestAnimationFrame API 来使整个画面动起来——我们并不要求你事先学习过这些 API 的相关知识，希望你完成这个练习之后会想去探索更多。这个过程中我们会用到一些漂亮的小东西并向你展示一些技巧，比如小球从墙上反弹，检查它们是否撞到了对方（也就是碰撞检测）。

让我们开始吧
------

首先下载 bouncing-balls-start.zip，其中包含以下三个文件：index.html、style.css 和 main.js。它们分别包含以下内容：

1. 一个非常简单的 HTML 文档，包括一个 `<h1>` 元素、一个`<canvas>` 元素来画小球，还有一些元素将 CSS 和 JavaScript 运用到我们的 HTML 中。
2. 一些非常简单的样式，主要是 `<h1>` 元素的样式和定位，另外还能使画面填充整个页面从而摆脱滚动条和边缘的空白（这样看起来非常简洁）
3. 一些 JavaScript 用来设置 `<canvas>` 元素，并提供我们要用到的基本函数。

脚本的第一部分是这样的：

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

const ctx = canvas.getContext("2d");

const width = (canvas.width = window.innerWidth);
const height = (canvas.height = window.innerHeight);
```

这个脚本使用变量代指了 `<canvas>` 元素，然后对其调用 `getContext()` 从而我们获得一个开始画画的环境。存储以上操作结果的变量（`ctx`）是一个对象，直接代指画布上的一块允许我们绘制 2D 图形的区域。

接下来，我们设置 `width` 和 `height` 变量，并且让画布元素的宽和高（分别使用 `canvas.width` 和 `canvas.height` 表示）等于浏览器的宽和高（也就是网页显示的区域 — 可以从 `Window.innerWidth` 和 `Window.innerHeight`参数获得）。

你会看到我们在这里串联了多个赋值表达式在一起，这样能更快地设置变量——这是完全正确的。

原始脚本最后的部分如下：

```js
function random(min, max) {
  return Math.floor(Math.random() \* (max - min)) + min;
}

function randomColor() {
  return (
    "rgb(" +
    random(0, 255) +
    ", " +
    random(0, 255) +
    ", " +
    random(0, 255) +
    ")"
  );
}
```

第一个函数为我们生成一个 `min` 至 `max` 之间的随机整数，第二个函数为我们生成一个随机的颜色值。

为程序中的小球建立模型
-----------

我们的项目中会有很多小球在屏幕上跳来跳去。因此这些小球会以相同的方式运作，从而我们可以通过一个对象实例化它们。首先，我们将下面的构造器加入到代码的底部。

```js
function Ball(x, y, velX, velY, color, size) {
  this.x = x;
  this.y = y;
  this.velX = velX;
  this.velY = velY;
  this.color = color;
  this.size = size;
}
```

这个构造器中定义了每个小球需要的参数：

* `x` 和 `y` 坐标——小球在屏幕上最开始时候的坐标。坐标的范围从 0（左上角）到浏览器视口的宽和高（右下角）。
* 水平和竖直速度（`velX` 和 `velY`）——我们会给每个小球一个水平和竖直速度。实际上，当我们让这些球开始运动时候，每过一帧都会给小球的 `x` 和 `y` 坐标加一次这些值。
* `color`——每一个小球会有自己的颜色。
* `size`——每一个小球会有自己的大小——也就是小球的半径，以像素为单位。

这里说明了小球的属性，那么方法呢？别忘了我们要让小球动起来。

### 画小球

首先给小球的原型加上 `draw()` 方法：

```js
Ball.prototype.draw = function () {
  ctx.beginPath();
  ctx.fillStyle = this.color;
  ctx.arc(this.x, this.y, this.size, 0, 2 \* Math.PI);
  ctx.fill();
};
```

通过使用这个函数，通过使用我们之前定义的 `ctx`对象 的方法，我们就可以让在屏幕上画出小球了。`ctx` 的内容区域就像是一张纸，现在我们就可以命令我们的笔画一点东西。

* 首先，我们使用 `beginPath()` 来声明我们现在要开始在纸上画一个图形了。
* 然后，我们使用 `fillStyle` 来定义这个图形的颜色 — 这个值正是小球的颜色属性。
* 接下来，我们使用 `arc()` 方法来在纸上画出一段圆弧。有这些参数：
    + `x` 和 `y` 是圆弧的中心的坐标——也就是小球的中心坐标。
    + 圆弧的半径——小球的半径。
    + 最后两个参数是开始和结束，也就是圆弧对应的夹角，单位以弧度表示。这里我们用的是 0 和 `2 * PI`，也就是 360 度（如果你设置成 0 和 `1 * PI`，则只会出现一个半圆，也就是 180 度）
* 最后，我们使用 `fill()` 方法，也就是声明我们结束了以 `beginPath()` 开始的绘画，并且使用我们之前设置的颜色进行填充。

现在你已经可以测试你的对象了。

1. 保存代码，将 HTML 加载到浏览器中。
2. 打开浏览器中的 JavaScript 控制台，刷新页面，从而画布可以根据可视的区域调整自己的大小。
3. 通过下面的代码创建一个小球实例。

```js
let testBall = new Ball(50, 100, 4, 4, "blue", 10);
```

4. 你可以调用实例的这些属性。

```js
testBall.x;
testBall.size;
testBall.color;
testBall.draw();
```

5. 当你键入最后一行的时候，你会在你的画布上看到一个小球被画出来了。
### 更新小球的数据

我们可以在一个固定位置画出小球，但是他们不会动，我们需要一个函数来更新一些东西。在 JavaScript 文件底部加上下面的代码，也就是给小球原型加上一个 update() 方法。

```js
Ball.prototype.update = function () {
  if (this.x + this.size >= width) {
    this.velX = -this.velX;
  }

  if (this.x - this.size <= 0) {
    this.velX = -this.velX;
  }

  if (this.y + this.size >= height) {
    this.velY = -this.velY;
  }

  if (this.y - this.size <= 0) {
    this.velY = -this.velY;
  }

  this.x += this.velX;
  this.y += this.velY;
};
```

函数的前四个部分用来检查小球是否碰到画布的边缘。如果碰到，我们反转小球的速度方向来让它向反方向移动。就比如说，如果小球正向上移动（正 `velY`），然后垂直速度发生改变，小球就向下移动（负 `velY`）。

在这四部分中，我们：

* 检查小球的 `x` 坐标是否大于画布的宽度（小球会从右边缘离开）。
* 检查小球的 `x` 坐标是否小于 0（小球会从左边缘离开）。
* 检查小球的 `y` 坐标是否大于画布的高度（小球会从下边缘离开）。
* 检查小球的 `y` 坐标是否小于 0（小球会从上边缘离开）。

在每种情况下，我们都会加上小球的半径，因为 `x`/`y` 坐标是小球中心的坐标，我们希望小球在其边界接触浏览器窗口的边界时反弹，而不是小球的一部分都不见了再返回。

最后两行，我们将 `velX` 的值加到 `x` 的坐标上，将 `velY` 的值加到 `y` 坐标上——每次调用这个方法的时候小球就移动这么多。

暂时先这样做；让我们继续做一些动画！

让球动起来
-----

现在就变得非常有趣了。我们在画布上加上一些小球，并且让他们动起来。

1. 首先我们需要一个地方储存小球，下面的数组会干这件事——现在将它添加到你的代码底部：

```js
let balls = [];

while (balls.length < 25) {
  let size = random(10, 20);
  let ball = new Ball(
    // 为避免绘制错误，球至少离画布边缘球本身一倍宽度的距离
    random(0 + size, width - size),
    random(0 + size, height - size),
    random(-7, 7),
    random(-7, 7),
    randomColor(),
    size,
  );
  balls.push(ball);
}
```

2. 几乎所有的动画效果都会用到一个运动循环，也就是每一帧都自动更新视图。这是大多数游戏或者其他类似项目的基础。
3. 现在将它添加到你的代码底部：

```js
function loop() {
  ctx.fillStyle = "rgba(0, 0, 0, 0.25)";
  ctx.fillRect(0, 0, width, height);

  for (let i = 0; i < balls.length; i++) {
    balls[i].draw();
    balls[i].update();
  }

  requestAnimationFrame(loop);
}
```

`loop()` 函数做了下面的事情：
    * 将整个画布的颜色设置成半透明的黑色。然后使用 `fillRect()`（这四个参数分别是起始的坐标、绘制的矩形的宽和高）画出一个填充满整个画布的矩形。这是在下一个视图画出来时用来遮住之前的视图的。如果不这样做得话，你就会在屏幕上看到一条蛇的形状而不是小球的运动了。用来填充的颜色设置成半透明的`rgba(0,0,0,0.25)`，也就是让之前的视图留下来一点点，从而你可以看到小球运动时的轨迹。如果你将 0.25 设置成 1 时，你就完全看不到了。试着改变其中的值查看造成的影响。
    * 当且仅当小球数量小于 25 时，将 `random()` 函数产生的数字传入新的小球实例从而创建一个新的小球，并且加入到数组中。因此当屏幕上有 25 个小球时，不会再出现更多小球。你可以改变这个值，从而看到不同小球个数造成的影响。如果你的电脑或者浏览器性能不怎么样的话，几千个小球的速度就会明显慢下来。
    * 遍历数组中的所有小球，并且让每个小球都调用 `draw()` 和 `update()` 函数来将自己画出来，并且再接下来的每一帧都按照其速度进行位置的更新。
    * 使用 `requestAnimationFrame()` 方法再运行一次函数——当一个函数正在运行时传递相同的函数名，从而每隔一小段时间都会运行一次这个函数，这样我们可以得到一个平滑的动画效果。这主要是通过递归完成的——也就是说函数每次运行的时候都会调用自己，从而可以一遍又一遍得运行。
4. 最后但是非常重要的是，加上下面这一行——让动画开始运行的话我们需要调用这个函数。

```js
loop();
```

完成这些基础的之后在浏览器打开测试一下！

添加碰撞检测
------

现在会更加有趣，给我们的项目加上碰撞检测，从而小球会知道他们正在撞击其他的球。

1. 首先在 `update()` 方法后添加以下方法（即 `Ball.prototype.update` 的下面）。

```js
Ball.prototype.collisionDetect = function () {
  for (let j = 0; j < balls.length; j++) {
    if (this !== balls[j]) {
      const dx = this.x - balls[j].x;
      const dy = this.y - balls[j].y;
      const distance = Math.sqrt(dx \* dx + dy \* dy);

      if (distance < this.size + balls[j].size) {
        balls[j].color = this.color = randomColor();
      }
    }
  }
};
```

这个方法有一点点复杂，如果不理解的话不必过分担心，下面是对它的解释：
    * 对于每个小球，我们都要检查其他的小球是否和当前这个小球相撞了。为了达到此目的，我们构造另外一个 `for` 循环来遍历 `balls[]` 数组中的小球。
    * 在循环里面，我们使用一个 `if` 语句来检查遍历的小球是否是当前的小球。我们不希望检测到一个小球撞到了自己！为了达到这个目的，我们需要检查当前小球 (即正在调用 `collisionDetect` 方法的球) 是否和被循环到的小球 (`for` 循环检测中的当前遍历所引用的球) 是不是同一个。我们使用 `!` 来否定判断，因此只有两个小球**不是**同一个时，条件判断中的代码才会运行。
    * 我们使用了一个常见的算法来检测两个小球是否相撞了，两个小球中心的距离是否小于两个小球的半径之和。这些会在 2D 碰撞检测 介绍地更加详细。
    * 如果检测到了碰撞，会运行 `if` 语句中的代码。我们会将两个小球的颜色都设置成随机的一种。我们也可以将这步操作变得复杂一点，比如让两个小球弹开，那样需要植入更加复杂的代码。像这样的物理场景，有以下专门的库比如 PhysicsJS，matter.js，Phaser 等。
2. 我们也需要在每一帧动画中都调用这个函数，因此在 `balls[i].update()` 加上下面的代码：

```js
balls[i].collisionDetect();
```

3. 保存文件，刷新浏览器，你就会看到小球在撞击时会变色！

**备注：** 如果示例无法顺利执行，可参考我们的 最终版本，或者 在线试用。

概要
--

我们希望你玩得开心，编写出你自己的随机弹跳球的例子，在整个程序中使用各种对象和面向对象的技术！在你实际运用对象中能提供一些有用的帮助。

对象文章就到这里了。现在剩下的就是在下一节的对象评估中测试你的技能。

参见
--

* Canvas tutorial——2D canvas 初学者指南。
* requestAnimationFrame()
* 2D 碰撞检测
* 3D 碰撞检测
* 纯 JavaScript 编写的 2D 消除游戏——一个很好的 2D 游戏开发初学者教程。
* Phaser 编写的 2D 消除游戏——JavaScript 游戏库构建 2D 游戏的基础知识。
* 上一页
* Overview: Objects
* 下一页


技能测试：面向对象的 Javascript - 学习 Web 开发

技能测试：面向对象的 Javascript
=====================

这个测试的目的是为了评估你是否已经理解了我们的 JavaScript 中的类这一文章。

**备注：** 你可以尝试在下方的交互式编辑器，但是若你下载源码并使用在线工具（例如 CodePen、jsFiddle 或 Glitch）来尝试这些任务，可能会更有帮助。

如果你遇到了困难，请联系我们以寻求帮助——见本页面底部的评估或进一步帮助。

**备注：** 在下方的示例中，如果你的代码中存在错误内容，这些错误将在页面的结果面板中显示，以此来帮助你想出解决方案（若是下载的版本，请进入浏览器的 JavaScript 控制台）。

面向对象的 Javascript 1
------------------

在这个任务中，我们为你提供了 `Shape` 类的初始定义。它有三个属性：`name`、`sides` 和 `sideLength`。这个类只模拟了所有边长都相同的形状，例如：正方形或等边三角形。

我们希望你：

* 向这个类添加构造函数。构造函数接受 `name`、`sides` 和 `sideLength` 属性的参数，并对它们进行初始化。
* 向这个类添加一个新的 `calcPerimeter()` 方法，用于计算它的周长（形状外边缘的长度），并将结果记录到控制台中。
* 创建 `Shape` 类的一个新的实例，名为 `square`。将其 `name` 属性值设为 `square`，`sides` 属性值设为 `4`，`sideLength` 属性值设为 `5`。
* 调用 `calcPerimeter()` 方法，以查看它是否按预期将计算结果记录到浏览器的控制台中。
* 创建 `Shape` 类的一个新的实例，名为 `triangle`。将其 `name` 属性值设为 `triangle`，`sides` 属性值设为 `3`，`sideLength` 属性值设为 `3`。
* 调用 `triangle.calcPerimeter()`，以查看它是否正常工作。

尝试更新下面的实时代码，以重现完成的示例：

下载此任务的初始代码，以便在你自己的编辑器或在线编辑器中尝试。

面向对象的 Javascript 2
------------------

接下来，我们希望你创建一个继承自 `Shape` 的 `Square` 类，并添加一个 `calcArea()` 方法，用于计算正方形的面积。同时，设置构造函数，以便 `Square` 对象实例的 `name` 属性自动设置为 `square`，`sides` 属性自动设置为 `4`。因此，在调用构造函数时，你只需要提供 `sideLength` 属性。

创建一个使用适当属性值的 `Square` 类的实例，名为 `square`，并调用它的 `calcPerimeter()` 和 `calcArea()` 方法，以表明其是否正常工作。

尝试更新下面的实时代码，以重现完成的示例：

下载此任务的初始代码，以便在你自己的编辑器或在线编辑器中尝试。

评估或进一步帮助
--------

你可以在上面的交互式编辑器中练习这些示例。

如果你希望对自己的工作成果进行打分评估，或者遇到了困难并希望寻求帮助：

1. 将你完成的内容放到在线、可分享的编辑器，如：CodePen、jsFiddle 或 Glitch。你可以自行编写代码，或使用上面小节中链接的初始文件。
2. 在 MDN Discourse forum Learning category 中发贴寻求打分评估或帮助。你的贴子需要包含：
    * 一个描述的标题，如：“Assessment wanted for OOJS 1 skill test”。
    * 你已经尝试过的详细内容，以及你希望我们能够做什么，例如：你遇到了困难，想要寻求帮助；或希望得到打分评估。
    * 一个指向用于打分评估或寻求帮助的示例代码的链接（使用上文中提到的任意一种在线的、可分享的编辑器）。这是一种很好的形式——如果别人没法看到你的代码，他们很难对你提供代码上的帮助。
    * 一个指向实际任务或评估页面的链接，以便我们找到你需要得到帮助的问题。


面向对象编程基本概念 - 学习 Web 开发

面向对象编程基本概念
==========

* 上一页
* Overview: Objects
* 下一页

面向对象编程（OOP）是如今多种编程语言所实现的一种编程范式，包括 Java、C++。在本文中，我们会简要介绍面向对象编程（OOP）的基本概念，其中包括三个主要概念：**类与实例**、**继承**、**封装**。现在，我们会脱离特定的 JavaScript 代码来探讨这些概念，所有提及到的例子将会以伪代码的形式描述。

**备注：** 准确地说，本文所提及到的特性是一种特别的面向对象编程方式，即**基于类的面向对象编程**（class-based OOP）。当人们谈论面向对象编程时，通常来说是指基于类的面向对象编程。

在本文的最后，我们会探讨 JavaScript 中的构造函数和原型链是如何与面向对象编程中的概念产生联系的，以及它们与面向对象编程中的概念又有何不同。在下一篇文章中，我们会学习 JavaScript 中一些附加的特性，这些特性使得实现面向对象编程变得更加容易。

|  |  |
| --- | --- |
| 前提： |
 理解 JavaScript 函数，熟悉 JavaScript 基础
 （参见
 JavaScript 第一步和创建 JavaScript 代码块），熟悉对象基础概念（参见
 JavaScript 对象基础和对象原型）。
  |
| 目标： | 理解基于类的面向对象编程中的基础概念。 |

面向对象编程将一个系统抽象为许多对象的集合，每一个对象代表了这个系统的特定方面。对象包括函数（方法）和数据。一个对象可以向其他部分的代码提供一个公共接口，而其他部分的代码可以通过公共接口执行该对象的特定操作，系统的其他部分不需要关心对象内部是如何完成任务的，这样保持了对象自己内部状态的私有性。

类与实例
----

当我们使用面向对象编程的术语对一个问题进行建模时，我们会创建一系列抽象的定义，这些定义代表了系统中存在的各类对象。例如，如果我们要对一个学校进行建模，我们可能会建立许多用于代表教授的对象，所有教授通常都存在以下属性：教授们都有一个名字；都有一门他们各自所教的课程。此外，所有教授都可以做一些特定的事情，包括：他们可以为学生的论文打分；他们可以在学年的开始向学生介绍自己。

因此，教授可以成为系统中的 `Professor` **类**。`Professor` 类的定义包括了所有教授都拥有的数据（属性，property）与行为（方法，method）。

`Professor` 类用伪代码描述如下：
```

class Professor
    properties
        name
        teaches
    methods
        grade(paper)
        introduceSelf()
```

`Professor` 类的定义包括如下内容：

* 两个属性：姓名 `name` 和所教的课程 `teaches`
* 两个方法：`grade()` 方法用于为学生的论文打分；`introduceSelf()` 方法用于介绍自己。

就其本身而言，类并不做任何事情，类只是一种用于创建具体对象的模板。`Professor` 类可以创建一个具体的教授，我们称这样创建出来的具体教授为 `Professor` 类的**实例**。由类创建实例的过程是由一个特别的函数——构造函数所完成的。开发人员将类所需要的值传入构造函数，构造函数即可根据传入的值初始化实例的内部状态。

通常来说，需要将构造函数作为类定义的一部分明确声明，并且构造函数通常具有和类名相同的函数名。
```

class Professor
    properties
        name
        teaches
    constructor
        Professor(name, teaches)
    methods
        grade(paper)
        introduceSelf()
```

在这个例子中，构造函数需要两个参数，因此，我们可以在创建新实例时初始化实例的 `name` 属性和 `teaches` 属性。

当我们定义构造函数后，我们就可以创建出具体的教授了。编程语言通常使用 `new` 关键字来表示执行构造函数。

```js
walsh = new Professor("沃尔什", "心理学");
lillian = new Professor("丽莲", "诗歌");

walsh.teaches; // '心理学'
walsh.introduceSelf(); // '我是沃尔什，我是你们的心理学老师。'

lillian.teaches; // '丽莲'
lillian.introduceSelf(); // '我是丽莲，我是你们的诗歌老师'
```

这段代码中我们创建了两个对象，这两个对象都是 `Professor` 类的实例。

继承
--

假设在我们的学校中，还需要定义一个新的类来代表学生。与教授不同，学生不能为他们自己的作业打分，也不需要教授任何课程，他们的特点是每一个学生都属于一个特定的年级。

然而，学生同样具有一个名字，并且他们可能也想介绍他们自己，因此，我们可能会将学生类的定义写成：
```

class Student
    properties
        name
        year
    constructor
        Student(name, year)
    methods
        introduceSelf()
```

如果我们可以用某种特别的方式共享教授和学生中相同属性的声明，那么这会节省我们不少的精力。更准确的说，在某种层级上，二者实际上是*同种事物*，他们能够具有相同的属性也是合理的。继承（Inheritance）可以帮助我们完成这一操作。

很容易注意到教授和学生都是人，而人是具有姓名，并且可以介绍自己的。我们可以将人定义为一个新类，即 `Person` 类，在 `Person` 类中，我们可以定义所有作为人的通用属性。接下来，我们可以定义 `Professor` 类和 `Student` 类由 `Person` 类**派生**（derive）而来，在伪代码中定义如下：
```

class Person
    properties
        name
    constructor
        Person(name)
    methods
        introduceSelf()

class Professor : extends Person
    properties
        teaches
    constructor
        Professor(name, teaches)
    methods
        grade(paper)
        introduceSelf()

class Student : extends Person
    properties
        year
    constructor
        Student(name, year)
    methods
        introduceSelf()
```

在这种情况下，我们称 `Person` 类是 `Professor` 类和 `Student` 类的**超类**（superclass）或**父类**（parent class）。反之，我们称 `Professor` 类和 `Student` 类是 `Person` 类的**子类**（subclass 或 child class）。

你可能注意到了我们在三个类中都定义了 `introduceSelf()` 方法。这么做的原因如下：尽管所有人都想要介绍他们自己，但是他们可能会以不同的方式去做这件事。

```js
walsh = new Professor("沃尔什", "心理学");
walsh.introduceSelf(); // '我是沃尔什，我是你们的心理学老师。'

summers = new Student("萨摩斯", 1);
summers.introduceSelf(); // '我是萨摩斯，我是一年级的学生。'
```

我们可能会为那些不是教授或学生的人设定一个默认的打招呼方式：

```js
pratt = new Person("普拉特");
pratt.introduceSelf(); // '我是普拉特。'
```

当一个方法拥有相同的函数名，但是在不同的类中可以具有不同的实现时，我们称这一特性为**多态**（polymorphism）。当一个方法在子类中替换了父类中的实现时，我们称之为子类**重写/重载**（override）了父类中的实现。

封装
--

当其他部分的代码想要执行对象的某些操作时，可以借助对象向外部提供的接口完成操作，借此，对象保持了自身的内部状态不会被外部代码随意修改。也就是说，对象的内部状态保持了私有性，而外部代码只能通过对象所提供的接口访问和修改对象的内部状态，不能直接访问和修改对象的内部状态。保持对象内部状态的私有性、明确划分对象的公共接口和内部状态，这些特性称之为**封装**（encapsulation）。

封装的好处在于，当程序员需要修改一个对象的某个操作时，程序员只需要修改对象对应方法的内部实现即可，而不需要在所有代码中找出该方法的所有实现，并逐一修改。某种意义上来说，封装在对象内部和对象外部设立了一种特别的“防火墙”。

例如，假设学生只有在二年级以后才能学习弓箭课，我们可以将学生的 `year` 属性暴露给外部，从而外部代码可以通过检查学生的 `year` 属性来确认该学生是否可以选修该课程。

```js
if (student.year > 1) {
  // 允许学生选修弓箭课
}
```

问题在于，如果我们决定修改选修弓箭课的标准（例如需要家长的同意），我们需要在选课系统的代码中修改每一个相关的部分，这是相当麻烦的，并且这一过程中程序员很可能出错。现在，如果我们向 `Student` 类中添加一个 `canStudyArchery()` 方法（用于检查学生是否能够选修弓箭课），那么相应代码的实现逻辑就会集中在一个地方：
```

class Student : extends Person
    properties
       year
    constructor
        Student(name, year)
    methods
       introduceSelf()
       canStudyArchery() { return this.year > 1 }
```

```js
if (student.canStudyArchery()) {
  // 允许学生选修弓箭课课
}
```

这样一来，如果我们要修改选修弓箭课的规则，我们只需要更新 `Student` 类中的相应方法即可，而其他地方的代码无需修改，整个系统仍旧可以正常工作。

在许多面向对象编程语言中，我们可以使用 `private` 关键字标记对象的私有部分，也就是外部代码无法直接访问的部分。如果一个属性在被标记为 `private` 的情况下，外部代码依旧尝试访问该属性，那么通常来说，计算机会抛出一个错误。
```

class Student : extends Person
    properties
       private year
    constructor
        Student(name, year)
    methods
       introduceSelf()
       canStudyArchery() { return this.year > 1 }

student = new Student('Weber', 1)
student.year // 错误：'year'是学生类的私有属性
```

也有部分语言并不采用强制措施阻止外部代码访问对象的私有属性，在这种情况下，程序员们通常会采用一些约定俗称的命名方式来标记对象的私有部分，例如将以下划线开头的变量名看作是对象的私有部分。

面向对象编程与 JavaScript
------------------

在本文中，我们所描述的这些基本特性都属于基于类的面向对象编程方式，像是 Java、C++ 这些编程语言都实现了这些特性。

在先前的两篇文章中，我们探讨了 JavaScript 中的一对核心特性：构造函数和原型。毫无疑问，这些特性完全可以描述面向对象编程的这些概念。

* **构造函数**：在 JavaScript 中，构造函数可以实现类的定义，帮助我们在一个地方描述类的“形状”，包括定义类的方法。不过，原型也可以用于实现类的定义。例如，如果一个方法定义于构造函数的 `prototype` 属性中，那么所有由该构造函数创造出来的对象都可以通过原型使用该方法，而我们也不再需要将它定义在构造函数中。
* **原型链**：原型链很自然地实现了继承特性。例如，如果我们由 `Person` 原型构造了一个 `Student` 类，那么我们可以继承 `Person` 类的 `name` 属性，重写 `introduceSelf()` 方法。

理解 JavaScript 的这一对特性与基于类的面向对象编程之间有什么不同，这一点也是十分重要的，这里我们将简要探讨二者的区别。

首先，在基于类的面向对象编程中，类与对象是两个不同的概念，对象通常是由类创造出来的实例。由此，定义类的方式（定义类的语法）和实例化对象的方式（构造函数）也是不同的。而在 JavaScript 中，我们经常会使用函数或对象字面量创建对象，也就是说，JavaScript 可以在没有特定的类定义的情况下创建对象。相对于基于类的面向对象编程来说，这种方式更为轻量，帮助我们更为方便地使用对象。

其次，尽管原型链看起来很像是继承的层级结构，并且在某些方面，原型链的行为与继承的行为也很类似，但是在其他方面，二者之间仍然存在区别。在继承方式下，当一个子类完成继承时，由该子类所创建的对象既具有其子类中单独定义的属性，又具有其父类中定义的属性（以及父类的父类，依此类推）。而在原型链中，每一个层级都代表了一个不同的对象，不同的对象之间通过 `__proto__` 属性链接起来。原型链的行为并不太像是继承，而更像是**委派**（delegation）。委派同样是对象中的一种编程模式。当我们要求对象执行某项任务时，在委派模式下，对象可以自己执行该项任务，或者要求另一个对象（委派的对象）以其自己的方式执行这项任务。在许多方面，相对于继承来说，委派可以更为灵活地在许多对象之间建立联系（例如，委派模式可以在程序运行时改变、甚至完全替换委派对象）。

尽管如此，构造函数和原型仍然可以在 JavaScript 中实现基于类的面向对象编程特性。但是直接使用构造函数和原型去实现这些特性（例如继承）仍是棘手的，因此，JavaScript 提供了一些额外的特性，这些特性在原型这一模型之上又抽象出一层模型，将基于类的面向对象编程中的概念映射到原型中，从而能够更为直接地在 JavaScript 中使用基于类的面向对象编程中的概念。这些额外的特性将是下一篇文章的主题。

总结
--

本文探讨了基于类的面向对象编程中的基本特性，并且简要对比了 JavaScript 中的构造函数与原型和这些概念的联系与区别。

在下一篇文章中，我们会关注 JavaScript 所提供的其他关于类的特性，这些特性用于支持基于类的面向对象编程。

* 上一页
* Overview: Objects
* 下一页


异步 JavaScript 简介 - 学习 Web 开发

异步 JavaScript 简介
================

* Overview: Asynchronous
* 下一页

在本文中，我们将解释什么是异步编程，为什么我们需要它，并简要讨论 JavaScript 历史上异步函数是怎样被实现的。

|  |  |
| --- | --- |
| 前提： | 基本的计算机素养，以及对 JavaScript 基础知识的一定了解，包括函数和事件处理程序。 |
| 目标： | 熟悉异步 JavaScript 的概念，了解它与同步 JavaScript 的不同，以及我们需要它的原因。 |

异步编程技术使你的程序可以在执行一个可能长期运行的任务的同时继续对其他事件做出反应而不必等待任务完成。与此同时，你的程序也将在任务完成后显示结果。

浏览器提供的许多功能（尤其是最有趣的那一部分）可能需要很长的时间来完成，因此需要异步完成，例如：

* 使用 `fetch()` 发起 HTTP 请求
* 使用 `getUserMedia()` 访问用户的摄像头和麦克风
* 使用 `showOpenFilePicker()` 请求用户选择文件以供访问

因此，即使你可能不需要经常*实现*自己的异步函数，你也很可能需要*正确使用*它们。

在这篇文章中，我们将从同步函数长时间运行时存在的问题开始，并以此进一步认识异步编程的必要性。

同步编程
----

观察下面的代码：

```js
const name = "Miriam";
const greeting = `Hello, my name is ${name}!`;
console.log(greeting);
// "Hello, my name is Miriam!"
```

这段代码：

1. 声明了一个叫做 `name` 的字符串常量
2. 声明了另一个叫做 `greeting` 的字符串常量（并使用了 `name` 常量的值）
3. 将 `greeting` 常量输出到 JavaScript 控制台中。

我们应该注意的是，实际上浏览器是按照我们书写代码的顺序一行一行地执行程序的。浏览器会等待代码的解析和工作，在上一行完成后才会执行下一行。这样做是很有必要的，因为每一行新的代码都是建立在前面代码的基础之上的。

这也使得它成为一个**同步程序**。

事实上，调用函数的时候也是同步的，就像这样：

```js
function makeGreeting(name) {
  return `Hello, my name is ${name}!`;
}
const name = "Miriam";
const greeting = makeGreeting(name);
console.log(greeting);
// "Hello, my name is Miriam!"
```

在这里 `makeGreeting()` 就是一个**同步函数**，因为在函数返回之前，调用者必须等待函数完成其工作。

### 一个耗时的同步函数

如果同步函数需要很长的时间怎么办？

当用户点击“生成素数”按钮时，这个程序将使用一种非常低效的算法生成一些大素数。你可以控制要生成的素数数量，这也会影响操作需要的时间。

```html
<label for="quota">素数个数：</label>
<input type="text" id="quota" name="quota" value="1000000" />

<button id="generate">生成素数</button>
<button id="reload">重载</button>

<div id="output"></div>
```

```js
function generatePrimes(quota) {
  function isPrime(n) {
    for (let c = 2; c <= Math.sqrt(n); ++c) {
      if (n % c === 0) {
        return false;
      }
    }
    return true;
  }
  const primes = [];
  const maximum = 1000000;
  while (primes.length < quota) {
    const candidate = Math.floor(Math.random() \* (maximum + 1));
    if (isPrime(candidate)) {
      primes.push(candidate);
    }
  }
  return primes;
}
document.querySelector("#generate").addEventListener("click", () => {
  const quota = document.querySelector("#quota").value;
  const primes = generatePrimes(quota);
  document.querySelector("#output").textContent =
    `完成！已生成素数${quota}个。`;
});
document.querySelector("#reload").addEventListener("click", () => {
  document.location.reload();
});
```

试着点击“生成素数”按钮。在程序显示“完成！”信息之前可能需要几秒钟（取决于你的电脑性能）。

### 耗时同步函数的问题

接下来的示例和上一个一样，不过我们增加了一个文本框供你输入。这一次，试着点击“生成素数”，然后在文本框中输入。

你会发现，当我们的 `generatePrimes()` 函数运行时，我们的程序完全没有反应：用户不能输入任何东西，也不能点击任何东西，或做任何其他事情。
```

<label for="quota">素数个数</label>
<input type="text" id="quota" name="quota" value="1000000" />
<button id="generate">生成素数</button>
<button id="reload">重载</button>
<textarea id="user-input" rows="5" cols="62">
点击“生成素数”按钮后试着在这里输入
</textarea>
<div id="output"></div>
```

```
textarea {
  display: block;
  margin: 1rem 0;
}
```

```
function generatePrimes(quota) {
  function isPrime(n) {
    for (let c = 2; c <= Math.sqrt(n); ++c) {
      if (n % c === 0) {
        return false;
      }
    }
    return true;
  }
  const primes = [];
  const maximum = 1000000;
  while (primes.length < quota) {
    const candidate = Math.floor(Math.random() \* (maximum + 1));
    if (isPrime(candidate)) {
      primes.push(candidate);
    }
  }
  return primes;
}
document.querySelector("#generate").addEventListener("click", () => {
  const quota = document.querySelector("#quota").value;
  const primes = generatePrimes(quota);
  document.querySelector("#output").textContent =
    `完成！已生成素数${quota}个。`;
});
document.querySelector("#reload").addEventListener("click", () => {
  document.querySelector("#user-input").value =
    "点击“生成素数”按钮后试着在这里输入";
  document.location.reload();
});
```

这就是耗时的同步函数的基本问题。在这里我们想要的是一种方法，以让我们的程序可以：

* 通过调用一个函数来启动一个长期运行的操作
* 让函数开始操作并立即返回，这样我们的程序就可以保持对其他事件做出反应的能力
* 当操作最终完成时，通知我们操作的结果。

这就是异步函数为我们提供的能力，本模块的其余部分将解释它们是如何在 JavaScript 中实现的。

事件处理程序
------

我们刚才看到的对异步函数的描述可能会让你想起事件处理程序，这么想是对的。事件处理程序实际上就是异步编程的一种形式：你提供的函数（事件处理程序）将在事件发生时被调用（而不是立即被调用）。如果“事件”是“异步操作已经完成”，那么你就可以看到事件如何被用来通知调用者异步函数调用的结果的。

一些早期的异步 API 正是以这种方式来使用事件的。`XMLHttpRequest` API 可以让你用 JavaScript 向远程服务器发起 HTTP 请求。由于这样的操作可能需要很长的时间，所以它被设计成异步 API，你可以通过给 `XMLHttpRequest` 对象附加事件监听器来让程序在请求进展和最终完成时获得通知。

下面的例子展示了这样的操作。点击“点击发起请求”按钮来发送一个请求。我们将创建一个新的 `XMLHttpRequest` 并监听它的 `loadend` 事件。而我们的事件处理程序则会在控制台中输出一个“完成！”的消息和请求的状态代码。

我们在添加了事件监听器后发送请求。注意，在这之后，我们仍然可以在控制台中输出“请求已发起”，也就是说，我们的程序可以在请求进行的同时继续运行，而我们的事件处理程序将在请求完成时被调用。

```html
<button id="xhr">点击发起请求</button>
<button id="reload">重载</button>

<pre readonly class="event-log"></pre>
```

```
pre {
  display: block;
  margin: 1rem 0;
}
```

```js
const log = document.querySelector(".event-log");
document.querySelector("#xhr").addEventListener("click", () => {
  log.textContent = "";
  const xhr = new XMLHttpRequest();
  xhr.addEventListener("loadend", () => {
    log.textContent = `${log.textContent}完成！状态码：${xhr.status}`;
  });
  xhr.open(
    "GET",
    "https://raw.githubusercontent.com/mdn/content/main/files/en-us/\_wikihistory.json",
  );
  xhr.send();
  log.textContent = `${log.textContent}请求已发起\n`;
});
document.querySelector("#reload").addEventListener("click", () => {
  log.textContent = "";
  document.location.reload();
});
```

这就像我们在以前的模块中遇到的事件处理程序，只是这次的事件不是像点击按钮那样的用户行为，而是某个对象的状态变化。

回调
--

事件处理程序是一种特殊类型的回调函数。而回调函数则是一个被传递到另一个函数中的会在适当的时候被调用的函数。正如我们刚刚所看到的：回调函数曾经是 JavaScript 中实现异步函数的主要方式。

然而，当回调函数本身需要调用其他同样接受回调函数的函数时，基于回调的代码会变得难以理解。当你需要执行一些分解成一系列异步函数的操作时，这将变得十分常见。例如下面这种情况：

```js
function doStep1(init) {
  return init + 1;
}
function doStep2(init) {
  return init + 2;
}
function doStep3(init) {
  return init + 3;
}
function doOperation() {
  let result = 0;
  result = doStep1(result);
  result = doStep2(result);
  result = doStep3(result);
  console.log(`结果：${result}`);
}
doOperation();
```

现在我们有一个被分成三步的操作，每一步都依赖于上一步。在这个例子中，第一步给输入的数据加 1，第二步加 2，第三步加 3。从输入 0 开始，最终结果是 6（0+1+2+3）。作为同步代码，这很容易理解。但是如果我们用回调来实现这些步骤呢？

```js
function doStep1(init, callback) {
  const result = init + 1;
  callback(result);
}
function doStep2(init, callback) {
  const result = init + 2;
  callback(result);
}
function doStep3(init, callback) {
  const result = init + 3;
  callback(result);
}
function doOperation() {
  doStep1(0, (result1) => {
    doStep2(result1, (result2) => {
      doStep3(result2, (result3) => {
        console.log(`结果：${result3}`);
      });
    });
  });
}
doOperation();
```

因为必须在回调函数中调用回调函数，我们就得到了这个深度嵌套的 `doOperation()` 函数，这就更难阅读和调试了。在一些地方这被称为“回调地狱”或“厄运金字塔”（因为缩进看起来像一个金字塔的侧面）。

面对这样的嵌套回调，处理错误也会变得非常困难：你必须在“金字塔”的每一级处理错误，而不是在最高一级一次完成错误处理。

由于以上这些原因，大多数现代异步 API 都不使用回调。事实上，JavaScript 中异步编程的基础是 `Promise`，这也是我们下一篇文章要讲述的主题。

* Overview: Asynchronous
* 下一页


workers 简介 - 学习 Web 开发

workers 简介
==========

* 上一页
* Overview: Asynchronous
* 下一页

在"异步 JavaScript" 模块的最后一篇文章中，我们将介绍 workers，它使你能够在单独执行 线程 中运行一些任务。

|  |  |
| --- | --- |
| 前置条件： | 基于计算机知识，对 JavaScript 基础有一个合理的了解，包括事件处理。 |
| 目标： | 了解如何使用 web workers。 |

在本模块的第一篇文章中，我们看到了当在你的程序中具有一个长期运行的的同步任务时发生了什么 ── 整个窗口变得完全没有响应。从根本上讲，出现这种情况的原因是程序是单线程的。一个线程是程序遵循的一系列指令。因为程序由一个线程组成，它在同一时间只能做一件事情：所以如果它正在等待我们的长期运行的同步调用返回，它就不能做其他任何事情。

Workers 给了你在不同线程中运行某些任务的能力，因此你可以启动任务，然后继续其他的处理（例如处理用户操作）。

但是这是要付出代价的。对于多线程代码，你永远不知道你的线程什么时候将会被挂起，其他线程将会得到运行的机会。因此，如果两个线程都可以访问相同的变量，那么变量就有可能在任何时候发生意外的变化，这将导致很难发现的 Bug。

为了避免 Web 中的这些问题，你的主代码和你的 worker 代码永远不能直接访问彼此的变量。Workers 和主代码运行在完全分离的环境中，只有通过相互发送消息来进行交互。特别是，这意味着 workers 不能访问 DOM（窗口、文档、页面元素等等）。

有三种不同类型的 workers：

* dedicated workers
* shared workers
* service workers

在本文中，我们将介绍第一类 workers 的一个例子，然后简要的讨论另外两类。

使用 web workers
--------------

还记得在第一篇文章中，我们有一个计算质数的页面吗？我们将使用一个 worker 来运行质数运算，因此我们的页面对用户操作保持响应。

### 同步的质数生成器

让我们先看一下我们上一个例子中的 JavaScript：

```js
function generatePrimes(quota) {
  function isPrime(n) {
    for (let c = 2; c <= Math.sqrt(n); ++c) {
      if (n % c === 0) {
        return false;
      }
    }
    return true;
  }

  const primes = [];
  const maximum = 1000000;

  while (primes.length < quota) {
    const candidate = Math.floor(Math.random() \* (maximum + 1));
    if (isPrime(candidate)) {
      primes.push(candidate);
    }
  }

  return primes;
}

document.querySelector("#generate").addEventListener("click", () => {
  const quota = document.querySelector("#quota").value;
  const primes = generatePrimes(quota);
  document.querySelector("#output").textContent =
    `Finished generating ${quota} primes!`;
});

document.querySelector("#reload").addEventListener("click", () => {
  document.querySelector("#user-input").value =
    'Try typing in here immediately after pressing "Generate primes"';
  document.location.reload();
});
```

在这个程序中，在我们调用 `generatePrimes()` 之后，程序变得完全没有响应。

### 用 worker 进行质数生成

在这个例子中，首先在 https://github.com/mdn/learning-area/blob/main/javascript/asynchronous/workers/start 将文件拷贝到本地。在这个目录下有四个文件：

* index.html
* style.css
* main.js
* generate.js

"index.html" 文件和 "style.css" 文件已完成：

```html
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <script type="text/javascript" src="main.js" defer></script>
    <link href="style.css" rel="stylesheet" />
  </head>

  <body>
    <label for="quota">Number of primes:</label>
    <input type="text" id="quota" name="quota" value="1000000" />

    <button id="generate">Generate primes</button>
    <button id="reload">Reload</button>

    <textarea id="user-input" rows="5" cols="62">
Try typing in here immediately after pressing "Generate primes"</textarea
 >

    <div id="output"></div>
  </body>
</html>
```

```css
textarea {
  display: block;
  margin: 1rem 0;
}
```

"main.js" 和 "generate.js" 文件是空的。我们将向 "main.js" 中添加主代码，向 "generate.js" 中添加 worker 代码。

首先，我们可以看到 worker 代码被保存在一个与主代码隔离的脚本中。我们还可以看到，在上面的 "index.html" 中，只有主代码被包含在 `<script>` 标签中。

现在将下面的代码拷贝到 "main.js"中：

```js
// 在 "generate.js" 中创建一个新的 worker
const worker = new Worker("./generate.js");

// 当用户点击 "Generate primes" 时，给 worker 发送一条消息。
// 消息中的 command 属性是 "generate", 还包含另外一个属性 "quota"，即要生成的质数。
document.querySelector("#generate").addEventListener("click", () => {
  const quota = document.querySelector("#quota").value;
  worker.postMessage({
    command: "generate",
    quota: quota,
  });
});

// 当 worker 给主线程回发一条消息时，为用户更新 output 框，包含生成的质数（从 message 中获取）。
worker.addEventListener("message", (message) => {
  document.querySelector("#output").textContent =
    `Finished generating ${message.data} primes!`;
});

document.querySelector("#reload").addEventListener("click", () => {
  document.querySelector("#user-input").value =
    'Try typing in here immediately after pressing "Generate primes"';
  document.location.reload();
});
```

* 首先，我们使用 `Worker()` 构造函数创建 worker。我们传递一个指向 worker 脚本的 URL。只要 worker 被创建了，worker 脚本就会执行。
* 其次，与同步版本一样，我们向 "Generate primes" 按钮添加一个 `click` 事件处理器。但是现在，我们不再调用 `generatePrimes()` 函数，而是使用 `worker.postMessage()` 向 worker 发送一条消息。这条消息可以携带一个参数，在本示例中我们传递一个包含两个属性的 JSON 对象：
    + `command`：一个用于标识我们希望 worker 所做事情的字符串（以防我们的 worker 可以做多个事情）。
    + `quota`：要生成的质数的数量。
* 然后，我们向 worker 添加一个 `message` 消息处理器。这样 worker 就能告诉我们它是什么时候完成的，并且传递给我们任何结果数据。我们的处理器从消息的 `data` 属性获取数据，然后将其写入 output 元素（数据与 `quota` 是完全相同的，这虽然没有意义，但是这展示了其中原理）。
* 最后，我们为 "Reload" 按钮实现了 `click` 事件处理器。这与同步版本完全相同。

现在到 worker 代码了。拷贝下面的代码到 "generate.js" 中：

```js
// 监听主线程中的消息。
// 如果消息中的 command 是 "generate"，则调用 `generatePrimse()`
addEventListener("message", (message) => {
  if (message.data.command === "generate") {
    generatePrimes(message.data.quota);
  }
});

// 生成质数 (非常低效)
function generatePrimes(quota) {
  function isPrime(n) {
    for (let c = 2; c <= Math.sqrt(n); ++c) {
      if (n % c === 0) {
        return false;
      }
    }
    return true;
  }

  const primes = [];
  const maximum = 1000000;

  while (primes.length < quota) {
    const candidate = Math.floor(Math.random() \* (maximum + 1));
    if (isPrime(candidate)) {
      primes.push(candidate);
    }
  }

  // 完成后给主线程发送一条包含我们生成的质数数量的消息消息。
  postMessage(primes.length);
}
```

请记住，只要主脚本创建 worker，这些代码就会运行。

worker 要做的第一件事情就是开始监听来自主脚本的消息。这通过使用 `addEventListener()` 实现，它在 worker 中是一个全局函数。在 `message` 事件处理器内部，事件的 `data` 属性包含一个来自主脚本的参数的副本。如果主脚本传递 `generate` 命令，我们就调用 `generatePrimes()`，传入来自消息事件的 `quota` 值。

`generatePrimes()` 函数与同步版本类似，只不过我们在完成后向主脚本发送一条消息，而不是返回一个值。我们对此使用 `postMessage()` (en-US) 函数，就像在 worker 中 `addEventListener`是全局函数一样。如我们所见，主脚本正在监听这条消息并且将会在收到消息后更新 DOM。

**备注：** 要运行此站点，你必须运行一个本地 web 服务器，因为 file:// URLs 不允许加载 workers。参考我们的设置一个本地测试服务器的指导。完成后，你应该可以点击 "Generate primes" 并且使你的主页面保持响应。
 如果你在创建和运行这个样例的过程中有疑问，你可以在 https://github.com/mdn/learning-area/blob/main/javascript/asynchronous/workers/finished 查看完成后的版本，并且在 https://mdn.github.io/learning-area/javascript/asynchronous/workers/finished 进行在线尝试。

其他类型的 worker
------------

我们刚刚创建的 worker 被称为 *dedicated worker*。这意味着它由一个脚本实例使用。

不过，还有其他类型的 worker：

* `SharedWorker` 可以由运行在不同窗口中的多个不同脚本共享。
* *Service worker* 的行为就像代理服务器，缓存资源以便于 web 应用程序可以在用户离线时工作。他们是渐进式 Web 应用的关键组件。
总结
--

在本文中，我们介绍了 web workers，它使得 web 应用能够离线加载任务到单独的线程中。主线程和 worker 不直接共享任何变量，但是可以通过发送消息来进行通信，这些消息作为 `message` 事件被对方接受。

Workers 尽管不能访问主应用程序能访问的所有 API，尤其是不能访问 DOM，但是可以作为使主应用程序保持响应的一个有效的方式。

参见
--

* 使用 web workers
* 使用 service workers
* Web workers API
* 上一页
* Overview: Asynchronous
* 下一页


如何实现基于 Promise 的 API - 学习 Web 开发

如何实现基于 Promise 的 API
====================

* 上一页
* Overview: Asynchronous
* 下一页

在上一篇文章中，我们讨论了如何使用返回 promises 的 APIs。在本文中，我们将研究另一方面--如何实现返回 promises 的 APIs。跟使用基于 promise 的 APIs 相比，这是一个不太常见的任务，但它仍然值得了解。

|  |  |
| --- | --- |
| 前置条件： | 基于计算机知识，对 JavaScript 基础有一个合理的了解，包括事件处理和 promises 基础。 |
| 目标： | 了解如何实现基于 promise 的 APIs。 |

通常情况下，当你实现一个基于 promise 的 API 时，你会使用事件、普通回调，或者消息传递模型来包裹一个异步操作。你将会使用一个 `Promise` 对象来合理的处理操作的成功或者失败。

实现 alarm() API
--------------

在这个示例中我们将会实现一个基于 promise 的 alarm API，叫做 `alarm()` 。它将以被唤醒人的名字和一个在人被唤醒前以毫秒为单位的延迟作为参数。在延迟之后，本函数将会发送一个包含需要被唤醒人名字的 "Wake up!" 消息。

### 用 setTimeout() 包裹

我们将会使用 `setTimeout()` 来实现 `alarm()` 函数。`setTimeout()` 以一个回调函数和一个以毫秒为单位的延迟作为参数。当调用 `setTimeout()` 时，它将启动一个设置为给定延迟的计时器，当时间过期时，它就会调用给定的回调函数。

在下面的例子中，我们使用一个回调函数和一个 1000 毫秒的延迟调用 `setTimeout()`：

```html
<button id="set-alarm">Set alarm</button>
<div id="output"></div>
```

```
div {
  margin: 0.5rem 0;
}
```

```js
const output = document.querySelector("#output");
const button = document.querySelector("#set-alarm");

function setAlarm() {
  window.setTimeout(() => {
    output.textContent = "Wake up!";
  }, 1000);
}

button.addEventListener("click", setAlarm);
```

### Promise() 构造器

我们的 `alarm()` 函数返回一个在定时器过期时才会被兑现的 `Promise`。它将会传递一个 "Wake up!" 消息到 `then()` 处理器中，也会在当调用者提供一个负延迟值时拒绝这个 promise。

这里的关键组件是 `Promise()` 构造器。`Promise()` 构造器使用单个函数作为参数。我们把这个函数称作`执行器`（executor）。当你创建一个新的 promise 的时候你需要实现这个执行器。

这个执行器本身采用两个参数，这两个参数都是函数，通常被称作 `resolve` 和 `reject`。在你的执行器实现里，你调用原始的异步函数。如果异步函数成功了，就调用 `resolve`，如果失败了，就调用 `reject`。如果执行器函数抛出了一个错误，`reject` 会被自动调用。你可以将任何类型的单个参数传递到 `resolve` 和 `reject` 中。

所以我们可以像下面这样实现 `alarm()`：

```js
function alarm(person, delay) {
  return new Promise((resolve, reject) => {
    if (delay < 0) {
      throw new Error("Alarm delay must not be negative");
    }
    window.setTimeout(() => {
      resolve(`Wake up, ${person}!`);
    }, delay);
  });
}
```

此函数创建并且返回一个新的 `Promise`。对于执行器中的 promise，我们：

* 检查 `delay`（延迟）是否为负数，如果是的话就抛出一个错误。
* 调用 `window.setTimeout()`，传递一个回调函数和 `delay`（延迟）。当计时器过期时回调会被调用，在回调函数内，我们调用了 `resolve`，并且传递了 `"Wake up!"` 消息。
使用 alarm() API
--------------

这一部分同上一篇文章是相当相似的。我们可以调用 `alarm()`，在返回的 promise 中调用 `then()` 和 `catch()` 来设置 promise 兑现和拒绝状态的处理器。
```

<div>
  <label for="name">Name:</label>
  <input type="text" id="name" name="name" size="4" value="Matilda" />
</div>
<div>
  <label for="delay">Delay:</label>
  <input type="text" id="delay" name="delay" size="4" value="1000" />
</div>
<button id="set-alarm">Set alarm</button>
<div id="output"></div>
```

```
button {
  display: block;
}

div,
button {
  margin: 0.5rem 0;
}
```

```js
const name = document.querySelector("#name");
const delay = document.querySelector("#delay");
const button = document.querySelector("#set-alarm");
const output = document.querySelector("#output");

function alarm(person, delay) {
  return new Promise((resolve, reject) => {
    if (delay < 0) {
      throw new Error("Alarm delay must not be negative");
    }
    window.setTimeout(() => {
      resolve(`Wake up, ${person}!`);
    }, delay);
  });
}

button.addEventListener("click", () => {
  alarm(name.value, delay.value)
    .then((message) => (output.textContent = message))
    .catch((error) => (output.textContent = `Couldn't set alarm: ${error}`));
});
```

尝试对 "Name" 和 "Delay" 设置不同的值。尝试为 "Delay" 设置一个负值。

在 alarm() API 上使用 async 和 await
-------------------------------

自从 `alarm()` 返回了一个 `Promise`，我们可以对它做任何我们可以对其他任何 promise 做的事情：`Promise.all()`，和 `async` / `await`：
```

<div>
  <label for="name">Name:</label>
  <input type="text" id="name" name="name" size="4" value="Matilda" />
</div>
<div>
  <label for="delay">Delay:</label>
  <input type="text" id="delay" name="delay" size="4" value="1000" />
</div>
<button id="set-alarm">Set alarm</button>
<div id="output"></div>
```

```
button {
  display: block;
}

div,
button {
  margin: 0.5rem 0;
}
```

```js
const name = document.querySelector("#name");
const delay = document.querySelector("#delay");
const button = document.querySelector("#set-alarm");
const output = document.querySelector("#output");

function alarm(person, delay) {
  return new Promise((resolve, reject) => {
    if (delay < 0) {
      throw new Error("Alarm delay must not be negative");
    }
    window.setTimeout(() => {
      resolve(`Wake up, ${person}!`);
    }, delay);
  });
}

button.addEventListener("click", async () => {
  try {
    const message = await alarm(name.value, delay.value);
    output.textContent = message;
  } catch (error) {
    output.textContent = `Couldn't set alarm: ${error}`;
  }
});
```

参见
--

* Promise() 构造器
* 使用 Promises
* 上一页
* Overview: Asynchronous
* 下一页


序列动画 - 学习 Web 开发

序列动画
====

* 上一页
* Overview: Asynchronous

在本测验中，你将使用我们在如何使用 Promise 中学习的一些技术来更新页面，使它能够按顺序播放一系列动画。

|  |  |
| --- | --- |
| 前置条件： | 基本的计算机知识，对 JavaScript 基础知识的一定了解，能够使用基于 Promise 的 API。 |
| 目标： | 测试使用基于 Promise 的 API 的能力。 |

起点
--

创建一份 https://github.com/mdn/learning-area/tree/main/javascript/asynchronous/sequencing-animations/start 的本地副本。它包含下列文件：

* alice.svg
* index.html
* main.js
* style.css

你需要编辑的唯一一个文件是“main.js”。

如果你在浏览器中打开“index.html”，你会看到三张图片呈对角排列：

![序列动画测验页面的屏幕截图](/zh-CN/docs/Learn/JavaScript/Asynchronous/Sequencing_animations/sequencing-animations.png)

这些图像取自我们的 Using the Web Animations API 指南。

项目简介
----

我们希望更新此页面，来将动画依次应用于所有三个图像。也就是说，当第一个动画完成时，我们开始第二个动画，当第二个完成时，我们开始第三个动画。

要应用的动画已经在“main.js”中被定义：效果是旋转图像并缩小它至消失。

请查看完成的示例来更好地了解页面的预期工作方式。注意：动画只会执行一次，你需要重新加载页面来重新运行它们。

完成步骤
----

### 设置第一个图像的动画

我们使用 Web Animations API 对图像进行动画处理，特别是 `element.animate()` 方法。

更新“main.js”以添加对 `alice1.animate()` 的调用，就像这样：

```js
const aliceTumbling = [
  { transform: "rotate(0) scale(1)" },
  { transform: "rotate(360deg) scale(0)" },
];

const aliceTiming = {
  duration: 2000,
  iterations: 1,
  fill: "forwards",
};

const alice1 = document.querySelector("#alice1");
const alice2 = document.querySelector("#alice2");
const alice3 = document.querySelector("#alice3");

alice1.animate(aliceTumbling, aliceTiming);
```

重载页面，你应该就会看到第一个图像旋转收缩的动画。

### 设置所有图像的动画

接下来，我们希望在 `alice1` 完成时启动 `alice2` 的动画，在 `alice2` 完成时启动 `alice3` 的动画。

`animate()` 方法返回 `Animation` 对象。这个对象有一个 `finished` 属性，这是会在一个在动画播放结束时兑现（fulfilled）的 `Promise`。所以我们可以利用这个 Promise 获得开始下一个动画的时机。

我们希望你尝试一些不同的方式来实现这个功能，以便于加强对使用 Promise 的不同方式的理解。

1. 首先，实现一个能够工作的代码，但它存在“回调地狱”问题（我们在关于回调的讨论中提到过）的 Promise 版本。
2. 接下来，使用 Promise 链来实现它。注意：可以用箭头函数的不同形式来编写这个函数。尝试这些不同的形式。哪个最简洁？哪个可读性最好？
3. 使用 `async` 和 `await` 来实现它。

别忘了，`element.animate()` 并不返回一个 `Promise`：它返回一个 `Animation` 对象，该对象具有一个 `finished` 属性，这个属性才是 `Promise`。

评估或进一步的帮助
---------

如果你希望自己的作品被评估，或者遇到困难想寻求帮助：

1. 将作品放入在线共享编辑器中，如 CodePen，jsFiddle，或者 Glitch。
2. 在 MDN 论坛学习分类中写一篇文章寻求评估和/或帮助。你的文章应该包括：
    * 一个描述性的标题，如“请帮助评估测序动画”。
    * 详细说明你已经尝试过的方法，以及你希望我们做的事情，例如：你遇到困难需要帮助，或者你完成了作品，需要评估。
    * 一个指向你的作品的链接（托管在在线共享编辑器中，如步骤 1 所述），这是一个很好的实践ーー如果你看不到他们的代码，就很难帮助有编码问题的人。
    * 指向实际任务或评估页面的链接，这样我们就可以找到你需要帮助的问题。
* 上一页
* Overview: Asynchronous


如何使用 Promise - 学习 Web 开发

如何使用 Promise
============

* 上一页
* Overview: Asynchronous
* 下一页

**Promise** 是现代 JavaScript 中异步编程的基础，是一个由异步函数返回的可以向我们指示当前操作所处的状态的对象。在 Promise 返回给调用者的时候，操作往往还没有完成，但 Promise 对象可以让我们操作最终完成时对其进行处理（无论成功还是失败）。

|  |  |
| --- | --- |
| 前提： | 基本的计算机素养，以及对 JavaScript 基础知识的一定了解，包括函数和事件处理程序。 |
| 目标： | 理解 Promise 在 JavaScript 中是怎样被使用的 |

在上一篇文章中，我们谈到使用回调实现异步函数的方法。在这种设计中，我们需要在调用异步函数的同时传入回调函数。这个异步函数会立即返回，并在操作完成后调用传入的回调。

在基于 Promise 的 API 中，异步函数会启动操作并返回 `Promise` 对象。然后，你可以将处理函数附加到 Promise 对象上，当操作完成时（成功或失败），这些处理函数将被执行。

使用 fetch() API
--------------

**备注：** 在这篇文章中，我们将通过复制页面上的代码示例到浏览器的 JavaScript 控制台中运行的方式来学习 Promise。因此在正式开始学习之前你需要进行以下设置：

1. 在浏览器的新标签页中访问https://example.org。
2. 在该标签页中，打开浏览器开发者工具中的 JavaScript 控制台
3. 把我们展示的代码示例复制到控制台中运行。值得注意的是，你必须在每次输入新的示例之前重新加载页面，否则控制台会报错“重新定义了 `fetchPromise`”。
 在这个例子中，我们将从https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json下载 JSON 文件，并记录一些相关信息。

要做到这一点，我们将向服务器发出一个 **HTTP 请求**。在 HTTP 请求中，我们向远程服务器发送一个请求信息，然后它向我们发送一个响应。这次，我们将发送一个请求，从服务器上获得一个 JSON 文件。还记得在上一篇文章中，我们使用 `XMLHttpRequest` API 进行 HTTP 请求吗？那么，在这篇文章中，我们将使用 `fetch()` API，一个现代的、基于 Promise 的、用于替代 `XMLHttpRequest` 的方法。

把下列代码复制到你的浏览器 JavaScript 控制台中：

```js
const fetchPromise = fetch(
  "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);

console.log(fetchPromise);

fetchPromise.then((response) => {
  console.log(`已收到响应：${response.status}`);
});

console.log("已发送请求……");
```

我们在这里：

1. 调用 `fetch()` API，并将返回值赋给 `fetchPromise` 变量。
2. 紧接着，输出 `fetchPromise` 变量，输出结果应该像这样：`Promise { <state>: "pending" }`。这告诉我们有一个 `Promise` 对象，它有一个 `state`属性，值是 `"pending"`。`"pending"` 状态意味着操作仍在进行中。
3. 将一个处理函数传递给 Promise 的 **`then()`** 方法。当（如果）获取操作成功时，Promise 将调用我们的处理函数，传入一个包含服务器的响应的 `Response` 对象。
4. 输出一条信息，说明我们已经发送了这个请求。

完整的输出结果应该是这样的：
```

Promise { <state>: "pending" }
已发送请求……
已收到响应：200
```

请注意，`已发送请求……` 的消息在我们收到响应之前就被输出了。与同步函数不同，`fetch()` 在请求仍在进行时返回，这使我们的程序能够保持响应性。响应显示了 `200`（OK）的状态码，意味着我们的请求成功了。

可能这看起来很像上一篇文章中的例子中我们把事件处理程序添加到 `XMLHttpRequest` 对象中。但不同的是，我们这一次将处理程序传递到返回的 Promise 对象的 `then()` 方法中。

链式使用 Promise
------------

在你通过 `fetch()` API 得到一个 `Response` 对象的时候，你需要调用另一个函数来获取响应数据。这次，我们想获得 JSON 格式的响应数据，所以我们会调用 `Response` 对象的 `json()` 方法。事实上，`json()` 也是异步的，因此我们必须连续调用两个异步函数。

试试这个：

```js
const fetchPromise = fetch(
  "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);

fetchPromise.then((response) => {
  const jsonPromise = response.json();
  jsonPromise.then((json) => {
    console.log(json[0].name);
  });
});
```

在这个示例中，就像我们之前做的那样，我们给 `fetch()` 返回的 Promise 对象添加了一个 `then()` 处理程序。但这次我们的处理程序调用 `response.json()` 方法，然后将一个新的 `then()` 处理程序传递到 `response.json()` 返回的 Promise 中。

执行代码后应该会输出“baked beans”（“products.json”中第一个产品的名称）。

等等！还记得上一篇文章吗？我们好像说过，在回调中调用另一个回调会出现多层嵌套的情况？我们是不是还说过，这种“回调地狱”使我们的代码难以理解？这不是也一样吗，只不过变成了用 `then()` 调用而已？

当然如此。但 Promise 的优雅之处在于 *`then()` 本身也会返回一个 Promise，这个 Promise 将指示 `then()` 中调用的异步函数的完成状态*。这意味着我们可以（当然也应该）把上面的代码改写成这样：

```js
const fetchPromise = fetch(
  "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);

fetchPromise
  .then((response) => {
    return response.json();
  })
  .then((json) => {
    console.log(json[0].name);
  });
```

不必在第一个 `then()` 的处理程序中调用第二个 `then()`，我们可以直接*返回* `json()` 返回的 Promise，并在该返回值上调用第二个 "then()"。这被称为 **Promise 链**，意味着当我们需要连续进行异步函数调用时，我们就可以避免不断嵌套带来的缩进增加。

在进入下一步之前，还有一件事要补充：我们需要在尝试读取请求之前检查服务器是否接受并处理了该请求。我们将通过检查响应中的状态码来做到这一点，如果状态码不是“OK”，就抛出一个错误：

```js
const fetchPromise = fetch(
  "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);

fetchPromise
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    return response.json();
  })
  .then((json) => {
    console.log(json[0].name);
  });
```

错误捕获
----

这给我们带来了最后一个问题：我们如何处理错误？`fetch()` API 可能因为很多原因抛出错误（例如，没有网络连接或 URL 本身存在问题），我们也会在服务器返回错误消息时抛出一个错误。

在上一篇文章中，我们看到在嵌套回调中进行错误处理非常困难，我们需要在每一个嵌套层中单独捕获错误。

`Promise` 对象提供了一个 `catch()` 方法来支持错误处理。这很像 `then()`：你调用它并传入一个处理函数。然后，当异步操作*成功*时，传递给 `then()` 的处理函数被调用，而当异步操作*失败*时，传递给 `catch()` 的处理函数被调用。

如果将 `catch()` 添加到 Promise 链的末尾，它就可以在任何异步函数失败时被调用。于是，我们就可以将一个操作实现为几个连续的异步函数调用，并在一个地方处理所有错误。

试试这个版本的 `fetch()` 代码。我们使用 `catch()` 添加了一个错误处理函数，并修改了 URL（这样请求就会失败）。

```js
const fetchPromise = fetch(
  "bad-scheme://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);

fetchPromise
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP 请求错误：${response.status}`);
    }
    return response.json();
  })
  .then((json) => {
    console.log(json[0].name);
  })
  .catch((error) => {
    console.error(`无法获取产品列表：${error}`);
  });
```

尝试运行这个版本：你应该会看到 `catch()` 处理函数输出的错误。

Promise 术语
----------

Promise 中有一些具体的术语值得我们弄清楚。

首先，Promise 有三种状态：

* **待定（pending）**：初始状态，既没有被兑现，也没有被拒绝。这是调用 `fetch()` 返回 Promise 时的状态，此时请求还在进行中。
* **已兑现（fulfilled）**：意味着操作成功完成。当 Promise 完成时，它的 `then()` 处理函数被调用。
* **已拒绝（rejected）**：意味着操作失败。当一个 Promise 失败时，它的 `catch()` 处理函数被调用。

注意，这里的“成功”或“失败”的含义取决于所使用的 API：例如，`fetch()` 认为服务器返回一个错误（如404 Not Found）时请求成功，但如果网络错误阻止请求被发送，则认为请求失败。

有时我们用 **已敲定（settled）** 这个词来同时表示 **已兑现（fulfilled）** 和 **已拒绝（rejected）** 两种情况。

如果一个 Promise 处于已决议（resolved）状态，或者它被“锁定”以跟随另一个 Promise 的状态，那么它就是 **已兑现（fulfilled）**。

文章 Let's talk about how to talk about promises 对这些术语的细节做了很好的解释。

合并使用多个 Promise
--------------

当你的操作由几个异步函数组成，而且你需要在开始下一个函数之前完成之前每一个函数时，你需要的就是 Promise 链。但是在其他的一些情况下，你可能需要合并多个异步函数的调用，`Promise` API 为解决这一问题提供了帮助。

有时你需要所有的 Promise 都得到实现，但它们并不相互依赖。在这种情况下，将它们一起启动然后在它们全部被兑现后得到通知会更有效率。这里需要 `Promise.all()` 方法。它接收一个 Promise 数组，并返回一个单一的 Promise。

由`Promise.all()`返回的 Promise：

* 当且仅当数组中*所有*的 Promise 都被兑现时，才会通知 `then()` 处理函数并提供一个包含所有响应的数组，数组中响应的顺序与被传入 `all()` 的 Promise 的顺序相同。
* 会被拒绝——如果数组中有*任何一个* Promise 被拒绝。此时，`catch()` 处理函数被调用，并提供被拒绝的 Promise 所抛出的错误。

譬如：

```js
const fetchPromise1 = fetch(
  "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
const fetchPromise2 = fetch(
  "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found",
);
const fetchPromise3 = fetch(
  "https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json",
);

Promise.all([fetchPromise1, fetchPromise2, fetchPromise3])
  .then((responses) => {
    for (const response of responses) {
      console.log(`${response.url}：${response.status}`);
    }
  })
  .catch((error) => {
    console.error(`获取失败：${error}`);
  });
```

这里我们向三个不同的 URL 发出三个 `fetch()` 请求。如果它们都被兑现了，我们将输出每个请求的响应状态。如果其中任何一个被拒绝了，我们将输出失败的情况。

根据我们提供的 URL，应该所有的请求都会被兑现，尽管因为第二个请求中请求的文件不存在，服务器将返回 `404`（Not Found）而不是 `200`（OK）。所以输出应该是：
```

https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json：200
https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found：404
https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json：200
```

如果我们用一个错误编码的 URL 尝试同样的代码，就像这样：

```js
const fetchPromise1 = fetch(
  "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
const fetchPromise2 = fetch(
  "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found",
);
const fetchPromise3 = fetch(
  "bad-scheme://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json",
);

Promise.all([fetchPromise1, fetchPromise2, fetchPromise3])
  .then((responses) => {
    for (const response of responses) {
      console.log(`${response.url}：${response.status}`);
    }
  })
  .catch((error) => {
    console.error(`获取失败：${error}`);
  });
```

……然后 `catch()` 处理程序将被运行，我们应该看到像这样的输出：
```

获取失败：TypeError: Failed to fetch
```

有时，你可能需要等待一组 Promise 中的某一个 Promise 的执行，而不关心是哪一个。在这种情况下，你需要 `Promise.any()`。这就像 `Promise.all()`，不过在 Promise 数组中的任何一个被兑现时它就会被兑现，如果所有的 Promise 都被拒绝，它也会被拒绝。

```js
const fetchPromise1 = fetch(
  "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
const fetchPromise2 = fetch(
  "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found",
);
const fetchPromise3 = fetch(
  "https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json",
);

Promise.any([fetchPromise1, fetchPromise2, fetchPromise3])
  .then((response) => {
    console.log(`${response.url}：${response.status}`);
  })
  .catch((error) => {
    console.error(`获取失败：${error}`);
  });
```

值得注意的是，在这种情况下，我们无法预测哪个获取请求会先被兑现。

这两个用于组合多个承诺的函数只是额外的 `Promise` 函数中的两个。要了解其余的内容，参见 `Promise` 参考文档。

async 和 await
-------------

`async` 关键字为你提供了一种更简单的方法来处理基于异步 Promise 的代码。在一个函数的开头添加 `async`，就可以使其成为一个异步函数。

```js
async function myFunction() {
  // 这是一个异步函数
}
```

在异步函数中，你可以在调用一个返回 Promise 的函数之前使用 `await` 关键字。这使得代码在该点上等待，直到 Promise 被完成，这时 Promise 的响应被当作返回值，或者被拒绝的响应被作为错误抛出。

这使你能够编写像同步代码一样的异步函数。例如，我们可以用它来重写我们的 fetch 示例。

```js
async function fetchProducts() {
  try {
    // 在这一行之后，我们的函数将等待 `fetch()` 调用完成
    // 调用 `fetch()` 将返回一个“响应”或抛出一个错误
    const response = await fetch(
      "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
    );
    if (!response.ok) {
      throw new Error(`HTTP 请求错误：${response.status}`);
    }
    // 在这一行之后，我们的函数将等待 `response.json()` 的调用完成
    // `response.json()` 调用将返回 JSON 对象或抛出一个错误
    const json = await response.json();
    console.log(json[0].name);
  } catch (error) {
    console.error(`无法获取产品列表：${error}`);
  }
}

fetchProducts();
```

这里我们调用 `await fetch()`，我们的调用者得到的并不是 `Promise`，而是一个完整的 `Response` 对象，就好像 `fetch()` 是一个同步函数一样。

我们甚至可以使用 `try...catch` 块来处理错误，就像我们在写同步代码时一样。

但请注意，这个写法只在异步函数中起作用。异步函数总是返回一个 Pomise，所以你不能做这样的事情：

```js
async function fetchProducts() {
  try {
    const response = await fetch(
      "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
    );
    if (!response.ok) {
      throw new Error(`HTTP 请求错误：${response.status}`);
    }
    const json = await response.json();
    return json;
  } catch (error) {
    console.error(`无法获取产品列表：${error}`);
  }
}

const json = fetchProducts();
console.log(json[0].name); // json 是一个 Promise 对象，因此这句代码无法正常工作
```

相反，你需要做一些事情，比如：

```js
async function fetchProducts() {
  try {
    const response = await fetch(
      "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
    );
    if (!response.ok) {
      throw new Error(`HTTP 请求错误：${response.status}`);
    }
    const json = await response.json();
    return json;
  } catch (error) {
    console.error(`无法获取产品列表：${error}`);
  }
}

const jsonPromise = fetchProducts();
jsonPromise.then((json) => console.log(json[0].name));
```

你可能会在需要使用 Promise 链地方使用 `async` 函数，这也使得 Promise 的工作更加直观。

请记住，就像一个 Promise 链一样，`await` 强制异步操作以串联的方式完成。如果下一个操作的结果取决于上一个操作的结果，这是必要的，但如果不是这样，像 `Promise.all()` 这样的操作会有更好的性能。

小结
--

Promise 是现代 JavaScript 异步编程的基础。它避免了深度嵌套回调，使表达和理解异步操作序列变得更加容易，并且它们还支持一种类似于同步编程中 `try...catch` 语句的错误处理方式。

`async` 和 `await` 关键字使得从一系列连续的异步函数调用中建立一个操作变得更加容易，避免了创建显式 Promise 链，并允许你像编写同步代码那样编写异步代码。

Promise 在所有现代浏览器的最新版本中都可以使用；唯一会出现支持问题的地方是 Opera Mini 和 IE11 及更早的版本。

在这篇文章中，我们没有涉及到所有的 Promise 功能，只是介绍了最有趣和最有用的那一部分。随着你开始学习更多关于 Promise 的知识，你会遇到更多有趣的特性。

许多现代 Web API 是基于 Promise 的，包括 WebRTC、Web Audio API、媒体捕捉与媒体流等等。

参见
--

* `Promise()`
* 使用 Promise
* We have a problem with promises by Nolan Lawson
* Let's talk about how to talk about promises
* 上一页
* Overview: Asynchronous
* 下一页


图片库 - 学习 Web 开发

图片库
===

* 上一页
* Overview: Building blocks

我们已经学习了 JavaScript 基础的块结构，下面我们通过编写一个常见的基于 JavaScript 的图片库来测验一下你对于循环、函数、条件和事件的掌握情况。

|  |  |
| --- | --- |
| 前提： | 请读完本章其他所有小节的内容后再开始这个测验。 |
| 目标： | 测试你对 JavaScript 的循环、函数、条件语句和事件处理的掌握程度。 |

起点
--

下载压缩包并在本地解压。

另外，你还可以使用类似 JSBin 或 Glitch）这些在线编辑器来完成测验。你可以将 HTML、CSS 和 JavaScript 粘贴到这几个在线编辑器中。如果你使用的在线编辑器没有单独的 JavaScript/CSS 面板，请将它们放入 HTML 页面内联的 `<script>`/`<style>` 元素中。

**备注：** 如果你遇到了困难，请联系我们以寻求帮助——见本页面底部的评估或进一步帮助。

项目简介
----

我们提供了一些 HTML、CSS、照片和几行 JavaScript 代码。需要你来编写必要的 JavaScript 代码让这个项目运行起来。HTML 的 body 部分如下：

```html
<h1>Image gallery example</h1>

<div class="full-img">
  <img class="displayed-img" src="images/pic1.jpg" />
  <div class="overlay"></div>
  <button class="dark">Darken</button>
</div>

<div class="thumb-bar"></div>
```

该示例如下所示：

![](/zh-CN/docs/Learn/JavaScript/Building_blocks/Image_gallery/gallery.png)

以下是本例中 CSS 文件最值得关注的部分：

* `full-img <div>` 中有三个绝对定位的元素：一个显示全尺寸图片的 `<img>`，一个空 `<div>`（覆盖于 `<img>` 之上，且与之大小相同，用来设置半透明背景色来使图片变暗），和一个用来控制变暗效果的 `<button>`。
* 将 `thumb-bar <div>` 中图片（即“缩略图”）的宽度设置为 20%，并且将它们沿左侧浮动，使得它们在同一行上依次排列。

JavaScript 部分则需要实现：

* 声明一个 `const` 数组，用于列出每张图像的文件名，例如 `'pic1.jpg'`。
* 迭代数组中的文件名，为每一个文件名创建一个 `<img>` 元素，并将其插入到 `thumb-bar <div>` 中，这样图片就会嵌入页面。
* 为 `thumb-bar <div>` 里的每个 `<img>` 元素添加一个 click 事件监听器，在图片被点击时相应的图片将被显示到 `displayed-img <img>` 元素上。
* 给 `<button>` 元素添加一个 click 事件监听器，当按钮被点击时，将全尺寸图片变暗，再次点击时取消。

可以看一下完成的示例，体会一下。（别偷看代码哦）

详细步骤
----

以下是你的工作。

声明图像文件名数组
---------

你需要创建一个用于列出所有需要包含在图片库中的图像的文件名。这个数组应该以常量的形式声明。

### 迭代照片

我们提供的代码中用一个名为 `thumbBar` 的变量用来存储 `thumb-bar <div>` 的引用，创建了一个新的 `<img>` 元素，将它的 `src` 属性值设置成 `xxx` 占位符，并且将这个新的 `<img>` 元素添加到 `thumbBar` 中。

你需要：

1. 将“Looping through images”注释下方的代码放到一个循环之中，这个循环会迭代数组中所有的文件名。
2. 对于每一次循环迭代，将占位符 `xxx` 替换为代表图像路径的字符串。我们需要设置每一个图像的 `src` 属性。请注意，每一张图像都在 `images` 文件夹内，文件名为 `pic1.jpg`、`pic2.jpg`，等等。
### 为每一个缩略图添加 click 事件监听器

每次迭代中，你需要给当前的 `newImage` 加上一个 click 事件监听器——它应该查询当前图像的 `scr` 属性值，然后将获取的 `src` 属性值设置为 `displayed-img <img>` 元素 `src` 属性的值。

或者，你可以只为缩略图栏（`thumb-bar <div>`）添加事件监听器。

### 为变亮/变暗按钮编写处理器

最后还剩变亮/变暗的 `<button>`。我们已经提供了一个名为 `btn` 的变量来存储 `<button>` 的引用。需要添加一个 click 事件监听器：

1. 检查当前 `<button>` 按钮的类名称，你可以使用 `getAttribute()` 方法获得。
2. 如果类名是 `"dark"`，则将 `<button>` 的类名变为 `"light"`（使用 `setAttribute()`），文本内容变为“Lighten”，然后将蒙板 `<div>` 的 `background-color` 设为 `"rgba(0,0,0,0.5)"`。
3. 如果类名不是 `"dark"`，则将 `<button>` 的类名变为 `"dark"`，文本内容变为 "Darken"，然后将蒙板 `<div>` 的 `background-color` 设为 `"rgba(0,0,0,0)"`。

以下是实现上述 2、3 点所提功能的基本代码：

```js
btn.setAttribute("class", xxx);
btn.textContent = xxx;
overlay.style.backgroundColor = xxx;
```

提示
--

* 完全不需要修改 HTML 和 CSS 文件。
评估或进一步帮助
--------

如果你希望对自己的工作成果进行打分评估，或者遇到了困难并希望寻求帮助：

1. 将你完成的内容放到在线、可分享的编辑器，如：CodePen、jsFiddle 或 Glitch。
2. 在 MDN Discourse forum Learning category 中发贴寻求打分评估或帮助。你的贴子需要包含：
    * 一个描述的标题，如：“Assessment wanted for Image gallery”。
    * 你尝试的详细过程，以及你希望我们能够做什么，例如：你遇到了困难，想要寻求帮助；或希望得到打分评估。
    * 一个指向用于打分评估或寻求帮助的示例代码的链接（使用上文中提到的任意一种在线的、可分享的编辑器）。这是一种很好的形式——如果别人没法看到你的代码，他们很难对你提供代码上的帮助。
    * 一个指向实际任务或评估页面的链接，以便我们找到你需要得到帮助的问题。
* 上一页
* Overview: Building blocks


创建你自己的函数 - 学习 Web 开发

创建你自己的函数
========

* 上一页
* Overview: Building blocks
* 下一页

我们在之前的文章里大多学的是理论，这篇文章将提供一个练习的机会——你将练习构建一些你自己风格的函数。在练习过程中，我们也会解释一些针对函数的更深层的实用细节。

|  |  |
| --- | --- |
| 先修知识： |
 基本的电脑常识，对于 HTML 和 CSS 的基本了解，
 JavaScript 第一步，
 函数 - 可复用代码块。
  |
| 目标： | 提供一些练习来构建一个传统的函数，并解释一些有用的相关细节。 |

先活跃下气氛：构建一个函数
-------------

我们将构建的传统函数将被命名为 `displayMessage()`，它向用户展示一个传统的消息盒子于 web 页面的顶部。它充当浏览器内建的 alert() 函数更有用的替代品。你已经看过了这个，但是我们回复一下我们的记忆——在你的浏览器的 JavaScript 控制台中，在任意一个页面里尝试以下代码

```js
alert("This is a message");
```

这个函数只带有一个参数——在 alert box 中展示的字符串。你可以尝试改变字符串来改变消息。

这个`alert()`函数不是很好的：你可以`alert()`出这条信息，但是你不能很容易的表达其他内容，例如颜色，图标或者是其他东西。接下来我们将会构建一个更有趣的函数。

**备注：** 这个例子能够在现代浏览器上很好的工作，但是这个风格在老的浏览器上并没那么有趣。我们建议你实现这个例子时在现代浏览器上，例如 Firefox，Opera 或者 Chrome 浏览器。

基本函数
----

首先，让我们来组织一个基本的函数。

**备注：** 对于函数命名约定，应遵循与变量命名约定相同的规则。这很好，尽你所能理解它们之间的区别 - 函数名称后带有括号，而变量则没有。

1. 我们希望你首先访问function-start.html文件并创建一个本地拷贝。你将会看到这个 HTML 很简单 — 我们的 body 块仅包含一个按钮。我们还提供了一些基本的 CSS 来装饰自定义消息框，以及一个用于放置 JavaScript 代码的`<script>`元素。
2. 接下来，将下面的代码添加至 `<script>` 元素中：

```js
function displayMessage() {}
```

我们从表示定义一个函数的关键字 `function`开始，这之后是我们想给我们的函数取的名字；一组括号；和一组大括号。我们要传给我们的函数的任何参数都在括号内，当我们调用该函数时运行的代码均在大括号内。
3. 最后，添加以下代码到大括号中：

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

const panel = document.createElement("div");
panel.setAttribute("class", "msgBox");
html.appendChild(panel);

const msg = document.createElement("p");
msg.textContent = "This is a message box";
panel.appendChild(msg);

const closeBtn = document.createElement("button");
closeBtn.textContent = "x";
panel.appendChild(closeBtn);

closeBtn.onclick = function () {
  panel.parentNode.removeChild(panel);
};
```

天哪，这么多代码！好吧，一行一行的解释给你听。

第一行代码使用了一个 DOM（文档对象模型）的内置方法 `document.querySelector()` 来选择`<html>` 元素并且把它存放在一个叫 `html`的常量中，这样方便我们接下来使用这个元素：

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

下段代码使用了另一个名字叫做 `Document.createElement()` 的 DOM 方法，用来创建 `<div>` 元素并且把该新建元素的引用（实际上是新建对象的地址）放在一个叫做 `panel`的常量中。这个元素将成为我们的消息框的外部容器。

然后我们又使用了一个叫做 `Element.setAttribute()` 的 DOM 方法给 panel 元素添加了一个值为`msgBox` 的`class` 类属性。这样做方便我们来给这个元素添加样式 — 查看 CSS 代码你就知道我们使用`.msgBox` 类选择器来给消息框和消息内容设置样式。

最后，我们还使用了一个叫做 `Node.appendChild()` 的 DOM 方法，给 `html` 常量（我们之前定义好的）追加了我们设置好样式的 panel 元素。该方法追加了元素的同时也把 panel`<div>`元素指定为`<html>`的子元素。这样做是因为我们创建了一个元素之后这个元素并不会莫名其妙的出现在我们的页面上（浏览器只知道我们创建了一个元素，但是不知道把这个元素怎么呈现出来） — 因此，我们给这个元素了一个定位，就是显示在 html 里面！

```js
const panel = document.createElement("div");
panel.setAttribute("class", "msgBox");
html.appendChild(panel);
```

下面这两段使用了我们之前使用过的方法`createElement()`和`appendChild()` — 创建了一个 `<p>` 元素和一个`<button>`元素 — 并且把他们追加到了 panel`<div>`之下。我们使用元素的 `Node.textContent`（Node 泛指一个元素并不是说是某个元素是叫 Node）属性— 表示一个元素的文本属性 — 给一个 p 元素赋值，同样按钮也有这个属性，该属性就是按钮显示的‘X’。这个按钮的功能就是关闭消息提示框。

```js
const msg = document.createElement("p");
msg.textContent = "This is a message box";
panel.appendChild(msg);

const closeBtn = document.createElement("button");
closeBtn.textContent = "x";
panel.appendChild(closeBtn);
```

最后我们使用一个叫做 `GlobalEventHandlers.onclick` 的事件句柄给按钮添加了一个点击事件，点击事件后定义了一个匿名函数，功能是将消息提示框从父容器中删除 — 达到了关闭的效果。

简单来说，这个 `onclick` 句柄是一个按钮的属性 (事实上，页面上的任何元素) 当按钮被点击的时候能够执行一些代码。你可以在之后的介绍事件的章节了解详情。我们给 `onclick` 句柄绑定了一个匿名函数，函数中代码在元素被点击的时候运行。函数里面的这行代码使用了 `Node.removeChild()` DOM 方法指定了我们想要移除的 HTML 的子元素 — 在这里指 panel`<div>`.

PS：我来解释下是什么意思，panel 是消息框，panel.parentNode 就是指 panel 的上一级，就是整个 DOM，然后再来用这个父亲来干掉这个儿子，儿子不能自己干掉自己，所以要这么做。

```js
closeBtn.onclick = function () {
  panel.parentNode.removeChild(panel);
};
```

大体上，这一整块的代码我就不解释了就是一个 div，一个段落，一个按钮，把这个加在页面上：

```html
<div class="msgBox">
  <p>This is a message box</p>
  <button>x</button>
</div>
```

啊，看完了这么多代码，是不是很累？ — 不用担心，你现在没有必要完全知道这些代码的细节！这里我们只关心函数的结构和使用方式，下面的例子将展示一些有意思的东西。

调用函数
----

相信你已经迫不及待的在你的`<script>` 标签中写好了一个函数，但仅仅是定义而已，这玩意不会做任何事情。

1. 把下面这行代码加在写好的函数下面来调用函数（当然，不一定要放在函数下面来调用，在 C 语言中确实是还要先定义后使用，但是我们现在用的是 JavaScript，这玩意很强大，不管你是先定义后调用还是先调用后定义都行，但是别忘了定义）:

```js
displayMessage();
```

这行代码调用了你写的函数，当浏览器解析到这行代码时会立即执行函数内的代码。当你保存好你的代码以后在浏览器中刷新，你会马上看到一个小小的提示框弹出来，但是只弹出了一次。毕竟我们只调用了一次函数是不？
2. 现在打开浏览器开发工具，找到 JavaScript 控制台把上面这一句再输入一遍然后回车，你会看到又弹出了一次！有点意思... — 现在我们有了一个能够重复调用的函数，只要你高兴可以随时调用它。
 但是，这玩意有什么用呢？在真实的应用当中这样的消息提示框一般用来提示一些什么新的东西，或者是出现了一个什么错误，或者当用户删除配置文件的时候 ("你确定要这样做？"), 或者用户添加一个新的联系人之后提示操作成功..等等。在这个例子里面，当用户点击这个按钮的时候这个提示框会出现。
3. 删掉你之前加的那一行代码。
4. 下一步我们用选择器找到这个按钮并赋值给一个常量。在你的函数定义之前把这行代码加上去：

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

5. 最后，把这行代码加在上面这行的下面：

```js
btn.onclick = displayMessage;
```

跟关闭按钮类似 `closeBtn.onclick...` , 当按钮被点击的时候我们运行了点代码。但不同的是，之前等号的右边是一个匿名函数，看起来是这样的：`btn.onclick = function(){...}`, 我们现在是直接使用函数名称来调用。
6. 保存好以后刷新页面 — 现在你应该能看到当你点击按钮的时候提示框弹出来。

你会想“怎么函数名后面没有括号呢？”. 这是因为我们不想直接调用这个函数 — 而是只有当按钮被点击的时候才调用这个函数。试试把代码改成这样：

```js
btn.onclick = displayMessage();
```

保存刷新，你会发现按钮都还没点击提示框就出来了！在函数名后面的这个括号叫做“函数调用运算符”（function invocation operator）。你只有在想直接调用函数的地方才这么写。同样要重视的是，匿名函数里面的代码也不是直接运行的，只要代码在函数作用域内。

如果你做了这个函数括号的实验，在继续之前把代码恢复到之前的状态。

使用参数列表改进函数
----------

就现在看来，我们的函数还不是特别有用 — 我们想要的不仅仅是每点击一次展示一个默认的消息。我们来改造下我们的函数，给它添加几个参数，允许我们以不同的方式调用这个函数。

1. 第一步，修改函数的第一行代码：

```js
function displayMessage() {
```

改成这样的：

```js
function displayMessage(msgText, msgType) {
```

当我们调用函数的时候，我们可以在括号里添加两个变量，来指定显示在消息框里面的消息，和消息的类型。
2. 为了使用第一个参数，把接下来的一行：

```js
msg.textContent = "This is a message box";
```

改成这样：

```js
msg.textContent = msgText;
```

3. 最后但同样重要的一点，我们来调用这个函数，并且使用了带参数的形式，修改下面这行：

```js
btn.onclick = displayMessage;
```

改成这样：

```js
btn.onclick = function () {
  displayMessage("Woo, this is a different message!");
};
```

如果我们要在点击事件里面绑定这个新函数，我们不能直接使用（`btn.onclick = displayMessage('Woo, this is a different message!');`）前面已经讲过— 我们要把它放在一个匿名函数里面，不然函数会直接调用，而不是按钮点击之后才会调用，这不是我们想要的结果。
4. 保存刷新，就像你所期待的那样现在你可以随意的指定消息框里面显示的消息！
### 一个更加复杂的参数

刚才我们只使用了我们定义的第一个参数`msgText`，对于第二个参数`msgType`，这个就涉及了稍微多一点的东西— 我们要设置一些依赖于这个 `msgType` 参数的东西，我们的函数将会显示不同的图标和不同的背景颜色。

1. 第一步，从 Github 上下载我们需要的图标 (警告图标 和 聊天图标) 。把图标保存在一个叫做`icons` 的文件夹下，和你的 HTML 文件在同一个目录下。

**备注：** 警告和聊天图标是在这个网站 iconfinder.com 上找到的，设计者是 Nazarrudin Ansyari。感谢他！
2. 下一步，找到页面的 CSS 文件。我们要修改下以便我们使用图标。首先，修改 `.msgBox` 的宽度：

```css
width: 200px;
```

改成：

```css
width: 242px;
```

3. 下一步，在 `.msgBox p { ... }` 里面添加几条新规则：

```css
padding-left: 82px;
background-position: 25px center;
background-repeat: no-repeat;
```

4. CSS 改完了以后我们就要来修改函数 `displayMessage()` 让它能够显示图标。在你的函数结束符之前`}`添加下面这几行代码：

```js
if (msgType === "warning") {
  msg.style.backgroundImage = "url(icons/warning.png)";
  panel.style.backgroundColor = "red";
} else if (msgType === "chat") {
  msg.style.backgroundImage = "url(icons/chat.png)";
  panel.style.backgroundColor = "aqua";
} else {
  msg.style.paddingLeft = "20px";
}
```

来解释下，如果第二个参数 `msgType` 的值为 `'warning'`, 我们的消息框将显示一个警告图标和一个红色的背景。如果这个参数的值是 `'chat'`, 将显示聊天图标和水蓝色的背景。如果 `msgType` 没有指定任何值 (或者不是`'warning'`和`'chat'`), 然后这个 `else { ... }` 代码块将会被执行，代码的意思是给消息段落设置了一个简单的左内边距并且没有图标，也没有背景颜色。这么做是为了当没有提供 `msgType` 参数的时候给函数一个默认行为，意思是这是一个可选参数（你没发现？其实我们已经用过了！就在这里`btn.onclick = function() { displayMessage('Woo, this is a different message!'); };`只是当时我们没有写这个`else`段，也就是啥操作也没做）！
5. 现在来测试下我们的新函数，可以直接调用 `displayMessage()` 像这样：

```js
displayMessage("Woo, this is a different message!");
```

或者这样：

```js
displayMessage("Your inbox is almost full — delete some mails", "warning");
displayMessage("Brian: Hi there, how are you today?", "chat");
```

你能看到我们现在的函数稍微有了点用 (不是非常有用) ，一个小的新功能被我们写出来了（当然，函数可以做很多你想的到的和想不到的事）！

**备注：** 如果你写这个例子遇到了困难，在这里查看免费的代码 完整版本的代码 (或者在线运行的完整代码), 也可以向我们寻求帮助。

技能测试！
-----

你已经来到了本文章的结尾，但是你还能记得最重要的知识吗？你可以在离开这里找到一些更深度的测试来证实你已经记住了这些知识——查看技能测试：函数 (en-US)（英文）。后两章文本包含了这个测试需要的技能，所以你可能先需要阅读再尝试该测试。

结论
--

恭喜你，终于到了这里（等你好久了）! 这篇文章介绍了如何写一个自定义函数，要把这个新技能在真实项目中使用上你可能还要花点功夫。下一篇文章中我们将会介绍函数的另一个相关概念 — 返回值。

* 上一页
* Overview: Building blocks
* 下一页


函数——可复用代码块 - 学习 Web 开发

函数——可复用代码块
==========

* 上一页
* Overview: Building blocks
* 下一页

在 JavaScript 中另一个基本概念是**函数**, 它允许你在一个代码块中存储一段用于处理单任务的代码，然后在任何你需要的时候用一个简短的命令来调用，而不是把相同的代码写很多次。在本文中，我们将探索函数的基本概念，如基本语法、如何定义和调用、范围和参数。

|  |  |
| --- | --- |
| 前提： |
 基本的电脑知识，对 HTML 与 CSS 有基本的了解，及已阅读：
 JavaScript first steps（JS 的入门）。
  |
| 目标： | 了解 Javascript 函数背后的基本概念。 |

我能在哪找到函数？
---------

在 JavaScript 中，你将发现函数无处不在。事实上，到目前为止，我们一直在使用函数，只是我们还没很好的讨论它们。然而现在是时候了，让我们开始聊聊函数，并探索它们的语法。

几乎任何时候，只要你使用一个带有一对圆括号（`()`）的 JavaScript 结构，并且你不是在使用一个常见的比如 for 循环、while 或 do…while 循环，或者 if 语句这样的内置语言结构时，那么你就正在使用函数。

浏览器内置函数
-------

在这套课程中我们已经使用了很多浏览器内置函数，当我们操作一个字符串的时候，例如：

```js
var myText = "I am a string";
var newString = myText.replace("string", "sausage");
console.log(newString);
// replace() 字符串函数接受一个字符串，
// 用另一个子字符串替换一个子字符串，并返回
// 替换后的新字符串
```

或者当我们操作一个数组的时候：

```js
var myArray = ["I", "love", "chocolate", "frogs"];
var madeAString = myArray.join(" ");
console.log(madeAString);
// join() 函数接受一个数组，将
// 所有数组元素连接成一个单一的
// 字符串，并返回这个新字符串
```

或者当我们生成一个随机数时：

```js
var myNumber = Math.random();
// random() 函数生成一个随机
// 数字在 0 和 1 之间，并返回该
// 数字
```

...我们已经使用过函数了！

**备注：** 如果需要，你可以随意将这些代码输入浏览器控制台以便于你熟悉其功能。

JavaScript 有许多内置的函数，可以让你做很多有用的事情，而无需自己编写所有的代码。事实上，许多你调用（运行或者执行的专业词语）浏览器内置函数时调用的代码并不是使用 JavaScript 来编写——大多数调用浏览器后台的函数的代码，是使用像 C++ 这样更低级的系统语言编写的，而不是像 JavaScript 这样的 web 编程语言。

请记住，这些内置浏览器函数不是核心 JavaScript 语言的一部分——被定义为浏览器 API 的一部分，它建立在默认语言之上，以提供更多的功能（请参阅本课程的早期部分以获得更多的描述）。我们将在以后的模块中更详细地使用浏览器 API。

函数与方法
-----

程序员把函数称为对象**方法**（method）的一部分。你还不必了解 JavaScript 中已建构的对象在更深层次上是如何运作的——你可以等到下一小节，我们会教给你有关对象运作方式的一切。在我们继续前进之前，我们需要澄清一些有关方法和函数概念之间可能存在的误会——当你在网络上浏览相关信息的时候，你很可能会碰上这两个术语。

到目前为止我们所使用的内置代码同属于这两种形式：函数和方法。你可以在这里查看内置函数，内置对象以及其相关方法的完整列表。

严格说来，内置浏览器函数并不是函数——它们是**方法**。这听起来有点可怕和令人困惑，但不要担心——函数和方法在很大程度上是可互换的，至少在我们的学习阶段是这样的。

二者区别在于方法是在对象内定义的函数。浏览器内置函数（方法）和变量（称为属性）存储在结构化对象内，以使代码更加高效，易于处理。

自定义函数
-----

你在过去的课程中还看到很多定制功能 - 在代码中定义的功能，而不是在浏览器中。每当你看到一个自定义名称后面都带有括号，那么你使用的是自定义函数。在我们的循环文章中的 random-canvas-circles.html 示例（参见完整的源代码）中，我们包括一个如下所示的自定义函数：`draw()`

```js
function draw() {
  ctx.clearRect(0, 0, WIDTH, HEIGHT);
  for (var i = 0; i < 100; i++) {
    ctx.beginPath();
    ctx.fillStyle = "rgba(255,0,0,0.5)";
    ctx.arc(random(WIDTH), random(HEIGHT), random(50), 0, 2 \* Math.PI);
    ctx.fill();
  }
}
```

该函数在 `<canvas>` 元素中绘制 100 个随机圆。每次我们想要这样做，我们可以使用这个函数来调用这个功能：

```js
draw();
```

而不是每次我们想重复一遍，都要写出所有的代码。函数可以包含任何你喜欢的代码 - 甚至可以从内部函数调用其他函数。以上函数例如调用`random()`函数三次，由以下代码定义：

```js
function random(number) {
  return Math.floor(Math.random() \* number);
}
```

我们需要这个函数，因为浏览器的内置 Math.random() 函数只生成一个 0 到 1 之间的随机十进制数。我们想要一个 0 到一个指定数字之间的随机整数。

调用函数
----

现在你可能很清楚这一点，但仅仅为了防止……，要在函数定义之后，实际使用它，你必须运行或调用它。这是通过将函数名包含在代码的某个地方，后跟圆括号来完成的。

```js
function myFunction() {
  alert("hello");
}

myFunction();
// 调用一次该函数
```

匿名函数
----

你可能会以稍微不同的方式看到定义和调用的函数。到目前为止，我们刚刚创建了如下函数：

```js
function myFunction() {
  alert("hello");
}
```

但是你也可以创建一个没有名称的函数：

```js
function() {
  alert('hello');
}
```

这个函数叫做**匿名函数**——它没有函数名！它也不会自己做任何事情。你通常将匿名函数与事件处理程序一起使用，例如，如果单击相关按钮，以下操作将在函数内运行代码：

```js
var myButton = document.querySelector("button");

myButton.onclick = function () {
  alert("hello");
};
```

上述示例将要求 `<button>` 在页面上提供可用于选择并单击的元素。你在整个课程中已经看到过这种结构了几次，你将在下一篇文章中了解更多信息并在其中使用。

你还可以将匿名函数分配为变量的值，例如：

```js
var myGreeting = function () {
  alert("hello");
};
```

现在可以使用以下方式调用此函数：

```js
myGreeting();
```

有效地给变量一个名字;还可以将该函数分配为多个变量的值，例如：

```js
var anotherGreeting = function () {
  alert("hello");
};
```

现在可以使用以下任一方法调用此函数

```js
myGreeting();
anotherGreeting();
```

但这只会令人费解，所以不要这样做！创建方法时，最好坚持下列形式：

```js
function myGreeting() {
  alert("hello");
}
```

你将主要使用匿名函数来运行负载的代码以响应事件触发（如点击按钮）——使用事件处理程序。再次，这看起来像这样：

```js
myButton.onclick = function () {
  alert("hello");
  // 我可以在这里放入
  // 尽可能多的代码
};
```

**备注：** 匿名函数也称为函数表达式。函数表达式与函数声明有一些区别。函数声明会进行声明提升（declaration hoisting），而函数表达式不会。

函数参数
----

一些函数需要在调用它们时指定参数——这些参数值需要放在函数括号内，才能正确地完成其工作。

**备注：** 参数有时称为参数（argument）、属性（propertie）或甚至特性（attribute）。

例如，浏览器的内置 Math.random() 函数不需要任何参数。当被调用时，它总是返回 0 到 1 之间的随机数：

```js
var myNumber = Math.random();
```

浏览器的内置字符串replace（）函数需要两个参数：在主字符串中查找的子字符串，以及用以下替换该字符串的子字符串：

```js
var myText = "I am a string";
var newString = myText.replace("string", "sausage");
```

**备注：** 当你需要指定多个参数时，它们以逗号分隔。

还应该注意，有时参数不是必须的——你不必指定它们。如果没有，该功能一般会采用某种默认行为。作为示例，数组 `join()` 函数的参数是可选的：

```js
const myArray = ["我", "爱", "巧克力", "青蛙"];
const madeAString = myArray.join(" ");
console.log(madeAString);
// 返回 '我 爱 巧克力 青蛙'

const madeAnotherString = myArray.join();
console.log(madeAnotherString);
// 返回 '我,爱,巧克力,青蛙'
```

如果没有包含参数来指定加入/分隔符，默认情况下会使用逗号。

函数作用域和冲突
--------

我们来谈一谈作用域——处理函数时一个非常重要的概念。当你创建一个函数时，函数内定义的变量和其他东西都在它们自己的单独的范围内，意味着它们被锁在自己独立的隔间中，不能被函数外的代码访问。

所有函数的最外层被称为全局作用域。在全局作用域内定义的值可以在任意地方访问。

JavaScript 由于各种原因而建立，但主要是由于安全性和组织性。有时你不希望变量可以在代码中的任何地方访问 - 你从其他地方调用的外部脚本可能会开始搞乱你的代码并导致问题，因为它们恰好与代码的其他部分使用了相同的变量名称，造成冲突。这可能是恶意的，或者是偶然的。

例如，假设你有一个 HTML 文件，它调用两个外部 JavaScript 文件，并且它们都有一个使用相同名称定义的变量和函数：

```html
<!-- Excerpt from my HTML -->
<script src="first.js"></script>
<script src="second.js"></script>
<script>
 greeting();
</script>
```

```js
// first.js
let name = "Chris";
function greeting() {
  alert("Hello " + name + ": welcome to our company.");
}
```

```js
// second.js
let name = "Zaptec";
function greeting() {
  alert("Our company is called " + name + ".");
}
```

这两个函数都使用 `greeting()` 形式调用，但是你只能访问到 first.js 文件的`greeting()`函数（第二个文件被忽视了）。另外，第二次尝试使用 `let` 关键字定义 `name` 变量导致了一个错误。

**备注：** 你可以在 GitHub 上参考运行的实例（查看完整源代码）。

将代码锁定在函数中的部分避免了这样的问题，并被认为是最佳实践。

这有点像一个动物园。狮子，斑马，老虎和企鹅都保留在自己的园子中，只能拿到到它们园子中的东西——与其函数作用域相同。如果他们能进入其他园子，就会出现问题。不同的动物会在不熟悉的栖息地内感到真的不舒服——一只狮子或老虎会在企鹅的水多的，冰冷的的领域中感到可怕。最糟糕的是，狮子和老虎可能会尝试吃企鹅！

![](/zh-CN/docs/Learn/JavaScript/Building_blocks/Functions/mdn-mozilla-zoo.png)

动物园管理员就像全局作用域 - 他或她有钥匙访问每个园子，重新投喂食物，照顾生病的动物等。

### 主动学习：和 scope 玩耍

我们来看一个真正的例子来展示范围

1. 首先，创建我们的 function-scope.html 示例的本地副本。它包含两个函数 `a()` 和 `b()`，和三个变量——`x`、`y`和`z`——其中两个在函数中被定义，另一个被定义在全局作用域内。它还包含一个名为 `output()` 的函数，它接收一个参数，并将其输出到页面的一个段落中。
2. 在浏览器和文本编辑器中打开示例。
3. 在浏览器开发工具中打开 JavaScript 控制台。在 JavaScript 控制台中，输入以下命令：

```js
output(x);
```

你应该看到变量`x`输出到屏幕的值。
4. 现在尝试在你的控制台中输入以下内容

```js
output(y);
output(z);
```

这两个都应该返回错误沿“ReferenceError：y 未定义”。这是为什么？由于函数作用域 - `y` 和 `z` 被锁定在函数 `a()` 和 `b()` 函数中，所以 `output()` 从全局作用域调用时无法访问它们。
5. 但是，从另一个函数里面调用什么呢？尝试编辑 `a()`、`b()` 所以它们看起来像这样：

```js
function a() {
  var y = 2;
  output(y);
}

function b() {
  var z = 3;
  output(z);
}
```

保存代码并重新加载到浏览器中，然后尝试从 JavaScript 控制台调用 `a()` 和 `b()` 函数：

```js
a();
b();
```

你应该看到页面中输出的 `y` 和 `z` 的值。这样就没问题，因为 `output()` 函数在其他函数的内部被调用——在这种情况下，输出变量的定义和函数的调用都在同一个作用域中。`output()` 它可以从任何地方被调用，因为它在全局作用域中被定义。
6. 现在尝试更新你的代码，如下所示：

```js
function a() {
  var y = 2;
  output(x);
}

function b() {
  var z = 3;
  output(x);
}
```

7. 再次保存并重新加载，并在 JavaScript 控制台中再次尝试：

```js
a();
b();
```

函数 `a()`和`b()`都应该输出 x---1 的值。这些没有问题，因为即使`output()`的调用与`x`的定义不在同一个作用域内，但`x`是一个全局变量，所以在所有代码中都可用。
8. 最后，尝试更新你的代码，如下所示：

```js
function a() {
  var y = 2;
  output(z);
}

function b() {
  var z = 3;
  output(y);
}
```

9. 再次保存并重新加载，并在 JavaScript 控制台中再次尝试：

```js
a();
b();
```

这次`a()`和`b()`调用都会返回那个令人讨厌的 "ReferenceError: z is not defined" error — 这是因为`output()`函数的调用和输出变量的定义不在同一个函数作用域内 - 变量对这些函数调用是不可见的。

**备注：** 相同的范围规则不适用于循环（for（）{...}）和条件块（if（）{...}） - 它们看起来非常相似，但它们不一样！小心不要让这些困惑。

**备注：** ReferenceError："x"未定义错误是你遇到的最常见的错误。如果你收到此错误，并且确定你已经定义了该问题的变量，请检查它的范围。

### 函数内部的函数

请记住，你可以从任何地方调用函数，甚至可以在另一个函数中调用函数。这通常被用作保持代码整洁的方式 - 如果你有一个复杂的函数，如果将其分解成几个子函数，它更容易理解：

```js
function myBigFunction() {
  var myValue;

  subFunction1();
  subFunction2();
  subFunction3();
}

function subFunction1() {
  console.log(myValue);
}

function subFunction2() {
  console.log(myValue);
}

function subFunction3() {
  console.log(myValue);
}
```

要确保函数调取的数值处在有效的作用域内。上面的例子中会产生一个错误提示，`ReferenceError：myValue is not define`，因为尽管 `myValue` 变量与函数调用指令处在同一个作用域中，但它却没有在函数内被定义——实际代码在调用函数时就开始运行了。为了使代码正确运作，你必须将值作为参数传递给函数，如下所示：

```js
function myBigFunction() {
  var myValue = 1;

  subFunction1(myValue);
  subFunction2(myValue);
  subFunction3(myValue);
}

function subFunction1(value) {
  console.log(value);
}

function subFunction2(value) {
  console.log(value);
}

function subFunction3(value) {
  console.log(value);
}
```

技能测试！
-----

你已经来到了本文章的结尾，但是你还能记得最重要的知识吗？你可以在离开这里找到一些更深度的测试来证实你已经记住了这些知识——查看技能测试：函数 (en-US)（英文）。后两章文本包含了这个测试需要的技能，所以你可能先需要阅读再尝试该测试。

总结
--

本文探讨了函数背后的基本概念，为之后的学习奠定了基础。下一步，我们将进行实践，并带你一步步建立起你自己的函数。

参见
--

* Functions detailed guide — covers some advanced features not included here.
* Functions reference
* Default parameters, Arrow functions — advanced concept references
* 上一页
* Overview: Building blocks
* 下一页


循环吧，代码 - 学习 Web 开发

循环吧，代码
======

* 上一页
* Overview: Building blocks
* 下一页

编程语言可以很迅速方便地帮我们完成一些重复性的任务，从多个基本计算到几乎完成了很多类似工作的其他情况。现在我们来看看处理这种需求的 JavaScript 中可用的循环结构。

|  |  |
| --- | --- |
| 前提： |
 基本的电脑知识，对 HTML 与 CSS 有基本的了解，及已阅读：
 JavaScript first steps（JS 的入门）.
  |
| 目标： | 学习如何在 JS 里面使用循环语句。 |

来一起循环
-----

循环，循环，循环。就与这些：popular breakfast cereals, roller coasters and musical production 一样，类似存在于编程中。编程中的循环也是一直重复着去做一件事 - 此处循环便是编程中的术语。

让我们来想一下下图，这位农夫考虑为他的家庭做一周的食物计划，他或许就需要执行一段循环：

![](https://raw.githubusercontent.com/agnoCJY/agno.github.io/master/loop_js-02-farm-cn.png)

一段循环通常需要一个或多个条件：

* \*\*一个开始条件，\*\*它被初始化为一个特定的值 - 这是循环的起点 ("开始：我没有食物”，上面）。
* \*\*一个结束条件，\*\*这是循环停止的标准 - 通常计数器达到一定值。以上所说的“我有足够的食物”吗？假设他需要 10 份食物来养活他的家人。
* \*\*一个迭代器，\*\*这通常在每个连续循环上递增少量的计数器，直到达到退出条件。我们以前没有明确说明，但是我们可以考虑一下农民能够每小时收集 2 份食物。每小时后，他收集的食物量增加了两倍，他检查他是否有足够的食物。如果他已经达到 10 分（退出条件），他可以停止收集回家。

在 伪代码 中，这看起来就像下面这样：

```js
loop(food = 0; foodNeeded = 10) {
  if (food = foodNeeded) {
    exit loop;
    // 我们有足够的食物了，回家吧。
  } else {
    food += 2; // 花一个小时多收集两个食物。
    // 循环将会继续执行。
  }
}
```

所以需要的食物量定为 10，农民目前的数量为 0。在循环的每次迭代中，我们检查农民的食物量是否等于他需要的量。如果是这样，我们可以退出循环。如果没有，农民花一个小时收集两部分食物，循环再次运行。

### 为何？

在这一点上，你可能会了解循环中的高级概念，但你可能会认为“好的，但是，这有助于我编写更好的 JavaScript 代码？”正如我们前面所说，循环与所做的事情都是一样的，这对于快速完成重复任务是非常有用的。

通常，循环的每个连续迭代的代码将略有不同，这意味着你可以完成相同但略有不同的任务的全部负载 - 如果你有很多不同的计算要做，做不同的一个，不一样的一个又一个！

让我们来看一个例子来完美地说明为什么循环是一件好事。假设我们想在`<canvas>`元素上绘制 100 个随机圆（按更新按钮一次又一次地运行示例以查看不同的随机集）：
```

<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Random canvas circles</title>
    <style>
 html {
 width: 100%;
 height: inherit;
 background: #ddd;
 }

 canvas {
 display: block;
 }

 body {
 margin: 0;
 }

 button {
 position: absolute;
 top: 5px;
 left: 5px;
 }
 </style>
  </head>
  <body>
    <button>Update</button>

    <canvas></canvas>

    <script>
 var btn = document.querySelector("button");
 var canvas = document.querySelector("canvas");
 var ctx = canvas.getContext("2d");

 var WIDTH = document.documentElement.clientWidth;
 var HEIGHT = document.documentElement.clientHeight;

 canvas.width = WIDTH;
 canvas.height = HEIGHT;

 function random(number) {
 return Math.floor(Math.random() \* number);
 }

 function draw() {
 ctx.clearRect(0, 0, WIDTH, HEIGHT);
 for (var i = 0; i < 100; i++) {
 ctx.beginPath();
 ctx.fillStyle = "rgba(255,0,0,0.5)";
 ctx.arc(random(WIDTH), random(HEIGHT), random(50), 0, 2 \* Math.PI);
 ctx.fill();
 }
 }

 btn.addEventListener("click", draw);
 </script>
  </body>
</html>
```

你现在不需要理解所有代码，但我们来看看实际绘制 100 个圆的那部分代码：

```js
for (var i = 0; i < 100; i++) {
  ctx.beginPath();
  ctx.fillStyle = "rgba(255,0,0,0.5)";
  ctx.arc(random(WIDTH), random(HEIGHT), random(50), 0, 2 \* Math.PI);
  ctx.fill();
}
```

* `random()`,在前面的代码中定义过了，返回一个 `0` 到 x-1 间的整数。
* `WIDTH` 和`HEIGHT` 浏览器内部窗口的宽度和高度。

你应该有一个基本的想法 - 我们使用一个循环来运行这个代码的 100 次迭代，其中每一个在页面上的随机位置绘制一个圆。无论我们绘制 100 个圆，1000 还是 10,000，所需的代码量将是相同的。只有一个数字必须改变。

如果我们在这里没有使用循环，我们必须为我们想要绘制的每个圆重复以下代码：

```js
ctx.beginPath();
ctx.fillStyle = "rgba(255,0,0,0.5)";
ctx.arc(random(WIDTH), random(HEIGHT), random(50), 0, 2 \* Math.PI);
ctx.fill();
```

这将非常无聊而且很难维持高速。循环真的相当好用。

循环的标准
-----

我们开始探索一些特定的循环结构。第一个，你会经常使用到它，for 循环 - 以下为 for 循环的语法：
```

for (initializer; exit-condition; final-expression) {
  // code to run
}
```

我们有：

1. 关键字`for`，后跟一些括号。
2. 在括号内，我们有三个项目，以分号分隔：
    1. 一个**初始化器** - 这通常是一个设置为一个数字的变量，它被递增来计算循环运行的次数。它也有时被称为**计数变量**。
    2. 一个**退出条件** - 如前面提到的，这个定义循环何时停止循环。这通常是一个表现为比较运算符的表达式，用于查看退出条件是否已满足的测试。
    3. 一个**最终条件** - 这总是被判断（或运行），每个循环已经通过一个完整的迭代消失时间。它通常用于增加（或在某些情况下递减）计数器变量，使其更接近退出条件值。
3. 一些包含代码块的花括号 - 每次循环迭代时都会运行这个代码。

我们来看一个真实的例子，所以我们可以看出这些做得更清楚。

```js
var cats = ["Bill", "Jeff", "Pete", "Biggles", "Jasmin"];
var info = "My cats are called ";
var para = document.querySelector("p");

for (var i = 0; i < cats.length; i++) {
  info += cats[i] + ", ";
}

para.textContent = info;
```

这给我们以下输出：
```

<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Basic for loop example</title>
    <style></style>
  </head>
  <body>
    <p></p>

    <script>
 var cats = ["Bill", "Jeff", "Pete", "Biggles", "Jasmin"];
 var info = "My cats are called ";
 var para = document.querySelector("p");

 for (var i = 0; i < cats.length; i++) {
 info += cats[i] + ", ";
 }

 para.textContent = info;
 </script>
  </body>
</html>
```

**备注：** 你可以在 GitHub 上找到这段示例代码。 （也可以在线运行）。

这显示了一个循环用于迭代数组中的项目，并与每个项目进行一些操作 - JavaScript 中非常常见的模式。这里：

1. 迭代器`i`从 0 开始（`var i = 0`）。
2. 循环将会一直运行直到它不再小于猫数组的长度。这很重要 - 退出条件显示循环仍然运行的条件。所以在这种情况下，`< cats.length` 仍然是真的，循环仍然运行。
3. 在循环中，我们将当前的循环项（`cats[i]`是`cats[当前下标的任何东西]`）以及逗号和空格连接到`info`变量的末尾。所以：
    1. 在第一次运行中，`i = 0`，所以 `cats[0] +', '` 将被连接到 `info("Bill")` 上。
    2. 在第二次运行中，`i = 1`，所以 `cats[1] +', '` 将被连接到 `info("Jeff")` 上。
    3. 等等。每次循环运行后，1 将被添加到 i（`i++`），然后进程将再次启动。
4. 当等于`cats.length`时，循环将停止，浏览器将移动到循环下面的下一个代码位。

**备注：** 我们将退出条件设为 `< cats.length`，而不是 `<= cats.length` 是因为计算机从 0 开始，而不是 1 - 开始时 `i` 是 0，并且逐步增加到 `i = 4`（最后一个数组的索引）。 `cats.length` 返回 5，因为数组中有 5 个项目，但是我们不希望达到 `i = 5`，因为这将返回未定义的最后一个项目（没有索引为 5 的数组项目）。所以我们想要比 `cats.length` 少 1（`i <`），而不是 `cats.length`（`i <=`）。

**备注：** 退出条件的一个常见错误是使它们使用“等于”(`===`) 而不是说“小于或等于”(`<=`)。如果我们想要运行我的循环到`i = 5`，退出条件将需要是`i <= cats.length`。如果我们设置为`i === cats.length`，循环将不会运行，因为在第一次循环迭代时 i 不等于 5，所以循环会立即停止。

我们留下的一个小问题是最后的输出句子形式不是很好：

>
> My cats are called Bill, Jeff, Pete, Biggles, Jasmin,
>
>
>

理想情况下，我们想改变最后循环迭代中的连接，以便在句子末尾没有逗号。嗯，没问题 - 我们可以很高兴地在我们的 for 循环中插入一个条件来处理这种特殊情况：

```js
for (var i = 0; i < cats.length; i++) {
  if (i === cats.length - 1) {
    info += "and " + cats[i] + ".";
  } else {
    info += cats[i] + ", ";
  }
}
```

**备注：** 你可以在 GitHub 上找到这个例子。（也可以在线运行）

**警告：** 使用`for`- 与所有循环一样，你必须确保初始化程序被迭代，以便最终达到退出条件。如果没有，循环将永不停止，浏览器将强制它停止，否则会崩溃。这被称为无限循环。

使用 break 退出循环
-------------

如果要在所有迭代完成之前退出循环，可以使用 break 语句。当我们查看 switch 语句时，我们已经在上一篇文章中遇到过这样的情况 - 当 switch 语句中符合输入表达式的情况满足时，break 语句立即退出 switch 语句并移动到代码之后。

与循环相同 - break 语句将立即退出循环，并使浏览器移动到跟随它的任何代码。

说我们想搜索一系列联系人和电话号码，只返回我们想要找的号码？首先，一些简单的 HTML - 一个文本`<input>`，允许我们输入一个名称来搜索，一个`<button>`元素来提交搜索，以及一个`<p>`元素显示结果：

```html
<label for="search">Search by contact name: </label>
<input id="search" type="text" />
<button>Search</button>

<p></p>
```

然后是 JavaScript：

```js
const contacts = [
  "Chris:2232322",
  "Sarah:3453456",
  "Bill:7654322",
  "Mary:9998769",
  "Dianne:9384975",
];
const para = document.querySelector("p");
const input = document.querySelector("input");
const btn = document.querySelector("button");

btn.addEventListener("click", function () {
  let searchName = input.value.toLowerCase();
  input.value = "";
  input.focus();
  for (let i = 0; i < contacts.length; i++) {
    let splitContact = contacts[i].split(":");
    if (splitContact[0].toLowerCase() === searchName) {
      para.textContent =
        splitContact[0] + "'s number is " + splitContact[1] + ".";
      break;
    } else if (i === contacts.length - 1) {
      para.textContent = "Contact not found.";
    }
  }
});
```

```
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Simple contact search example</title>
    <style></style>
  </head>
  <body>
    <label for="search">Search by contact name: </label>
    <input id="search" type="text" />
    <button>Search</button>

    <p></p>

    <script>
 var contacts = ['Chris:2232322', 'Sarah:3453456', 'Bill:7654322', 'Mary:9998769', 'Dianne:9384975'];
 var para = document.querySelector('p');
 var input = document.querySelector('input');
 var btn = document.querySelector('button');

 btn.addEventListener('click', function() {
 var searchName = input.value;
 input.value = '';
 input.focus();
 for (var i = 0; i < contacts.length; i++) {
 var splitContact = contacts[i].split(':');
 if (splitContact[0] === searchName) {
 para.textContent = splitContact[0] + '\'s number is ' + splitContact[1] + '.';
 break;
 } else if (i === contacts.length-1)
 para.textContent = 'Contact not found.';
 }
 }
 });
 </script>
  </body>
</html>
```

1. 首先我们有一些变量定义 - 我们有一个联系信息数组，每个项目是一个字符串，包含一个以冒号分隔的名称和电话号码。
2. 接下来，我们将一个事件监听器附加到按钮（`btn`）上，这样当按下它时，运行一些代码来执行搜索并返回结果。
3. 我们将输入的值输入到一个名为`searchName`的变量中，然后清空文本输入并重新对准它，准备进行下一个搜索。
4. 现在有趣的部分，for 循环：
    1. 我们的计数器开始时为在 0，直到计数器不再小于`contacts.length`，并在循环的每次迭代之后将`i`递增 1。
    2. 在循环中，我们首先将当前联系人（`contacts [i]`）拆分为冒号字符，并将生成的两个值存储在名为`splitContact`的数组中。
    3. 然后，我们使用条件语句来测试`splitContact [0]`（联系人姓名）是否等于输入的`searchName`。如果是，我们在段落中输入一个字符串来报告联系人的号码，并使用 break 来结束循环。
5. 在`(contacts.length-1)` 迭代后，如果联系人姓名与输入的搜索不符，则段落文本设置为“未找到联系人”，循环继续迭代。

**备注：** 你可以在 GitHub 上找到这个例子或在线运行。

使用 continue 跳过迭代
----------------

continue 语句以类似的方式工作，而不是完全跳出循环，而是跳过当前循环而执行下一个循环。我们来看另外一个例子，它把一个数字作为一个输入，并且只返回开平方之后为整数的数字（整数）。

HTML 与最后一个例子基本相同 - 一个简单的文本输入和一个输出段落。JavaScript 也是一样的，虽然循环本身有点不同：

```js
var num = input.value;

for (var i = 1; i <= num; i++) {
  var sqRoot = Math.sqrt(i);
  if (Math.floor(sqRoot) !== sqRoot) {
    continue;
  }

  para.textContent += i + " ";
}
```

Here's the output:
```

<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Integer squares generator</title>
    <style></style>
  </head>
  <body>
    <label for="number">Enter number: </label>
    <input id="number" type="text" />
    <button>Generate integer squares</button>

    <p>Output:</p>

    <script>
 var para = document.querySelector("p");
 var input = document.querySelector("input");
 var btn = document.querySelector("button");

 btn.addEventListener("click", function () {
 para.textContent = "Output: ";
 var num = input.value;
 input.value = "";
 input.focus();
 for (var i = 1; i <= num; i++) {
 var sqRoot = Math.sqrt(i);
 if (Math.floor(sqRoot) !== sqRoot) {
 continue;
 }

 para.textContent += i + " ";
 }
 });
 </script>
  </body>
</html>
```

1. 在这种情况下，输入应为数字（`num`）。for 循环给定一个从 1 开始的计数器（在这种情况下，我们对 0 不感兴趣），一个退出条件，当计数器大于输入`num`时循环将停止，并且迭代器的计数器将每次增加 1。
2. 在循环中，我们使用`Math.sqrt(i)`找到每个数字的平方根，然后测试平方根是否是一个整数，通过判断当它被向下取整时，它是否与自身相同（这是`Math.floor(...)`对传递的数字的作用）。
3. 如果平方根和四舍五入的平方根不相等（`!==`），则表示平方根不是整数，因此我们对此不感兴趣。在这种情况下，我们使用 continue 语句跳过当前循环而执行下一个循环迭代，而不在任何地方记录该数字。
4. 如果平方根是一个整数，我们完全跳过 if 块，所以 continue 语句不被执行; 相反，我们将当前 i 值加上一个空格连接到段落内容的末尾。

**备注：** 你可以在 GitHub 上查看完整代码，或者在线运行。

while 语句和 do ... while 语句
-------------------------

`for` 不是 JavaScript 中唯一可用的循环类型。实际上还有很多其他的，而现在你不需要理解所有这些，所以值得看几个人的结构，这样你就可以在稍微不同的方式识别出相同的功能。

首先，我们来看看 while 循环。这个循环的语法如下所示：
```

initializer
while (exit-condition) {
  // code to run

  final-expression
}
```

除了在循环之前设置初始化器变量，并且在运行代码之后，循环中包含 final-expression，而不是这两个项目被包含在括号中，这与以前的 for 循环非常类似。退出条件包含在括号内，前面是 while 关键字而不是 for。

同样的三个项目仍然存在，它们仍然以与 for 循环中相同的顺序定义 - 这是有道理的，因为你必须先定义一个初始化器，然后才能检查它是否已到达退出条件; 在循环中的代码运行（迭代已经完成）之后，运行最后的条件，这只有在尚未达到退出条件时才会发生。

我们再来看看我们的猫列表示例，但是重写了一个 while 循环：

```js
var i = 0;

while (i < cats.length) {
  if (i === cats.length - 1) {
    info += "and " + cats[i] + ".";
  } else {
    info += cats[i] + ", ";
  }

  i++;
}
```

**备注：** This still works just the same as expected — have a look at it running live on GitHub (also view the full source code).

do...while循环非常类似但在 while 后提供了终止条件：
```

initializer
do {
  // code to run

  final-expression
} while (exit-condition)
```

在这种情况下，在循环开始之前，初始化程序先重新开始。do 关键字直接在包含要运行的代码的花括号和终止条件之前。

这里的区别在于退出条件是一切都包含在括号中，而后面是一个 while 关键字。在 do ... while 循环中，花括号中的代码总是在检查之前运行一次，以查看是否应该再次执行（在 while 和 for 中，检查首先出现，因此代码可能永远不会执行）。

我们再次重写我们的猫列表示例，以使用 do...while 循环：

```js
var i = 0;

do {
  if (i === cats.length - 1) {
    info += "and " + cats[i] + ".";
  } else {
    info += cats[i] + ", ";
  }

  i++;
} while (i < cats.length);
```

**备注：** 再一次，它正如我们期望的那样工作 — 看一看它在Github 在线运行 (或者查看完整源代码).

**警告：** 使用 while 和 do...while — 所有循环都一样 — 你必须保证初始变量是迭代的，那么它才会逐渐地达到退出条件。不然，它将会永远循环下去，要么浏览器会强制终止它，要么它自己会崩溃。这称为无限循环。

主动学习：启动倒计时！
-----------

在这个练习中，我们希望你打印出一个简单的启动倒计时到输出框，从 10 到关闭。具体来说，我们希望你：

* 从 10 下降到 0.我们为你提供了一个初始化器 - var i = 10;
* 对于每次迭代，创建一个新的段落并将其附加到输出<div>，我们使用`var output = document.querySelector('.output');`。在评论中，我们为你提供了需要在循环中某处使用的三条代码行：
    + `var para = document.createElement('p');` —新建一个段落。
    + `output.appendChild(para);` — 将段落附加到输出 `<div>`中。
    + `para.textContent =` — 段落内的文字等于你放在右侧的任何内容。
* 不同的迭代数字需要将不同的文本放在该迭代的段落中（你需要一个条件语句和多个`para.textContent = lines`）：
    + 如果数字是 10，打印“Countdown 10”到段落。
    + 如果数字为 0，请打印“Blast off！”到段落。
    + 对于任何其他数字，只打印段落的数字。
* 记住要包括一个迭代器！然而，在这个例子中，我们在每次迭代之后都下降，而不是上升，所以你不想要`i++` - 你如何向下迭代？

如果你犯了错误，你可以随时使用“重置”按钮重置该示例。如果你真的卡住了，请按“显示解决方案”来查看解决方案。
```

<div class="output" style="height: 410px;overflow: auto;"></div>

<textarea id="code" class="playable-code" style="height: 300px;">
var output = document.querySelector('.output');
output.innerHTML = '';

// var i = 10;

// var para = document.createElement('p');
// para.textContent = ;
// output.appendChild(para);
</textarea>

<div class="playable-buttons">
  <input id="reset" type="button" value="Reset" />
  <input id="solution" type="button" value="Show solution" />
</div>
```

```
var textarea = document.getElementById("code");
var reset = document.getElementById("reset");
var solution = document.getElementById("solution");
var code = textarea.value;

function updateCode() {
  eval(textarea.value);
}

reset.addEventListener("click", function () {
  textarea.value = code;
  updateCode();
});

solution.addEventListener("click", function () {
  textarea.value = jsSolution;
  updateCode();
});

var jsSolution =
  "var output = document.querySelector('.output');\noutput.innerHTML = '';\n\nvar i = 10;\n\nwhile(i >= 0) {\n var para = document.createElement('p');\n if(i === 10) {\n para.textContent = 'Countdown ' + i;\n } else if(i === 0) {\n para.textContent = 'Blast off!';\n } else {\n para.textContent = i;\n }\n\n output.appendChild(para);\n\n i--;\n}";

textarea.addEventListener("input", updateCode);
window.addEventListener("load", updateCode);
```

主动学习：填写来宾列表
-----------

在本练习中，我们希望你获取存储在数组中的名称列表，并将其放入来宾列表中。但这不是那么容易 - 我们不想让菲尔和洛拉进来，因为他们是贪婪和粗鲁的，总是吃所有的食物！我们有两个名单，一个是客人承认的，一个是客人拒绝的。

具体来说，我们希望你：

* 编写一个循环，它将从 0 迭代到 people 数组的长度。你需要从一个初始化器`var i = 0`开始，但是你需要什么退出条件？
* 在每个循环迭代期间，使用条件语句检查当前数组项是否等于“Phil”或“Lola”：
    + 如果是，则将数组项连接到拒绝段落的`textContent`的末尾，后跟逗号和空格。
    + 如果不是，则将数组项连接到接收段落的`textContent`的末尾，后跟逗号和空格。

我们已经提供给你：

* `var i = 0;` — 你的初始化程序
* `refused.textContent +=` - 将连接某些东西的行的开头，结束于`refused.textContent`。
* `admitted.textContent +=` - 将连接某些内容到一行的结尾的行的开始。

额外的奖金问题 - 成功完成上述任务后，你将留下两个名称列表，用逗号分隔，但它们将不整齐 - 每个结尾处都会有一个逗号。你可以制定出如何在每种情况下编写最后一个逗号的行，并添加一个完整的停止？看看有用的字符串方法文章的帮助。

如果你犯了错误，你可以随时使用“重置”按钮重置该示例。如果你真的卡住了，请按“显示解决方案”来查看解决方案。
```

<div class="output" style="height: 100px;overflow: auto;">
  <p class="admitted">Admit:</p>
  <p class="refused">Refuse:</p>
</div>

<textarea id="code" class="playable-code" style="height: 400px;">
var people = ['Chris', 'Anne', 'Colin', 'Terri', 'Phil', 'Lola', 'Sam', 'Kay', 'Bruce'];

var admitted = document.querySelector('.admitted');
var refused = document.querySelector('.refused');
admitted.textContent = 'Admit: ';
refused.textContent = 'Refuse: '

// var i = 0;

// refused.textContent += ;
// admitted.textContent += ;

</textarea>

<div class="playable-buttons">
  <input id="reset" type="button" value="Reset" />
  <input id="solution" type="button" value="Show solution" />
</div>
```

```
var textarea = document.getElementById("code");
var reset = document.getElementById("reset");
var solution = document.getElementById("solution");
var code = textarea.value;

function updateCode() {
  eval(textarea.value);
}

reset.addEventListener("click", function () {
  textarea.value = code;
  updateCode();
});

solution.addEventListener("click", function () {
  textarea.value = jsSolution;
  updateCode();
});

var jsSolution =
  "var people = ['Chris', 'Anne', 'Colin', 'Terri', 'Phil', 'Lola', 'Sam', 'Kay', 'Bruce'];\n\nvar admitted = document.querySelector('.admitted');\nvar refused = document.querySelector('.refused');\n\nvar i = 0;\n\ndo {\n if(people[i] === 'Phil' || people[i] === 'Lola') {\n refused.textContent += people[i] + ', ';\n } else {\n admitted.textContent += people[i] + ', ';\n }\n i++;\n} while(i < people.length);\n\nrefused.textContent = refused.textContent.slice(0,refused.textContent.length-2) + '.';\nadmitted.textContent = admitted.textContent.slice(0,admitted.textContent.length-2) + '.';";

textarea.addEventListener("input", updateCode);
window.addEventListener("load", updateCode);
```

应该使用哪种循环类型？
-----------

对于基本用途，for，while 和 do ... while 循环大部分可互换。他们都可以用来解决相同的问题，你使用哪一个将在很大程度上取决于你的个人偏好 - 哪一个你最容易记住或最直观的。我们再来看看他们。

首先是 `for`:
```

for (initializer; exit-condition; final-expression) {
  // code to run
}
```

`while`:
```

initializer
while (exit-condition) {
  // 要运行的代码

  final-expression
}
```

最后是 `do...while`:
```

initializer
do {
  // 要运行的代码

  final-expression
} while (exit-condition)
```

我们建议使用`for`，因为它可能是最简单地帮你记住一切 - 初始化程序，退出条件和最终表达式都必须整齐地放入括号，所以很容易看到他们在哪里并检查你没有丢失他们。

**备注：** 还有其他循环类型/特性，这些特性在 高级/专门 的情况下是有用的，超出了本文的范围。如果你想进一步了解循环学习，请阅读我们的高级循环和迭代指南。

结论
--

本文向你展示了背后的基本概念，以及 JavaScript 中循环代码时可用的不同选项。你现在应该明白为什么循环是一个处理重复代码的好机制，并且在你自己的例子中使用它们！

如果你有什么不明白的地方，可以再通读一遍，或者联系我们寻求帮助。

参见
--

* Loops and iteration in detail
* for statement reference
* while and do...while references
* break and continue references
* What's the Best Way to Write a JavaScript For Loop? — some advanced loop best practices
* 上一页
* Overview: Building blocks
* 下一页