JavaScript 模块 - JavaScript

JavaScript 模块
=============

* « 上一页

这篇指南会给你入门 JavaScript 模块的全部信息。

模块化的背景
------

JavaScript 程序本来很小——在早期，它们大多被用来执行独立的脚本任务，在你的 web 页面需要的地方提供一定交互，所以一般不需要多大的脚本。过了几年，我们现在有了运行大量 JavaScript 脚本的复杂程序，还有一些被用在其他环境（例如 Node.js）。

因此，近年来，有必要开始考虑提供一种**将 JavaScript 程序拆分为可按需导入的单独模块**的机制。Node.js 已经提供这个能力很长时间了，还有很多的 JavaScript 库和框架已经开始了模块的使用（例如，CommonJS 和基于 AMD 的其他模块系统 如 RequireJS，以及最新的 Webpack 和 Babel）。

好消息是，最新的浏览器开始原生支持模块功能了，这是本文要重点讲述的。这会是一个好事情 —- 浏览器能够最优化加载模块，使它比使用库更有效率：使用库通常需要做额外的客户端处理。

浏览器兼容性
------

### javascript.statements.import

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

### javascript.statements.export

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

介绍一个例子
------

为了演示模块的使用，我们创建了一系列简单的示例 ，你可以在 GitHub 上找到。这个例子演示了一个简单的模块的集合用来在 web 页面上创建了一个 `<canvas>` 标签，在 canvas 上绘制 (并记录有关的信息) 不同形状。

这的确有点简单，但是保持足够简单能够清晰地演示模块。

**备注：** 如果你想去下载这个例子在本地运行，你需要通过本地 web 服务器去运行。

基本的示例文件的结构
----------

在我们的第一个例子（参见 basic-modules）文件结构如下：
```

index.html
main.js
modules/
    canvas.js
    square.js
```

**备注：** 在这个指南的全部示例项目的文件结构是基本相同的；需要熟悉上面的内容

modules 目录下的两个模块的描述如下：

* `canvas.js` — 包含与设置画布相关的功能：
    + `create()` — 在指定 ID 的包装器 `<div>` 内创建指定 `width` 和 `height` 的画布，该 ID 本身附加在指定的父元素内。返回包含画布的 2D 上下文和包装器 ID 的对象。
    + `createReportList()` — 创建一个附加在指定包装器元素内的无序列表，该列表可用于将报告数据输出到。返回列表的 ID。
* `square.js` — 包含：
    + `name` — 包含字符串 'square' 的常量。
    + `draw()` — 在指定画布上绘制一个正方形，具有指定的大小，位置和颜色。返回包含正方形大小，位置和颜色的对象。
    + `reportArea()` — 在给定长度的情况下，将正方形区域写入特定报告列表。
    + `reportPerimeter()` — 在给定长度的情况下，将正方形的周长写入特定的报告列表。

**备注：** 在原生 JavaScript 模块中，扩展名 `.mjs` 非常重要，因为使用 MIME-type 为 `javascript/esm` 来导入文件（其他的 JavaScript 兼容 MIME-type 像 `application/javascript` 也可以），它避免了严格的 MIME 类型检查错误，像 "The server responded with a non-JavaScript MIME type"。除此之外，`.mjs` 的扩展名很明了（比如这个就是一个模块，而不是一个传统 JavaScript 文件），还能够和其他工具互相适用。看这个 Google's note for further details。

`.mjs` 与 `.js`
--------------

纵观此文，我们使用 `.js` 扩展名的模块文件，但在其他一些文章中，你可能会看到 `.mjs` 扩展名的使用。V8 推荐了这样的做法，比如有下列理由：

* 比较清晰，这可以指出哪些文件是模块，哪些是常规的 JavaScript。
* 这能保证你的模块可以被运行时环境和构建工具识别，比如 Node.js 和 Babel。

但是我们决定继续使用 `.js` 扩展名，未来可能会更改。为了使模块可以在浏览器中正常地工作，你需要确保你的服务器能够正常地处理 `Content-Type` 头，其应该包含 JavaScript 的 MIME 类型 `text/javascript`。如果没有这么做，你可能会得到 一个严格 MIME 类型检查错误：“The server responded with a non-JavaScript MIME type（服务器返回了非 JavaScript MIME 类型）”，并且浏览器会拒绝执行相应的 JavaScript 代码。多数服务器可以正确地处理 `.js` 文件的类型，但是 `.mjs` 还不行。已经可以正常响应 `.mjs` 的服务器有 GitHub 页面 和 Node.js 的 `http-server`。

如果你已经在使用相应的环境了，那么一切正常。或者如果你还没有，但你知道你在做什么（比如你可以配置服务器以为 `.mjs` 设置正确的 `Content-Type`）。但如果你不能控制提供服务，或者用于公开文件发布的服务器，这可能会导致混乱。

为了学习和保证代码的可移植的目的，我们建议使用 `.js`。

如果你认为使用 `.mjs` 仅用于模块带来的清晰性非常重要，但不想引入上面描述的相应问题，你可以仅在开发过程中使用 `.mjs`，而在构建过程中将其转换为 `.js`。

另注意：

* 一些工具不支持 `.mjs`，比如 TypeScript。
* `<script type="module">` 属性用于指示引入的模块，你会在下面看到。
导出模块的功能
-------

为了获得模块的功能要做的第一件事是把它们导出来。使用 `export` 语句来完成。

最简单的方法是把它（指上面的 export 语句）放到你想要导出的项前面，比如：

```js
export const name = "square";

export function draw(ctx, length, x, y, color) {
  ctx.fillStyle = color;
  ctx.fillRect(x, y, length, length);

  return {
    length: length,
    x: x,
    y: y,
    color: color,
  };
}
```

你能够导出函数，`var`，`let`，`const`, 和等会会看到的类。export 要放在最外层；比如你不能够在函数内使用 `export`。

一个更方便的方法导出所有你想要导出的模块的方法是在模块文件的末尾使用一个 export 语句，语句是用花括号括起来的用逗号分割的列表。比如：

```js
export { name, draw, reportArea, reportPerimeter };
```

导入功能到你的脚本
---------

你想在模块外面使用一些功能，那你就需要导入他们才能使用。最简单的就像下面这样的：
```

import { name, draw, reportArea, reportPerimeter } from '/js-examples/modules/basic-modules/modules/square.js';
```

使用 `import` 语句，然后你被花括号包围的用逗号分隔的你想导入的功能列表，然后是关键字 from，然后是模块文件的路径。模块文件的路径是相对于站点根目录的相对路径，对于我们的 `basic-modules` 应该是 `/js-examples/modules/basic-modules`。

当然，我们写的路径有一点不同——我们使用点语法意味“当前路径”，跟随着包含我们想要找的文件的路径。这比每次都要写下整个相对路径要好得多，因为它更短，使得 URL 可移植——如果在站点层中你把它移动到不同的路径下面仍然能够工作（修订版 1889482）。

那么看看例子吧：
```

/js/examples/modules/basic-modules/modules/square.js
```

变成了
```

./modules/square.js
```

你可以在 `main.js` 中看到这些。

**备注：** 在一些模块系统中你可以忽略文件扩展名（比如 `'/model/squre'`）。这在原生 JavaScript 模块系统中不工作。~~此外，记住你需要包含最前面的正斜杠。~~ （修订版 1889482）

因为你导入了这些功能到你的脚本文件，你可以像定义在相同的文件中的一样去使用它。下面展示的是在 `main.js` 中的 import 语句下面的内容。

```js
let myCanvas = create("myCanvas", document.body, 480, 320);
let reportList = createReportList(myCanvas.id);

let square1 = draw(myCanvas.ctx, 50, 50, 100, "blue");
reportArea(square1.length, reportList);
reportPerimeter(square1.length, reportList);
```

应用模块到你的 HTML
------------

现在我们只需要将 main.js 模块应用到我们的 HTML 页面。这与我们将常规脚本应用于页面的方式非常相似，但有一些显着的差异。

首先，你需要把 `type="module"` 放到 `<script>` 标签中，来声明这个脚本是一个模块：
```

<script type="module" src="main.js"></script>
```

你导入模块功能的脚本基本是作为顶级模块。如果省略它，Firefox 就会给出错误“SyntaxError: import declarations may only appear at top level of a module。

你只能在模块内部使用 `import` 和`export` 语句；不是普通脚本文件。

**备注：** 你还可以将模块导入内部脚本，只要包含 `type="module"`，例如 `<script type="module"> //include script here </script>`。

其他模块与标准脚本的不同
------------

* 你需要注意本地测试——如果你通过本地加载 HTML 文件（比如一个 `file://` 路径的文件），你将会遇到 CORS 错误，因为 JavaScript 模块安全性需要。你需要通过一个服务器来测试。
* 另请注意，你可能会从模块内部定义的脚本部分获得与标准脚本中不同的行为。这是因为模块自动使用严格模式。
* 加载一个模块脚本时不需要使用 `defer` 属性 (see `<script>` attributes) 模块会自动延迟加载。
* 最后一个但不是不重要，你需要明白模块功能导入到单独的脚本文件的范围——他们无法在全局获得。因此，你只能在导入这些功能的脚本文件中使用他们，你也无法通过 JavaScript console 中获取到他们，比如，在 DevTools 中你仍然能够获取到语法错误，但是你可能无法像你想的那样使用一些 debug 技术。
默认导出 versus 命名导出
----------------

到目前为止我们导出的功能都是由 **named exports** 组成 —- 每个项目（无论是函数，常量等）在导出时都由其名称引用，并且该名称也用于在导入时引用它。

还有一种导出类型叫做 **default export** —- 这样可以很容易地使模块提供默认功能，并且还可以帮助 JavaScript 模块与现有的 CommonJS 和 AMD 模块系统进行互操作（正如 ES6 In Depth: Modules by Jason Orendorff 的模块中所解释的那样；搜索“默认导出”）。

看个例子来解释它如何工作。在我们的基本模块 `square.js` 中，你可以找到一个名为 `randomSquare()` 的函数，它创建一个具有随机颜色，大小和位置的正方形。我们想作为默认导出，所以在文件的底部我们这样写：

```js
export default randomSquare;
```

注意，不要大括号。

我们可以把 `export default` 放到函数前面，定义它为一个匿名函数，像这样：

```js
export default function(ctx) {
  ...
}
```

在我们的 `main.js` 文件中，我们使用以下行导入默认函数：

```js
import randomSquare from "./modules/square.js";
```

同样，没有大括号，因为每个模块只允许有一个默认导出，我们知道 `randomSquare` 就是需要的那个。上面的那一行相当于下面的缩写：

```js
import { default as randomSquare } from "./modules/square.js";
```

**备注：** 重命名导出项的 as 语法在下面的 重命名导出与导入 部分中进行了说明。

避免命名冲突
------

到目前为止，我们的 canvas 图形绘制模块看起来工作的很好。但是如果我们添加一个绘制其他形状的比如圆形或者矩形的模块会发生什么？这些形状可能会有相关的函数比如 `draw()`、`reportArea()`，等等；如果我们用相同的名字导入不同的函数到顶级模块文件中，我们会收到冲突和错误。

幸运的是，有很多方法来避免。我们将会在下一个节看到。

重命名导出与导入
--------

在你的 `import` 和 `export` 语句的大括号中，可以使用 `as` 关键字跟一个新的名字，来改变你在顶级模块中将要使用的功能的标识名字。因此，例如，以下两者都会做同样的工作，尽管方式略有不同：

```js
// inside module.js
export { function1 as newFunctionName, function2 as anotherNewFunctionName };

// inside main.js
import { newFunctionName, anotherNewFunctionName } from "/modules/module.js";
```

```js
// inside module.js
export { function1, function2 };

// inside main.js
import {
  function1 as newFunctionName,
  function2 as anotherNewFunctionName,
} from "/modules/module.js";
```

让我们看一个真实的例子。在我们的 renaming 目录中，你将看到与上一个示例中相同的模块系统，除了我们添加了 `circle.js` 和 `triangle.js` 模块以绘制和报告圆和三角形。

在每个模块中，我们都有 `export` 相同名称的功能，因此每个模块底部都有相同的导出语句：

```js
export { name, draw, reportArea, reportPerimeter };
```

将它们导入 `main.js` 时，如果我们尝试使用

```js
import { name, draw, reportArea, reportPerimeter } from "./modules/square.js";
import { name, draw, reportArea, reportPerimeter } from "./modules/circle.js";
import { name, draw, reportArea, reportPerimeter } from "./modules/triangle.js";
```

浏览器会抛出一个错误，例如“SyntaxError: redeclaration of import name”（Firefox）。

相反，我们需要重命名导入，使它们是唯一的：

```js
import {
  name as squareName,
  draw as drawSquare,
  reportArea as reportSquareArea,
  reportPerimeter as reportSquarePerimeter,
} from "./modules/square.js";

import {
  name as circleName,
  draw as drawCircle,
  reportArea as reportCircleArea,
  reportPerimeter as reportCirclePerimeter,
} from "./modules/circle.js";

import {
  name as triangleName,
  draw as drawTriangle,
  reportArea as reportTriangleArea,
  reportPerimeter as reportTrianglePerimeter,
} from "./modules/triangle.js";
```

请注意，你可以在模块文件中解决问题，例如

```js
// in square.js
export {
  name as squareName,
  draw as drawSquare,
  reportArea as reportSquareArea,
  reportPerimeter as reportSquarePerimeter,
};
```

```js
// in main.js
import {
  squareName,
  drawSquare,
  reportSquareArea,
  reportSquarePerimeter,
} from "/js-examples/modules/renaming/modules/square.js";
```

它也会起作用。你使用什么样的风格取决于你，但是单独保留模块代码并在导入中进行更改可能更有意义。当你从没有任何控制权的第三方模块导入时，这尤其有意义。

创建模块对象
------

上面的方法工作的挺好，但是有一点点混乱、亢长。一个更好的解决方是，导入每一个模块功能到一个模块功能对象上。可以使用以下语法形式：

```js
import \* as Module from "/modules/module.js";
```

这将获取 `module.js` 中所有可用的导出，并使它们可以作为对象模块的成员使用，从而有效地为其提供自己的命名空间。例如：

```js
Module.function1();
Module.function2();
```

再次，让我们看一个真实的例子。如果你转到我们的 module-objects 目录，将再次看到相同的示例，但利用上述的新语法进行重写。在模块中，导出都是以下简单形式：

```js
export { name, draw, reportArea, reportPerimeter };
```

另一方面，导入看起来像这样：

```js
import \* as Canvas from "./modules/canvas.js";

import \* as Square from "/./modules/square.js";
import \* as Circle from "./modules/circle.js";
import \* as Triangle from "./modules/triangle.js";
```

在每种情况下，你现在可以访问指定对象名称下面的模块导入。

```js
let square1 = Square.draw(myCanvas.ctx, 50, 50, 100, "blue");
Square.reportArea(square1.length, reportList);
Square.reportPerimeter(square1.length, reportList);
```

因此，你现在可以像以前一样编写代码（只要你在需要时包含对象名称），并且导入更加整洁。

模块与类（class）
-----------

正如我们之前提到的那样，你还可以导出和导入类；这是避免代码冲突的另一种选择，如果你已经以面向对象的方式编写了模块代码，那么它尤其有用。

你可以在我们的 classes 目录中看到使用 ES 类重写的形状绘制模块的示例。例如，`square.js` 文件现在包含单个类中的所有功能：

```js
class Square {
  constructor(ctx, listId, length, x, y, color) {
    // …
  }

  draw() {
    // …
  }

  // …
}
```

然后我们导出：

```js
export { Square };
```

在 `main.js` 中，我们像这样导入它：

```js
import { Square } from "./modules/square.js";
```

然后使用该类绘制我们的方块：

```js
let square1 = new Square(myCanvas.ctx, myCanvas.listId, 50, 50, 100, "blue");
square1.draw();
square1.reportArea();
square1.reportPerimeter();
```

合并模块
----

有时你会想要将模块聚合在一起。你可能有多个级别的依赖项，你希望简化事物，将多个子模块组合到一个父模块中。这可以使用父模块中以下表单的导出语法：

```js
export \* from "x.js";
export { name } from "x.js";
```

**备注：** 这实际上是导入后跟导出的简写，即“我导入模块 `x.js`，然后重新导出部分或全部导出”。

有关示例，请参阅我们的 module-aggregation。在这个例子中（基于我们之前的类示例），我们有一个名为 `shapes.js` 的额外模块，它将 `circle.js`，`square.js` 和 `riangle.mjs` 中的所有功能聚合在一起。我们还将子模块移动到名为 shapes 的 modules 目录中的子目录中。所以模块结构现在是这样的：
```

modules/
  canvas.js
  shapes.js
  shapes/
    circle.js
    square.js
    triangle.js
```

在每个子模块中，输出具有相同的形式，例如，

```js
export { Square };
```

接下来是聚合部分。在 `shapes.js` 里面，我们包括以下几行：

```js
export { Square } from "/js-examples/modules/module-aggregation/modules/shapes/square.js";
export { Triangle } from "/js-examples/modules/module-aggregation/modules/shapes/triangle.js";
export { Circle } from "/js-examples/modules/module-aggregation/modules/shapes/circle.js";
```

它们从各个子模块中获取导出，并有效地从 `shapes.js` 模块中获取它们。

**备注：** 即使 `shapes.js` 文件位于 modules 目录中，我们仍然需要相对于模块根目录编写这些 URL，因此需要 `/modules/`。这是使用 JavaScript 模块时混淆的常见原因。

**备注：** `shapes.js` 中引用的导出基本上通过文件重定向，并且实际上并不存在，因此你将无法在同一文件中编写任何有用的相关代码。

所以现在在 `main.js` 文件中，我们可以通过替换来访问所有三个模块类

```js
import { Square } from "./modules/square.js";
import { Circle } from "./modules/circle.js";
import { Triangle } from "./modules/triangle.js";
```

使用以下单行：

```js
import { Square, Circle, Triangle } from "./modules/shapes.js";
```

动态加载模块
------

浏览器中可用的 JavaScript 模块功能的最新部分是动态模块加载。这允许你仅在需要时动态加载模块，而不必预先加载所有模块。这有一些明显的性能优势；让我们继续阅读，看看它是如何工作的。

这个新功能允许你将 `import()` 作为函数调用，将模块的路径作为参数传递。它返回一个 promise，它用一个模块对象来实现（参见创建模块对象），让你可以访问该对象的导出，例如

```js
import("/modules/mymodule.js").then((module) => {
  // Do something with the module.
});
```

我们来看一个例子。在 dynamic-module-imports 目录中，我们有另一个基于类示例的示例。但是这次我们在示例加载时没有在画布上绘制任何东西。相反，我们包括三个按钮——“圆形”、“方形”和“三角形”——按下时，动态加载所需的模块，然后使用它来绘制相关的形状。

在这个例子中，我们只对 index.html 和 main.js 文件进行了更改——模块导出保持与以前相同。

在`main.js`中，我们使用`document.querySelector()`调用获取了对每个按钮的引用，例如：

```js
let squareBtn = document.querySelector(".square");
```

然后，我们为每个按钮附加一个事件监听器，以便在按下时，相关模块被动态加载并用于绘制形状：

```js
squareBtn.addEventListener("click", () => {
  import("/js-examples/modules/dynamic-module-imports/modules/square.js").then(
    (Module) => {
      let square1 = new Module.Square(
        myCanvas.ctx,
        myCanvas.listId,
        50,
        50,
        100,
        "blue",
      );
      square1.draw();
      square1.reportArea();
      square1.reportPerimeter();
    },
  );
});
```

请注意，由于 promise 履行会返回一个模块对象，因此该类成为对象的子特征，因此我们现在需要使用 `Module` 访问构造函数。在它之前，例如 `Module.Square( ... )`。

故障排除
----

如果为了你的模块有问题，这里有一些提示有可能帮助到你。如果你发现更多的内容欢迎添加进来！

* 在前面已经提到了，在这里再重申一次： `.mjs` 后缀的文件需要以 MIME-type 为 `javascript/esm` 来加载 (或者其他的 JavaScript 兼容的 MIME-type，比如 `application/javascript`), 否则，你会一个严格的 MIME 类型检查错误，像是这样的 "The server responded with a non-JavaScript MIME type".
* 如果你尝试用本地文件加载 HTML 文件 (i.e. with a `file://` URL)，由于 JavaScript 模块的安全性要求，你会遇到 CORS 错误。你需要通过服务器来做你的测试。GitHub pages is ideal as it also serves `.mjs` files with the correct MIME type.
* 因为 `.mjs` 是一个相当新的文件后缀，一些操作系统可能无法识别，或者尝试把它替换成别的。比如，我们发现 macOS 悄悄地在我们的 `.mjs` 后缀的文件后面添加上 `.js` 然后自动隐藏这个后缀。所以我们的文件实际上都是 `x.mjs.js`。当我们关闭自动隐藏文件后缀名，让它去接受认可 `.mjs`。问题解决。
参见
--

* Using JavaScript modules on the web, by Addy Osmani and Mathias Bynens
* ES modules: A cartoon deep-dive, Hacks blog post by Lin Clark
* ES6 in Depth: Modules, Hacks blog post by Jason Orendorff
* Axel Rauschmayer's book Exploring JS: Modules
* « 上一页


语法和数据类型 - JavaScript

语法和数据类型
=======

* « 上一页
* 下一页 »

本章讨论 JavaScript 的基本语法、变量声明、数据类型和字面量（literal）。

基础
--

JavaScript 借鉴了 Java 的大部分语法，但同时也受到 Awk、Perl 和 Python 的影响。

JavaScript 是**区分大小写**的，并使用 **Unicode** 字符集。举个例子，可以将单词 Früh（在德语中意思是“早”）用作变量名。
```

var Früh = "foobar";
```

但是，由于 JavaScript 是大小写敏感的，因此变量 `früh` 和 `Früh` 则是两个不同的变量。

在 JavaScript 中，指令被称为语句，并用分号（;）进行分隔。

如果一条语句独占一行的话，那么分号是可以省略的。（译者注：并不建议这么做。）但如果一行中有多条语句，那么这些语句必须以分号分开。

**备注：** ECMAScript 规定了在语句的末尾自动插入分号（ASI）。（如果想要了解更多信息，请参阅 JavaScript 词法语法 。）

虽然不是必需的，但是在一条语句的末尾加上分号是一个很好的习惯。这个习惯可以大大减少代码中产生 bug 的可能性。

Javascript 源码从左往右被扫描并转换成一系列由 token、控制字符、行终止符、注释和空白字符组成的输入元素。空白字符指的是空格、制表符和换行符等。

注释
--

**Javascript 注释**的语法和 C++ 或许多其他语言类似：

```js
// 单行注释

/\* 这是一个更长的，
 多行注释
\*/

/\* 然而，你不能，/\* 嵌套注释 \*/ 语法错误 \*/
```

在代码执行过程中，注释将被自动跳过（不执行）。

**备注：** 你可能会在一些 JavaScript 脚本中见到像这样 `#!/usr/bin/env node` 的第三种注释

这种注释我们称为**hashbang 注释**，这种注释被用来指定执行 JaveScript 脚本的引擎的路径，查看这篇文章来了解更详细的信息：Hashbang 注释

声明
--

JavaScript 有三种声明方式。

`var`

声明一个变量，可选初始化一个值。

`let`

声明一个块作用域的局部变量，可选初始化一个值。

`const`

声明一个块作用域的只读常量。

### 变量

在应用程序中，使用变量来作为值的符号名。变量的名字又叫做标识符，其需要遵守一定的规则。

一个 JavaScript 标识符必须以字母、下划线（\_）或者美元符号（$）开头；后续的字符也可以是数字（0-9）。因为 JavaScript 语言是区分大小写的，所以字母可以是从“A”到“Z”的大写字母和从“a”到“z”的小写字母。

你可以使用大部分 ISO 8859-1 或 Unicode 编码的字符作标识符，例如 å 和 ü（详情可查看这篇博客文章）。你也可以使用 Unicode 转义字符作标识符。

合法的标识符示例：`Number_hits`、`temp99`、`$credit` 和 `_name`。

### 声明变量

你可以用以下三种方式声明变量：

* 使用关键词 `var` 。例如 `var x = 42`。这个语法可以用来声明局部变量和全局变量。
* 直接赋值。例如 `x = 42`。在函数外使用这种形式赋值，会产生一个全局变量。在严格模式下会产生错误。因此你不应该使用这种方式来声明变量。
* 使用关键词 `let` 。例如 `let y = 13`。这个语法可以用来声明块作用域的局部变量。參考下方变量的作用域 (Variable scope)。

你可以使用解构赋值将对象字面量的属性绑定到变量。就像这样 `let { bar } = foo`。这会创建一个名为 `bar` 的变量，并且将 `foo` 对象中属性名与之相同的属性的值赋给它。

你也可以直接给属性赋值。像这样 `x = 42`。这种声明方式将会创建一个未声明全局变量。这样做还会产生 JavaScript 警告。因为未声明的全局变量常常导致预期之外的行为，所以不建议使用。

### 变量求值

用 `var` 或 `let` 语句声明的变量，如果没有赋初始值，则其值为 `undefined`。

如果访问一个未声明的变量会导致抛出 `ReferenceError` 异常：

```js
var a;
console.log("The value of a is " + a); // a 的值是 undefined

console.log("The value of b is " + b); // b 的值是 undefined
var b;
// 在你阅读下面的‘变量声明提升’前你可能会疑惑

console.log("The value of c is " + c); // 未捕获的引用错误：c 未被定义

let x;
console.log("The value of x is " + x); // x 的值是 undefined

console.log("The value of y is " + y); // 未捕获的引用错误：y 未被定义
let y;
```

你可以使用 `undefined` 来判断一个变量是否已赋值。在以下的代码中，变量`input`未被赋值，因此 `if` (en-US) 条件语句的求值结果是 `true`。

```js
var input;
if (input === undefined) {
  doThis();
} else {
  doThat();
}
```

`undefined` 值在布尔类型环境中会被当作 `false` 。例如，下面的代码将会执行函数 `myFunction`，因为数组 `myArray` 中的元素未被赋值：

```js
var myArray = [];
if (!myArray[0]) myFunction();
```

数值类型环境中 `undefined` 值会被转换为 `NaN`。

```js
var a;
a + 2; // 计算为 NaN
```

当你对一个 `null` 变量求值时，空值 `null` 在数值类型环境中会被当作 0 来对待，而布尔类型环境中会被当作 `false`。例如：

```js
var n = null;
console.log(n \* 32); // 在控制台中会显示 0
```

### 变量的作用域

在函数之外声明的变量，叫做*全局*变量，因为它可被当前文档中的任何其他代码所访问。在函数内部声明的变量，叫做*局部*变量，因为它只能在当前函数的内部访问。

ECMAScript 6 之前的 JavaScript 没有语句块作用域；相反，语句块中声明的变量将成为语句块所在函数（或全局作用域）的局部变量。例如，如下的代码将在控制台输出 5，因为 `x` 的作用域是声明了 `x` 的那个函数（或全局范围），而不是 `if` 语句块。

```js
if (true) {
  var x = 5;
}
console.log(x); // 5
```

如果使用 ECMAScript 6 中的 `let` 声明，上述行为将发生变化。

```js
if (true) {
  let y = 5;
}
console.log(y); // ReferenceError: y 没有被声明
```

### 变量提升

JavaScript 变量的另一个不同寻常的地方是，你可以先使用变量稍后再声明变量而不会引发异常。这一概念称为变量提升；JavaScript 变量感觉上是被“提升”或移到了函数或语句的最前面。但是，提升后的变量将返回 undefined 值。因此在使用或引用某个变量之后进行声明和初始化操作，这个被提升的变量仍将返回 undefined 值。

```js
/\*\*
 \* 例子 1
 \*/
console.log(x === undefined); // true
var x = 3;

/\*\*
 \* 例子 2
 \*/
// will return a value of undefined
var myvar = "my value";

(function () {
  console.log(myvar); // undefined
  var myvar = "local value";
})();
```

上面的例子，也可写作：

```js
/\*\*
 \* 例子 1
 \*/
var x;
console.log(x === undefined); // true
x = 3;

/\*\*
 \* 例子 2
 \*/
var myvar = "my value";

(function () {
  var myvar;
  console.log(myvar); // undefined
  myvar = "local value";
})();
```

由于存在变量提升，一个函数中所有的`var`语句应尽可能地放在接近函数顶部的地方。这个习惯将大大提升代码的清晰度。

在 ECMAScript 6 中，`let` 和 `const` 同样**会被提升**变量到代码块的顶部但是不会被赋予初始值。在变量声明之前引用这个变量，将抛出引用错误（ReferenceError）。这个变量将从代码块一开始的时候就处在一个“暂时性死区”，直到这个变量被声明为止。

```js
console.log(x); // ReferenceError
let x = 3;
```

### 函数提升

对于函数来说，只有函数声明会被提升到顶部，而函数表达式不会被提升。

```js
/\* 函数声明 \*/

foo(); // "bar"

function foo() {
  console.log("bar");
}

/\* 函数表达式 \*/

baz(); // 类型错误：baz 不是一个函数

var baz = function () {
  console.log("bar2");
};
```

### 全局变量

实际上，全局变量是*全局对象*的属性。在网页中，（译注：缺省的）全局对象是 `window` ，所以你可以用形如 `window.`*`variable`* 的语法来设置和访问全局变量。

因此，你可以通过指定 window 或 frame 的名字，在当前 window 或 frame 访问另一个 window 或 frame 中声明的变量。例如，在文档里声明一个叫 `phoneNumber` 的变量，那么你就可以在子框架里使用 `parent.phoneNumber` 的方式来引用它。

### 常量

你可以用关键字 `const` 创建一个只读的常量。常量标识符的命名规则和变量相同：必须以字母、下划线（\_）或美元符号（$）开头并可以包含有字母、数字或下划线。

```js
const PI = 3.14;
```

常量不可以通过重新赋值改变其值，也不可以在代码运行时重新声明。它必须被初始化为某个值。

常量的作用域规则与 `let` 块级作用域变量相同。若省略 `const` 关键字，则该标识符将被视为变量。

在同一作用域中，不能使用与变量名或函数名相同的名字来命名常量。例如：

```js
// 这会造成错误
function f() {}
const f = 5;

// 这也会造成错误
function f() {
  const g = 5;
  var g;

  //语句
}
```

然而，对象属性被赋值为常量是不受保护的，所以下面的语句执行时不会产生错误。

```js
const MY\_OBJECT = { key: "value" };
MY\_OBJECT.key = "otherValue";
```

同样的，数组的被定义为常量也是不受保护的，所以下面的语句执行时也不会产生错误。

```js
const MY\_ARRAY = ["HTML", "CSS"];
MY\_ARRAY.push("JAVASCRIPT");
console.log(MY\_ARRAY); //logs ['HTML','CSS','JAVASCRIPT'];
```

数据结构和类型
-------

### 数据类型

最新的 ECMAScript 标准定义了 8 种数据类型：

* 七种基本数据类型：
    + 布尔值（Boolean），有 2 个值分别是：`true` 和 `false`。
    + null，一个表明 null 值的特殊关键字。JavaScript 是大小写敏感的，因此 `null` 与 `Null`、`NULL`或变体完全不同。
    + undefined，和 null 一样是一个特殊的关键字，undefined 表示变量未赋值时的属性。
    + 数字（Number），整数或浮点数，例如： `42` 或者 `3.14159`。
    + 任意精度的整数（BigInt），可以安全地存储和操作大整数，甚至可以超过数字的安全整数限制。
    + 字符串（String），字符串是一串表示文本值的字符序列，例如：`"Howdy"`。
    + 代表（Symbol，在 ECMAScript 6 中新添加的类型）。一种实例是唯一且不可改变的数据类型。
* 以及对象（Object）。

虽然这些数据类型相对来说比较少，但是通过他们你可以在程序中开发有用的功能。对象和函数是这门语言的另外两个基本元素。你可以把对象当作存放值的一个命名容器，然后将函数当作你的程序能够执行的步骤。

### 数据类型的转换

JavaScript 是一种动态类型语言 (dynamically typed language)。这意味着你在声明变量时可以不必指定数据类型，而数据类型会在代码执行时会根据需要自动转换。因此，你可以按照如下方式来定义变量：

```js
var answer = 42;
```

然后，你还可以给同一个变量赋予一个字符串值，例如：

```js
answer = "Thanks for all the fish...";
```

因为 JavaScript 是动态类型的，这种赋值方式并不会提示出错。

### 数字转换为字符串

在包含的数字和字符串的表达式中使用加法运算符（+），JavaScript 会把数字转换成字符串。例如，观察以下语句：

```js
x = "The answer is " + 42; // "The answer is 42"
y = 42 + " is the answer"; // "42 is the answer"
```

在涉及其他运算符时，JavaScript 语言不会把数字变为字符串。例如：

```js
"37" - 7; // 30
"37" + 7; // "377"
```

### 字符串转换为数字

有一些方法可以将内存中表示一个数字的字符串转换为对应的数字。

* `parseInt()` (en-US)
* `parseFloat()`

`parseInt` 方法只能返回整数，所以使用它会丢失小数部分。另外，调用 parseInt 时最好总是带上进制（radix）参数，这个参数用于指定使用哪一种进制。

将字符串转换为数字的另一种方法是使用一元**加法运算符**。

```js
"1.1" + "1.1" = "1.11.1"
(+"1.1") + (+"1.1") = 2.2
// 注意：加入括号为清楚起见，不是必需的。
```

字面量
---

在 JavaScript 中，你可以使用各种字面量。这些字面量是脚本中按字面意思给出的固定的值，而不是变量。本节将介绍以下类型的字面量：

* 数组字面量
* 布尔字面量
* 数字字面量
* 对象字面量
* RegExp 字面量
* 字符串字面量
### 数组字面量

数组字面值是一个封闭在方括号对 ([]) 中的包含有零个或多个表达式的列表，其中每个表达式代表数组的一个元素。当你使用数组字面值创建一个数组时，该数组将会以指定的值作为其元素进行初始化，而其长度被设定为元素的个数。

下面的示例用 3 个元素生成数组 `coffees`，它的长度是 3。

```js
var coffees = ["French Roast", "Colombian", "Kona"];

var a = [3];

console.log(a.length); // 1

console.log(a[0]); // 3
```

**备注：** 这里的数组字面值也是一种对象初始化器。参考对象初始化器的使用。

若在顶层（全局）脚本里用字面值创建数组，JavaScript 语言将会在每次对包含该数组字面值的表达式求值时解释该数组。另一方面，在函数中使用的数组，将在每次调用函数时都会被创建一次。

数组字面值同时也是数组对象。有关数组对象的详情请参见 `Array`。

#### 数组字面值中的多余逗号

你不必列举数组字面值中的所有元素。若你在同一行中连写两个逗号（,），数组中就会产生一个没有被指定的元素，其初始值是 `undefined`。以下示例创建了一个名为 `fish` 的数组：

```js
var fish = ["Lion", , "Angel"];
```

在这个数组中，有两个已被赋值的元素，和一个空元素（fish[0] 是 "Lion"，fish[1] 是 undefined，而 fish[2] 是 "Angel"）。

如果你在元素列表的尾部添加了一个逗号，它将会被忽略。在下面的例子中，数组的长度是 3，并不存在 myList[3] 这个元素。元素列表中其他所有的逗号都表示一个新元素（的开始）。

**备注：** 尾部的逗号在早期版本的浏览器中会产生错误，因而编程时的最佳实践方式就是移除它们。

```js
var myList = ["home", , "school"];
```

在下面的例子中，数组的长度是 4，元素 `myList[0]` 和 `myList[2]` 缺失。

```js
var myList = [, "home", , "school"];
```

再看一个例子。在这里，该数组的长度是 4，元素 `myList[1]` 和 `myList[3]` 被漏掉了。（但是）只有最后的那个逗号被忽略。

```js
var myList = ["home", , "school", ,];
```

理解多余的逗号（在脚本运行时会被如何处理）的含义，对于从语言层面理解 JavaScript 是十分重要的。但是，在你自己写代码时：**显式地将缺失的元素声明为 `undefined`，将大大提高你的代码的清晰度和可维护性**。

### 布尔字面量

布尔类型有两种字面量：`true`和`false`。

不要混淆作为布尔对象的真和假与布尔类型的原始值 true 和 false。布尔对象是原始布尔数据类型的一个包装器。参见布尔对象。

### 数字字面量

JavaScript 数字字面量包括多种基数的整数字面量和以 10 为基数的浮点数字面量

值得一提的是，语言标准要求数字字面量必须是无符号的。但是像`-123.4`这样的代码片段还是没有问题的，会被解释为一元操作符`-`应用于数字字面量`123.4`

#### 整数字面量

整数可以用十进制（基数为 10）、十六进制（基数为 16）、八进制（基数为 8）以及二进制（基数为 2）表示。

* 十进制整数字面量由一串数字序列组成，且没有前缀 0。
* 八进制的整数以 0（或 0O、0o）开头，只能包括数字 0-7。
* 十六进制整数以 0x（或 0X）开头，可以包含数字（0-9）和字母 a~f 或 A~F。
* 二进制整数以 0b（或 0B）开头，只能包含数字 0 和 1。

严格模式下，八进制整数字面量必须以 0o 或 0O 开头，而不能以 0 开头。

整数字面量举例：
```

0, 117 and -345 (十进制，基数为 10)
015, 0001 and -0o77 (八进制，基数为 8)
0x1123, 0x00111 and -0xF1A7 (十六进制，基数为 16 或"hex")
0b11, 0b0011 and -0b11 (二进制，基数为 2)
```

#### 浮点数字面量

浮点数字面值可以有以下的组成部分：

* 一个十进制整数，可以带正负号（即前缀“+”或“-”），
* 小数点（“.”），
* 小数部分（由一串十进制数表示），
* 指数部分。

指数部分以“e”或“E”开头，后面跟着一个整数，可以有正负号（即前缀“+”或“-”）。浮点数字面量至少有一位数字，而且必须带小数点或者“e”（大写“E”也可）。

简言之，其语法是：

```js
[(+|-)][digits][.digits][(E|e)[(+|-)]digits]
```

例如：

```js
3.1415926
.123456789
3.1E+12
.1e-23
```

### 对象字面量

对象字面值是封闭在花括号对（{}）中的一个对象的零个或多个“属性名—值”对的（元素）列表。

**警告：** 你不能在一条语句的开头就使用对象字面值，这将导致错误或产生超出预料的行为，因为此时左花括号（{）会被认为是一个语句块的起始符号。

以下是一个对象字面值的例子。对象`car`的第一个元素（译注：即一个属性/值对）定义了属性 `myCar`；第二个元素，属性 `getCar`，引用了一个函数调用（即 CarTypes("Honda")）；第三个元素，属性 `special`，使用了一个已有的变量（即 Sales）。

```js
var Sales = "Toyota";

function CarTypes(name) {
  return name === "Honda" ? name : "Sorry, we don't sell " + name + ".";
}

var car = { myCar: "Saturn", getCar: CarTypes("Honda"), special: Sales };

console.log(car.myCar); // Saturn
console.log(car.getCar); // Honda
console.log(car.special); // Toyota
```

更进一步的，你可以使用数字或字符串字面值作为属性的名字，或者在另一个字面值内嵌套上一个字面值。如下的示例中使用了这些可选项。

```js
var car = { manyCars: { a: "Saab", b: "Jeep" }, 7: "Mazda" };

console.log(car.manyCars.b); // Jeep
console.log(car[7]); // Mazda
```

对象属性名字可以是任意字符串，包括空串。如果对象属性名字不是合法的 javascript 标识符，它必须用引号包裹。

属性的名字不合法，那么便不能用点（`.`）访问属性值。

```js
const unusualPropertyNames = {
  '': '空字符串',
  '!': '砰！'
}
console.log(unusualPropertyNames.'');   // SyntaxError: Unexpected string
console.log(unusualPropertyNames.!);    // SyntaxError: Unexpected token !
```

而是通过方括号表示法（`[]`）来访问。

```js
console.log(unusualPropertyNames[""]); // 空字符串
console.log(unusualPropertyNames["!"]); // 砰！
```

#### 增强的对象字面量

在 ES2015，对象字面值扩展支持在创建时设置原型，简写了 foo: foo 形式的属性赋值，方法定义，支持父方法调用，以及使用表达式动态计算属性名。总之，这些也使对象字面值和类声明更加紧密地联系起来，让基于对象的设计从这些便利中更加受益。

```js
var obj = {
  // \_\_proto\_\_
  \_\_proto\_\_: theProtoObj,
  // Shorthand for ‘handler: handler’
  handler,
  // Methods
  toString() {
    // Super calls
    return "d " + super.toString();
  },
  // Computed (dynamic) property names
  ["prop\_" + (() => 42)()]: 42,
};
```

请注意：

```js
var foo = { a: "alpha", 2: "two" };
console.log(foo.a); // alpha
console.log(foo[2]); // two
//console.log(foo.2); // SyntaxError: missing ) after argument list
//console.log(foo[a]); // ReferenceError: a is not defined
console.log(foo["a"]); // alpha
console.log(foo["2"]); // two
```

### RegExp 字面值

一个正则表达式是字符被斜线（译注：正斜杠“/”）围成的表达式。下面是一个正则表达式文字的一个例子。

```js
var re = /ab+c/;
```

### 字符串字面量

字符串字面量是由双引号（"）对或单引号（'）括起来的零个或多个字符。字符串被限定在同种引号之间；也即，必须是成对单引号或成对双引号。下面的例子都是字符串字面值：

```js
'foo'
"bar"
'1234'
'one line \n another line'
"Joyo's cat"
```

你可以在字符串字面值上使用字符串对象的所有方法——JavaScript 会自动将字符串字面值转换为一个临时字符串对象，调用该方法，然后废弃掉那个临时的字符串对象。你也能用对字符串字面值使用类似 String.length 的属性：

```js
console.log("John's cat".length);
// 将打印字符串中的字符个数（包括空格）
// 结果为：10
```

在 ES2015 中，还提供了一种模板字面量，模板字符串提供了一些语法糖来帮你构造字符串。这与 Perl、Python 还有其他语言中的字符串插值的特性非常相似。除此之外，你可以在通过模板字符串前添加一个 tag 来自定义模板字符串的解析过程，这可以用来防止注入攻击，或者用来建立基于字符串的高级数据抽象。

```js
// Basic literal string creation
`In JavaScript '\n' is a line-feed.` // Multiline strings
`In JavaScript this is
 not legal.`;

// String interpolation
var name = "Bob",
  time = "today";
`Hello ${name}, how are you ${time}?`;

// Construct an HTTP request prefix is used to interpret the replacements and construction
POST`http://foo.org/bar?a=${a}&b=${b}
 Content-Type: application/json
 X-Credentials: ${credentials}
 { "foo": ${foo},
 "bar": ${bar}}`(myOnReadyStateChangeHandler);
```

除非有特别需要使用字符串对象，否则，你应当始终使用字符串字面值。要查看字符串对象的有关细节，请参见字符串对象。

#### 在字符串中使用的特殊字符

作为一般字符的扩展，你可以在字符串中使用特殊字符，如下例所示。

```js
"one line \n another line";
```

以下表格列举了你能在 JavaScript 的字符串中使用的特殊字符。

| 字符 | 意思 |
| --- | --- |
| \0 | Null 字节 |
| \b | 退格符 |
| \f | 换页符 |
| \n | 换行符 |
| \r | 回车符 |
| \t | Tab (制表符) |
| \v | 垂直制表符 |
| \' | 单引号 |
| \" | 双引号 |
| \\ | 反斜杠字符（\） |
| \*XXX* | 由从 0 到 377 最多三位八进制数*XXX*表示的 Latin-1 字符。例如，\251 是版权符号的八进制序列。 |
| \x*XX* | 由从 00 和 FF 的两位十六进制数字 XX 表示的 Latin-1 字符。例如，\ xA9 是版权符号的十六进制序列。 |
| *\uXXXX* | 由四位十六进制数字 XXXX 表示的 Unicode 字符。例如，\ u00A9 是版权符号的 Unicode 序列。见Unicode escape sequences (Unicode 转义字符). |
| \u\*{XXXXX}\* | Unicode 代码点 (code point) 转义字符。例如，\u{2F804} 相当于 Unicode 转义字符 \uD87E\uDC04 的简写。 |

#### 转义字符

对于那些未出现在表 2.1 中的字符，其所带的前导反斜线 '\' 将被忽略。但是，这一用法已被废弃，应当避免使用。

通过在引号前加上反斜线 '\'，可以在字符串中插入引号，这就是*引号转义*。例如：

```js
var quote = "He read \"The Cremation of Sam McGee\" by R.W. Service.";
console.log(quote);
```

代码的运行结果为：

```js
He read "The Cremation of Sam McGee" by R.W. Service.
```

要在字符串中插入'\'字面值，必须转义反斜线。例如，要把文件路径 c:\temp 赋值给一个字符串，可以采用如下方式：

```js
var home = "c:\\temp";
```

也可以在换行之前加上反斜线以转义换行（译注：实际上就是一条语句拆成多行书写），这样反斜线和换行都不会出现在字符串的值中。

```js
var str =
  "this string \
is broken \
across multiple\
lines.";
console.log(str); // this string is broken across multiplelines.
```

Javascript 没有“heredoc”语法，但可以用行末的换行符转义和转义的换行来近似实现

```js
var poem =
  "Roses are red,\n\
Violets are blue.\n\
Sugar is sweet,\n\
and so is foo.";
```

ECMAScript 2015 增加了一种新的字面量，叫做模板字面量。它包含一些新特征，包括了多行字符串！

```js
var poem = `Roses are red,
Violets are blue.
Sugar is sweet,
and so is foo.`;
```

更多信息
----

本章重点包括声明和类型的基本语法。学习更多关于的 JavaScript 语言，可参见本站以下章节：

* 流程控制与错误处理
* 循环与迭代
* 函数
* 表达式与运算符

下一章，流程控制与错误处理

* « 上一页
* 下一页 »


带键的集合 - JavaScript

带键的集合
=====

* « 上一页
* 下一页 »

本章将介绍由键索引的数据集合；`Map` 和 `Set` 对象包含可按插入顺序迭代的元素。

Map
---

### Map 对象

`Map` 对象就是一个简单的键/值对映射集合，可以按照数据插入时的顺序遍历所有的元素。

下面的代码演示了使用 `Map` 进行的一些基本操作。请参考 `Map` 以获取更多的样例和完整的 API。你可以使用 `for...of` 循环来在每一次迭代中得到 `[key, value]` 数组。

```js
const sayings = new Map();
sayings.set("dog", "woof");
sayings.set("cat", "meow");
sayings.set("elephant", "toot");
sayings.size; // 3
sayings.get("dog"); // woof
sayings.get("fox"); // undefined
sayings.has("bird"); // false
sayings.delete("dog");
sayings.has("dog"); // false

for (const [key, value] of sayings) {
  console.log(`${key} goes ${value}`);
}
// "cat goes meow"
// "elephant goes toot"

sayings.clear();
sayings.size; // 0
```

### Object 和 Map 的比较

一般地，object 会被用于将字符串类型映射到值。`Object` 允许设置键值对、根据键获取值、删除键、检测某个键是否存在。不过，`Map` 对象还有一些优点，可以更好地应用于映射关系表示中。

* `Object` 的键均为字符串或 Symbol 类型，在 `Map` 里键可以是任意类型。
* 必须手动计算 `Object` 的大小，但是可以很容易地获取 `Map` 的大小（`size`）。
* `Map` 的遍历遵循元素的插入顺序。
* `Object` 有原型，所以映射中有一些缺省的键。（可以用 `map = Object.create(null)` 回避）。

这三条提示可以帮你决定用 `Map` 还是 `Object`：

* 如果键在运行时才能知道，或者所有的键类型相同，所有的值类型相同，那就使用 `Map`。
* 如果需要将原始值存储为键，则使用 `Map`，因为 `Object` 将每个键视为字符串，不管它是一个数字值、布尔值还是任何其他原始值。
* 如果存在需要对个别元素进行操作的逻辑，使用 `Object`。
### WeakMap 对象

`WeakMap` 是键/值对的集合，其键必须是对象或非注册符号，其值为任意 JavaScript 类型，并且不会对其键创建强引用。也就是说，一个对象作为键出现在 `WeakMap` 中并不会阻止该对象被垃圾回收。一旦作为键的对象被收集，其在任何 `WeakMap` 中的相应值也会被垃圾收集，只要它们没有在其他地方被强引用。唯一可用作 `WeakMap` 键的原始类型类型是 symbol，更具体地说，是非注册 symbol，因为非注册 symbol 保证是唯一的，并且不能被重新创建。

`WeakMap` API 与 `Map` API 基本相同。不过，`WeakMap` 不允许观察其键的有效性，这也是它不允许枚举的原因。因此，没有任何方法可以获取 `WeakMap` 中的键的列表。如果有的话，该列表将取决于垃圾回收的状态，从而引入非确定性。

可以在 `WeakMap` 参考页的“为什么使用 WeakMap”小节查看更多信息和示例。

`WeakMap` 对象的一个用例是存储一个对象的私有数据或隐藏实现细节。Nick Fitzgerald 的博文“使用 ECMAScript 6 WeakMap 隐藏实现细节”提供了一个例子。对象内部的私有数据和方法被存储在 `WeakMap` 类型的 `privates` 变量中。所有暴露出的原型和实例都是公开的，而其他内容都是外界不可访问的，因为模块并未导出 `privates` 对象。

```js
const privates = new WeakMap();

function Public() {
  const me = {
    // 这里是私有数据
  };
  privates.set(this, me);
}

Public.prototype.method = function () {
  const me = privates.get(this);
  // 处理 `me` 中的私有数据
  // …
};

module.exports = Public;
```

Set
---

### Set 对象

`Set` 对象是一组唯一值的集合，可以按照添加顺序来遍历。`Set` 中的值只能出现一次；它在集合 `Set` 中是唯一的。

以下代码演示了 `Set` 的基本操作，更多示例和完整 API 可以参考 `Set`。

```js
const mySet = new Set();
mySet.add(1);
mySet.add("some text");
mySet.add("foo");

mySet.has(1); // true
mySet.delete("foo");
mySet.size; // 2

for (const item of mySet) {
  console.log(item);
}
// 1
// "some text"
```

### 数组和 Set 之间的转换

可以使用 `Array.from` 或展开语法来完成集合到数组的转换。同样，`Set` 的构造函数接受数组作为参数，可以完成从 `Array` 到 `Set` 的转换。

**备注：** `Set` 对象存储*唯一值*，所以数组转换为 Set 时，任何重复值将会被删除！

```js
Array.from(mySet);
[...mySet2];

mySet2 = new Set([1, 2, 3, 4]);
```

### 数组和 Set 的对比

一般情况下，在 JavaScript 中使用数组来存储一组元素，而新的 `Set` 对象有这些优势：

* 根据值（`arr.splice(arr.indexOf(val), 1)`）删除数组元素效率低下。
* `Set` 对象允许根据值删除元素，而数组中必须使用基于元素下标的 `splice` 方法。
* 数组的 `indexOf` 方法无法找到 `NaN` 值。
* `Set` 对象存储唯一值，所以不需要手动处理包含重复值的情况。
### WeakSet 对象

`WeakSet` 对象是可收集垃圾值的集合，包括对象和非注册 symbol。`WeakSet` 中的值只能出现一次。它在 `WeakSet` 的集合中是唯一的。

与 `Set` 对象的主要区别有：

* 与 `Set` 相反，`WeakSet` 是**对象或 symbol 的集合**，而不是任何类型的任意值的集合。
* `WeakSet` 的*弱*是指集合中的对象是弱引用的。如果 `WeakSet` 中存储的一个对象不存在其他的引用，那么它就会被垃圾回收。这也意味着集合中不再存储当前对象。
* `WeakSet` 不可枚举。

`WeakSet` 对象的用例有限。它们不会泄露内存，因此可以安全地使用 DOM 元素作为键，并将其标记用于跟踪等目的。

Map 和 Set 的键值相等
---------------

`Map` 对象和 `Set` 对象的键和值的等值判断都基于 SameValueZero 算法：

* 等价的作用类似于严格相等运算符 `===`。
* `-0` 和 `+0` 相等。
* `NaN` 与自身相等（与 `===` 有所不同）。
* « 上一页
* 下一页 »


使用对象 - JavaScript

使用对象
====

* « 上一页
* 下一页 »

JavaScript 的设计是一个简单的基于对象的范式。一个对象就是一系列属性的集合，一个属性包含一个名和一个值。一个属性的值可以是函数，这种情况下属性也被称为*方法*。除了浏览器里面预定义的那些对象之外，你也可以定义你自己的对象。本章节讲述了怎么使用对象、属性、函数和方法，怎样实现自定义对象。

对象概述
----

javascript 中的对象 (物体)，和其他编程语言中的对象一样，可以比照现实生活中的对象 (物体) 来理解它。javascript 中对象 (物体) 的概念可以比照着现实生活中实实在在的物体来理解。

在 javascript 中，一个对象可以是一个单独的拥有属性和类型的实体。我们拿它和一个杯子做下类比。一个杯子是一个对象 (物体)，拥有属性。杯子有颜色，图案，重量，由什么材质构成等等。同样，javascript 对象也有属性来定义它的特征。

对象和属性
-----

一个 javascript 对象有很多属性。一个对象的属性可以被解释成一个附加到对象上的变量。对象的属性和普通的 javascript 变量基本没什么区别，仅仅是属性属于某个对象。属性定义了对象的特征。你可以通过点符号来访问一个对象的属性。

```js
objectName.propertyName;
```

和其他 javascript 变量一样，对象的名字 (可以是普通的变量) 和属性的名字都是大小写敏感的。你可以在定义一个属性的时候就给它赋值。例如，我们创建一个 myCar 的对象然后给他三个属性，make，model，year。具体如下所示：

```js
var myCar = new Object();
myCar.make = "Ford";
myCar.model = "Mustang";
myCar.year = 1969;
```

对象中未赋值的属性的值为`undefined`（而不是`null`）。

```js
myCar.noProperty; // undefined
```

JavaScript 对象的属性也可以通过方括号访问或者设置（更多信息查看 property accessors）. 对象有时也被叫作关联数组，因为每个属性都有一个用于访问它的字符串值。例如，你可以按如下方式访问 `myCar` 对象的属性：

```js
myCar["make"] = "Ford";
myCar["model"] = "Mustang";
myCar["year"] = 1969;
```

一个对象的属性名可以是任何有效的 JavaScript 字符串，或者可以被转换为字符串的任何类型，包括空字符串。然而，一个属性的名称如果不是一个有效的 JavaScript 标识符（例如，一个由空格或连字符，或者以数字开头的属性名），就只能通过方括号标记访问。这个标记法在属性名称是动态判定（属性名只有到运行时才能判定）时非常有用。例如：
```

// 同时创建四个变量，用逗号分隔
var myObj = new Object(),
    str = "myString",
    rand = Math.random(),
    obj = new Object();

myObj.type              = "Dot syntax";
myObj["date created"]   = "String with space";
myObj[str]              = "String value";
myObj[rand]             = "Random Number";
myObj[obj]              = "Object";
myObj[""]               = "Even an empty string";

console.log(myObj);
```

请注意，方括号中的所有键都将转换为字符串类型，因为 JavaScript 中的对象只能使用 String 类型作为键类型。例如，在上面的代码中，当将键 obj 添加到 myObj 时，JavaScript 将调用 obj.toString() 方法，并将此结果字符串用作新键。

你也可以通过存储在变量中的字符串来访问属性：

```js
var propertyName = "make";
myCar[propertyName] = "Ford";

propertyName = "model";
myCar[propertyName] = "Mustang";
```

你可以在 for...in 语句中使用方括号标记以枚举一个对象的所有属性。为了展示它如何工作，下面的函数当你将对象及其名称作为参数传入时，显示对象的属性：

```js
function showProps(obj, objName) {
  var result = "";
  for (var i in obj) {
    if (obj.hasOwnProperty(i)) {
      result += objName + "." + i + " = " + obj[i] + "\n";
    }
  }
  return result;
}
```

因而，对于函数调用 `showProps(myCar, "myCar")` 将返回以下值：

```js
myCar.make = Ford;
myCar.model = Mustang;
myCar.year = 1969;
```

枚举一个对象的所有属性
-----------

从 ECMAScript 5 开始，有三种原生的方法用于列出或枚举对象的属性：

* for...in 循环
 该方法依次访问一个对象及其原型链中所有可枚举的属性。
* Object.keys(o)
 该方法返回对象 `o` 自身包含（不包括原型中）的所有可枚举属性的名称的数组。
* Object.getOwnPropertyNames(o)
 该方法返回对象 `o` 自身包含（不包括原型中）的所有属性 (无论是否可枚举) 的名称的数组。

在 ECMAScript 5 之前，没有原生的方法枚举一个对象的所有属性。然而，可以通过以下函数完成：

```js
function listAllProperties(o) {
  var objectToInspect;
  var result = [];

  for (
    objectToInspect = o;
    objectToInspect !== null;
    objectToInspect = Object.getPrototypeOf(objectToInspect)
  ) {
    result = result.concat(Object.getOwnPropertyNames(objectToInspect));
  }

  return result;
}
```

这在展示“隐藏”（在原型中的不能通过对象访问的属性，因为另一个同名的属性存在于原型链的早期）的属性时很有用。如果只想列出可访问的属性，那么只需要去除数组中的重复元素即可。

创建新对象
-----

JavaScript 拥有一系列预定义的对象。另外，你可以创建你自己的对象。从 JavaScript 1.2 之后，你可以通过对象初始化器（Object Initializer）创建对象。或者你可以创建一个构造函数并使用该函数和 `new` 操作符初始化对象。

### 使用对象初始化器

除了通过构造函数创建对象之外，你也可以通过对象初始化器创建对象。使用对象初始化器也被称作通过字面值创建对象。对象初始化器与 C++ 术语相一致。

通过对象初始化器创建对象的语法如下：

```js
var obj = {
  property\_1: value_1, // property\_# 可以是一个标识符...
  2: value_2, // 或一个数字...
  ["property" + 3]: value_3, // 或一个可计算的 key 名...
  // ...,
  "property n": value_n,
}; // 或一个字符串
```

这里 `obj` 是新对象的名称，每一个 `property_i` 是一个标识符（可以是一个名称、数字或字符串字面量），并且每个 `value_i` 是一个其值将被赋予 `property_i` 的表达式。`obj` 与赋值是可选的；如果你不需要在其他地方引用对象，你就不需要将它赋给一个变量。（注意在接受一条语句的地方，你可能需要将对象字面量括在括号里，从而避免将字面量与块语句相混淆）

如果一个对象是通过在顶级脚本的对象初始化器创建的，则 JavaScript 在每次遇到包含该对象字面量的表达式时都会创建对象。同样的，在函数中的初始化器在每次函数调用时也会被创建。

下面的语句只有当 `cond` 表达式的值为 `true` 时创建对象并将其赋给变量 `x`。

```js
if (cond) var x = { hi: "there" };
```

下例创建了有三个属性的 `myHonda` 对象。注意它的 `engine` 属性也是一个拥有自己属性的对象。

```js
var myHonda = { color: "red", wheels: 4, engine: { cylinders: 4, size: 2.2 } };
```

你也可以用对象初始化器来创建数组。参见数组字面量

在 JavaScript 1.1 及更早版本中，你不能使用对象初始化器。你只能通过使用构造函数或其他对象的函数来创建对象。参见 使用构造函数.

### 使用构造函数

作为另一种方式，你可以通过两步来创建对象：

1. 通过创建一个构造函数来定义对象的类型。首字母大写是非常普遍而且很恰当的惯用法。
2. 通过 `new` 创建对象实例。

为了定义对象类型，为对象类型创建一个函数以声明类型的名称、属性和方法。例如，你想为汽车创建一个类型，并且将这类对象称为 `car` ，并且拥有属性 make, model，和 year，你可以创建如下的函数：

```js
function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}
```

注意通过使用 this 将传入函数的值赋给对象的属性。

现在你可以象这样创建一个 `mycar` 对象：

```js
var mycar = new Car("Eagle", "Talon TSi", 1993);
```

该创建了 `mycar` 并且将指定的值赋给它的属性。因而 `mycar.make` 的值是字符串 "Eagle"， `mycar.year` 的值是整数 1993，依此类推。

你可以通过调用 `new` 创建任意数量的 `car` 对象。例如：

```js
var kenscar = new Car("Nissan", "300ZX", 1992);
var vpgscar = new Car("Mazda", "Miata", 1990);
```

一个对象的属性值可以是另一个对象。例如，假设你按如下方式定义了 `person` 对象：

```js
function Person(name, age, sex) {
  this.name = name;
  this.age = age;
  this.sex = sex;
}
```

然后按如下方式创建了两个 `person` 实例：

```js
var rand = new Person("Rand McKinnon", 33, "M");
var ken = new Person("Ken Jones", 39, "M");
```

那么，你可以重写 `car` 的定义以包含一个拥有它的 `owner` 属性，如：

```js
function Car(make, model, year, owner) {
  this.make = make;
  this.model = model;
  this.year = year;
  this.owner = owner;
}
```

你可以按如下方式创建新对象：

```js
var car1 = new Car("Eagle", "Talon TSi", 1993, rand);
var car2 = new Car("Nissan", "300ZX", 1992, ken);
```

注意在创建新对象时，上面的语句将 `rand` 和 `ken` 作为 `owner` 的参数值，而不是传入字符串字面量或整数值。接下来你如果想找出 car2 的拥有者的姓名，你可以访问如下属性：

```js
car2.owner.name;
```

注意你总是可以为之前定义的对象增加新的属性。例如，语句

```js
car1.color = "black";
```

为 `car1` 增加了 `color` 属性，并将其值设为 "black." 然而，这并不影响其他的对象。想要为某个类型的所有对象增加新属性，你必须将属性加入到 `car` 对象类型的定义中。

### 使用 Object.create 方法

对象也可以用 `Object.create()` 方法创建。该方法非常有用，因为它允许你为创建的对象选择一个原型对象，而不用定义构造函数。

```js
// Animal properties and method encapsulation
var Animal = {
  type: "Invertebrates", // 属性默认值
  displayType: function () {
    // 用于显示 type 属性的方法
    console.log(this.type);
  },
};

// 创建一种新的动物——animal1
var animal1 = Object.create(Animal);
animal1.displayType(); // Output:Invertebrates

// 创建一种新的动物——Fishes
var fish = Object.create(Animal);
fish.type = "Fishes";
fish.displayType(); // Output:Fishes
```

继承
--

所有的 JavaScript 对象至少继承于一个对象。被继承的对象被称作原型，并且继承的属性可通过构造函数的 `prototype` 对象找到。查看更多详细 Inheritance and the prototype chain

对象属性索引
------

在 JavaScript 1.0 中，你可以通过名称或序号访问一个属性。但是在 JavaScript 1.1 及之后版本中，如果你最初使用名称定义了一个属性，则你必须通过名称来访问它；而如果你最初使用序号来定义一个属性，则你必须通过索引来访问它。

这个限制发生在你通过构造函数创建一个对象和它的属性（就象我们之前通过 `Car` 对象类型所做的那样）并且显式地定义了单独的属性（如 `myCar.color = "red"`）之时。如果你最初使用索引定义了一个对象属性，例如 `myCar[5] = "25"`，则你只可能通过 `myCar[5]` 引用它。

这条规则的例外是从与 HTML 对应的对象，例如 `forms` 数组。对于这些数组的元素，你总是既可以通过其序号（依据其在文档中出现的顺序），也可以按照其名称（如果有的话）访问它。举例而言，如果文档中的第二个 `<form>` 标签有一个 `NAME` 属性且值为 `"myForm"`，访问该 `form` 的方式可以是 `document.forms[1]`，`document.forms["myForm"]` 或 `document.myForm`。

为对象类型定义属性

你可以通过 `prototype`属性为之前定义的对象类型增加属性。这为该类型的所有对象，而不是仅仅一个对象增加了一个属性。下面的代码为所有类型为 `car` 的对象增加了 `color` 属性，然后为对象 `car1` 的 `color` 属性赋值：

```js
Car.prototype.color = null;
car1.color = "black";
```

参见 JavaScript Reference 中 Function 对象的 `prototype` 属性 (en-US) 。

定义方法
----

一个*方法* 是关联到某个对象的函数，或者简单地说，一个方法是一个值为某个函数的对象属性。定义方法就像定义普通的函数，除了它们必须被赋给对象的某个属性。查看 method definitions了解更多详情例如：

```js
objectName.methodname = function_name;

var myObj = {
  myMethod: function(params) {
    // ...do something
  }

  // 或者 这样写也可以

  myOtherMethod(params) {
    // ...do something else
  }
};
```

这里 `objectName` 是一个已经存在的对象，`methodname` 是方法的名称，而 `function_name` 是函数的名称。

你可以在对象的上下文中象这样调用方法：

```js
object.methodname(params);
```

你可以在对象的构造函数中包含方法定义来为某个对象类型定义方法。例如，你可以为之前定义的 `car` 对象定义一个函数格式化并显示其属性：

```js
function displayCar() {
  var result = `A Beautiful ${this.year} ${this.make} ${this.model}`;
  pretty\_print(result);
}
```

这里 `pretty_print` 是一个显示横线和一个字符串的函数。注意使用 this 指代方法所属的对象。

你可以在对象定义中通过增加下述语句将这个函数变成 `Car` 的方法：

```js
this.displayCar = displayCar;
```

因此，`Car` 的完整定义看上去将是：

```js
function Car(make, model, year, owner) {
  this.make = make;
  this.model = model;
  this.year = year;
  this.owner = owner;
  this.displayCar = displayCar;
}
```

然后你可以按如下方式为每个对象调用 `displayCar` 方法：

```js
car1.displayCar();
car2.displayCar();
```

通过 `this` 引用对象
--------------

JavaScript 有一个特殊的关键字 `this`，它可以在方法中使用以指代当前对象。例如，假设你有一个名为 `validate` 的函数，它根据给出的最大与最小值检查某个对象的 `value` 属性：

```js
function validate(obj, lowval, hival) {
  if (obj.value < lowval || obj.value > hival) {
    alert("Invalid Value!");
  }
}
```

然后，你可以在每个元素的 `onchange` 事件处理器中调用 `validate`，并通过 `this` 传入相应元素，代码如下：

```html
<input type="text" name="age" size="3" onChange="validate(this, 18, 99)" />
```

总的说来， `this` 在一个方法中指调用的对象。

当与 `form` 属性一起使用时，`this`可以指代当前对象的父窗体。在下面的例子中，窗体 `myForm` 包含一个 `Text` 对象和一个按钮，当用户点击按键，`Text` 对象的值被设为窗体的名称。按钮的 `onclick` 事件处理器使用 `this.form` 以指代其父窗体，即 `myForm`。

```html
<form name="myForm">
  <p>
    <label>Form name:<input type="text" name="text1" value="Beluga" /></label>
  </p>
  <p>
    <input
 name="button1"
 type="button"
 value="Show Form Name"
 onclick="this.form.text1.value = this.form.name" />
  </p>
</form>
```

定义 getter 与 setter
------------------

一个 getter 是一个获取某个特定属性的值的方法。一个 setter 是一个设定某个属性的值的方法。你可以为预定义的或用户定义的对象定义 getter 和 setter 以支持新增的属性。定义 getter 和 setter 的语法采用对象字面量语法。

下面例子描述了 getter 和 setter 是如何为用户定义的对象 `o` 工作的。

```js
var o = {
  a: 7,
  get b() {
    return this.a + 1;
  },
  set c(x) {
    this.a = x / 2;
  },
};

console.log(o.a); // 7
console.log(o.b); // 8
o.c = 50;
console.log(o.a); // 25
```

`o` 对象的属性如下：

* `o.a` — 数字
* `o.b` — 返回 `o.a` + 1 的 getter
* `o.c` — 由 `o.c 的值所设置 o.a 值的` setter

请注意在一个对象字面量语法中定义 getter 和 setter 使用"[gs]et property()"的方式（相比较于\_\_define[GS]etter\_\_) 时，并不是获取和设置某个属性自身，容易让人误以为是"[gs]et propertyName(){ }"这样错误的使用方法。定义一个 getter 或 setter 函数使用语法"[gs]et property()"，定义一个已经声明的函数作为的 getter 和 setter 方法，使用`Object.defineProperty`(或者 `Object.prototype.__defineGetter__` (en-US) 旧语法回退)

下面这个例子展示使用 getter 和 setter 方法扩展 `Date`原型，为预定义好的 Date 类添加一个 year 的属性。定义属性 year 的 getter 和 setter 方法用到了 Date 类中已存在的 getFullYear 和 setFullYear 方法。

定义属性 year 的 getter 和 setter：

```js
var d = Date.prototype;
Object.defineProperty(d, "year", {
  get: function () {
    return this.getFullYear();
  },
  set: function (y) {
    this.setFullYear(y);
  },
});
```

通过一个 Date 对象使用 getter 和 setter:

```js
var now = new Date();
console.log(now.year); // 2000
now.year = 2001; // 987617605170
console.log(now);
// Wed Apr 18 11:13:25 GMT-0700 (Pacific Daylight Time) 2001
```

原则上，getter 和 setter 既可以：

* 使用 使用对象初始化器 定义
* 也可以之后随时使用 getter 和 setter 添加方法添加到任何对象

当使用 使用对象初始化器 的方式定义 getter 和 setter 时，只需要在 getter 方法前加 get，在 setter 方法前加 set，当然，getter 方法必须是无参数的，setter 方法只接受一个参数 (设置为新值），例如：

```js
var o = {
  a: 7,
  get b() {
    return this.a + 1;
  },
  set c(x) {
    this.a = x / 2;
  },
};
```

使用 Object.defineProperties 的方法，同样也可以对一个已创建的对象在任何时候为其添加 getter 或 setter 方法。这个方法的第一个参数是你想定义 getter 或 setter 方法的对象，第二个参数是一个对象，这个对象的属性名用作 getter 或 setter 的名字，属性名对应的属性值用作定义 getter 或 setter 方法的函数，下面是一个例子定义了和前面例子一样的 getter 和 setter 方法：

```js
var o = { a: 0 };

Object.defineProperties(o, {
  b: {
    get: function () {
      return this.a + 1;
    },
  },
  c: {
    set: function (x) {
      this.a = x / 2;
    },
  },
});

o.c = 10; // Runs the setter, which assigns 10 / 2 (5) to the 'a' property
console.log(o.b); // Runs the getter, which yields a + 1 or 6
```

这两种定义方式的选择取决于你的编程风格和手头的工作量。当你定义一个原型准备进行初始化时，可以选择第一种方式，这种方式更简洁和自然。但是，当你需要添加 getter 和 setter 方法——因为并没有编写原型或者特定的对象——使用第二种方式更好。第二种方式可能更能表现 JavaScript 语法的动态特性——但也会使代码变得难以阅读和理解。

删除属性
----

你可以用 `delete` 操作符删除一个**不是继承而来**的属性。下面的例子说明如何删除一个属性：

```js
//Creates a new object, myobj, with two properties, a and b.
var myobj = new Object();
myobj.a = 5;
myobj.b = 12;

//Removes the a property, leaving myobj with only the b property.
delete myobj.a;
```

如果一个全局变量不是用 `var` 关键字声明的话，你也可以用 `delete` 删除它：

```js
g = 17;
delete g;
```

比较对象
----

在 JavaScript 中 objects 是一种引用类型。两个独立声明的对象永远也不会相等，即使他们有相同的属性，只有在比较一个对象和这个对象的引用时，才会返回 true.

```js
// 两个变量，两个具有同样的属性、但不相同的对象
var fruit = { name: "apple" };
var fruitbear = { name: "apple" };

fruit == fruitbear; // return false
fruit === fruitbear; // return false
```

**备注：** "===" 运算符用来检查数值是否相等：1 === "1"返回 false，而 1 == "1" 返回 true

```js
// 两个变量，同一个对象
var fruit = { name: "apple" };
var fruitbear = fruit; // 将 fruit 的对象引用 (reference) 赋值给 fruitbear
// 也称为将 fruitbear“指向”fruit 对象
// fruit 与 fruitbear 都指向同样的对象
fruit == fruitbear; // return true
fruit === fruitbear; // return true
```

了解更多关于比较操作符的用法，查看 Comparison operators (en-US).

附加参考
----

* 想要深入了解，请阅读details of javaScript's objects model。
* 想要学习 ECMAScript 2015 中类（一种创建对象的新方式），请阅读 JavaScript classes 章节。
* « 上一页
* 下一页 »


数字和日期 - JavaScript

数字和日期
=====

 * « 上一页
* 下一页 »

本章节介绍了在 JavaScript 中使用数字和日期来处理和执行计算的概念，对象和函数。
本章节介绍如何掌握 Javascript 里的数字和日期类型

数字
--

在 JavaScript 里面，数字均为双精度浮点类型（double-precision 64-bit binary format IEEE 754），即一个介于 ±2^−1023 和 ±2^+1024 之间的数字，或约为 ±10^−308 到 ±10^+308，数字精度为 53 位。整数数值仅在 ±(2^53 - 1) 的范围内可以表示准确。

除了能够表示浮点数，数字类型也还能表示三种符号值：`+``Infinity`（正无穷）、`-``Infinity`（负无穷）和 `NaN` (not-a-number，非数字)。

JavaScript 最近添加了 `BigInt` 的支持，能够用于表示极大的数字。使用 BigInt 的时候有一些注意事项，例如，你不能让 BigInt 和 `Number` 直接进行运算，你也不能用 `Math` 对象去操作 BigInt 数字。

请参见 Javascript 指南中的 JavaScript 数据类型和数据结构，了解其他更多的基本类型。

你可以使用四种数字进制：十进制、二进制、八进制和十六进制。

### 十进制数字

```js
1234567890
42
```

请注意，十进制可以以 0 开头，后面接其他十进制数字，但是假如下一个接的十进制数字小于 8，那么该数字将会被当做八进制处理。

```js
0888 // 888 以十进制解析
0777 // 以八进制解析，为十进制的 511
```

### 二进制数字

二进制数字语法是以零为开头，后面接一个小写或大写的拉丁文字母 B(`0b` 或者是 `0B`)。假如 0b 后面的数字不是 0 或者 1，那么就会提示这样的语法错误（`SyntaxError`）：“Missing binary digits after 0b（0b 之后缺失二有效的二进制数据）”。

```js
0b10000000000000000000000000000000 // 2147483648
0b01111111100000000000000000000000 // 2139095040
0B00000000011111111111111111111111 // 8388607
```

### 八进制数字

八进制数字语法是以 0 为开头的。假如 0 后面的数字不在 0 到 7 的范围内，该数字将会被转换成十进制数字。

```js
0O755 // 493
0o644 // 420
```

在 ECMAScript 5 严格模式下禁止使用八进制语法。八进制语法并不是 ECMAScript 5 规范的一部分，但是通过在八进制数字添加一个前缀 0 就可以被所有的浏览器支持：`0644 === 420` 而且 `"\045" === "%"`。在 ECMAScript 6 中使用八进制数字是需要给一个数字添加前缀“0o”。

```js
const n = 0755; // 493
const m = 0644; // 420
```

### 十六进制

十六进制数字语法是以零为开头，后面接一个小写或大写的拉丁文字母 X(`0x 或者是 0X`)。假如`0x`后面的数字超出规定范围 (0123456789ABCDEF)，那么就会提示这样的语法错误（`SyntaxError`）：“Identifier starts immediately after numeric literal”。

```js
0xFFFFFFFFFFFFFFFFF // 295147905179352830000
0x123456789ABCDEF   // 81985529216486900
0XA                 // 10
```

### 指数形式

```js
0e-5   // 0
0e+5   // 0
5e1    // 50
175e-2 // 1.75
1e3    // 1000
1e-3   // 0.001
1E3    // 1000
```

数字对象
----

内置的 `Number` 对象有一些有关数字的常量属性，如最大值、不是一个数字和无穷大的。你不能改变这些属性，但可以按下边的方式使用它们：

```js
const biggestNum = Number.MAX\_VALUE;
const smallestNum = Number.MIN\_VALUE;
const infiniteNum = Number.POSITIVE\_INFINITY;
const negInfiniteNum = Number.NEGATIVE\_INFINITY;
const notANum = Number.NaN;
```

你永远只用从 Number 对象引用上边显示的属性，而不是你自己创建的 Number 对象的属性。

下面的表格汇总了数字对象的属性：

| 属性 | 描述 |
| --- | --- |
| `Number.MAX_VALUE` | 可表示的最大值 |
| `Number.MIN_VALUE` | 可表示的最小值 |
| `Number.NaN` | 特指”非数字“ |
| `Number.NEGATIVE_INFINITY` | 特指“负无穷”;在溢出时返回 |
| `Number.POSITIVE_INFINITY` | 特指“正无穷”;在溢出时返回 |
| `Number.EPSILON` | 表示 1 和比最接近 1 且大于 1 的最小`Number`之间的差别 |
| `Number.MIN_SAFE_INTEGER` | JavaScript 最小安全整数。 |
| `Number.MAX_SAFE_INTEGER` | JavaScript 最大安全整数。 |

| 方法 | 描述 |
| --- | --- |
| `Number.parseFloat()` | 把字符串参数解析成浮点数，和全局方法 `parseFloat()` 作用一致。 |
| `Number.parseInt()` | 把字符串解析成特定基数对应的整型数字，和全局方法 `parseInt()` 作用一致。 |
| `Number.isFinite()` | 判断传递的值是否为有限数字。 |
| `Number.isInteger()` | 判断传递的值是否为整数。 |
| `Number.isNaN()` | 判断传递的值是否为 `NaN`. More robust version of the original global `isNaN()`. |
| `Number.isSafeInteger()` | 判断传递的值是否为安全整数。 |

数字的类型提供了不同格式的方法以从数字对象中检索信息。以下表格总结了 数字类型原型上的方法。

| 方法 | 描述 |
| --- | --- |
| `toExponential()` | 返回一个数字的指数形式的字符串，形如：1.23e+2 |
| `toFixed()` | 返回指定小数位数的表示形式，var a=123,b=a.toFixed(2)//b="123.00" |
| `toPrecision()` | 返回一个指定精度的数字。如下例子中，a=123 中，3 会由于精度限制消失 var a=123,b=a.toPrecision(2)//b="1.2e+2" |

数学对象（Math）
----------

对于内置的`Math`数学常项和函数也有一些属性和方法。比方说， `Math` 对象的 `PI` 属性会有属性值 pi (3.141...)，你可以像这样调用它：

```js
Math.PI; // π
```

同理，标准数学函数也是 Math 的方法。这些包括三角函数、对数、指数，和其他函数。比方说你想使用三角函数 `sin`，你可以这么写：

```js
Math.sin(1.56);
```

需要注意的是 Math 的所有三角函数参数都是弧度制。

下面的表格总结了 `Math` 对象的方法。

Math 的方法

| 方法 | 描述 |
| --- | --- |
| `abs()` | 绝对值 |
| `sin()`, `cos()`, `tan()` | 标准三角函数;参数为弧度 |
| `asin()`, `acos()`, `atan()`, `atan2()` | 反三角函数; 返回值为弧度 |
| `sinh()`, `cosh()`, `tanh()` | 双曲三角函数; 参数为弧度。 |
| `asinh()`, `acosh()`, `atanh()` | 反双曲三角函数;返回值为弧度。 |
| `pow()`, `exp()`, `expm1()`, `log10()`, `log1p()`, `log2()` | 指数与对数函数 |
| `floor()`, `ceil()` | 返回小于等于参数的最大整数；返回大于等于参数的最小整数 |
| `min()`, `max()` | 返回一个以逗号间隔的数字参数列表中的较小或较大值 (分别地) |
| `random()` | 返回 0 和 1 之间的随机数。 |
| `round()`, `fround()`, `trunc()`, | 四舍五入和截断函数 |
| `sqrt()`, `cbrt()`, `hypot()` | 平方根，立方根，所有参数平方和的平方根两个参数平方和的平方根 |
| `sign()` | 数字的符号，说明数字是否为正、负、零。 |
| `clz32()`, `imul()` | 在 32 位 2 进制表示中，开头的 0 的数量。*返回传入的两个参数相乘结果的类 C 的 32 位表现形式* |

和其他对象不同，你不能够创建一个自己的 Math 对象。你只能使用内置的 Math 对象。

日期对象
----

JavaScript 没有日期数据类型。但是你可以在你的程序里使用 `Date` 对象和其方法来处理日期和时间。Date 对象有大量的设置、获取和操作日期的方法。它并不含有任何属性。

JavaScript 处理日期数据类似于 Java。这两种语言有许多一样的处理日期的方法，也都是以 1970 年 1 月 1 日 00:00:00 以来的毫秒数来储存数据类型的。

`Date` 对象的范围是相对距离 UTC 1970 年 1 月 1 日 的前后 100,000,000 天。

创建一个日期对象：

```js
var dateObjectName = new Date([parameters]);
```

这里的 dateObjectName 对象是所创建的 Date 对象的一个名字，它可以成为一个新的对象或者已存在的其他对象的一个属性。

不使用 *new* 关键字来调用 Date 对象将返回当前时间和日期的字符串

前边的语法中的参数（parameters）可以是一下任何一种：

* 无参数 : 创建今天的日期和时间，例如： `today = new Date();`.
* 一个符合以下格式的表示日期的字符串："月 日，年 时：分：秒"。例如： `var Xmas95 = new Date("December 25, 1995 13:30:00")。`如果你省略时、分、秒，那么他们的值将被设置为 0。
* 一个年，月，日的整型值的集合，例如： `var Xmas95 = new Date(1995, 11, 25)`。
* 一个年，月，日，时，分，秒的集合，例如： `var Xmas95 = new Date(1995, 11, 25, 9, 30, 0);`.
### `Date 对象的方法`

处理日期时间的 Date 对象方法可分为以下几类：

* "set" 方法，用于设置 Date 对象的日期和时间的值。
* "get" 方法，用于获取 Date 对象的日期和时间的值。
* "to" 方法，用于返回 Date 对象的字符串格式的值。
* parse 和 UTC 方法，用于解析 Date 字符串。

通过“get”和“set”方法，你可以分别设置和获取秒，分，时，日，星期，月份，年。这里有个 getDay 方法可以返回星期，但是没有相应的 setDay 方法用来设置星期，因为星期是自动设置的。这些方法用整数来代表以下这些值：

* 秒，分：0 至 59
* 时：0 至 23
* 星期：0 (周日) 至 6 (周六)
* 日期：1 至 31
* 月份：0 (一月) to 11 (十二月)
* 年份：从 1900 开始的年数

例如，假设你定义了如下日期：

```js
var Xmas95 = new Date("December 25, 1995");
```

Then `Xmas95.getMonth()` 返回 11, and `Xmas95.getFullYear()` 返回 1995.

`getTime` 和 `setTime` 方法对于比较日期是非常有用的。`getTime`方法返回从 1970 年 1 月 1 日 00:00:00 的毫秒数。

例如，以下代码展示了今年剩下的天数：

```js
var today = new Date();
var endYear = new Date(1995, 11, 31, 23, 59, 59, 999); // 设置日和月，注意，月份是 0-11
endYear.setFullYear(today.getFullYear()); // 把年设置为今年
var msPerDay = 24 \* 60 \* 60 \* 1000; // 每天的毫秒数
var daysLeft = (endYear.getTime() - today.getTime()) / msPerDay;
var daysLeft = Math.round(daysLeft); //返回今年剩下的天数
```

这个例子中，创建了一个包含今天的日期的`Date`对象，并命名为`today`，然后创建了一个名为`endYear`的`Date`对象，并把年份设置为当前年份，接着使用`today`和`endYear`的`getTime`分别获取今天和年底的毫秒数，再根据每一天的毫秒数，计算出了今天到年底的天数，最后四舍五入得到今年剩下的天数。

parse 方法对于从日期字符串赋值给现有的 Date 对象很有用，例如：以下代码使用`parse`和`setTime`分配了一个日期值给`IPOdate`对象：

```js
var IPOdate = new Date();
IPOdate.setTime(Date.parse("Aug 9, 1995"));
```

### 示例

在下边的例子中，JSClock() 函数返回了用数字时钟格式的时间：

```js
function JSClock() {
  var time = new Date();
  var hour = time.getHours();
  var minute = time.getMinutes();
  var second = time.getSeconds();
  var temp = "" + (hour > 12 ? hour - 12 : hour);
  if (hour == 0) temp = "12";
  temp += (minute < 10 ? ":0" : ":") + minute;
  temp += (second < 10 ? ":0" : ":") + second;
  temp += hour >= 12 ? " P.M." : " A.M.";
  return temp;
}
```

`JSClock`函数首先创建了一个叫做`time`的新的`Date`对象，因为没有参数，所以`time`代表了当前日期和时间。然后调用了`getHours`，`getMinutes`以及`getSeconds`方法把当前的时分秒分别赋值给了`hour`，`minute`，`second`。

接下来的 4 句在`time`的基础上创建了一个字符串，第一句创建了一个变量`temp`，并通过一个条件表达式进行了赋值，如果小时大于 12，就为 (`hour - 12`)，其他情况就为`hour`，除非`hour`为 0，这种情况下，它会变成 12。

接下来的语句拼接了`minute`的值到`temp`后。如果`minute`小于 10，条件表达式就会在`minute`前边加个 0，其他情况下加一个冒号。然后按同样的方式在`temp`后拼接上了秒。

最后，如果`hour`是 12 或者更大，条件表达式会在`temp`后拼接"P.M."，否则拼接"A.M."。

* « 上一页
* 下一页 »


正则表达式 - JavaScript

正则表达式
=====

* « 上一页
* 下一页 »

正则表达式是用于匹配字符串中字符组合的模式。在 JavaScript 中，正则表达式也是对象。这些模式被用于 `RegExp` 的 `exec` 和 `test` 方法，以及 `String` 的 `match`、`matchAll`、`replace`、`search` 和 `split` 方法。本章介绍 JavaScript 正则表达式。

创建一个正则表达式
---------

你可以使用以下两种方法构建一个正则表达式：

使用一个正则表达式字面量，其由包含在斜杠之间的模式组成，如下所示：

```js
var re = /ab+c/;
```

脚本加载后，正则表达式字面量就会被编译。当正则表达式保持不变时，使用此方法可获得更好的性能。

或者调用`RegExp`对象的构造函数，如下所示：

```js
var re = new RegExp("ab+c");
```

在脚本运行过程中，用构造函数创建的正则表达式会被编译。如果正则表达式将会改变，或者它将会从用户输入等来源中动态地产生，就需要使用构造函数来创建正则表达式。

编写一个正则表达式的模式
------------

一个正则表达式模式是由简单的字符所构成的，比如 `/abc/`；或者是简单和特殊字符的组合，比如 `/ab*c/` 或 `/Chapter (\d+)\.\d*/`。最后的例子中用到了括号，它在正则表达式中常用作记忆设备。即这部分所匹配的字符将会被记住以备后续使用，例如使用括号的子字符串匹配。

### 使用简单模式

简单模式是由你想直接找到的字符构成。比如，`/abc/` 这个模式就能且仅能匹配 "abc" 字符按照顺序同时出现的情况。例如在 "Hi, do you know your abc's?" 和 "The latest airplane designs evolved from slabcraft." 中会匹配成功。在上述两个例子中，匹配的子字符串是 "abc"。但是在 "Grab crab" 中会匹配失败，因为它虽然包含子字符串 "ab c"，但并不是准确的 "abc"。

### 使用特殊字符

当你需要匹配一个不确定的字符串时，比如寻找一个或多个 "b"，或者寻找空格，可以在模式中使用特殊字符。比如，你可以使用 `/ab*c/` 去匹配一个单独的 "a" 后面跟了零个或者多个 "b"，同时后面跟着 "c" 的字符串：`*`的意思是前一项出现零次或者多次。在字符串 "cbbabbbbcdebc" 中，这个模式匹配了子字符串 "abbbbc"。

下面的页面与表格列出了一个正则表达式中可以利用的特殊字符的完整列表和描述。

断言（Assertions）

表示一个匹配在某些条件下发生。断言包含先行断言、后行断言和条件表达式。

字符类（Character Classes）

区分不同类型的字符，例如区分字母和数字。

组和范围（Groups and Ranges）

表示表达式字符的分组和范围。

量词（Quantifiers）

表示匹配的字符或表达式的数量。

Unicode 属性转义（Unicode Property Escapes）

基于 unicode 字符属性区分字符。例如大写和小写字母、数学符号和标点。

正则表达式中的特殊字符| 字符 | 含义 |
| --- | --- |
| `\` | 依照下列规则匹配：

 在非特殊字符之前的反斜杠表示下一个字符是特殊字符，不能按照字面理解。例如，前面没有
 "\" 的 "b" 通常匹配小写字母
 "b"，即字符会被作为字面理解，无论它出现在哪里。但如果前面加了
 "\"，它将不再匹配任何字符，而是表示一个字符边界。

 在特殊字符之前的反斜杠表示下一个字符不是特殊字符，应该按照字面理解。详情请参阅下文中的
 "转义（Escaping）" 部分。

 如果你想将字符串传递给 RegExp
 构造函数，不要忘记在字符串字面量中反斜杠是转义字符。所以为了在模式中添加一个反斜杠，你需要在字符串字面量中转义它。`/[a-z]\s/i`
 和
 `new RegExp("[a-z]\\s", "i")`
 创建了相同的正则表达式：一个用于搜索后面紧跟着空白字符（`\s`
 可看后文）并且在 a-z 范围内的任意字符的表达式。为了通过字符串字面量给
 RegExp
 构造函数创建包含反斜杠的表达式，你需要在字符串级别和正则表达式级别都对它进行转义。例如
 `/[a-z]:\\/i` 和
 `new RegExp("[a-z]:\\\\","i")`
 会创建相同的表达式，即匹配类似 "C:\" 字符串。
  |
| `^` |
 匹配输入的开始。如果多行标志被设置为
 true，那么也匹配换行符后紧跟的位置。

 例如，`/^A/` 并不会匹配 "an A" 中的 'A'，但是会匹配 "An E"
 中的 'A'。

 当 '`^`'
 作为第一个字符出现在一个字符集合模式时，它将会有不同的含义。反向字符集合
 一节有详细介绍和示例。
  |
| `$` | 匹配输入的结束。如果多行标志被设置为 true，那么也匹配换行符前的位置。

 例如，`/t$/` 并不会匹配 "eater" 中的 't'，但是会匹配 "eat"
 中的 't'。
  |
| `*` | 匹配前一个表达式 0 次或多次。等价于 `{0,}`。

 例如，`/bo*/` 会匹配 "A ghost boooooed" 中的 'booooo' 和 "A
 bird warbled" 中的 'b'，但是在 "A goat grunted" 中不会匹配任何内容。
  |
| `+` | 匹配前面一个表达式 1 次或者多次。等价于 `{1,}`。

 例如，`/a+/` 会匹配 "candy" 中的 'a' 和 "caaaaaaandy"
 中所有的 'a'，但是在 "cndy" 中不会匹配任何内容。
  |
| `?` | 匹配前面一个表达式 0 次或者 1 次。等价于 `{0,1}`。

 例如，`/e?le?/` 匹配 "angel" 中的 'el'、"angle" 中的 'le'
 以及 "oslo' 中的 'l'。

 如果**紧跟在任何量词 \*、 +、? 或 {} 的后面**，将会使量词变为**非贪婪**（匹配尽量少的字符），和缺省使用的**贪婪模式**（匹配尽可能多的字符）正好相反。例如，对
 "123abc" 使用 `/\d+/` 将会匹配 "123"，而使用
 `/\d+?/` 则只会匹配到 "1"。

 还用于先行断言中，如本表的 `x(?=y)` 和
 `x(?!y)` 条目所述。
  |
| `.` | （小数点）默认匹配除换行符之外的任何单个字符。

 例如，`/.n/` 将会匹配 "nay, an apple is on the tree" 中的
 'an' 和 'on'，但是不会匹配 'nay'。

如果 `s` ("dotAll") 标志位被设为 true，它也会匹配换行符。 |
| `(x)` |
 像下面的例子展示的那样，它会匹配 'x'
 并且记住匹配项。其中括号被称为*捕获括号*。

 模式 `/(foo) (bar) \1 \2/` 中的 '`(foo)`' 和
 '`(bar)`' 匹配并记住字符串 "foo bar foo bar"
 中前两个单词。模式中的 `\1` 和
 `\2` 表示第一个和第二个被捕获括号匹配的子字符串，即
 `foo` 和
 `bar`，匹配了原字符串中的后两个单词。注意
 `\1`、`\2`、...、`\n`
 是用在正则表达式的匹配环节，详情可以参阅后文的
 \n
 条目。而在正则表达式的替换环节，则要使用像
 `$1`、`$2`、...、`$n`
 这样的语法，例如，`'bar foo'.replace(/(...) (...)/, '$2 $1')`。`$&` 表示整个用于匹配的原字符串。
  |
| `(?:x)` |
 匹配 'x'
 但是不记住匹配项。这种括号叫作*非捕获括号*，使得你能够定义与正则表达式运算符一起使用的子表达式。看看这个例子
 `/(?:foo){1,2}/`。如果表达式是
 `/foo{1,2}/`，`{1,2}` 将只应用于 'foo'
 的最后一个字符 'o'。如果使用非捕获括号，则
 `{1,2}` 会应用于整个 'foo' 单词。更多信息，可以参阅下文的
 使用括号的子字符串匹配
 条目。
  |
| `x(?=y)` | 匹配'x'仅仅当'x'后面跟着'y'.这种叫做先行断言。
例如，/Jack(?=Sprat)/会匹配到'Jack'仅当它后面跟着'Sprat'。/Jack(?=Sprat|Frost)/匹配‘Jack’仅当它后面跟着'Sprat'或者是‘Frost’。但是‘Sprat’和‘Frost’都不是匹配结果的一部分。 |
| `(?<=y)`x | 匹配'x'仅当'x'前面是'y'.这种叫做后行断言。

 例如，/(?<=Jack)Sprat/会匹配到' Sprat '仅仅当它前面是' Jack
 '。/(?<=Jack|Tom)Sprat/匹配‘Sprat
 ’仅仅当它前面是'Jack'或者是‘Tom’。但是‘Jack’和‘Tom’都不是匹配结果的一部分。
  |
| `x(?!y)` | 仅仅当'x'后面不跟着'y'时匹配'x'，这被称为正向否定查找。

 例如，仅仅当这个数字后面没有跟小数点的时候，/\d+(?!\.)/
 匹配一个数字。正则表达式/\d+(?!\.)/.exec("3.141")
 匹配‘141’而不是‘3.141’
  |
| `(?<!*y*)*x*` | 仅仅当'x'前面不是'y'时匹配'x'，这被称为反向否定查找。

 例如，仅仅当这个数字前面没有负号的时候，`/(?<!-)\d+/`
 匹配一个数字。`/(?<!-)\d+/.exec('3')` 匹配到
 "3".`/(?<!-)\d+/.exec('-3')`
 因为这个数字前有负号，所以没有匹配到。
  |
| `x|y` | 匹配‘x’或者‘y’。
例如，/green|red/匹配“green apple”中的‘green’和“red apple”中的‘red’ |
| `{n}` |
 n 是一个正整数，匹配了前面一个字符刚好出现了 n 次。比如， /a{2}/
 不会匹配“candy”中的'a',但是会匹配“caandy”中所有的
 a，以及“caaandy”中的前两个'a'。
  |
| `{n,}` | n 是一个正整数，匹配前一个字符至少出现了 n 次。
例如，/a{2,}/ 匹配 "aa", "aaaa" 和 "aaaaa" 但是不匹配 "a"。 |
| `{n,m}` |
 n 和 m 都是整数。匹配前面的字符至少 n 次，最多 m 次。如果 n 或者 m
 的值是 0，这个值被忽略。

 例如，/a{1, 3}/ 并不匹配“cndy”中的任意字符，匹配“candy”中的
 a，匹配“caandy”中的前两个 a，也匹配“caaaaaaandy”中的前三个
 a。注意，当匹配”caaaaaaandy“时，匹配的值是“aaa”，即使原始的字符串中有更多的
 a。
  |
| `[xyz]` |
 一个字符集合。匹配方括号中的任意字符，包括转义序列。你可以使用破折号（-）来指定一个字符范围。对于点（.）和星号（\*）这样的特殊符号在一个字符集中没有特殊的意义。他们不必进行转义，不过转义也是起作用的。例如，[abcd]
 和 [a-d]
 是一样的。他们都匹配"brisket"中的‘b’,也都匹配“city”中的‘c’。/[a-z.]+/
 和/[\w.]+/与字符串“test.i.ng”匹配。
  |
| `[^xyz]` |
 一个反向字符集。也就是说，
 它匹配任何没有包含在方括号中的字符。你可以使用破折号（-）来指定一个字符范围。任何普通字符在这里都是起作用的。

 例如，[^abc] 和 [^a-c]
 是一样的。他们匹配"brisket"中的‘r’，也匹配“chop”中的‘h’。
  |
| `[\b]` | 匹配一个退格 (U+0008)。（不要和\b混淆了。） |
| `\b` |
 匹配一个词的边界。一个词的边界就是一个词不被另外一个“字”字符跟随的位置或者前面跟其他“字”字符的位置，例如在字母和空格之间。注意，匹配中不包括匹配的字边界。换句话说，一个匹配的词的边界的内容的长度是
 0。（不要和 [\b] 混淆了）

使用"moon"举例：/\bm/匹配“moon”中的‘m’；/oo\b/并不匹配"moon"中的'oo'，因为'oo'被一个“字”字符'n'紧跟着。/oon\b/匹配"moon"中的'oon'，因为'oon'是这个字符串的结束部分。这样他没有被一个“字”字符紧跟着。/\w\b\w/将不能匹配任何字符串，因为在一个单词中间的字符永远也不可能同时满足没有“字”字符跟随和有“字”字符跟随两种情况。

**备注：** JavaScript 的正则表达式引擎将特定的字符集定义为“字”字符。不在该集合中的任何字符都被认为是一个断词。这组字符相当有限：它只包括大写和小写的罗马字母，十进制数字和下划线字符。不幸的是，重要的字符，例如“é”或“ü”，被视为断词。
 |
| `\B` | 匹配一个非单词边界。匹配如下几种情况：* 字符串第一个字符为非“字”字符
* 字符串最后一个字符为非“字”字符
* 两个单词字符之间
* 两个非单词字符之间
* 空字符串

 例如，/\B../匹配"noonday"中的'oo', 而/y\B../匹配"possibly
 yesterday"中的’yes‘
  |
| `\c*X*` | 当 X 是处于 A 到 Z 之间的字符的时候，匹配字符串中的一个控制符。
例如，`/\cM/` 匹配字符串中的 control-M (U+000D)。 |
| `\d` | 匹配一个数字`。``等价于 [0-9]`。

 例如， `/\d/` 或者 `/[0-9]/` 匹配"B2 is the
 suite number."中的'2'。
  |
| `\D` | 匹配一个非数字字符`。``等价于 [^0-9]`。

 例如， `/\D/` 或者 `/[^0-9]/` 匹配"B2 is the
 suite number."中的'B' 。
  |
| `\f` | 匹配一个换页符 (U+000C)。 |
| `\n` | 匹配一个换行符 (U+000A)。 |
| `\r` | 匹配一个回车符 (U+000D)。 |
| `\s` |
 匹配一个空白字符，包括空格、制表符、换页符和换行符。等价于
 `[\f\n\r\t\v\u0020\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]`。

例如，`/\s\w*/` 匹配"foo bar."中的' bar'。

 经测试，\s不匹配"\u180e"，在当前版本 Chrome(v80.0.3987.122) 和 Firefox(76.0.1)
 控制台输入/\s/.test("\u180e") 均返回 false。
  |
| `\S` |
 匹配一个非空白字符。等价于
 `[^\f\n\r\t\v\u0020\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]`。

例如，`/\S\w*/` 匹配"foo bar."中的'foo'。 |
| `\t` | 匹配一个水平制表符 (U+0009)。 |
| `\v` | 匹配一个垂直制表符 (U+000B)。 |
| `\w` |
 匹配一个单字字符（字母、数字或者下划线）。等价于
 `[A-Za-z0-9_]`。

 例如，`/\w/` 匹配 "apple," 中的 'a'，"$5.28,"中的 '5' 和
 "3D." 中的 '3'。
  |
| `\W` | 匹配一个非单字字符。等价于 `[^A-Za-z0-9_]`。

 例如，`/\W/` 或者 `/[^A-Za-z0-9_]/` 匹配 "50%."
 中的 '%'。
  |
| `\*n*` |
 在正则表达式中，它返回最后的第 n 个子捕获匹配的子字符串
 (捕获的数目以左括号计数)。

 比如 `/apple(,)\sorange\1/` 匹配"apple, orange, cherry,
 peach."中的'apple, orange,' 。
  |
| `\0` |
 匹配 NULL（U+0000）字符，不要在这后面跟其他小数，因为
 `\0<digits>` 是一个八进制转义序列。
  |
| `\xhh` | 匹配一个两位十六进制数（\x00-\xFF）表示的字符。 |
| `\uhhhh` | 匹配一个四位十六进制数表示的 UTF-16 代码单元。 |
| `\u{hhhh}或\u{hhhhh}` | （仅当设置了 u 标志时）匹配一个十六进制数表示的 Unicode 字符。 |

### Escaping

如果你需要使用任何特殊字符的字面值（例如，搜索字符'\*'），你必须通过在它前面放一个反斜杠来转义它。例如，要搜索'a'后跟'\*'后跟'b'，你应该使用`/a\*b/`- 反斜杠“转义”字符'\*'，使其成为文字而非特殊符号。

类似地，如果你正在编写正则表达式文字并且需要匹配斜杠（'/'），那么需要转义它（否则，斜杠是正则终止符）。例如，要搜索字符串“/ example /”后跟一个或多个字母字符，你需要使用`/\/example\/[a-z]+/i`——每个斜杠之前使用反斜杠使它们成为普通字符。

要匹配文本符号反斜杠，你需要转义反斜杠。例如，要匹配字符串“C:\”，其中“C”可以是任何字母，你将使用`/[A-Z]:\\/`——第一个反斜杠转义后面的那个反斜杠，因此表达式搜索单个普通字符反斜杠。

如果将 RegExp 构造函数与字符串文字一起使用，请记住反斜杠是字符串文字中的转义，因此要在正则表达式中使用它，你需要在字符串文字级别转义它。 `/a\*b/` 和`new RegExp("a\\*b")`创建的表达式是相同的，搜索“a”后跟文字“\*”后跟“b”。

将用户输入转义为正则表达式中的一个字面字符串，可以通过简单的替换来实现：

```js
function escapeRegExp(string) {
  return string.replace(/[.\*+?^${}()|[\]\\]/g, "\\$&");
  //$&表示整个被匹配的字符串
}
```

正则表达式后的"g"是一个表示全局搜索选项或标记，将在整个字符串查找并返回所有匹配结果。这将在下面的通过标志进行高级搜索详述。

为什么这个没有内建在 JavaScript 中？之前有计划在 RegExp 对象中添加一个 Function，但在TC39中被否决了。

### 使用插入语

任何正则表达式的插入语都会使这部分匹配的副字符串被记忆。一旦被记忆，这个副字符串就可以被调用于其他用途，如同 使用括号的子字符串匹配之中所述。

比如， `/Chapter (\d+)\.\d*/` 解释了额外转义的和特殊的字符，并说明了这部分 pattern 应该被记忆。它精确地匹配后面跟着一个以上数字字符的字符 'Chapter ' (`\d` 意为任何数字字符，`+ 意为 1 次以上`)，跟着一个小数点（在这个字符中本身也是一个特殊字符；小数点前的 \ 意味着这个 pattern 必须寻找字面字符 '.')，跟着任何数字字符 0 次以上。 (`\d` 意为数字字符， `*` 意为 0 次以上)。另外，插入语也用来记忆第一个匹配的数字字符。

此模式可以匹配字符串"Open Chapter 4.3, paragraph 6"，并且'4'将会被记住。此模式并不能匹配"Chapter 3 and 4"，因为在这个字符串中'3'的后面没有点号'.'。

括号中的"?:"，这种模式匹配的子字符串将不会被记住。比如，(?:\d+) 匹配一次或多次数字字符，但是不能记住匹配的字符。

使用正则表达式
-------

正则表达式可以被用于 `RegExp` 的 `exec` 和 `test` (en-US) 方法以及 `String` 的 `match` (en-US)、`replace`、`search` (en-US) 和 `split` (en-US) 方法。这些方法在 JavaScript 手册中有详细的解释。

| 方法 | 描述 |
| --- | --- |
| `exec` | 一个在字符串中执行查找匹配的 RegExp 方法，它返回一个数组（未匹配到则返回 null）。 |
| `test` | 一个在字符串中测试是否匹配的 RegExp 方法，它返回 true 或 false。 |
| `match` | 一个在字符串中执行查找匹配的 String 方法，它返回一个数组，在未匹配到时会返回 null。 |
| `matchAll` | 一个在字符串中执行查找所有匹配的 String 方法，它返回一个迭代器（iterator）。 |
| `search` | 一个在字符串中测试匹配的 String 方法，它返回匹配到的位置索引，或者在失败时返回 -1。 |
| `replace` | 一个在字符串中执行查找匹配的 String 方法，并且使用替换字符串替换掉匹配到的子字符串。 |
| `split` | 一个使用正则表达式或者一个固定字符串分隔一个字符串，并将分隔后的子字符串存储到数组中的 `String` 方法。 |

当你想要知道在一个字符串中的一个匹配是否被找到，你可以使用 test 或 search 方法；想得到更多的信息（但是比较慢）则可以使用 exec 或 match 方法。如果你使用 exec 或 match 方法并且匹配成功了，那么这些方法将返回一个数组并且更新相关的正则表达式对象的属性和预定义的正则表达式对象（详见下）。如果匹配失败，那么 exec 方法返回 null（也就是 false）。

在接下来的例子中，脚本将使用 exec 方法在一个字符串中查找一个匹配。

```js
var myRe = /d(b+)d/g;
var myArray = myRe.exec("cdbbdbsbz");
```

如果你不需要访问正则表达式的属性，这个脚本通过另一个方法来创建 myArray：

```js
var myArray = /d(b+)d/g.exec("cdbbdbsbz");
// 和 "cdbbdbsbz".match(/d(b+)d/g); 相似。
// 但是 "cdbbdbsbz".match(/d(b+)d/g) 输出数组 [ "dbbd" ]，
// 而 /d(b+)d/g.exec('cdbbdbsbz') 输出数组 [ "dbbd", "bb", index: 1, input: "cdbbdbsbz" ].
```

如果你想通过一个字符串构建正则表达式，那么这个脚本还有另一种方法：

```js
var myRe = new RegExp("d(b+)d", "g");
var myArray = myRe.exec("cdbbdbsbz");
```

通过这些脚本，匹配成功后将返回一个数组并且更新正则表达式的属性，如下表所示。

正则表达式执行后的返回信息| 对象 | 属性或索引 | 描述 | 在例子中对应的值 |
| --- | --- | --- | --- |
| `myArray` |  | 匹配到的字符串和所有被记住的子字符串。 | `["dbbd", "bb"]` |
| `index` | 在输入的字符串中匹配到的以 0 开始的索引值。 | `1` |
| `input` | 初始字符串。 | `"cdbbdbsbz"` |
| `[0]` | 最近一个匹配到的字符串。 | `"dbbd"` |
| `myRe` | `lastIndex` |
 开始下一个匹配的起始索引值。（这个属性只有在使用 g 参数时可用在
 通过参数进行高级搜索
 一节有详细的描述.)
  | `5` |
| `source` | 模式字面文本。在正则表达式创建时更新，不执行。 | `"d(b+)d"` |

如这个例子中的第二种形式所示，你可以使用对象初始器创建一个正则表达式实例，但不分配给变量。如果你这样做，那么，每一次使用时都会创建一个新的正则表达式实例。因此，如果你不把正则表达式实例分配给一个变量，你以后将不能访问这个正则表达式实例的属性。例如，假如你有如下脚本：

```js
var myRe = /d(b+)d/g;
var myArray = myRe.exec("cdbbdbsbz");
console.log("The value of lastIndex is " + myRe.lastIndex);
```

这个脚本输出如下：
```

The value of lastIndex is 5
```

然而，如果你有如下脚本：

```js
var myArray = /d(b+)d/g.exec("cdbbdbsbz");
console.log("The value of lastIndex is " + /d(b+)d/g.lastIndex);
```

它显示为：
```

The value of lastIndex is 0
```

当发生/d(b+)d/g 使用两个不同状态的正则表达式对象，lastIndex 属性会得到不同的值。如果你需要访问一个正则表达式的属性，则需要创建一个对象初始化生成器，你应该首先把它赋值给一个变量。

### 使用括号的子字符串匹配

一个正则表达式模式使用括号，将导致相应的子匹配被记住。例如，/a(b)c /可以匹配字符串“abc”，并且记得“b”。回调这些括号中匹配的子串，使用数组元素 [1],……[n]。

使用括号匹配的子字符串的数量是无限的。返回的数组中保存所有被发现的子匹配。下面的例子说明了如何使用括号的子字符串匹配。

下面的脚本使用 replace() 方法来转换字符串中的单词。在匹配到的替换文本中，脚本使用替代的$ 1,$ 2 表示第一个和第二个括号的子字符串匹配。

```js
var re = /(\w+)\s(\w+)/;
var str = "John Smith";
var newstr = str.replace(re, "$2, $1");
console.log(newstr);
```

这个表达式输出 "Smith, John"。

### 通过标志进行高级搜索

正则表达式有六个可选参数 (`flags`) 允许全局和不分大小写搜索等。这些参数既可以单独使用也能以任意顺序一起使用，并且被包含在正则表达式实例中。

| 标志 | 描述 |
| --- | --- |
| `g` | 全局搜索。 |
| `i` | 不区分大小写搜索。 |
| `m` | 多行搜索。 |
| `s` | 允许 `.` 匹配换行符。 |
| `u` | 使用 unicode 码的模式进行匹配。 |
| `y` | 执行“粘性 (`sticky`)”搜索，匹配从目标字符串的当前位置开始。 |

为了在正则表达式中包含标志，请使用以下语法：

```js
var re = /pattern/flags;
```

或者

```js
var re = new RegExp("pattern", "flags");
```

值得注意的是，标志是一个正则表达式的一部分，它们在接下来的时间将不能添加或删除。

例如，re = /\w+\s/g 将创建一个查找一个或多个字符后有一个空格的正则表达式，或者组合起来像此要求的字符串。

```js
var re = /\w+\s/g;
var str = "fee fi fo fum";
var myArray = str.match(re);
console.log(myArray);

// ["fee ", "fi ", "fo "]
```

这段代码将输出 ["fee ", "fi ", "fo "]。在这个例子中，你可以将：

```js
var re = /\w+\s/g;
```

替换成：

```js
var re = new RegExp("\\w+\\s", "g");
```

并且能获取到相同的结果。

使用 `.exec()` 方法时，与 `g` 标志关联的行为是不同的。（“class”和“argument”的作用相反：在 `.match()` 的情况下，字符串类（或数据类型）拥有该方法，而正则表达式只是一个参数，而在 `.exec()` 的情况下，它是拥有该方法的正则表达式，其中字符串是参数。）对比 `str.match(re)` *与* `re.exec(str)`，`g` 标志与 `.exec()` 方法一起使用获得迭代进展。

```js
var xArray;
while ((xArray = re.exec(str))) console.log(xArray);
// produces:
// ["fee ", index: 0, input: "fee fi fo fum"]
// ["fi ", index: 4, input: "fee fi fo fum"]
// ["fo ", index: 7, input: "fee fi fo fum"]
```

m 标志用于指定多行输入字符串应该被视为多个行。如果使用 m 标志，^和$匹配的开始或结束输入字符串中的每一行，而不是整个字符串的开始或结束。

示例
--

以下例子说明了一些正则表达式的用途。

### 改变输入字符串的顺序

以下例子解释了正则表达式的构成和 `string.split()` 以及 `string.replace()`的用途。它会整理一个只有粗略格式的含有全名（名字首先出现）的输入字符串，这个字符串被空格、换行符和一个分号分隔。最终，它会颠倒名字顺序（姓氏首先出现）和 list 的类型。

```js
// 下面这个姓名字符串包含了多个空格和制表符，
// 且在姓和名之间可能有多个空格和制表符。
var names = "Orange Trump ;Fred Barney; Helen Rigby ; Bill Abel ; Chris Hand ";

var output = ["---------- Original String\n", names + "\n"];

// 准备两个模式的正则表达式放进数组里。
// 分割该字符串放进数组里。

// 匹配模式：匹配一个分号及紧接其前后所有可能出现的连续的不可见符号。
var pattern = /\s\*;\s\*/;

// 把通过上述匹配模式分割的字符串放进一个叫做 nameList 的数组里面。
var nameList = names.split(pattern);

// 新建一个匹配模式：匹配一个或多个连续的不可见字符及其前后紧接着由
// 一个或多个连续的基本拉丁字母表中的字母、数字和下划线组成的字符串
// 用一对圆括号来捕获该模式中的一部分匹配结果。
// 捕获的结果稍后会用到。
pattern = /(\w+)\s+(\w+)/;

// 新建一个数组 bySurnameList 用来临时存放正在处理的名字。
var bySurnameList = [];

// 输出 nameList 的元素并且把 nameList 里的名字
// 用逗号接空格的模式把姓和名分割开来然后存放进数组 bySurnameList 中。
//
// 下面的这个替换方法把 nameList 里的元素用 $2, $1 的模式
//（第二个捕获的匹配结果紧接着一个逗号一个空格然后紧接着第一个捕获的匹配结果）替换了
// 变量 $1 和变量 $2 是上面所捕获的匹配结果。

output.push("---------- After Split by Regular Expression");

var i, len;
for (i = 0, len = nameList.length; i < len; i++) {
  output.push(nameList[i]);
  bySurnameList[i] = nameList[i].replace(pattern, "$2, $1");
}

// 输出新的数组
output.push("---------- Names Reversed");
for (i = 0, len = bySurnameList.length; i < len; i++) {
  output.push(bySurnameList[i]);
}

// 根据姓来排序，然后输出排序后的数组。
bySurnameList.sort();
output.push("---------- Sorted");
for (i = 0, len = bySurnameList.length; i < len; i++) {
  output.push(bySurnameList[i]);
}

output.push("---------- End");

console.log(output.join("\n"));
```

### 用特殊字符检验输入

在以下例子中，我们期望用户输入一个电话号码。当用户点击“Check”按钮，我们的脚本开始检查这些数字是否合法。如果数字合法（匹配正则表达式所规定的字符序列），脚本显示一条感谢用户的信息并确认该数字。如果这串数字不合法，脚本提示用户电话号码不合法。.

包含非捕获括号 `(?:` 这个正则表达式寻找三个数字字符 `\d{3}` 或者 `|` 一个左半括号 `\(` 跟着三位数字 `\d{3}`, 跟着一个封闭括号 `\)`, (结束非捕获括号 `)`)，后跟着一个短破折号或正斜杠或小数点，随后跟随三个数字字符，当记忆字符 `([-\/\.])` 捕获并记住，后面跟着三位小数 `\d{3}`，再后面跟随记住的破折号、正斜杠或小数点 `\1`，最后跟着四位小数 `\d{4}`。

当用户按下 Enter 设置 RegExp.input，这些变化也能被激活。

```html
<!doctype html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
    <meta http-equiv="Content-Script-Type" content="text/javascript" />
    <script type="text/javascript">
 var re = /(?:\d{3}|\(\d{3}\))([-\/\.])\d{3}\1\d{4}/;
 function testInfo(phoneInput) {
 var OK = re.exec(phoneInput.value);
 if (!OK)
 window.alert(
 phoneInput.value + " isn't a phone number with area code!",
 );
 else window.alert("Thanks, your phone number is " + OK[0]);
 }
 </script>
  </head>
  <body>
    <p>
      Enter your phone number (with area code) and then click "Check". <br />The
      expected format is like ###-###-####.
    </p>
    <form action="#">
      <input id="phone" /><button
 onclick="testInfo(document.getElementById('phone'));">
        Check
      </button>
    </form>
  </body>
</html>
```

* « 上一页
* 下一页 »


函数 - JavaScript

函数
==

* « 上一页
* 下一页 »

函数是 JavaScript 中的基本组件之一。JavaScript 中的函数类似于过程——一组执行任务或计算值的语句。但要成为函数，这个过程应该接受输入并返回与输入存在某些明显关系的输出。要使用一个函数，你必须将其定义在你希望调用它的作用域内。

参见 JavaScript 函数的详细参考章节，以了解详情。

定义函数
----

### 函数声明

一个**函数定义**（也称为**函数声明**，或**函数语句**）由 `function` 关键字，并跟随以下部分组成：

* 函数名称。
* 函数参数列表，包围在括号中并由逗号分隔。
* 定义函数的 JavaScript 语句，用大括号括起来，`{ /* … */ }`。

例如，以下的代码定义了一个简单的名为 `square` 的函数：

```js
function square(number) {
  return number \* number;
}
```

函数 `square` 接收一个名为 `number` 的参数。这个函数只有一个语句，其表示该函数将函数的参数（即 `number`）自乘后返回。函数的 `return` 语句指定了函数的返回值：`number * number`。

参数本质上是**按值**传递给函数的——因此，即使函数体的代码为传递给函数的参数赋了新值，**这个改变也不会反映到全局或调用该函数的代码中**。

如果你将对象作为参数传递，而函数改变了这个对象的属性，这样的改变对函数外部是可见的，如下面的例子所示：

```js
function myFunc(theObject) {
  theObject.make = "Toyota";
}

const mycar = {
  make: "Honda",
  model: "Accord",
  year: 1998,
};

console.log(mycar.make); // "Honda"
myFunc(mycar);
console.log(mycar.make); // "Toyota"
```

如果你将数组作为参数传递，而函数改变了这个数组的值，这样的改变对函数外部也同样可见，如下面的例子所示：

```js
function myFunc(theArr) {
  theArr[0] = 30;
}

const arr = [45];

console.log(arr[0]); // 45
myFunc(arr);
console.log(arr[0]); // 30
```

### 函数表达式

虽然上面的函数声明在语法上是一个语句，但函数也可以由函数表达式创建。

这样的函数可以是**匿名**的；它不必有一个名称。例如，函数 `square` 也可这样来定义：

```js
const square = function (number) {
  return number \* number;
};

console.log(square(4)); // 16
```

然而，*也可以*为函数表达式提供名称，并且可以用于在函数内部代指其本身，或者在调试器堆栈跟踪中识别该函数：

```js
const factorial = function fac(n) {
  return n < 2 ? 1 : n \* fac(n - 1);
};

console.log(factorial(3)); // 6
```

当将函数作为参数传递给另一个函数时，函数表达式很方便。下面的例子演示了一个叫 `map` 的函数，该函数接收函数作为第一个参数，接收数组作为第二个参数：

```js
function map(f, a) {
  const result = new Array(a.length);
  for (let i = 0; i < a.length; i++) {
    result[i] = f(a[i]);
  }
  return result;
}
```

在以下代码中，该函数接收由函数表达式定义的函数，并对作为第二个参数接收的数组的每个元素执行该函数：

```js
function map(f, a) {
  const result = new Array(a.length);
  for (let i = 0; i < a.length; i++) {
    result[i] = f(a[i]);
  }
  return result;
}

const cube = function (x) {
  return x \* x \* x;
};

const numbers = [0, 1, 2, 5, 10];
console.log(map(cube, numbers)); // [0, 1, 8, 125, 1000]
```

在 JavaScript 中，可以根据条件来定义一个函数。比如下面的代码，当 `num` 等于 `0` 的时候才会定义 `myFunc`：

```js
let myFunc;
if (num === 0) {
  myFunc = function (theObject) {
    theObject.make = "Toyota";
  };
}
```

除了上述的定义函数方法外，你也可以在运行时用 `Function` 构造函数从一个字符串创建一个函数，很像 `eval()` 函数。

当一个函数是一个对象的属性时，称之为**方法**。了解更多关于对象和方法的知识，请阅读使用对象。

调用函数
----

*定义*的函数并不会自动*执行*它。定义了函数仅仅是赋予函数以名称并明确函数被调用时该做些什么。

**调用**函数才会以给定的参数真正执行这些动作。例如，一旦你定义了函数 `square`，你可以像这样调用它：

```js
square(5);
```

上述语句使用参数 `5` 来调用函数。函数执行完它的语句会返回值 `25`。

函数一定要处于调用它们的*作用域中*，但是函数的声明可以被提升（出现在调用语句之后）。函数声明的范围是声明它的函数（或者，如果它是在顶层声明的，则为整个程序）之内。

函数的参数并不局限于字符串或数字。你也可以将整个对象传递给函数。函数 `showProps()`（其定义参见使用对象）就是一个将对象作为参数的示例。

函数可以调用其本身。例如，下面这个函数就是用递归计算阶乘：

```js
function factorial(n) {
  if (n === 0 || n === 1) {
    return 1;
  } else {
    return n \* factorial(n - 1);
  }
}
```

你可以这样计算 `1` 到 `5` 的阶乘：

```js
console.log(factorial(1)); // 1
console.log(factorial(2)); // 2
console.log(factorial(3)); // 6
console.log(factorial(4)); // 24
console.log(factorial(5)); // 120
```

还有其他的方式来调用函数。常见的一些情形是某些地方需要动态调用函数，或者函数的实参数量是变化的，或者调用函数的上下文需要指定为在运行时确定的特定对象。

显然，*函数本身就是对象*，因此这些对象也有方法（参见 `Function` 对象）。`call()` 和 `apply()` 方法可用于实现这些目的。

### 函数提升

考虑以下示例：

```js
console.log(square(5)); // 25

function square(n) {
  return n \* n;
}
```

尽管 `square()` 函数在声明之前被调用，但此代码的运行并没有任何错误。这是因为 JavaScript 解释器会将整个函数声明提升到当前作用域的顶部，因此上面的代码等价于：

```js
// 所有函数声明实际上都位于作用域的顶部
function square(n) {
  return n \* n;
}

console.log(square(5)); // 25
```

函数提升仅适用于函数*声明*，而不适用于函数*表达式*。以下代码无法运行：

```js
console.log(square(5)); // ReferenceError: Cannot access 'square' before initialization
const square = function (n) {
  return n \* n;
};
```

函数作用域
-----

在函数内定义的变量不能在函数之外的任何地方访问，因为变量仅仅在该函数的作用域内定义。相对应的，一个函数可以访问定义在其范围内的任何变量和函数。

换言之，定义在全局域中的函数可以访问所有定义在全局域中的变量。在另一个函数中定义的函数也可以访问在其父函数中定义的所有变量和父函数有权访问的任何其他变量。

```js
// 下面的变量定义在全局作用域中
const num1 = 20;
const num2 = 3;
const name = "Chamakh";

// 此函数定义在全局作用域中
function multiply() {
  return num1 \* num2;
}

console.log(multiply()); // 60

// 嵌套函数示例
function getScore() {
  const num1 = 2;
  const num2 = 3;

  function add() {
    return `${name} 的得分为 ${num1 + num2}`;
  }

  return add();
}

console.log(getScore()); // "Chamakh 的得分为 5"
```

作用域和函数栈
-------

### 递归

一个函数可以指向并调用自身。有三种方法可以达到这个目的：

1. 函数名
2. `arguments.callee`
3. 作用域内一个指向该函数的变量名

例如，思考如下的函数定义：

```js
const foo = function bar() {
  // 这里编写语句
};
```

在这个函数体内，以下的语句是等价的：

1. `bar()`
2. `arguments.callee()`
3. `foo()`

调用自身的函数我们称之为*递归函数*。在某种意义上说，递归近似于循环。两者都重复执行相同的代码，并且两者都需要一个终止条件（避免无限循环，或者在这种情况下更确切地说是无限递归）。

例如，考虑以下的循环：

```js
let x = 0;
// “x < 10”是循环条件
while (x < 10) {
  // 做些什么
  x++;
}
```

可以被转化成一个递归函数声明，然后调用该函数：

```js
function loop(x) {
  // “x >= 10”是退出条件（等同于“!(x < 10)”）
  if (x >= 10) {
    return;
  }
  // 做些什么
  loop(x + 1); // 递归调用
}
loop(0);
```

不过，有些算法并不能简单的用迭代来实现。例如，获取树结构（例如 DOM）中所有的节点时，使用递归实现要容易得多：

```js
function walkTree(node) {
  if (node === null) {
    return;
  }
  // 对节点做些什么
  for (let i = 0; i < node.childNodes.length; i++) {
    walkTree(node.childNodes[i]);
  }
}
```

跟 `loop` 函数相比，这里每个递归调用都产生了更多的递归调用。

将递归算法转换为非递归算法是可能的，不过逻辑上通常会更加复杂，而且需要使用栈。

事实上，递归本身就使用了栈：函数栈。类似栈的行为可以在以下示例中看到：

```js
function foo(i) {
  if (i < 0) {
    return;
  }
  console.log(`开始：${i}`);
  foo(i - 1);
  console.log(`结束：${i}`);
}
foo(3);

// 打印：
// 开始：3
// 开始：2
// 开始：1
// 开始：0
// 结束：0
// 结束：1
// 结束：2
// 结束：3
```

### 嵌套函数和闭包

你可以在一个函数里面嵌套另外一个函数。嵌套（内部）函数对其容器（外部）函数是私有的。

它自身也形成了一个*闭包*（closure）。闭包是可以拥有独立变量以及绑定了这些变量的环境（“封闭”了表达式）的表达式（通常是函数）。

既然嵌套函数是一个闭包，就意味着一个嵌套函数可以“继承”容器函数的参数和变量。换句话说，内部函数包含外部函数的作用域。

可以总结如下：

* 内部函数只可以在外部函数中访问。
* 内部函数形成了一个闭包：它可以访问外部函数的参数和变量，但是外部函数却不能使用它的参数和变量。

下面的例子展示了嵌套函数：

```js
function addSquares(a, b) {
  function square(x) {
    return x \* x;
  }
  return square(a) + square(b);
}

console.log(addSquares(2, 3)); // 13
console.log(addSquares(3, 4)); // 25
console.log(addSquares(4, 5)); // 41
```

由于内部函数形成了闭包，因此你可以调用外部函数并为外部函数和内部函数指定参数：

```js
function outside(x) {
  function inside(y) {
    return x + y;
  }
  return inside;
}

const fnInside = outside(3); // 可以这样想：给我一个可以将提供的值加上 3 的函数
console.log(fnInside(5)); // 8
console.log(outside(3)(5)); // 8
```

### 保存变量

注意到上例中 `inside` 被返回时 `x` 是怎么被保留下来的。一个闭包必须保存它可见作用域中所有参数和变量。因为每一次调用传入的参数都可能不同，每一次对外部函数的调用实际上重新创建了一遍这个闭包。只有当返回的 `inside` 没有再被引用时，内存才会被释放。

这与在其他对象中存储引用没什么不同，但是通常不太明显，因为并不能直接设置引用，也不能检查它们。

### 多层嵌套函数

函数可以被多层嵌套。例如：

* 函数（`A`）可以包含函数（`B`），后者可以再包含函数（`C`）。
* 这里的函数 `B` 和 `C` 都形成了闭包，所以 `B` 可以访问 `A`，`C` 可以访问 `B`。
* 此外，因为 `C` 可以访问 `B`（而 `B` 可以访问 `A`），所以 `C` 也可以访问 `A`。

因此，闭包可以包含多个作用域；它们递归地包含了所有包含它的函数作用域。这个称之为*作用域链*。（稍后解释它被称为“链”的原因。）

思考以下示例：

```js
function A(x) {
  function B(y) {
    function C(z) {
      console.log(x + y + z);
    }
    C(3);
  }
  B(2);
}
A(1); // 打印 6（即 1 + 2 + 3）
```

在这个示例中，`C` 可以访问 `B` 的 `y` 和 `A` 的 `x`。

这是因为：

1. `B` 形成了一个包含 `A` 的闭包（即，`B` 可以访问 `A` 的参数和变量）
2. `C` 形成了一个包含 `B` 的闭包。
3. `C` 的闭包包含 `B`，且 `B` 的闭包包含 `A`，所以 `C` 的闭包也包含 `A`。这意味着 `C` *同时*可以访问 `B` *和* `A` 的参数和变量。换言之，`C` *用这个顺序链接*了 `B` 和 `A` 的作用域。

反过来却不是这样。`A` 不能访问 `C`，因为 `A` 不能访问 `B` 中的参数和变量，`C` 是 `B` 中的一个变量，所以 `C` 是 `B` 私有的。

### 命名冲突

当同一个闭包作用域下两个参数或者变量同名时，就会产生*命名冲突*。更近的作用域有更高的优先权，所以最近的优先级最高，最远的优先级最低。这就是作用域链。链的第一个元素就是最里面的作用域，最后一个元素便是最外层的作用域。考虑以下示例：

```js
function outside() {
  const x = 5;
  function inside(x) {
    return x \* 2;
  }
  return inside;
}

console.log(outside()(10)); // 20（而不是 10）
```

命名冲突发生在语句 `return x * 2` 上，`inside` 的参数 `x` 和 `outside` 的变量 `x` 发生了冲突。这里的作用链域是 {`inside`、`outside`、全局对象}。因此 `inside` 的 `x` 优先于 `outside` 的 `x`，因此返回 `20`（`inside` 的 `x`）而不是 `10`（`outside` 的 `x`）。

闭包
--

闭包是 JavaScript 中最强大的特性之一。JavaScript 允许函数嵌套，并且内部函数具有定义在外部函数中的所有变量和函数（以及外部函数能访问的所有变量和函数）的完全访问权限。

但是，外部函数却*不能*访问定义在内部函数中的变量和函数。这给内部函数的变量提供了一种封装。

此外，由于内部函数可以访问外部函数的作用域，因此当内部函数生存周期大于外部函数时，外部函数中定义的变量和函数的生存周期将比内部函数执行的持续时间要长。当内部函数以某一种方式被任何一个外部函数之外的任何作用域访问时，就会创建闭包。

```js
// 外部函数定义了一个名为“name”的变量
const pet = function (name) {
  const getName = function () {
    // 内部函数可以访问外部函数的“name”变量
    return name;
  };
  return getName; // 返回内部函数，从而将其暴露给外部作用域
};
const myPet = pet("Vivie");

console.log(myPet()); // "Vivie"
```

实际上可能会比上面的代码复杂的多。它可以返回一个包含用于操作外部函数的内部变量的方法的对象。

```js
const createPet = function (name) {
  let sex;

  const pet = {
    // 在这个上下文中：setName(newName) 等价于 setName: function (newName)
    setName(newName) {
      name = newName;
    },

    getName() {
      return name;
    },

    getSex() {
      return sex;
    },

    setSex(newSex) {
      if (
        typeof newSex === "string" &&
        (newSex.toLowerCase() === "male" || newSex.toLowerCase() === "female")
      ) {
        sex = newSex;
      }
    },
  };

  return pet;
};

const pet = createPet("Vivie");
console.log(pet.getName()); // Vivie

pet.setName("Oliver");
pet.setSex("male");
console.log(pet.getSex()); // male
console.log(pet.getName()); // Oliver
```

在上面的代码中，外部函数的 `name` 变量对内部函数来说是可访问的，而除了通过内部函数本身，没有其他任何方法可以取得内部的变量。内部函数的内部变量就像对外部参数和变量的保险柜。它们会为内部函数保留“稳定”而又“被封装”的数据参与运行。而这些内部函数甚至不会被分配给一个变量，或者有个名称。

```js
const getCode = (function () {
  const apiCode = "0]Eal(eh&2"; // 我们不希望外部能够修改的代码......

  return function () {
    return apiCode;
  };
})();

console.log(getCode()); // "0]Eal(eh&2"
```

**备注：** 使用闭包时需要注意许多陷阱！

如果一个闭包的函数定义了一个和外部的某个变量名称相同的变量，那么这个闭包将无法引用外部作用域中的这个变量。（内部作用域的变量“覆盖”外部作用域，直至程序退出内部作用域。可以将其视作命名冲突。）

```js
const createPet = function (name) {
  // 外部函数定义了一个名为“name”的变量。
  return {
    setName(name) {
      // 闭包函数还定义了一个名为“name”的变量。
      name = name; // 我们如何访问外部函数定义的“name”？
    },
  };
};
```

使用 arguments 对象
---------------

函数的实际参数会被保存在一个类似数组的 arguments 对象中。在函数内，你可以按如下方式找出传入的参数：

```js
arguments[i];
```

其中 `i` 是参数的序号，从 `0` 开始。所以第一个传入函数的参数会是 `arguments[0]`。参数的数量由 `arguments.length` 表示。

使用 `arguments` 对象，你可以处理比声明更多的参数来调用函数。这在你事先不知道会需要将多少参数传递给函数时十分有用。你可以用 `arguments.length` 来获得实际传递给函数的参数的数量，然后用 `arguments` 对象来访问每个参数。

例如，考虑有一个用来连接字符串的函数。唯一正式的参数是在连接后的字符串中用来分隔各个连接部分的字符。该函数定义如下：

```js
function myConcat(separator) {
  let result = ""; // 初始化列表
  // 迭代 arguments
  for (let i = 1; i < arguments.length; i++) {
    result += arguments[i] + separator;
  }
  return result;
}
```

你可以给这个函数传递任意数量的参数，它会将各个参数连接成一个字符串“列表”：

```js
console.log(myConcat("、", "红", "橙", "蓝"));
// "红、橙、蓝、"

console.log(myConcat("；", "大象", "长颈鹿", "狮子", "猎豹"));
// "大象；长颈鹿；狮子；猎豹；"

console.log(myConcat("。", "智者", "罗勒", "牛至", "胡椒", "香菜"));
// "智者。罗勒。牛至。胡椒。香菜。"
```

**备注：** `arguments` 变量只是“类数组”，而不是数组。它与数组类似，有索引编号和 `length` 属性。尽管如此，它并不*具备* Array 对象的所有数组操作方法。

更多信息请阅读 JavaScript 参考中的 `Function` 对象。

函数参数
----

有两种特殊的参数语法：*默认参数*和*剩余参数*。

### 默认参数

在 JavaScript 中，函数参数的默认值是 `undefined`。然而，在某些情况下设置不同的默认值可能会很有用。这正是默认参数的作用。

在过去，用于设定默认参数的一般策略是在函数的主体中测试参数值是否为 `undefined`，如果是则赋予这个参数一个默认值。

在下面的示例中，如果调用函数时没有给 `b` 提供值，那么它的值就是 `undefined`，在执行 `a*b` 时，调用乘法通常会返回 `NaN`。但是，这已经被示例的第二行所避免了：

```js
function multiply(a, b) {
  b = typeof b !== "undefined" ? b : 1;
  return a \* b;
}

console.log(multiply(5)); // 5
```

使用*默认参数*，在函数体的手动检查就不再必要了。现在，你可以在函数头简单地把 `1` 设定为 `b` 的默认值：

```js
function multiply(a, b = 1) {
  return a \* b;
}

console.log(multiply(5)); // 5
```

见参考的默认参数，以获取详情。

### 剩余参数

剩余参数语法允许将不确定数量的参数表示为数组。

在下面的示例中，`multiply` 函数使用*剩余参数*收集从第二个参数开始到最后的参数。然后，该函数将它们与第一个参数相乘。

```js
function multiply(multiplier, ...theArgs) {
  return theArgs.map((x) => multiplier \* x);
}

const arr = multiply(2, 1, 2, 3);
console.log(arr); // [2, 4, 6]
```

箭头函数
----

箭头函数表达式（也称*胖箭头*，以区分未来 JavaScript 中假设的 `->` 语法）相比函数表达式具有较短的语法且没有它自己的 `this`、`arguments`、`super` 和 `new.target`。箭头函数总是匿名的。

有两个因素会影响对箭头函数的引入：*更简洁的函数*和 `this` 的*无绑定性*。

### 更简洁的函数

在一些函数模式中，更简洁的函数很受欢迎。对比一下：

```js
const a = ["Hydrogen", "Helium", "Lithium", "Beryllium"];

const a2 = a.map(function (s) {
  return s.length;
});

console.log(a2); // [8, 6, 7, 9]

const a3 = a.map((s) => s.length);

console.log(a3); // [8, 6, 7, 9]
```

### 无单独的 this

在箭头函数出现之前，每一个新函数都定义了自己的 `this` 值（在构造函数中是一个新的对象；在严格模式下是 undefined；在作为“对象方法”调用的函数中指向这个对象；等等）。事实证明，这对于面向对象的编程风格来说并不理想。

```js
function Person() {
  // 构造函数 Person() 将 `this` 定义为自身。
  this.age = 0;

  setInterval(function growUp() {
    // 在非严格模式下，growUp() 函数将 `this` 定义为“全局对象”，
    // 这与 Person() 定义的 `this` 不同。
    this.age++;
  }, 1000);
}

const p = new Person();
```

在 ECMAScript 3/5 中，通过把 `this` 的值赋值给一个变量可以修复这个问题。

```js
function Person() {
  // 有的人习惯用 `that` 而不是 `self`。
  // 请选择一种方式，并保持前后代码的一致性
  const self = this;
  self.age = 0;

  setInterval(function growUp() {
    // 回调引用 `self` 变量，其值为预期的对象。
    self.age++;
  }, 1000);
}
```

另外，创建一个绑定函数可以使得 `this` 值被正确传递给 `growUp()` 函数。

箭头函数没有自己的 `this`，而是使用封闭执行上下文的 `this` 值。因此，在以下代码中，传递到 `setInterval` 中的函数内的 `this` 与闭包函数中的 `this` 相同：

```js
function Person() {
  this.age = 0;

  setInterval(() => {
    this.age++; // 这里的 `this` 正确地指向 person 对象
  }, 1000);
}

const p = new Person();
```

预定义函数
-----

JavaScript 语言有几个顶级的内置函数：

`eval()`

**`eval()`** 方法执行方法计算以字符串表示的 JavaScript 代码。

`isFinite()`

**`isFinite()`** 全局函数判断传入的值是否是有限的数值。如果需要的话，其参数首先被转换为一个数值。

`isNaN()`

**`isNaN()`** 函数判断一个值是否是 `NaN`。注意：`isNaN` 函数内部的强制转换规则十分有趣。你也可以使用 `Number.isNaN()` 来判断该值是否为 NaN。

`parseFloat()`

**`parseFloat()`** 函数解析字符串参数，并返回一个浮点数。

`parseInt()`

**`parseInt()`** 函数解析字符串参数，并返回指定的基数（基础数学中的数制）的整数。

`decodeURI()`

**`decodeURI()`** 函数对先前经过 `encodeURI` 函数或者其他类似方法编码过的统一资源标志符（URI）进行解码。

`decodeURIComponent()`

**`decodeURIComponent()`** 方法对先前经过 `encodeURIComponent` 函数或者其他类似方法编码的统一资源标志符（URI）进行解码。

`encodeURI()`

**`encodeURI()`** 方法通过以表示字符的 UTF-8 编码的一个、两个、三个或四个转义序列替换统一资源标识符（URI）的某些字符来进行编码（对于由两个“代理（surrogate）”字符组成的字符，只会编码为四个转义序列）。

`encodeURIComponent()`

**`encodeURIComponent()`** 方法通过以表示字符的 UTF-8 编码的一个、两个、三个或四个转义序列替换统一资源标识符（URI）的某些字符来进行编码（对于由两个“代理”字符组成的字符，只会编码为四个转义序列）。

`escape()`
已弃用

**`escape()`** 方法生成一个新的字符串，其中的某些字符已被替换为十六进制转义序列。其已被弃用，请使用 `encodeURI()` 或 `encodeURIComponent()` 代替。

`unescape()`
已弃用

**`unescape()`** 方法计算生成一个新的字符串，其中的十六进制转义序列将被其表示的字符替换。上述的转义序列就像 `escape` 介绍的一样。其已被弃用，请使用 `decodeURI()` 或 `decodeURIComponent()` 替代。

* « 上一页
* 下一页 »


循环与迭代 - JavaScript

循环与迭代
=====

* « 上一页
* 下一页 »

循环提供了一种快速和简单的方式去做一些重复的事。JavaScript 入门的这个章节会介绍在 JavaScript 中存在哪些不同的迭代语句。

你可以把循环想成一种计算机化的游戏，告诉某人在一个方向上走 X 步，然后在另一个方向上走 Y 步；例如，“向东走 5 步”可以用一个循环来这样表达：

```js
var step;
for (step = 0; step < 5; step++) {
  // Runs 5 times, with values of step 0 through 4.
  console.log("Walking east one step");
}
```

循环有很多种类，但本质上它们都做的是同一件事：它们把一个动作重复了很多次（实际上重复的次数有可能为 0）。各种循环机制提供了不同的方法去确定循环的开始和结束。不同情况下，某一种类型循环会比其他的循环用起来更简单。

JavaScript 中提供了这些循环语句：

* for 语句
* do...while 语句
* while 语句
* label 语句
* break 语句
* continue 语句
* for...in 语句
* for...of 语句
`for` 语句
--------

一个 `for` 循环会一直重复执行，直到指定的循环条件为 false。JavaScript 的 for 循环，和 Java、C 的 for 循环，是很相似的。一个 for 语句是这个样子的：
```

for ([initialExpression]; [condition]; [incrementExpression])
  statement
```

当一个 `for` 循环执行的时候，会发生以下过程：

1. 如果有初始化表达式 `initialExpression`，它将被执行。这个表达式通常会初始化一个或多个循环计数器，但语法上是允许一个任意复杂度的表达式的。这个表达式也可以声明变量。
2. 计算 `condition` 表达式的值。如果 `condition` 的值是 true，循环中的语句会被执行。如果 `condition` 的值是 false，`for` 循环终止。如果 `condition` 表达式整个都被省略掉了，condition 的值会被认为是 true。
3. 循环中的 `statement` 被执行。如果需要执行多条语句，可以使用块（`{ ... }`）来包裹这些语句。
4. 如果有更新表达式 `incrementExpression`，执行更新表达式。
5. 回到步骤 2。
### 示例

下面的函数包含一个含有 `for` 循环去计算一个滑动列表中被选中项目的个数（一个 `<select>` 元素允许选择多项）。`for` 循环声明了变量 i 并将它的初始值设为 0。它检查 `i` 是否比 `<select>` 元素中的选项数量少，执行了随后的 `if` 语句，然后在每次完成循环后，`i` 的值增加 1。

```html
<form name="selectForm">
  <p>
    <label for="musicTypes"
 >Choose some music types, then click the button below:</label
 >
    <select id="musicTypes" name="musicTypes" multiple="multiple">
      <option selected="selected">R&B</option>
      <option>爵士</option>
      <option>布鲁斯</option>
      <option>新纪元</option>
      <option>古典</option>
      <option>歌剧</option>
    </select>
  </p>
  <p><input id="btn" type="button" value="选择了多少个选项？" /></p>
</form>

<script>
 function howMany(selectObject) {
 var numberSelected = 0;
 for (var i = 0; i < selectObject.options.length; i++) {
 if (selectObject.options[i].selected) {
 numberSelected++;
 }
 }
 return numberSelected;
 }

 var btn = document.getElementById("btn");
 btn.addEventListener("click", function () {
 alert("选择选项的数量是：" + howMany(document.selectForm.musicTypes));
 });
</script>
```

`do...while` 语句
---------------

`do...while` 语句一直重复直到指定的条件求值得到假值（false）。一个 do...while 语句看起来像这样：
```

do
  statement
while (condition);
```

`statement` 在检查条件之前会执行一次。要执行多条语句（语句块），要使用块语句（`{ ... }`）包括起来。如果 `condition` 为真（true），`statement` 将再次执行。在每个执行的结尾会进行条件的检查。当 `condition` 为假（false），执行会停止并且把控制权交回给 `do...while` 后面的语句。

### 示例

在下面的例子中，这个 `do` 循环将至少重复一次，并且一直重复直到 `i` 不再小于 5。

```js
var i = 0;
do {
  i += 1;
  console.log(i);
} while (i < 5);
```

`while` 语句
----------

一个 `while` 语句只要指定的条件求值为真（true）就会一直执行它的语句块。一个 `while` 语句看起来像这样：
```

while (condition)
  statement
```

如果这个条件变为假，循环里的 `statement` 将会停止执行并把控制权交回给 `while` 语句后面的代码。

条件检测会在每次 `statement` 执行之前发生。如果条件返回为真， `statement` 会被执行并紧接着再次测试条件。如果条件返回为假，执行将停止并把控制权交回给 while 后面的语句。

要执行多条语句（语句块），要使用语句块 (`{ ... }`) 包括起来。

### 示例 1

只要 `n` 小于 3，下面的 `while` 循环就会一直执行：

```js
var n = 0;
var x = 0;
while (n < 3) {
  n++;
  x += n;
}
```

在每次循环里， `n` 会增加 1，并被加到 `x` 上。所以，x 和 n 的变化是：

* 第一次完成后：`n` = 1，`x` = 1
* 第二次完成后：`n` = 2，`x` = 3
* 第三次完成后：`n` = 3，`x` = 6

在三次完成后，条件 `n < 3` 的结果不再为真，所以循环终止了。

### 示例 2

避免无穷循环（无限循环）。保证循环的条件结果最终会变成假；否则，循环永远不会停止。因为条件永远不会变成假值，下面这个 while 循环将会永远执行：

```js
while (true) {
  console.log("Hello, world");
}
```

`label` 语句
----------

一个 `label` 提供了一个让你在程序中其他位置引用它的标识符。例如，你可以用 label 标识一个循环，然后使用 `break` 或者 `continue` 来指出程序是否该停止循环还是继续循环。

label 语句的语法看起来像这样：
```

label :
   statement
```

`label` 的值可以是任何的非保留字的 JavaScript 标识符， `statement` 可以是任意你想要标识的语句（块）。

### 示例

在这个例子里，标记 `markLoop` 标识了一个 `while` 循环。

```js
markLoop: while (theMark == true) {
  doSomething();
}
```

举一个比较典型的例子，看完后即明白 Label 的应用：

未添加 Label：

```js
var num = 0;
for (var i = 0; i < 10; i++) {
  // i 循环
  for (var j = 0; j < 10; j++) {
    // j 循环
    if (i == 5 && j == 5) {
      break; // i = 5，j = 5 时，会跳出 j 循环
    } // 但 i 循环会继续执行，等于跳出之后又继续执行更多次 j 循环
    num++;
  }
}

alert(num); // 输出 95
```

添加 Label 后：

```js
var num = 0;
outPoint: for (var i = 0; i < 10; i++) {
  for (var j = 0; j < 10; j++) {
    if (i == 5 && j == 5) {
      break outPoint; // 在 i = 5，j = 5 时，跳出所有循环，
      // 返回到整个 outPoint 下方，继续执行
    }
    num++;
  }
}

alert(num); // 输出 55
```

使用 continue 语句，则可达到与未添加 label 相同的效果，但在这种有多层循环的情况下，循环的跳出进入流程更为明晰一些：

```js
var num = 0;
outPoint: for (var i = 0; i < 10; i++) {
  for (var j = 0; j < 10; j++) {
    if (i == 5 && j == 5) {
      continue outPoint;
    }
    num++;
  }
}
alert(num); // 95
```

从 alert(num) 的值可以看出，continue outPoint; 语句的作用是跳出当前循环，并跳转到 outPoint（标签）下的 for 循环继续执行。

`break` 语句
----------

使用 `break` 语句来终止循环，`switch`，或者是链接到 label 语句。

* 当你使用不带 label 的 `break` 时，它会立即终止当前所在的 `while`，`do-while`，`for`，或者 `switch` 并把控制权交回这些结构后面的语句。
* 当你使用带 label 的 `break` 时，它会终止指定的带标记（label）的语句。

`break` 语句的语法看起来像这样：
```

break [label];
```

在语法中，被 `[]` 包裹的内容是可省略的，也就是 `label` 可以省略。若省略，则终止当前所在的循环或 `switch`；若不省略，则终止指定的 label 语句。

### 示例 1

下面的例子循环数组里的元素，直到找到一个等于 `theValue` 的值：

```js
for (i = 0; i < a.length; i++) {
  if (a[i] == theValue) {
    break;
  }
}
```

### 示例 2：终止一个 label

```js
var x = 0;
var z = 0;
labelCancelLoops: while (true) {
  console.log("外部循环：" + x);
  x += 1;
  z = 1;
  while (true) {
    console.log("内部循环：" + z);
    z += 1;
    if (z === 10 && x === 10) {
      break labelCancelLoops;
    } else if (z === 10) {
      break;
    }
  }
}
```

`continue` 语句
-------------

`continue` 语句可以用来继续执行（跳过代码块的剩余部分并进入下一循环）一个 `while`、`do-while`、`for`，或者 `label` 语句。

* 当你使用不带 label 的 `continue` 时，它终止当前 `while`，`do-while`，或者 for 语句到结尾的这次的循环并且继续执行下一次循环。
* 当你使用带 label 的 `continue` 时，它会应用被 label 标识的循环语句。

`continue` 语句的语法看起来像这样：
```

continue [label];
```

### 示例 1

The following example shows a `while` loop with a `continue` statement that executes when the value of `i` is three. Thus, `n` takes on the values one, three, seven, and twelve.

```js
var i = 0;
var n = 0;
while (i < 5) {
  i++;
  if (i == 3) {
    continue;
  }
  n += i;
  console.log(n);
}
//1,3,7,12
```

```js
var i = 0;
var n = 0;
while (i < 5) {
  i++;
  if (i == 3) {
    // continue;
  }
  n += i;
  console.log(n);
}
// 1,3,6,10,15
```

### 示例 2

一个被标签为 `checkiandj` 的语句包含了一个标签为 `checkj` 的语句。

如果遇到 `continue` 语句，程序会结束当前 `chechj` 的迭代并开始下一轮的迭代。

每次遇到 `continue` 语句时，`checkj` 语句会一直重复执行，直到 `checkj` 语句的条件为 `false`。

当返回 `false` 后，将会执行 `checkiandj` 的剩余语句，`checkiandj` 会一直执行，直到 `checkiandj` 的条件为 `false`。

当 `checkiandj` 的返回值为 `false` 时，将会执行 `checkiandj` 的下面的语句。

如果 `continue` 有一个标记 `checkiandj`，程序将会从 `checkiandj` 语句块的顶部继续执行。

```js
var i = 0;
var j = 10;
checkiandj: while (i < 4) {
  console.log(i);
  i += 1;
  checkj: while (j > 4) {
    console.log(j);
    j -= 1;
    if (j % 2 == 0) {
      continue checkj;
    }
    console.log(j + " 是奇数。");
  }
  console.log("i = " + i);
  console.log("j = " + j);
}
```

`for...in` 语句
-------------

`for...in` 语句循环一个指定的变量来循环一个对象所有可枚举的属性。JavaScript 会为每一个不同的属性执行指定的语句。
```

for (variable in object) {
  statements
}
```

### 示例

下面的函数通过它的参数得到一个对象和这个对象的名字。然后循环这个对象的所有属性并且返回一个列出属性名和该属性值的字符串。

```js
function dump\_props(obj, obj\_name) {
  var result = "";
  for (var i in obj) {
    result += obj_name + "." + i + " = " + obj[i] + "<br>";
  }
  result += "<hr>";
  return result;
}
```

对于一个拥有 `make` 和 `model` 属性的 `car` 对象来说，执行结果 `result` 是：
```

car.make = Ford
car.model = Mustang
```

### 数组

虽然使用 **for...in** 来迭代数组 `Array` 元素听起来很诱人，但是它返回的东西除了数字索引外，还有可能是你自定义的属性名字。因此还是用带有数字索引的传统的 `for` 循环来迭代一个数组比较好，因为，如果你想改变数组对象，比如添加属性或者方法，**for...in** 语句迭代的是自定义的属性，而不是数组的元素。（译者注：下面的 `for...of` 语句，和 `forEach()`，也是理想的选择。）

`for...of` 语句
-------------

`for...of` 语句在可迭代对象（包括`Array`、`Map`、`Set`、`arguments` 等等）上创建了一个循环，对值的每一个独特属性调用一次迭代。
```

for (variable of object) {
  statement
}
```

下面的这个例子展示了 `for...of` 和 `for...in` 两种循环语句之间的区别。 `for...in` 循环遍历的结果是数组元素的下标，而 `for...of` 遍历的结果是元素的值：

```js
let arr = [3, 5, 7];
arr.foo = "hello";

for (let i in arr) {
  console.log(i); // 输出 "0", "1", "2", "foo"
}

for (let i of arr) {
  console.log(i); // 输出 "3", "5", "7"
}

// 注意 for...of 的输出没有出现 "hello"
```

* « 上一页
* 下一页 »


介绍 - JavaScript

介绍
==

* « 上一页
* 下一页 »

本章节介绍了 JavaScript，并且讨论了它的一些基本概念。

你应该已经掌握哪些知识？
------------

我们假设你已经掌握了以下基础技能：

* 对互联网和万维网（WWW (en-US)）有基本的认识。
* 熟悉超文本标记语言（HTML）。
* 一些编程经验。如果你初识编程，请先试着看看主页列出的有关 JavaScript 的教程。
去哪里获取有关 JavaScript 的信息？
-----------------------

MDN 上的 JavaScript 文档包括以下内容：

* 认识 Web 为初学者提供信息，并介绍一些关于编程和互联网的基本概念。
* JavaScript 入门 （即本教程）提供 JavaScript 这门语言及其目标的概述。
* JavaScript 参考 提供关于 JavaScript 的详细参考资料。

如果你刚开始学习 JavaScript，请详细阅读 学习区（Learning area） 和 JavaScript 入门 的文章。如果你已经掌握了 JavaScript 基础知识，你可以通过 JavaScript 参考 来了解更多关于单个（individual）对象和语句的细节。

什么是 JavaScript?
---------------

JavaScript 是一门跨平台、面向对象的脚本语言，它能使网页可交互（例如拥有复杂的动画，可点击的按钮，通俗的菜单等）。另外还有高级的服务端 Javascript 版本，例如 Node.js，它可以让你在网页上添加更多功能，不仅仅是下载文件（例如在多台电脑之间的协同合作）。在宿主环境（例如 web 浏览器）中，JavaScript 能够通过其所连接的环境提供的编程接口进行控制。

JavaScript 内置了一些对象的标准库，比如数组（`Array`），日期（`Date`），数学（`Math`）和一套核心语句，包括运算符、流程控制符以及声明方式等。JavaScript 的核心部分可以通过添加对象来扩展语言以适应不同用途；例如：

* 客户端的 JavaScript 通过提供对象，控制浏览器及其文档对象模型（DOM），来扩展语言核心。例如：客户端的拓展代码允许应用程序将元素放在某个 HTML 表单中，并且支持响应用户事件，比如鼠标点击、表单提交和页面导航。
* 服务端的 JavaScript 则通过提供有关在服务器上运行 JavaScript 的对象来可扩展语言核心。例如：服务端版本直接支持应用和数据库通信，提供应用不同调用间的信息连续性，或者在服务器上执行文件操作。

这意味着，在浏览器中，JavaScript 可以改变网页（DOM）的外观与样式。同样地，在服务器上，Node.js 中的 JavaScript 可以对浏览器上编写的代码发出的客户端请求做出响应。

JavaScript 和 Java
-----------------

JavaScript 和 Java 有一些共性但是在另一些方面有着根本性区别。JavaScript 语言类似 Java 但是并没有 Java 的静态类型和强类型检查特性。JavaScript 遵循了 Java 的表达式语法，命名规范以及基础流程控制，这也是 JavaScript 从 LiveScript 更名的原因。

与 Java 通过声明的方式构建类的编译时系统不同，JavaScript 采用基于少量的数据类型如数字、布尔、字符串值的运行时系统。JavaScript 拥有基于原型的对象模型提供的动态继承；也就是说，独立对象的继承是可以改变的。JavaScript 支持匿名函数。函数也可以作为对象的属性被当做宽松的类型方式执行。

与 Java 相比，Javascript 是一门形式自由的语言。你不必声明所有的变量，类和方法。你不必关心方法是否是公有、私有或者受保护的，也不需要实现接口。无需显式指定变量、参数、方法返回值的数据类型。

Java 是基于类的编程语言，设计的初衷就是为了确保快速执行和类型安全。类型安全，举个例子，你不能将一个 Java 整数变量转化为一个对象引用，或者由 Java 字节码访问专有存储器。Java 基于类的模型，意味着程序包含专有的类及其方法。Java 的类继承和强类型要求紧耦合的对象层级结构。这些要求使得 Java 编程比 JavaScript 要复杂的多。

相比之下，JavaScript 传承了 HyperTalk 和 dBASE 语句精简、动态类型等精髓，这些脚本语言为更多开发者提供了一种语法简单、内置功能强大以及用最小需求创建对象的编程工具。

| JavaScript | Java |
| --- | --- |
| 面向对象。不区分对象类型。通过原型机制继承，任何对象的属性和方法均可以被动态添加。 | 基于类系统。分为类和实例，通过类层级的定义实现继承。不能动态增加对象或类的属性或方法。 |
| 变量类型不需要提前声明 (动态类型)。 | 变量类型必须提前声明 (静态类型)。 |
| 不能直接自动写入硬盘。 | 可以直接自动写入硬盘。 |

请查看章节 对象模型的详情 来了解更多 JavaScript 和 Java 的不同。

JavaScript 和 ECMAScript 规范
--------------------------

JavaScript 的标准化组织是 ECMA——这个欧洲信息与通信系统标准化协会提供基于 Javascript 的标准化方案（ECMA 原先是欧洲计算机制造商协会的首字母缩写）。这种标准化版本的 JavaScript 被称作 ECMAScript，在所有支持该标准的应用中以相同的方式工作。公司可以使用开放标准语言来开发他们自己的 JavaScript 实现版本。ECMAScript 标准在 ECMA－262 规范中进行文档化。参照 JavaScript 的新特性以便学习更多关于不同版本的 JavaScript 和 ECMAScript 规范版本。

ECMA-262 标准也通过了 国际标准化组织（ISO）的 ISO-16262。你可以在这里找到该规范文件。ECMAScript 规范并没有描述文档对象模型（DOM），该模型由 万维网联盟（W3C）制定。DOM 定义了 HTML 文件对象被脚本操作的方法。为了更清楚地了解当使用 JavaScript 编程时用到的不同技术，请参阅 JavaScript 技术概述。

### JavaScript 文献 和 ECMAScript 规范

ECMAScript 规范是实现 ECMAScript 的一组需求;如果你想在 ECMAScript 实现或引擎 (如 Firefox 中的 SpiderMonkey 或 Chrome 中的 V8) 中实现符合标准的语言特性，那么它是非常有用的。

ECMAScript 文档不是用来帮助脚本程序员的;使用 JavaScript 文档获取关于编写脚本的信息。

ECMAScript 规范使用了 JavaScript 程序员可能不熟悉的术语和语法。尽管 ECMAScript 中对语言的描述可能有所不同，但语言本身保持不变。JavaScript 支持 ECMAScript 规范中列出的所有功能。

JavaScript 文档描述了适合 JavaScript 程序员的语言方面。

JavaScript 入门
-------------

开始使用 JavaScript 很容易，你只需要一个现代 Web 浏览器。这篇教程包含了一些只在最新版本的火狐浏览器上才有的功能，所以建议大家使用最新版本的火狐浏览器。

火狐浏览器内置的 Web 控制台非常适合学习 JavaScript,Web 控制台包含两个输入模式——单行模式、多行模式。

### 单行模式

Web 控制台 不仅可以展示当前已加载页面的信息，还包含一个可以在当前页面执行 Javascript 表达式的 命令行。

在火狐浏览器菜单栏的“工具" => "浏览器工具" => "Web 开发者工具"( 在 Windows 和 Linux 上`Ctrl`+`Shift`+`I` ，Mac 上`Cmd`+`Option`+`K`)，选择"Web 开发者工具栏"的控制台选项卡，它会如期出现在浏览器窗口的底部。如图，最下一行就是可以执行输入的命令行，面板上可以显示执行结果：

![](/zh-CN/docs/Web/JavaScript/Guide/Introduction/2019-04-04_00-15-29.png)

控制台的工作方式与 eval 完全相同：返回最后输入的表达式。为了简单起见，可以想象每次输入到控制台的东西实际上都被 console.log 所包围。

```js
console.log(eval("3 + 5"));
```

### 多行模式

Web 控制台的单行模式非常适合用来测试单个的 JavaScript 表达式，对于复杂的 JavaScript 表达式或者一定规模的代码块就显得力不从心了。这正是多行模式要解决的！

### Hello world

让我们用 Web 控制台的多行模式来写一个“Hello World"程序吧！JavaScript code:

```js
(function () {
  "use strict";
  function greetMe(yourName) {
    alert("Hello " + yourName);
  }

  greetMe("World");
})();
```

输入完成后，按下`Cmd`+`Enter`或者`Ctrl`+`Enter`(或者单击运行按钮) 就可以在浏览器中看到执行结果了！

在接下来的章节里，我们将向你介绍 JavaScript 的语法以及语言特性，届时你将可以使用它编写更加复杂的程序。

但是现在请记住，始终将 `(function(){"use strict";` 添加到你的代码之前，始终将`})();`添加到你的代码之后。严格模式和IIFE这两篇文章详细解释了它们的作用。现在关于它们的作用你只要知道以下两点：

1. 防止 JavaScript 的语义影响初学者
2. 防止控制台中的代码片段之间发生意料之外的交互
* « 上一页
* 下一页 »