使用 Promise - JavaScript

使用 Promise
==========

* « 上一页
* 下一页 »

`Promise` 是一个对象，它代表了一个异步操作的最终完成或者失败。因为大多数人仅仅是使用已创建的 Promise 实例对象，所以本教程将首先说明怎样使用 Promise，再说明如何创建 Promise。

本质上 Promise 是一个函数返回的对象，我们可以在它上面绑定回调函数，这样我们就不需要在一开始把回调函数作为参数传入这个函数了。

假设现在有一个名为 `createAudioFileAsync()` 的函数，它接收一些配置和两个回调函数，然后异步地生成音频文件。一个回调函数在文件成功创建时被调用，另一个则在出现异常时被调用。

以下为使用 `createAudioFileAsync()` 的示例：

```js
// 成功的回调函数
function successCallback(result) {
  console.log("音频文件创建成功：" + result);
}

// 失败的回调函数
function failureCallback(error) {
  console.log("音频文件创建失败：" + error);
}

createAudioFileAsync(audioSettings, successCallback, failureCallback);
```

如果重写 `createAudioFileAsync()` 为返回 Promise 的形式，你可以把回调函数附加到它上面：

```js
createAudioFileAsync(audioSettings).then(successCallback, failureCallback);
```

这种形式有若干优点，下面我们将会逐一讨论。

链式调用
----

连续执行两个或者多个异步操作是一个常见的需求，在上一个操作执行成功之后，开始下一个的操作，并带着上一步操作所返回的结果。在旧的回调风格中，这种操作会导致经典的回调地狱：

```js
doSomething(function (result) {
  doSomethingElse(result, function (newResult) {
    doThirdThing(newResult, function (finalResult) {
      console.log(`得到最终结果：${finalResult}`);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);
```

有了 Promise，我们就可以通过一个 Promise 链来解决这个问题。这就是 Promise API 的优势，因为回调函数是附加到返回的 Promise 对象上的，而不是传入一个函数中。

见证奇迹的时刻：`then()` 函数会返回一个和原来不同的**新的 Promise**：

```js
const promise = doSomething();
const promise2 = promise.then(successCallback, failureCallback);
```

`promise2` 不仅表示 `doSomething()` 函数的完成，也代表了你传入的 `successCallback` 或者 `failureCallback` 的完成，这两个函数也可以返回一个 Promise 对象，从而形成另一个异步操作，这样的话，在 `promise2` 上新增的回调函数会排在这个 Promise 对象的后面。

就像这样，每一个 Promise 都代表了链中另一个异步过程的完成。此外，`then` 的参数是可选的，`catch(failureCallback)` 等同于 `then(null, failureCallback)`——所以如果你的错误处理代码对所有步骤都是一样的，你可以把它附加到链的末尾：

```js
doSomething()
  .then(function (result) {
    return doSomethingElse(result);
  })
  .then(function (newResult) {
    return doThirdThing(newResult);
  })
  .then(function (finalResult) {
    console.log(`得到最终结果：${finalResult}`);
  })
  .catch(failureCallback);
```

你或许会看到这种形式的箭头函数：

```js
doSomething()
  .then((result) => doSomethingElse(result))
  .then((newResult) => doThirdThing(newResult))
  .then((finalResult) => {
    console.log(`得到最终结果：${finalResult}`);
  })
  .catch(failureCallback);
```

**注意**：一定要有返回值，否则，回调将无法获取上一个 Promise 的结果。（如果使用箭头函数，`() => x` 比 `() => { return x; }` 更简洁一些，但后一种保留 `return` 的写法才支持使用多个语句）。如果上一个处理程序启动了一个 Promise 但并没有返回它，那就没有办法再追踪它的状态了，这个 Promise 就是“漂浮”的。

```js
doSomething()
  .then((url) => {
    // 忘记返回了！
    fetch(url);
  })
  .then((result) => {
    // 结果是 undefined，因为上一个处理程序没有返回任何东西。
    // 无法得知 fetch() 的返回值，不知道它是否成功。
  });
```

如果有竞争条件的话，情况会更糟——如果上一个处理程序的 Promise 没有返回，那么下一个 `then` 处理程序会提前调用，而它读取的任何值都可能是不完整的。

```js
const listOfIngredients = [];

doSomething()
  .then((url) => {
    // 忘记返回了！
    fetch(url)
      .then((res) => res.json())
      .then((data) => {
        listOfIngredients.push(data);
      });
  })
  .then(() => {
    console.log(listOfIngredients);
    // 永远是 []，因为 fetch 请求还没有完成。
  });
```

因此，一个经验法则是，每当你的操作遇到一个 Promise，就返回它，并把它的处理推迟到下一个 `then` 处理程序中。

```js
const listOfIngredients = [];

doSomething()
  .then((url) =>
    fetch(url)
      .then((res) => res.json())
      .then((data) => {
        listOfIngredients.push(data);
      }),
  )
  .then(() => {
    console.log(listOfIngredients);
  });

// 或

doSomething()
  .then((url) => fetch(url))
  .then((res) => res.json())
  .then((data) => {
    listOfIngredients.push(data);
  })
  .then(() => {
    console.log(listOfIngredients);
  });
```

嵌套
--

对比上述两个例子，第一个例子中有一个 Promise 链嵌套在另一个 `then()` 处理程序的返回值中；而第二个例子则是完全扁平化的链。简洁的 Promise 链式编程最好保持扁平化，不要嵌套 Promise，因为嵌套经常会是粗心导致的。详见常见错误。

嵌套 Promise 是一种可以限制 `catch` 语句的作用域的控制结构写法。明确来说，嵌套的 `catch` 只会捕获其作用域及以下的错误，而不会捕获链中更高层的错误。如果使用正确，可以实现高精度的错误恢复。

```js
doSomethingCritical()
  .then((result) =>
    doSomethingOptional() // 可选操作
      .then((optionalResult) => doSomethingExtraNice(optionalResult))
      .catch((e) => {
        console.log(e.message);
      }),
  ) // 即便可选操作失败了，也会继续执行
  .then(() => moreCriticalStuff())
  .catch((e) => console.log(`严重失败：${e.message}`));
```

注意，这里的可选操作是嵌套的——缩进并不是原因，而是因为可选操作被外层的 `(` 和 `)` 括号包裹起来了。

这个内部的 `catch` 语句仅能捕获到 `doSomethingOptional()` 和 `doSomethingExtraNice()` 的失败，并将该错误与外界屏蔽，之后就恢复到 `moreCriticalStuff()` 继续执行。值得注意的是，如果 `doSomethingCritical()` 失败，这个错误仅会被最后的（外部）`catch` 语句捕获到，并不会被内部 `catch` 吞掉。

### Catch 的后续链式操作

有可能会在一个回调失败*之后*继续使用链式操作，即，使用一个 `catch`，这对于在链式操作中抛出一个失败之后，再次进行新的操作会很有用。请阅读下面的例子：

```js
new Promise((resolve, reject) => {
  console.log("初始化");

  resolve();
})
  .then(() => {
    throw new Error("有哪里不对了");

    console.log("执行「这个」");
  })
  .catch(() => {
    console.log("执行「那个」");
  })
  .then(() => {
    console.log("执行「这个」，无论前面发生了什么");
  });
```

输出结果如下：
```

初始化
执行「那个」
执行「这个」，无论前面发生了什么
```

**备注：** 并没有输出“执行「这个」”，因为在第一个 `then()` 中的 `throw` 语句导致其被拒绝。

### 常见错误

在编写 Promise 链时，需要注意以下示例中展示的几个错误：

```js
// 错误示例，包含 3 个问题！

doSomething()
  .then(function (result) {
    // 忘记返回 Promise + 不必要的嵌套
    doSomethingElse(result).then((newResult) => doThirdThing(newResult));
  })
  .then(() => doFourthThing());
// 忘记使用 catch 终止 Promise 链
```

第一个错误是没有正确地链接。当我们创建一个新 Promise 但忘记返回它时，就会发生这种情况。结果就是，这条链断掉了——或者更确切地说，我们有两个独立的链在竞争。这意味着 `doFourthThing()` 不会等待 `doSomethingElse()` 或 `doThirdThing()` 完成，而是会与它们同时运行——这很可能不是我们想要的。单独的链也有单独的错误处理，这会导致错误无法被捕获。

第二个错误是不必要的嵌套。嵌套限制了内部错误处理程序的作用域，如果不是有意为之，可能会导致未捕获的错误。该错误的一个变体是 Promise 构造函数反模式，它将一个 Promise 代码片段嵌入了另一个 Promise 构造函数里。

第三个错误是忘记用 `catch` 终止链。在大多数浏览器中，未终止的 Promise 链会导致 Promise 的拒绝事件无法被捕获。参见错误处理。

一个好的经验法则是总是返回或终止 Promise 链，并且一旦你得到一个新的 Promise，就立即返回它，最终的链应是扁平化的：

```js
doSomething()
  .then(function (result) {
    // 如果使用完整的函数表达式：返回 Promise
    return doSomethingElse(result);
  })
  // 如果使用箭头函数：省略大括号并隐式返回结果
  .then((newResult) => doThirdThing(newResult))
  // 即便上一个 Promise 返回了一个结果，后一个 Promise 也不一定非要使用它。
  // 你可以传入一个不使用前一个结果的处理程序。
  .then((/\* 忽略上一个结果 \*/) => doFourthThing())
  // 总是使用 catch 终止 Promise 链，以保证任何未处理的拒绝事件都能被捕获！
  .catch((error) => console.error(error));
```

注意：`() => x` 是 `() => { return x; }` 的简写。

上述代码的写法就是具有适当错误处理的简单明确的链式写法。

使用 async/await 可以解决以上大多数错误，使用 `async/await` 时，最常见的语法错误就是忘记了 `await` 关键字。

错误处理
----

你或许还有印象，在之前的回调地狱示例中，有 3 次 `failureCallback` 的调用，而在 Promise 链中只有尾部的一次调用。

```js
doSomething()
  .then((result) => doSomethingElse(result))
  .then((newResult) => doThirdThing(newResult))
  .then((finalResult) => console.log(`得到最终结果：${finalResult}`))
  .catch(failureCallback);
```

通常，一遇到异常抛出，浏览器就会顺着 Promise 链寻找下一个 `onRejected` 失败回调函数或者由 `.catch()` 指定的回调函数。这和以下同步代码的工作原理（执行过程）非常相似。

```js
try {
  let result = syncDoSomething();
  let newResult = syncDoSomethingElse(result);
  let finalResult = syncDoThirdThing(newResult);
  console.log(`得到最终结果：${finalResult}`);
} catch (error) {
  failureCallback(error);
}
```

这种异步代码的对称性在 `async`/`await` 语法中达到了极致：

```js
async function foo() {
  try {
    const result = await doSomething();
    const newResult = await doSomethingElse(result);
    const finalResult = await doThirdThing(newResult);
    console.log(`得到最终结果：${finalResult}`);
  } catch (error) {
    failureCallback(error);
  }
}
```

这个例子是在 Promise 的基础上构建的，例如，`doSomething()` 与之前的函数是相同的。你可以在 async 函数和 `await` 参考中阅读更多的与此语法相关的文章。

通过捕获所有的错误，甚至抛出异常和程序错误，Promise 解决了回调地狱的基本缺陷。这对于构建异步操作的基础功能而言是很有必要的。

### Promise 拒绝事件

当一个 Promise 拒绝事件未被任何处理器处理时，它会冒泡到调用栈的顶部，主机需要将其暴露出来。在 Web 上，当 Promise 被拒绝时，会有下文所述的两个事件之一被派发到全局作用域（通常而言，就是 `window`；如果是在 web worker 中使用的话，就是 `Worker` 或者其他基于 worker 的接口）。这两个事件如下所示：

`rejectionhandled`

当 Promise 被拒绝、并且在 `reject` 函数处理该拒绝事件之后会派发此事件。

`unhandledrejection`

当 Promise 被拒绝，但没有提供 `reject` 函数来处理该拒绝事件时，会派发此事件。

上述两种事件（类型为 `PromiseRejectionEvent`）都有两个属性，一个是 `promise` 属性，该属性指向被拒绝的 Promise，另一个是 `reason` (en-US) 属性，该属性用来说明 Promise 被拒绝的原因。

因此，我们可以通过以上事件为 Promise 失败时提供补偿处理，也有利于调试 Promise 相关的问题。在每一个上下文中，该处理都是全局的，因此不管源码如何，所有的错误都会在同一个处理函数中被捕捉并处理。

在 Node.js 中，对拒绝事件的处理稍有不同。你可以通过为 Node.js 的 `unhandledRejection` 事件添加处理器（注意名称的大小写不同）来捕获未处理的拒绝，就像这样：

```js
process.on("unhandledRejection", (reason, promise) => {
  /\* 你可以在这里添加一些代码，以便检查 promise 和 reason \*/
});
```

对于 Node.js 来说，为了防止错误被记录到控制台（否则默认会发生），添加 `process.on()` 监听器就足够了；不需要类似浏览器运行时的 `preventDefault()` 方法这样的等效操作。

然而，如果你添加了 `process.on` 监听器，但没有在其中添加代码来处理被拒绝的 Promise，那么它们就会被丢弃，而且不会有任何提示。因此，最好在监听器中添加代码来检查每个被拒绝的 Promise，并确保它们不是由于代码错误而导致的。

组合
--

有四个组合工具可用来并发异步操作：`Promise.all()`、`Promise.allSettled()`、`Promise.any()` 和 `Promise.race()`。

我们可以同时启动所有操作，再等待它们全部完成，就像这样：

```js
Promise.all([func1(), func2(), func3()]).then(([result1, result2, result3]) => {
  /\* 使用 result1、result2 和 result3 \*/
});
```

如果数组中的某个 Promise 被拒绝，`Promise.all()` 就会立即拒绝返回的 Promise，并终止其他操作。这可能会导致一些意外的状态或行为。`Promise.allSettled()` 是另一个组合工具，它会等待所有操作完成后再处理返回的 Promise。

所有的这些方法都是并发运行 Promise 的——一系列 Promise 同时启动，而不是彼此等待。顺序执行也是可能的，这需要一些巧妙的 JavaScript 手段：

```js
[func1, func2, func3]
  .reduce((p, f) => p.then(f), Promise.resolve())
  .then((result3) => {
    /\* 使用 result3 \*/
  });
```

在这个例子中，我们使用 reduce 把一个异步函数数组变为一个 Promise 链。上面的代码等同于：

```js
Promise.resolve()
  .then(func1)
  .then(func2)
  .then(func3)
  .then((result3) => {
    /\* 使用 result3 \*/
  });
```

我们也可以写成可复用的函数形式，这在函数式编程中极为普遍：

```js
const applyAsync = (acc, val) => acc.then(val);
const composeAsync =
  (...funcs) =>
  (x) =>
    funcs.reduce(applyAsync, Promise.resolve(x));
```

`composeAsync()` 函数将会接受任意数量的函数作为其参数，并返回一个新的函数，而该函数又接受一个初始值，该组合的参数传递管线如下所示：

```js
const transformData = composeAsync(func1, func2, func3);
const result3 = transformData(data);
```

顺序组合还可以使用 async/await 更简洁地完成：

```js
let result;
for (const f of [func1, func2, func3]) {
  result = await f(result);
}
/\* 使用最后的结果（即 result3）\*/
```

然而，在你顺序组合 Promise 前，请考虑是否真的有必要——因为它们会阻塞彼此，除非一个 Promise 的执行依赖于另一个 Promise 的结果，否则最好并发运行 Promise。

在旧式回调 API 中创建 Promise
---------------------

可以通过 Promise 的构造函数从零开始创建 `Promise`。这种方式（通过构造函数的方式）应当只在封装旧 API 的时候用到。

理想状态下，所有的异步函数应该会返回 Promise。但有一些 API 仍然使用旧方式来传入成功（或者失败）的回调。最典型的例子就是 `setTimeout()` (en-US) 函数：

```js
setTimeout(() => saySomething("10 秒钟过去了"), 10 \* 1000);
```

混用旧式回调和 Promise 可能会造成运行时序问题。如果 `saySomething` 函数失败了，或者包含了编程错误，那就没有办法捕获它了。这得怪 `setTimeout`。

幸运地是，我们可以将 `setTimeout` 封装入 Promise 内。最好的做法是，将这些有问题的函数封装起来，留在底层，并且永远不要再直接调用它们：

```js
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

wait(10 \* 1000)
  .then(() => saySomething("10 秒钟"))
  .catch(failureCallback);
```

通常，Promise 的构造函数接收一个执行函数（executor），我们可以在这个执行函数里手动地解决（resolve）或拒绝（reject）一个 Promise。既然 `setTimeout` 并不会真的执行失败，那么我们可以在这种情况下忽略拒绝的情况。你可以在 `Promise()` 参考中查看更多关于执行函数的信息。

时序
--

最后，我们将深入了解更多技术细节——关于注册的回调函数何时被调用。

### 保证

在基于回调的 API 中，回调函数何时以及如何被调用取决于 API 的实现者。例如，回调可能是同步调用的，也可能是异步调用的：

```js
function doSomething(callback) {
  if (Math.random() > 0.5) {
    callback();
  } else {
    setTimeout(() => callback(), 1000);
  }
}
```

我们非常不建议使用上述这种设计，因为它会导致所谓的“Zalgo 状态”。在设计异步 API 的上下文中，这意味着回调在某些情况下是同步调用的，但在其他情况下是异步调用的，这为调用者带来的歧义。更多背景信息，请参见文章为异步设计 API，这是该术语首次被正式提出的地方。这种 API 设计使得副作用难以分析：

```js
let value = 1;
doSomething(() => {
  value = 2;
});
console.log(value); // 1 还是 2？
```

另一方面，Promise 是一种控制反转的形式——API 的实现者不控制回调何时被调用。相反，维护回调队列并决定何时调用回调的工作被委托给了 Promise 的实现者，这样一来，API 的使用者和开发者都会自动获得强大的语义保证，包括：

* 被添加到 `then()` 的回调永远不会在 JavaScript 事件循环的当前运行完成之前被调用。
* 即使异步操作已经完成（成功或失败），在这之后通过 `then()` 添加的回调函数也会被调用。
* 通过多次调用 `then()` 可以添加多个回调函数，它们会按照插入顺序进行执行。

以防万一的提醒：传入 `then()` 的函数永远不会被同步调用，即使 Promise 已经被解决了（resolved）：

```js
Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2
```

传入 `then()` 的函数不会立即运行，而是被放入微任务队列中，这意味着它会在稍后运行（仅在创建该函数的函数退出后，且 JavaScript 执行堆栈为空时），也就是在控制权返回事件循环之前。总而言之，不会等待太久：

```js
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

wait().then(() => console.log(4));
Promise.resolve()
  .then(() => console.log(2))
  .then(() => console.log(3));
console.log(1); // 1, 2, 3, 4
```

### 任务队列 vs. 微任务

Promise 回调被处理为微任务，而 `setTimeout()` 回调被处理为任务队列。

```js
const promise = new Promise((resolve, reject) => {
  console.log("Promise 执行函数");
  resolve();
}).then((result) => {
  console.log("Promise 回调（.then）");
});

setTimeout(() => {
  console.log("新一轮事件循环：Promise（已完成）", promise);
}, 0);

console.log("Promise（队列中）", promise);
```

上述代码的输出如下：
```

Promise 执行函数
Promise（队列中）Promise {<pending>}
Promise 回调（.then）
新一轮事件循环：Promise（已完成）Promise {<fulfilled>}
```

详见深入：微任务与 Javascript 运行时环境。

### 当 Promise 与 任务冲突时

你可能遇到如下情况：你的一些 Promise 和任务（例如事件或回调）会以不可预测的顺序启动。此时，你或许可以通过使用微任务检查状态或平衡 Promise，并以此有条件地创建 Promise。

如果你认为微任务可能会帮助你解决问题，那么请阅读微任务指南，学习如何用 `queueMicrotask()` 来将一个函数作为微任务添加到队列中。

参见
--

* `Promise`
* `async function`
* `await`
* Promises/A+ 规范
* 我们遇到了 promise 的问题 pouchdb.com（2015）
* « 上一页
* 下一页 »


索引集合类 - JavaScript

索引集合类
=====

* « 上一页
* 下一页 »

本章介绍按索引值排序的数据集合。包括数组和类数组结构，如 `Array` 对象和 `TypedArray` 对象。

*数组*是由名称和索引引用的值构成的有序列表。

例如，考虑一个名为 `emp` 的数组，它包含按数字雇员编号索引的雇员姓名。所以 `emp[0]` 是 0 号员工，`emp[1]` 是 1 号员工，以此类推。

JavaScript 中没有明确的数组数据类型。但是，你可以使用预定义的 `Array` 对象及其方法来处理应用程序中的数组。`Array` 对象具有以各种方式操作数组的方法，例如连接、反转和排序。它有一个用于确定数组长度的属性和用于正则表达式的其他属性。

在这篇文章中，我们将专注于数组，但许多相同的概念也适用于类型化数组，因为数组和类型化数组共享许多类似的方法。关于类型化数组的更多信息，请参见类型化数组参考。

创建数组
----

以下语句创建了等效的数组：

```js
const arr1 = new Array(element0, element1, /\* … ,\*/ elementN);
const arr2 = Array(element0, element1, /\* … ,\*/ elementN);
const arr3 = [element0, element1, /\* … ,\*/ elementN];
```

`element0, element1, …, elementN` 是数组元素的值列表。当指定这些值时，数组将用它们作为数组的元素初始化。数组的 `length` 属性被设置为参数的数量。

括号语法称为“数组字面量”或“数组初始化式”。它比其他形式的数组创建更短，因此通常是首选。详见数组字面量。

为了创建一个长度不为 0，但是又没有任何元素的数组，可选以下任何一种方式：

```js
// 这种方式...
const arr1 = new Array(arrayLength);

// ...与这种方式会形成相同数组
const arr2 = Array(arrayLength);

// 这个效果也一样
const arr3 = [];
arr3.length = arrayLength;
```

**备注：** 以上代码，`arrayLength` 必须为一个 `Number`。否则，将会创建一个只有单个元素（内含提供的值）的数组。调用 `arr.length` 会返回 `arrayLength`，但数组不包含任何元素。`for...in` 循环在数组上找不到任何属性。

除了上面所示的新定义的变量外，数组还可以被赋值为新对象或现有对象的属性：

```js
const obj = {};
// …
obj.prop = [element0, element1, /\* … ,\*/ elementN];

// 或
const obj = { prop: [element0, element1, /\* … ,\*/ elementN] };
```

如果你希望用单个元素初始化一个数组，而这个元素恰好又是 `Number`，那么你必须使用括号语法。当单个 `Number` 传递给 `Array()` 构造函数时，将会被解释为 `arrayLength`，并非单个元素。

```js
// 创建一个只有唯一元素的数组：数字 42。
const arr = [42];

// 创建一个没有元素的数组，且数组的长度被设置成 42。
const arr = Array(42);

// 上面的代码与下面的代码等价：
const arr = [];
arr.length = 42;
```

如果 `N` 不是一个整数，调用 `Array(N)` 将会报 `RangeError` 错误，下面的例子说明了这种行为：

```js
const arr = Array(9.3); // RangeError: Invalid array length
```

如果你需要创建任意类型的单元素数组，安全的方式是使用数组字面量。或者在向数组添加单个元素之前先创建一个空的数组。

你也可以使用 `Array.of` 静态方法来创建包含单个元素的数组。

```js
const wisenArray = Array.of(9.3); // wisenArray 只包含一个元素：9.3
```

引用数组元素
------

因为元素也是属性，你可以使用属性访问器来访问。假设你定义了以下数组：

```js
const myArray = ["Wind", "Rain", "Fire"];
```

你可以将数组的第一个元素引用为 `myArray[0]`，将数组的第二个元素引用为 `myArray[1]`，等等...元素的索引从零开始。

**备注：** 你也可以使用属性访问器来访问数组的其他属性，就像对象一样。

```js
const arr = ["one", "two", "three"];
arr[2]; // three
arr["length"]; // 3
```

填充数组
----

你可以通过给数组元素赋值来填充数组，例如：

```js
const emp = [];
emp[0] = "Casey Jones";
emp[1] = "Phil Lesh";
emp[2] = "August West";
```

**备注：** 如果你在以上代码中给数组运算符的是一个非整型数值，那么它将作为一个表示数组的对象的属性创建，而不是数组的元素。

```js
const arr = [];
arr[3.4] = "Oranges";
console.log(arr.length); // 0
console.log(Object.hasOwn(arr, 3.4)); // true
```

你也可以在创建数组的时候去填充它：

```js
const myArray = new Array("Hello", myVar, 3.14159);
// 或
const myArray = ["Mango", "Apple", "Orange"];
```

### 理解 length

在实现层面，JavaScript 实际上是将元素作为标准的对象属性来存储，把数组索引作为属性名。

`length` 属性是特殊的，如果存在最后一个元素，则其值总是大于其索引的正整数（在下面的示例中，`'Dusty'` 的索引是 `30`，所以 `cats.length` 返回 `30 + 1`）。

记住，JavaScript 数组索引是基于 0 的：它们从 `0` 开始，而不是 `1`。这意味着 `length` 属性将比最大的索引值大 1：

```js
const cats = [];
cats[30] = ["Dusty"];
console.log(cats.length); // 31
```

你也可以给 `length` 属性赋值。

写一个小于数组元素数量的值将截断数组，写 `0` 会彻底清空数组：

```js
const cats = ["Dusty", "Misty", "Twiggy"];
console.log(cats.length); // 3

cats.length = 2;
console.log(cats); // [ 'Dusty', 'Misty' ] - Twiggy 已经被移除了

cats.length = 0;
console.log(cats); // 输出 []，猫名称的数组现在已经空了

cats.length = 3;
console.log(cats); // 输出 [ <3 empty items> ]
```

### 遍历数组

一种常见的操作是遍历数组的值，以某种方式处理每个值。最简单的方法如下：

```js
const colors = ["red", "green", "blue"];
for (let i = 0; i < colors.length; i++) {
  console.log(colors[i]);
}
```

如果你确定数组中没有一个元素求值为 `false`（例如，如果你的数组只包含 DOM 节点），你可以选择一个更高效的土法子：

```js
const divs = document.getElementsByTagName("div");
for (let i = 0, div; (div = divs[i]); i++) {
  /\* 以某种方式处理 div \*/
}
```

这避免了检查数组长度的开销，并确保 `div` 变量在每次循环时都被重新赋值给当前项，从而增加了便利性。

`forEach()` 方法提供了遍历数组元素的其他方法：

```js
const colors = ["red", "green", "blue"];
colors.forEach((color) => console.log(color));
// red
// green
// blue
```

传递给 `forEach` 的函数对数组中的每个元素执行一次，数组元素作为参数传递给该函数。未赋值的值不会在 `forEach` 循环迭代。

注意，在数组定义时省略的元素不会在 `forEach` 遍历时被列出，但是手动赋值为 `undefined` 的元素是*会*被列出的：

```js
const sparseArray = ["first", "second", , "fourth"];

sparseArray.forEach((element) => {
  console.log(element);
});
// first
// second
// fourth

if (sparseArray[2] === undefined) {
  console.log("sparseArray[2] 是 undefined"); // true
}

const nonsparseArray = ["first", "second", undefined, "fourth"];

nonsparseArray.forEach((element) => {
  console.log(element);
});
// first
// second
// undefined
// fourth
```

由于 JavaScript 元素被保存为标准对象属性，因此不建议使用 `for...in` 循环遍历 JavaScript 数组，因为普通元素和所有可枚举属性都将被列出。

### 数组方法

`Array` 对象具有下列方法：

`concat()` 方法连接两个或多个数组并返回一个新的数组。

```js
let myArray = ["1", "2", "3"];
myArray = myArray.concat("a", "b", "c");
// myArray 现在是 ["1", "2", "3", "a", "b", "c"]
```

`join()` 方法将数组中的所有元素连接成一个字符串。

```js
const myArray = ["Wind", "Rain", "Fire"];
const list = myArray.join(" - "); // list 现在是 "Wind - Rain - Fire"
```

`push()` 方法在数组末尾添加一个或多个元素，并返回数组操作后的 `length`。

```js
const myArray = ["1", "2"];
myArray.push("3"); // myArray 现在是 ["1", "2", "3"]
```

`pop()` 方法从数组移出最后一个元素，并返回该元素。

```js
const myArray = ["1", "2", "3"];
const last = myArray.pop();
// myArray 现在是 ["1", "2"]，last 为 "3"
```

`shift()` 方法从数组移出第一个元素，并返回该元素。

```js
const myArray = ["1", "2", "3"];
const first = myArray.shift();
// myArray 现在是 ["2", "3"]，first 为 "1"
```

`unshift()` 方法在数组开头添加一个或多个元素，并返回数组的新长度。

```js
const myArray = ["1", "2", "3"];
myArray.unshift("4", "5");
// myArray 变成了 ["4", "5", "1", "2", "3"]
```

`slice()` 方法从数组提取一个片段，并作为一个新数组返回。

```js
let myArray = ["a", "b", "c", "d", "e"];
myArray = myArray.slice(1, 4); // [ "b", "c", "d"]
// 从索引 1 开始，提取所有的元素，直到索引 3 为止
```

`at()` 方法返回数组中指定索引处的元素，如果索引超出范围，则返回 `undefined`。它主要用于从数组末尾访问元素的负下标。

```js
const myArray = ["a", "b", "c", "d", "e"];
myArray.at(-2); // "d"，myArray 的倒数第二个元素
```

`splice()` 方法从数组移除一些元素，并（可选地）替换它们。它返回从数组中删除的元素。

```js
const myArray = ["1", "2", "3", "4", "5"];
myArray.splice(1, 3, "a", "b", "c", "d");
// myArray 现在是 ["1", "a", "b", "c", "d", "5"]
// 本代码从 1 号索引开始（或元素“2”所在的位置），
// 移除 3 个元素，然后将后续元素插入到那个位置上。
```

`reverse()` 方法原地颠倒数组元素的顺序：第一个数组元素变为最后一个数组元素，最后一个数组元素变为第一个数组元素。它返回对数组的引用。

```js
const myArray = ["1", "2", "3"];
myArray.reverse();
// 将原数组颠倒，myArray = [ "3", "2", "1" ]
```

`flat()` 方法返回一个新数组，所有子数组元素递归地连接到其中，直到指定的深度。

```js
let myArray = [1, 2, [3, 4]];
myArray = myArray.flat();
// myArray 现在是 [1, 2, 3, 4]，因为子数组 [3, 4] 已被展平
```

`sort()` 方法对数组的元素进行适当的排序，并返回对数组的引用。

```js
const myArray = ["Wind", "Rain", "Fire"];
myArray.sort();
// 对数组排序，myArray = ["Fire", "Rain", "Wind"]
```

`sort()` 也可以接受回调函数来决定如何比较数组元素。使用两个参数调用回调函数，它们是来自数组的两个值。该函数比较这两个值并返回正数、负数或零，表示这两个值的顺序。例如，以下命令将根据字符串的最后一个字母对数组进行排序：

```js
const sortFn = (a, b) => {
  if (a[a.length - 1] < b[b.length - 1]) {
    return -1; // 负数 => a < b，a 在 b 之前
  } else if (a[a.length - 1] > b[b.length - 1]) {
    return 1; // 正数 => a > b，a 在 b 之后
  }
  return 0; // 0 => a = b，a 和 b 保持原来的顺序
};
myArray.sort(sortFn);
// 对数组排序，myArray = ["Wind","Fire","Rain"]
```

* 如果 `a` 小于 `b`，返回 `-1`（或任何负数）
* 如果 `a` 大于 `b`，返回 `1`（或任何正数）
* 如果 `a` 和 `b` 相等，返回 `0`。

`indexOf()` 方法在数组中搜索 `searchElement` 并返回第一个匹配的索引。

```js
const a = ["a", "b", "a", "b", "a"];
console.log(a.indexOf("b")); // 1

// 再试一次，这次从最后一次匹配之后开始
console.log(a.indexOf("b", 2)); // 3
console.log(a.indexOf("z")); // -1, 因为找不到 'z'
```

`lastIndexOf()` 方法的工作原理类似于 `indexOf`，但这是从末尾开始，反向搜索。

```js
const a = ["a", "b", "c", "d", "a", "b"];
console.log(a.lastIndexOf("b")); // 5

// 再试一次，这次从最后一次匹配之前开始
console.log(a.lastIndexOf("b", 4)); // 1
console.log(a.lastIndexOf("z")); // -1
```

`forEach()` 方法对数组中的每个元素执行 `callback` 并返回 `undefined`。

```js
const a = ["a", "b", "c"];
a.forEach((element) => {
  console.log(element);
});
// 输出：
// a
// b
// c
```

接受回调的 `forEach` 方法（以及下面的其他方法）被称为*迭代方法*，因为它们以某种方式遍历整个数组。每个都接受第二个可选的参数 `thisArg`。如果提供，`thisArg` 将成为回调函数体中 `this` 关键字的值。如果没有提供，就像在明确的对象上下文之外被调用一样，当函数在严格模式下时，`this` 是 `undefined`，当函数在非严格模式下时，`this` 将引用全局对象（`window`、`globalThis` 等。）。

**备注：** 上面介绍的 `sort()` 方法不是迭代方法，因为它的回调函数只用于比较，不能基于元素顺序以任何特定顺序调用。`sort()` 也不接受 `thisArg` 形参。

`map()` 方法返回由每个数组元素上执行 `callback` 的返回值所组成的新数组。

```js
const a1 = ["a", "b", "c"];
const a2 = a1.map((item) => item.toUpperCase());
console.log(a2); // ['A', 'B', 'C']
```

`flatMap()` 方法先执行 `map()`，再执行深度为 1 的 `flat()`。

```js
const a1 = ["a", "b", "c"];
const a2 = a1.flatMap((item) => [item.toUpperCase(), item.toLowerCase()]);
console.log(a2); // ['A', 'a', 'B', 'b', 'C', 'c']
```

`filter()` 方法返回一个新数组，其中包含 `callback` 返回 `true` 的元素。

```js
const a1 = ["a", 10, "b", 20, "c", 30];
const a2 = a1.filter((item) => typeof item === "number");
console.log(a2); // [10, 20, 30]
```

`find()` 方法返回 `callback` 返回 `true` 的第一个元素。

```js
const a1 = ["a", 10, "b", 20, "c", 30];
const i = a1.find((item) => typeof item === "number");
console.log(i); // 10
```

`findLast()` 方法返回 `callback` 返回 `true` 的最后一个元素。

```js
const a1 = ["a", 10, "b", 20, "c", 30];
const i = a1.findLast((item) => typeof item === "number");
console.log(i); // 30
```

`findIndex()` 方法返回 `callback` 返回 `true` 的第一个元素的索引。

```js
const a1 = ["a", 10, "b", 20, "c", 30];
const i = a1.findIndex((item) => typeof item === "number");
console.log(i); // 1
```

`findLastIndex()` 方法返回 `callback` 返回 `true` 的最后一个元素的索引。

```js
const a1 = ["a", 10, "b", 20, "c", 30];
const i = a1.findLastIndex((item) => typeof item === "number");
console.log(i); // 5
```

如果 `callback` 对数组中的每一个元素都返回 `true`，则 `every()` 方法返回 `true`。

```js
function isNumber(value) {
  return typeof value === "number";
}
const a1 = [1, 2, 3];
console.log(a1.every(isNumber)); // true
const a2 = [1, "2", 3];
console.log(a2.every(isNumber)); // false
```

如果 `callback` 对数组中至少一个元素返回 `true`，则 `some()` 方法返回 `true`。

```js
function isNumber(value) {
  return typeof value === "number";
}
const a1 = [1, 2, 3];
console.log(a1.some(isNumber)); // true
const a2 = [1, "2", 3];
console.log(a2.some(isNumber)); // true
const a3 = ["1", "2", "3"];
console.log(a3.some(isNumber)); // false
```

`reduce()` 方法对数组中的每个值执行 `callback(accumulator, currentValue, currentIndex, Array)`，目的是将列表中的元素减少到单个值。`reduce` 函数返回 `callback` 函数返回的最终值。

如果指定了 `initialValue`，则调用 `callback`，并将 `initialValue` 作为第一个参数值，将数组中第一个元素的值作为第二个参数值。

如果*没有*指定 `initialValue`，那么 `callback` 的前两个参数值将是数组的第一个和第二个元素。之后的*每一次*调用，第一个参数的值将是前一个调用中返回的 `callback`，第二个参数的值将是数组中的下一个值。

如果 `callback` 需要访问正在处理的元素的索引，或者访问整个数组，它们可以作为可选参数。

```js
const a = [10, 20, 30];
const total = a.reduce(
  (accumulator, currentValue) => accumulator + currentValue,
  0,
);
console.log(total); // 60
```

`reduceRight()` 方法的工作原理类似于 `reduce()`，但从最后一个元素开始。

`reduce` 和 `reduceRight` 是迭代数组方法中最不被人熟知的两个函数。它们应该使用在那些需要把数组的元素两两递归处理，并最终计算成一个单一结果的算法。

稀疏数组
----

数组可以包含“空槽”，这与用值 `undefined` 填充的槽不一样。空槽可以通过以下方式之一创建：

```js
// Array 构造函数：
const a = Array(5); // [ <5 empty items> ]

// 数组字面量中的连续逗号：
const b = [1, 2, , , 5]; // [ 1, 2, <2 empty items>, 5 ]

// 直接给大于 array.length 的索引设置值以形成空槽：
const c = [1, 2];
c[4] = 5; // [ 1, 2, <2 empty items>, 5 ]

// 通过直接设置 .length 拉长一个数组：
const d = [1, 2];
d.length = 5; // [ 1, 2, <3 empty items> ]

// 删除一个元素：
const e = [1, 2, 3, 4, 5];
delete e[2]; // [ 1, 2, <1 empty item>, 4, 5 ]
```

在某些操作中，空槽的行为就像它们被填入了 `undefined` 那样。

```js
const arr = [1, 2, , , 5]; // 创建一个稀疏数组

// 通过索引访问
console.log(arr[2]); // undefined

// For...of
for (const i of arr) {
  console.log(i);
}

// 输出：1 2 undefined undefined 5

// 展开运算
const another = [...arr]; // "another" 为 [ 1, 2, undefined, undefined, 5 ]
```

在其他方法，特别是数组迭代方法时，空槽是被跳过的。

```js
const mapped = arr.map((i) => i + 1); // [ 2, 3, <2 empty items>, 6 ]
arr.forEach((i) => console.log(i)); // 1 2 5
const filtered = arr.filter(() => true); // [ 1, 2, 5 ]
const hasFalsy = arr.some((k) => !k); // false

// 属性迭代
const keys = Object.keys(arr); // [ '0', '1', '4' ]
for (const key in arr) {
  console.log(key);
}
// 输出：'0' '1' '4'
// 在对象中使用展开，使用属性枚举，而不是数组的迭代器
const objectSpread = { ...arr }; // { '0': 1, '1': 2, '4': 5 }
```

关于数组方法在稀疏数组中的行为的完整列表，请参见 `Array` 参考页。

多维数组
----

数组是可以嵌套的，这就意味着一个数组可以作为一个元素被包含在另外一个数组里面。利用 JavaScript 数组的这个特性，可以创建多维数组。

以下代码创建了一个二维数组。

```js
const a = new Array(4);
for (i = 0; i < 4; i++) {
  a[i] = new Array(4);
  for (j = 0; j < 4; j++) {
    a[i][j] = "[" + i + "," + j + "]";
  }
}
```

这个例子创建的数组拥有以下行数据：
```

Row 0: [0,0] [0,1] [0,2] [0,3]
Row 1: [1,0] [1,1] [1,2] [1,3]
Row 2: [2,0] [2,1] [2,2] [2,3]
Row 3: [3,0] [3,1] [3,2] [3,3]
```

使用数组存储其他属性
----------

数组也可以像对象那样使用，以存储相关信息：

```js
const arr = [1, 2, 3];
arr.property = "value";
console.log(arr.property); // "value"
```

例如，当一个数组作为字符串和正则表达式的匹配结果时，该数组将会返回相关匹配信息的属性和元素。`RegExp.prototype.exec()`、`String.prototype.match()` 和 `String.prototype.split()` 的返回值是一个数组。使用数组和正则表达式的的更多信息，参见正则表达式。

使用类数组对象
-------

一些 JavaScript 对象，如 `document.getElementsByTagName()` 返回的 `NodeList` 或 `arguments` 等 JavaScript 对象，有与数组相似的行为，但它们并不共享数组的所有方法。`arguments` 对象提供了 `length` 属性，但没有实现如 `forEach()` 等数组方法。

不能直接在类数组对象上调用数组方法。

```js
function printArguments() {
  arguments.forEach((item) => {
    console.log(item);
  }); // TypeError: arguments.forEach is not a function
}
```

但你可以通过 `Function.prototype.call()` 间接调用它们。

```js
function printArguments() {
  Array.prototype.forEach.call(arguments, (item) => {
    console.log(item);
  });
}
```

数组原型方法也可以用于字符串，因为它们以类似于数组的方式提供对其中字符的顺序访问：

```js
Array.prototype.forEach.call("a string", (chr) => {
  console.log(chr);
});
```

* « 上一页
* 下一页 »


流程控制与错误处理 - JavaScript

流程控制与错误处理
=========

* « 上一页
* 下一页 »

JavaScript 提供一套灵活的语句集，特别是控制流语句，你可以用它在你的应用程序中实现大量的交互性功能。本章节我们将带来关于 JavaScript 语句的一些概览。

这一章中的语句，在 JavaScript 参考 中包含更为详尽的细节。在 JavaScript 代码中，分号（;）字符被用来分割语句。

在 JavaScript 中，任何表达式 (expression) 都可以看作一条语句 (statement),如果你想了解表达式的详细信息，可以阅读表达式与运算符（Expressions and operators）这一章节。

语句块
---

最基本的语句是用于组合语句的语句块。该块由一对大括号界定：
```

{
   statement_1;
   statement_2;
   statement_3;
   .
   .
   .
   statement_n;
}
```

### 示例

语句块通常用于流程控制，如`if`，`for`，`while`等等。

```js
while (x < 10) {
  x++;
}
```

这里`{ x++; }`就是语句块。

**重要**：在 ECMAScript 6 标准之前，Javascript 没有块作用域。在一个块中引入的变量的作用域是包含函数或脚本，并且设置它们的效果会延续到块之外。换句话说，块语句不定义范围。JavaScript 中的“独立”块会产生与 C 或 Java 中完全不同的结果。示例：

```js
var x = 1;
{
  var x = 2;
}
alert(x); // 输出的结果为 2
```

这段代码的输出是**2**，这是因为块级作用域中的 var x 变量声明与之前的声明在同一个作用域内。在 C 语言或是 Java 语言中，同样的代码输出的结果是 1。

从 ECMAScript 2015 开始，使用 `let` 和`const`变量是块作用域的。更多信息请参考 `let` 和 `const`。

条件判断语句
------

条件判断语句指的是根据指定的条件所返回的结果（真或假或其他预定义的），来执行特定的语句。JavaScript 支持两种条件判断语句：`if...else`和`switch`。

### `if...else` 语句

当一个逻辑条件为真，用 if 语句执行一个语句。当这个条件为假，使用可选择的 else 从句来执行这个语句。if 语句如下所示：

```js
if (condition) {
  statement_1;
} else {
  statement_2;
} //推荐使用严格的语句块模式，语句 else 可选
```

条件可以是任何返回结果被计算为 true 或 false 的表达式。如果条件表达式返回的是 true，statement\_1 语句会被执行；否则，statement\_2 被执行。statement\_1 和 statement\_2 可以是任何语句，甚至你可以将另一个 if 语句嵌套其中。

你也可以组合语句通过使用 `else if` 来测试连续多种条件判断，就像下面一样：

```js
if (condition_1) {
  statement_1;
} else if (condition_2) {
  statement_2;
} else if (condition_n_1) {
  statement_n;
} else {
  statement_last;
}
```

要执行多个语句，可以使用语句块 ({ ... }) 来分组这些语句。通常，总是使用语句块是一个好的习惯，特别是在代码涉及比较多的 if 语句时：

```js
if (条件) {
  当条件为真的时候，执行语句 1;
  当条件为真的时候，执行语句 2;
} else {
  当条件为假的时候，执行语句 3;
  当条件为假的时候，执行语句 4;
}
```

不建议在条件表达式中使用赋值语句，因为在快速查阅代码时容易把它看成等值比较。例如，不要使用下面的代码：

**警告：**

```js
if (x = y) {
  /\* 语句 \*/
}
```

如果你需要在条件表达式中使用赋值，通常在赋值语句前后额外添加一对括号。例如：

```js
if ((x = y)) {
  /\* statements here \*/
}
```

#### 错误的值

下面这些值将被计算出 false (also known as Falsy values):

* `false`
* `undefined`
* `null`
* `0`
* `NaN`
* 空字符串（`""`）

当传递给条件语句所有其他的值，包括所有对象会被计算为真。

请不要混淆原始的布尔值`true`和`false` 与 `Boolean`对象的真和假。例如：

```js
var b = new Boolean(false);
if (b) //结果视为真
if (b == true) // 结果视为假
```

#### 示例

在以下示例中，如果`Text`对象中的字符数为 3，函数`checkData`将返回`true`；否则，显示警报并返回`false`。

```js
function checkData() {
  if (document.form1.threeChar.value.length == 3) {
    return true;
  } else {
    alert(
      "Enter exactly three characters. " +
        document.form1.threeChar.value +
        " is not valid.",
    );
    return false;
  }
}
```

### `switch` 语句

`switch` 语句允许一个程序求一个表达式的值并且尝试去匹配表达式的值到一个 `case` 标签。如果匹配成功，这个程序执行相关的语句。`switch` 语句如下所示：

```js
switch (expression) {
   case label\_1:
      statements_1
      [break;]
   case label\_2:
      statements_2
      [break;]
   ...
   default:
      statements_def
      [break;]
}
```

程序首先查找一个与 `expression` 匹配的 `case` 语句，然后将控制权转移到该子句，执行相关的语句。如果没有匹配值，程序会去找 `default` 语句，如果找到了，控制权转移到该子句，执行相关的语句。如果没有找到 `default`，程序会继续执行 `switch` 语句后面的语句。`default` 语句通常出现在 switch 语句里的最后面，当然这不是必须的。

`可选的 break` 语句与每个 `case` 语句相关联，保证在匹配的语句被执行后程序可以跳出 `switch` 并且继续执行 `switch` 后面的语句。如果 break 被忽略，则程序将继续执行 switch 语句中的下一条语句。

**示例**
 在如下示例中，如果 `fruittype` 等于 "Bananas", 程序匹配到对应 "Bananas" 的`case` 语句，并执行相关语句。当执行到 `break` 时，程序结束了 `switch` 并执行 `switch` 后面的语句。如果不写 `break` ，那么程序将会执行 `case "Cherries"` 下的语句。

```js
switch (fruittype) {
  case "Oranges":
    document.write("Oranges are $0.59 a pound.<br>");
    break;
  case "Apples":
    document.write("Apples are $0.32 a pound.<br>");
    break;
  case "Bananas":
    document.write("Bananas are $0.48 a pound.<br>");
    break;
  case "Cherries":
    document.write("Cherries are $3.00 a pound.<br>");
    break;
  case "Mangoes":
  case "Papayas":
    document.write("Mangoes and papayas are $2.79 a pound.<br>");
    break;
  default:
    document.write("Sorry, we are out of " + fruittype + ".<br>");
}
document.write("Is there anything else you'd like?<br>");
```

异常处理语句
------

你可以用 `throw` 语句抛出一个异常并且用 `try...catch` 语句捕获处理它。

* `throw`语句
* `try...catch`语句
### 异常类型

JavaScript 可以抛出任意对象。然而，不是所有对象能产生相同的结果。尽管抛出数值或者字母串作为错误信息十分常见，但是通常用下列其中一种异常类型来创建目标更为高效：

* ECMAScript exceptions
* `DOMException` and `DOMError`
### `throw` 语句

使用`throw`语句抛出一个异常。当你抛出异常，你规定一个含有值的表达式要被抛出。

```js
throw expression;
```

你可以抛出任意表达式而不是特定一种类型的表达式。下面的代码抛出了几个不同类型的表达式：

```js
throw "Error2"; // String type
throw 42; // Number type
throw true; // Boolean type
throw {
  toString: function () {
    return "I'm an object!";
  },
};
```

**备注：** 你可以在抛出异常时声明一个对象。那你就可以在 catch 块中查询到对象的属性。

```js
// Create an object type UserException
function UserException(message) {
  this.message = message;
  this.name = "UserException";
}

// Make the exception convert to a pretty string when used as
// a string (e.g. by the error console)
UserException.prototype.toString = function () {
  return this.name + ': "' + this.message + '"';
};

// Create an instance of the object type and throw it
throw new UserException("Value too high");
```

### `try...catch` 语句

`try...catch` 语句标记一块待尝试的语句，并规定一个以上的响应应该有一个异常被抛出。如果我们抛出一个异常，`try...catch`语句就捕获它。

`try...catch` 语句有一个包含一条或者多条语句的 try 代码块，0 个或 1 个的`catch`代码块，catch 代码块中的语句会在 try 代码块中抛出异常时执行。换句话说，如果你在 try 代码块中的代码如果没有执行成功，那么你希望将执行流程转入 catch 代码块。如果 try 代码块中的语句（或者`try` 代码块中调用的方法）一旦抛出了异常，那么执行流程会立即进入`catch` 代码块。如果 try 代码块没有抛出异常，catch 代码块就会被跳过。`finally` 代码块总会紧跟在 try 和 catch 代码块之后执行，但会在 try 和 catch 代码块之后的其他代码之前执行。

下面的例子使用了`try...catch`语句。示例调用了一个函数用于从一个数组中根据传递值来获取一个月份名称。如果该值与月份数值不相符，会抛出一个带有`"InvalidMonthNo"`值的异常，然后在捕捉块语句中设`monthName`变量为`unknown`。

```js
function getMonthName(mo) {
  mo = mo - 1; // Adjust month number for array index (1 = Jan, 12 = Dec)
  var months = [
    "Jan",
    "Feb",
    "Mar",
    "Apr",
    "May",
    "Jun",
    "Jul",
    "Aug",
    "Sep",
    "Oct",
    "Nov",
    "Dec",
  ];
  if (months[mo]) {
    return months[mo];
  } else {
    throw "InvalidMonthNo"; //throw keyword is used here
  }
}

try {
  // statements to try
  monthName = getMonthName(myMonth); // function could throw exception
} catch (e) {
  monthName = "unknown";
  logMyErrors(e); // pass exception object to error handler -> your own function
}
```

#### `catch` 块

你可以使用`catch`块来处理所有可能在`try`块中产生的异常。

```js
catch (catchID) {
  statements
}
```

捕捉块指定了一个标识符（上述语句中的`catchID`）来存放抛出语句指定的值；你可以用这个标识符来获取抛出的异常信息。在插入`throw`块时 JavaScript 创建这个标识符；标识符只存在于`catch`块的存续期间里；当`catch`块执行完成时，标识符不再可用。

举个例子，下面代码抛出了一个异常。当异常出现时跳到`catch`块。

```js
try {
  throw "myException"; // generates an exception
} catch (e) {
  // statements to handle any exceptions
  logMyErrors(e); // pass exception object to error handler
}
```

#### `finally`块

`finally`块包含了在 try 和 catch 块完成后、下面接着 try...catch 的语句之前执行的语句。`finally`块无论是否抛出异常都会执行。如果抛出了一个异常，就算没有异常处理，`finally`块里的语句也会执行。

你可以用`finally`块来令你的脚本在异常发生时优雅地退出；举个例子，你可能需要在绑定的脚本中释放资源。接下来的例子用文件处理语句打开了一个文件（服务端的 JavaScript 允许你进入文件）。如果在文件打开时一个异常抛出，`finally`块会在脚本错误之前关闭文件。

```js
openMyFile();
try {
  writeMyFile(theData); //This may throw a error
} catch (e) {
  handleError(e); // If we got a error we handle it
} finally {
  closeMyFile(); // always close the resource
}
```

如果`finally`块返回一个值，该值会是整个`try-catch-finally`流程的返回值，不管在`try`和`catch`块中语句返回了什么：

```js
function f() {
  try {
    console.log(0);
    throw "bogus";
  } catch (e) {
    console.log(1);
    return true; // this return statement is suspended
    // until finally block has completed
    console.log(2); // not reachable
  } finally {
    console.log(3);
    return false; // overwrites the previous "return"
    console.log(4); // not reachable
  }
  // "return false" is executed now
  console.log(5); // not reachable
}
f(); // console 0, 1, 3; returns false
```

用`finally`块覆盖返回值也适用于在`catch`块内抛出或重新抛出的异常：

```js
function f() {
  try {
    throw "bogus";
  } catch (e) {
    console.log('caught inner "bogus"');
    throw e; // this throw statement is suspended until
    // finally block has completed
  } finally {
    return false; // overwrites the previous "throw"
  }
  // "return false" is executed now
}

try {
  f();
} catch (e) {
  // this is never reached because the throw inside
  // the catch is overwritten
  // by the return in finally
  console.log('caught outer "bogus"');
}

// OUTPUT
// caught inner "bogus"
```

#### 嵌套 try...catch 语句

你可以嵌套一个或多个`try ... catch`语句。如果一个内部`try ... catch`语句没有`catch`块，它需要有一个`finally`块，并且封闭的`try ... catch`语句的`catch`块被检查匹配。有关更多信息，请参阅try... catch参考页上的嵌套 try-blocks。

### 使用`Error`对象

根据错误类型，你也许可以用'name'和'message'获取更精炼的信息。'name'提供了常规的错误类（如 'DOMException' 或 'Error'），而'message'通常提供了一条从错误对象转换成字符串的简明信息。

在抛出你个人所为的异常时，为了充分利用那些属性（比如你的`catch`块不能分辨是你个人所为的异常还是系统的异常时），你可以使用 Error 构造函数。比如：

```js
function doSomethingErrorProne () {
  if (ourCodeMakesAMistake()) {
    throw (new Error('The message'));
  } else {
    doSomethingToGetAJavascriptError();
  }
}
....
try {
  doSomethingErrorProne();
}
catch (e) {
  console.log(e.name); // logs 'Error'
  console.log(e.message); // logs 'The message' or a JavaScript error message)
}
```

* « 上一页
* 下一页 »


使用类 - JavaScript

使用类
===

* « 上一页
* 下一页 »

JavaScript 是一个基于原型的语言——一个对象的行为取决于它自身的属性及其原型的属性。对类来说，相较于与其他面向对象的语言，譬如 Java，创建对象的多层级结构及其属性的继承关系需要更多的代码行。本节，我们将展示如何利用类创建实例。

在许多其他语言中，*类*（或构造函数）与*对象*（或实例），是两个不同的概念。在 JavaScript 中，类可以看作是已有的原型继承机制的一种抽象——所有语法都可以转换为原型继承。类本身也是不过是 JavaScript 里一种普通的值，它们有其自己的原型链。事实上，大多数 JavaScript 纯函数都可用作构造函数——你可以用 `new` 运算符来调用一个构造函数以创建出一个新的对象。

本教程中，我们将研究类模型的方方面面。如果你想深入了解底层原型系统，请参阅继承与原型链指南。

本章节假定你已熟悉 JavaScript 并能使用常规的对象。

类的概述
----

如果你已经有动手实践过 JavaScript 的经历，或是阅读指南一路过来，你可能已经用过类了，即便你还没有自己创建过。例如，你可能会对这个很熟悉：

```js
const bigDay = new Date(2019, 6, 19);
console.log(bigDay.toLocaleDateString());
if (bigDay.getTime() < Date.now()) {
  console.log("Once upon a time...");
}
```

 在第一行，我们创建了一个 `Date` 类的实例，并将其命名为 `bigDay`。在第二行，我们在 `bigDay` 实例上调用了 `toLocaleDateString()` 方法，并返回了一个字符串。接下来，我们对比了两个数字：一个来自于方法 `getTime()` 的返回值，另个一来自于对 `Date` 类*本身*的直接调用，本例为 `Date.now()`。
 `Date` 是一个 JavaScript 的内建类。从这个例子中，我们可以得到一些关于类的基本概念：

* 类通过 `new` 运算符创建对象。
* 每个对象都有一些属性（数据或方法），这些属性是由类添加的。
* 类本身也有一些属性（数据或方法），这些属性通常用于与实例进行交互。

这些对应于类的三个关键特征：

* 构造函数；
* 实例方法和实例字段；
* 静态方法和静态字段。
声明一个类
-----

类通常通过*类声明*来创建。

```js
class MyClass {
  // 类体...
}
```

在类体内，有若干特性可用。

```js
class MyClass {
  // 构造函数
  constructor() {
    // 构造函数体
  }
  // 实例字段
  myField = "foo";
  // 实例方法
  myMethod() {
    // myMethod 体
  }
  // 静态字段
  static myStaticField = "bar";
  // 静态方法
  static myStaticMethod() {
    // myStaticMethod 体
  }
  // 静态块
  static {
    // 静态初始化代码
  }
  // 字段、方法、静态字段、静态方法、静态块都可以使用私有形式
  #myPrivateField = "bar";
}
```

如果你用过早于 ES6 的版本，你可能更熟悉使用函数作为构造函数。上面的模式大致可以转换为以下函数构造器：

```js
function MyClass() {
  this.myField = "foo";
  // 构造函数体
}
MyClass.myStaticField = "bar";
MyClass.myStaticMethod = function () {
  // myStaticMethod 体
};
MyClass.prototype.myMethod = function () {
  // myMethod 体
};

(function () {
  // 静态初始化代码
})();
```

**备注：** 私有字段和方法是类中的新特性，在函数构造器中并没有与之等价的语法。

### 构造一个类

在声明一个类之后，你可以使用 `new` 运算符来创建它的实例。

```js
const myInstance = new MyClass();
console.log(myInstance.myField); // 'foo'
myInstance.myMethod();
```

典型函数构造器可以使用 `new` 来构造，也可以不使用 `new` 来调用。然而，对于类的调用则必须使用 `new`，否则会导致错误。

```js
const myInstance = MyClass(); // TypeError: Class constructor MyClass cannot be invoked without 'new'
```

### 类声明提升

与函数声明不同，类声明并不会被提升（或者，在某些解释器中，可以被提升，但是有暂时性死区的限制），这意味着你不能在声明之前使用类。

```js
new MyClass(); // ReferenceError: Cannot access 'MyClass' before initialization

class MyClass {}
```

该行为与使用 `let` 和 `const` 声明变量类似。

### 类表达式

类似于函数，类声明也有其表达式形式。

```js
const MyClass = class {
  // 类体...
};
```

类表达式也可以有名字。表达式的名字只在类体内可见。

```js
const MyClass = class MyClassLongerName {
  // 类体。这里 MyClass 和 MyClassLongerName 指向同一个类
};
new MyClassLongerName(); // ReferenceError: MyClassLongerName is not defined
```

构造函数
----

类最重要的工作之一就是作为对象的“工厂”。例如，当我们使用 `Date` 构造函数时，我们期望它给我们一个新的对象，这个对象代表了我们传入的日期数据，而且我们可以使用该实例所暴露的其他方法来操作它。在类中，实例的创建是通过构造函数来完成的。

例如，我们创建一个名为 `Color` 的类，它代表了一个特定的颜色。用户通过传入一个 RGB (en-US) 三元组来创建颜色。

```js
class Color {
  constructor(r, g, b) {
    // 将 RGB 值作为 `this` 的属性
    this.values = [r, g, b];
  }
}
```

打开你的浏览器的开发者工具，将上面的代码粘贴到控制台中，然后创建一个实例：

```js
const red = new Color(255, 0, 0);
console.log(red);
```

你应该会看到如下输出：
```

Object { values: (3) […] }
  values: Array(3) [ 255, 0, 0 ]
```

你已经成功创建了一个 `Color` 实例，该实例有一个 `values` 属性，它是一个包含了你传入的 RGB 值的数组。这与下面的代码几乎是等价的：

```js
function createColor(r, g, b) {
  return {
    values: [r, g, b],
  };
}
```

构造函数的语法与普通函数完全相同——这意味着你可以使用其他语法，例如剩余参数：

```js
class Color {
  constructor(...values) {
    this.values = values;
  }
}

const red = new Color(255, 0, 0);
// 创建一个与上面等价的实例
```

每一次调用 `new` 都将创建一个新的实例。

```js
const red = new Color(255, 0, 0);
const anotherRed = new Color(255, 0, 0);
console.log(red === anotherRed); // false
```

在类的构造函数里，`this` 的值指向新创建的实例。你可以赋予它新的属性，或者读取已有的属性（尤其是方法——我们将在下一节中介绍）。

`this` 的值将自动作为 `new` 的结果返回。不建议从构造函数中返回任何值——因为如果你返回一个非原始类型的值，它将成为 `new` 表达式的值，而 `this` 的值将被丢弃。你可以在 new 运算符的描述中阅读更多关于 `new` 的内容。

```js
class MyClass {
  constructor() {
    this.myField = "foo";
    return {};
  }
}

console.log(new MyClass().myField); // undefined
```

实例方法
----

如果一个类只有构造函数，那么它与一个只创建普通对象的 `createX` 工厂函数并没有太大的区别。然而，类的强大之处在于它们可以作为“模板”，自动将方法分配给实例。

例如，对于 `Date` 实例，你可以用一系列方法来获取日期的不同部分，例如年份、月份、星期几等等。你也可以通过 `setX` 方法来设置这些值，例如 `setFullYear`。

对于我们的 `Color` 类，我们可以添加一个方法来获取红色值：

```js
class Color {
  constructor(r, g, b) {
    this.values = [r, g, b];
  }
  getRed() {
    return this.values[0];
  }
}

const red = new Color(255, 0, 0);
console.log(red.getRed()); // 255
```

没有方法的帮助，你可能会尝试在构造函数内部定义该函数：

```js
class Color {
  constructor(r, g, b) {
    this.values = [r, g, b];
    this.getRed = function () {
      return this.values[0];
    };
  }
}
```

这也是可以的。然而，这会导致每次创建 `Color` 实例时都会创建一个新的函数，即使它们都做着同样的事情！

```js
console.log(new Color().getRed === new Color().getRed); // false
```

与之相反地，如果你使用方法，它将在所有实例之间共享。一个函数可以在所有实例之间共享，且在不同实例调用时其行为也不同，因为 `this` 的值不同。你也许好奇这个方法存储在*哪里*——它被定义在所有实例的原型上，即 `Color.prototype`，详情参阅继承与原型链。

相似的，我们也可以添加一个 `setRed` 方法来设置红色值：

```js
class Color {
  constructor(r, g, b) {
    this.values = [r, g, b];
  }
  getRed() {
    return this.values[0];
  }
  setRed(value) {
    this.values[0] = value;
  }
}

const red = new Color(255, 0, 0);
red.setRed(0);
console.log(red.getRed()); // 0; 此时也即黑色
```

私有字段
----

你或许会好奇：为什么我们要费心使用 `getRed` 和 `setRed` 方法，而不是直接访问实例上的 `values` 数组呢？

```js
class Color {
  constructor(r, g, b) {
    this.values = [r, g, b];
  }
}

const red = new Color(255, 0, 0);
red.values[0] = 0;
console.log(red.values[0]); // 0
```

在面向对象编程中，有一个叫做“封装”的哲学。这是说你不应该访问对象的底层实现，而是使用抽象方法来与之交互。例如，如果我们突然决定将颜色表示为 HSL 而不是 RGB：

```js
class Color {
  constructor(r, g, b) {
    // values 现在是一个 HSL 数组！
    this.values = rgbToHSL([r, g, b]);
  }
  getRed() {
    return this.values[0];
  }
  setRed(value) {
    this.values[0] = value;
  }
}

const red = new Color(255, 0, 0);
console.log(red.values[0]); // 0; 不再是 255，因为 HSL 模型下纯红色的 H 分量为 0
```

用户对 `values` 数组代表 RGB 值的假设不再成立，这可能会打破他们的代码逻辑。因此，如果你是一个类的实现者，你应该隐藏实例的内部数据结构，以保持 API 的简洁性，并防止在你做了一些“无害的重构”时，用户代码不至于崩溃。在类中，这是通过*私有字段*来实现的。

私有字段是以 `#`（井号）开头的标识符。井号是这个字段名的必要部分，这也就意味着私有字段永远不会与公共属性发生命名冲突。为了在类中的任何地方引用一个私有字段，你必须在类体中*声明*它（你不能在类体外部创建私有字段）。除此之外，私有字段与普通属性几乎是等价的。

```js
class Color {
  // 声明：每个 Color 实例都有一个名为 #values 的私有字段。
  #values;
  constructor(r, g, b) {
    this.#values = [r, g, b];
  }
  getRed() {
    return this.#values[0];
  }
  setRed(value) {
    this.#values[0] = value;
  }
}

const red = new Color(255, 0, 0);
console.log(red.getRed()); // 255
```

在类外访问私有字段会导致语法错误，且该错误可以在早期被捕获（早期语法错误）。因为 `#privateField` 是一个特殊语法，所以解释器可以在执行代码之前做一些静态分析，找到所有访问私有字段的地方。

```js
console.log(red.#values); // SyntaxError: Private field '#values' must be declared in an enclosing class
```

**备注：** 在 Chrome 控制台中运行的代码可以在类外访问私有字段，JavaScript 为了方便调试而仅在 DevTools 中放宽了这一限制。

JavaScript 中的私有字段是*硬私有*的：如果类没有实现暴露这些私有字段的方法，也就没有任何机制可以从类外访问它们。这意味着你可以对类的私有字段做任何重构，只要暴露的方法的行为保持不变即可。

在我们将 `values` 字段私有化之后，我们可以在 `getRed` 和 `setRed` 方法中添加一些逻辑，而不仅仅是简单信息传递。例如，我们可以在 `setRed` 中添加一个检查逻辑，以确保它是一个有效的 R 值：

```js
class Color {
  #values;
  constructor(r, g, b) {
    this.#values = [r, g, b];
  }
  getRed() {
    return this.#values[0];
  }
  setRed(value) {
    if (value < 0 || value > 255) {
      throw new RangeError("无效的 R 值");
    }
    this.#values[0] = value;
  }
}

const red = new Color(255, 0, 0);
red.setRed(1000); // RangeError：无效的 R 值
```

如果我们暴露 `values` 属性，我们的用户就会很容易地绕过这个检查，直接给 `values[0]` 赋值，从而创建一个无效的颜色。但是通过良好封装的 API，我们可以使我们的代码更加健壮，防止下游的逻辑错误。

类方法可以读取其他实例的私有字段，只要它们属于同一个类即可。

```js
class Color {
  #values;
  constructor(r, g, b) {
    this.#values = [r, g, b];
  }
  redDifference(anotherColor) {
    // #values 不一定要从 this 访问：
    // 你也可以访问属于同一个类的其他实例的私有字段。
    return this.#values[0] - anotherColor.#values[0];
  }
}

const red = new Color(255, 0, 0);
const crimson = new Color(220, 20, 60);
red.redDifference(crimson); // 35
```

然而，若 `anotherColor` 并非一个 `Color` 实例，`#values` 将不存在（即使另一个类有一个同名的私有字段，它也不是同一个东西，也不能在这里访问）。访问一个不存在的私有字段会抛出错误，而不是像普通属性一样返回 `undefined`。如果你不知道一个对象上是否存在一个私有字段，且你希望在不使用 `try`/`catch` 来处理错误的情况下访问它，你可以使用 `in` 运算符。

```js
class Color {
  #values;
  constructor(r, g, b) {
    this.#values = [r, g, b];
  }
  redDifference(anotherColor) {
    if (!(#values in anotherColor)) {
      throw new TypeError("Color instance expected");
    }
    return this.#values[0] - anotherColor.#values[0];
  }
}
```

**备注：** 请记住，`#` 是一种特殊的标识符语法，你不能像字符串一样使用该字段名。`"#values" in anotherColor` 会查找一个名为 `"#values"` 的属性，而不是一个私有字段。

有一些限制在使用私有字段时需要注意：在单个类中，相同的名称不能声明两次，且它们不能被删除。这两种情况都会导致早期语法错误。

```js
class BadIdeas {
  #firstName;
  #firstName; // 这里会发生语法错误
  #lastName;
  constructor() {
    delete this.#lastName; // 也会发生语法错误
  }
}
```

方法、getter 与 setter 也可以是私有的。当你需要类内部做一些复杂的事情，但是不希望代码的其他部分调用时，它们就很有用。

例如，想象一下创建 HTML 自定义元素时，当点击、触摸等事件被激活时可能会做一些有点复杂的事情。此外，当元素被点击时发生的有点复杂的事情应该限制在这个类中，因为程序的其他部分（或者不应该）永远不会访问它。

```js
class Counter extends HTMLElement {
  #xValue = 0;
  constructor() {
    super();
    this.onclick = this.#clicked.bind(this);
  }
  get #x() {
    return this.#xValue;
  }
  set #x(value) {
    this.#xValue = value;
    window.requestAnimationFrame(this.#render.bind(this));
  }
  #clicked() {
    this.#x++;
  }
  #render() {
    this.textContent = this.#x.toString();
  }
  connectedCallback() {
    this.#render();
  }
}

customElements.define("num-counter", Counter);
```

在这个例子中，几乎每个字段和方法都是私有的。因此，它向程序的其他部分提供了一个接口，这个接口与内置的 HTML 元素非常相似，而 `Counter` 的内部则不受外部影响。

getter 字段
---------

`color.getRed()` 和 `color.setRed()` 允许我们读取和写入颜色的红色值。如果你熟悉像 Java 这样的语言，你会对这种模式非常熟悉。然而，在 JavaScript 中，使用方法来简单地访问属性仍然有些不便。*getter 字段*允许我们像访问“实际属性”一样操作某些东西。

```js
class Color {
  constructor(r, g, b) {
    this.values = [r, g, b];
  }
  get red() {
    return this.values[0];
  }
  set red(value) {
    this.values[0] = value;
  }
}

const red = new Color(255, 0, 0);
red.red = 0;
console.log(red.red); // 0
```

这就像是对象有了一个 `red` 属性——但实际上，实例上并没有这样的属性！实例只有两个方法，分别以 `get` 和 `set` 为前缀，而这使得我们可以像操作属性一样操作它们。

如果一个字段仅有一个 getter 而没有 setter，它将是只读的。

```js
class Color {
  constructor(r, g, b) {
    this.values = [r, g, b];
  }
  get red() {
    return this.values[0];
  }
}

const red = new Color(255, 0, 0);
red.red = 0;
console.log(red.red); // 255
```

在严格模式下，`red.red = 0` 这一行将抛出类型错误：“Cannot set property red of #<Color> which has only a getter”。在非严格模式下，赋值将被静默忽略。

公共字段
----

我们已经见过了私有字段，对应地，还有公共字段。公共字段使得实例可以获得属性，且它们常常独立于构造函数的参数。

```js
class MyClass {
  luckyNumber = Math.random();
}
console.log(new MyClass().luckyNumber); // 0.5
console.log(new MyClass().luckyNumber); // 0.3
```

公共字段几乎等价于将一个属性赋值给 `this`。例如，上面的例子也可以转换为：

```js
class MyClass {
  constructor() {
    this.luckyNumber = Math.random();
  }
}
```

静态属性
----

在上面的 `Date` 例子中，我们还遇到了 `Date.now()` 方法，它返回当前日期。这个方法不属于任何日期实例——它属于类本身。然而，它被放在 `Date` 类上，而不是作为全局的 `DateNow()` 函数，因为它在处理日期实例时最有用。

**备注：** 一个好的习惯是给工具方法一个前缀（这也称作“命名空间”）。例如，除了旧的、没有前缀的 `parseInt()` 方法之外，JavaScript 后来还添加了带有前缀的 `Number.parseInt()` 方法，以表明它是用于处理数字的。

*静态属性*是一组在类本身上定义的特性，而不是在类的实例上定义的特性。这些特性包括：

* 静态方法
* 静态字段
* 静态 getter 与 setter

可见，我们之前见过的所有类的特性都有其静态版本。例如，对于我们的 `Color` 类，我们可以创建一个静态方法，它检查给定的三元组是否是有效的 RGB 值：

```js
class Color {
  static isValid(r, g, b) {
    return r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255;
  }
}

Color.isValid(255, 0, 0); // true
Color.isValid(1000, 0, 0); // false
```

静态属性与实例属性的区别在于：

* 它们有 `static` 前缀，且
* 它们不能从实例中访问。

```js
console.log(new Color(0, 0, 0).isValid); // undefined
```

有一个特殊结构叫做*静态初始化块* (en-US)，它是一个在类第一次加载时运行的代码块。

```js
class MyClass {
  static {
    MyClass.myStaticProperty = "foo";
  }
}

console.log(MyClass.myStaticProperty); // 'foo'
```

静态初始化块几乎等价于在类声明之后立即执行一些代码。唯一的区别是它们可以访问静态私有属性。

扩展与继承
-----

类的一个关键特性（除了私有字段）是*继承*，这意味着一个对象可以“借用”另一个对象的大部分行为，同时覆盖或增强某些部分的逻辑。

例如，假定我们需要为 `Color` 类引入透明度支持。我们可能会尝试添加一个新的字段来表示它的透明度：

```js
class Color {
  #values;
  constructor(r, g, b, a = 1) {
    this.#values = [r, g, b, a];
  }
  get alpha() {
    return this.#values[3];
  }
  set alpha(value) {
    if (value < 0 || value > 1) {
      throw new RangeError("Alpha 值必须在 0 与 1 之间");
    }
    this.#values[3] = value;
  }
}
```

然而，这意味着每个实例——即使是大多数不透明的实例（那些 alpha 值为 1 的实例）——都必须有额外的 alpha 值，这并不是很优雅。此外，如果特性继续增长，我们的 `Color` 类将变得非常臃肿且难以维护。

所以，在面向对象编程中，我们更愿意创建一个*派生类*。派生类可以访问父类的所有公共属性。在 JavaScript 中，派生类是通过 `extends` 子句声明的，它指示它扩展自哪个类。

```js
class ColorWithAlpha extends Color {
  #alpha;
  constructor(r, g, b, a) {
    super(r, g, b);
    this.#alpha = a;
  }
  get alpha() {
    return this.#alpha;
  }
  set alpha(value) {
    if (value < 0 || value > 1) {
      throw new RangeError("Alpha 值必须在 0 与 1 之间");
    }
    this.#alpha = value;
  }
}
```

有一些事情需要注意。首先，在构造器中，我们调用了 `super(r, g, b)`。在访问 `this` 之前，必须调用 `super()`，这是 JavaScript 的要求。`super()` 调用父类的构造函数来初始化 `this`——这里大致相当于 `this = new Color(r, g, b)`。`super()` 之前也可以有代码，但你不能在 `super()` 之前访问 `this`——JavaScript 会阻止你访问未初始化的 `this`。

在父类完成对 `this` 的修改后，派生类才可以对其进行自己的逻辑。这里我们添加了一个名为 `#alpha` 的私有字段，并提供了一对 getter/setter 来与之交互。

派生类会继承父类的所有方法。例如，尽管 `ColorWithAlpha` 自身并没有声明一个 `get red()` getter，你仍然可以访问 `red`，因为这个行为是由父类指定的：

```js
const color = new ColorWithAlpha(255, 0, 0, 0.5);
console.log(color.red); // 255
```

派生类也可以覆盖父类的方法。例如，所有类都隐式继承自 `Object` 类，它定义了一些基本方法，例如 `toString()`。然而，基本的 `toString()` 方法是出了名的无用方法，因为它在大多数情况下打印 `[object Object]`：

```js
console.log(red.toString()); // [object Object]
```

所以，我们可以覆盖它，以便在打印颜色时打印它的 RGB 值：

```js
class Color {
  #values;
  // …
  toString() {
    return this.#values.join(", ");
  }
}

console.log(new Color(255, 0, 0).toString()); // '255, 0, 0'
```

在派生类内，你可以使用 `super` 访问父类的方法。这允许你可以在避免代码重复的情况下增强父类的方法。

```js
class ColorWithAlpha extends Color {
  #alpha;
  // …
  toString() {
    // 调用父类的 toString()，并以此构建新的返回值
    return `${super.toString()}, ${this.#alpha}`;
  }
}

console.log(new ColorWithAlpha(255, 0, 0, 0.5).toString()); // '255, 0, 0, 0.5'
```

当你用 `extends` 时，静态方法也会继承，因此你也可以覆盖或增强它们。

```js
class ColorWithAlpha extends Color {
  // ...
  static isValid(r, g, b, a) {
    // 调用父类的 isValid()，并在此基础上增强返回值
    return super.isValid(r, g, b) && a >= 0 && a <= 1;
  }
}

console.log(ColorWithAlpha.isValid(255, 0, 0, -1)); // false
```

派生类无权访问父类的私有字段——这是 JavaScript 私有字段的一个关键特性（“硬私有”）。私有字段的有效范围被严格限制在类体内，所以*任何*外部代码都无权访问。

```js
class ColorWithAlpha extends Color {
  log() {
    console.log(this.#values); // SyntaxError: Private field '#values' must be declared in an enclosing class
  }
}
```

一个类只能至多扩展自一个父类。这可以防止多重继承中的问题，例如菱形问题。然而，由于 JavaScript 的动态性，仍然可以通过类组合和 mixin 来实现多重继承的效果。

派生类的实例同时也是父类的实例，可用 `instancesof` 运算符来验证。

```js
const color = new ColorWithAlpha(255, 0, 0, 0.5);
console.log(color instanceof Color); // true
console.log(color instanceof ColorWithAlpha); // true
```

为什么用类？
------

本指南到目前为止一直很实用：我们专注于*如何*使用类，但有一个问题尚未解答：*为什么*要使用类？答案是：视情况而定。

类引入了一种*范式*，或者说是一种组织代码的方式。类是面向对象编程的基础，而面向对象编程是建立在诸如继承和多态（特别是*子类型多态*）等概念之上的。然而，许多人在哲学上反对某些面向对象编程的做法，因此不使用类。

例如，`Date` 对象的一个令人厌恶的特性是它是*可变的*。

```js
function incrementDay(date) {
  return date.setDate(date.getDate() + 1);
}
const date = new Date(); // 2019-06-19
const newDay = incrementDay(date);
console.log(newDay); // 2019-06-20
// 旧日期也被修改了！？
console.log(date); // 2019-06-20
```

可变性与内部状态是面向对象编程的重要方面，但通常会使代码难以理解——因为任何看似无害的操作都可能产生意想不到的副作用，并改变程序中其他部分的行为。

为了代码的可重复利用，我们通常会求助于扩展类，但这也会导致类的层次结构与继承关系变得复杂。

![一个典型 OOP 继承树，有五个类和三个层级](/zh-CN/docs/Web/JavaScript/Guide/Using_classes/figure8.1.png)

然而，如果我们只能继承自一个父类，我们又很难清楚地描述这种继承关系。一种常见的情况是，我们想要同时拥有来自多个类的行为。在 Java 中，我们可以利用接口；在 JavaScript 中，我们是用 mixin 来解决的。但归根结底，仍不是很方便。

往好的方面想，类是一种非常强大的工具，我们可以利用它提高我们组织代码的层次。例如，如若没有 `Color` 类，我们可能需要创建一堆工具函数：

```js
function isRed(color) {
  return color.red === 255;
}
function isValidColor(color) {
  return (
    color.red >= 0 &&
    color.red <= 255 &&
    color.green >= 0 &&
    color.green <= 255 &&
    color.blue >= 0 &&
    color.blue <= 255
  );
}
// ...
```

但利用类，我们可以将它们组织入 `Color` 命名空间内，这提高了代码的可读性。此外，私有字段允许我们隐藏内部数据结构，这使得我们可以在不破坏 API 的情况下对其进行重构。

简而言之，你应该在你想要储存一些内部数据、并暴露大量方法的时候考虑使用类。例如这些内置的 JavaScript 类：

* `Map` 与 `Set` 类存储了许多元素，你可以通过 `get()`、`set()`、`has()` 等方法访问它们。
* `Date` 类以 Unix 时间戳的形式存储日期，并允许你格式化、更新与读取单独的日期元素。
* `Error` 类存储了特定异常的信息，包括错误消息、堆栈跟踪、原因等。它是少数几个拥有丰富继承结构的类之一：有多个内置类（例如 `TypeError` 与 `ReferenceError`）继承自 `Error`。在发生错误时，这种继承允许细化错误的语义：每个错误类都代表一个特定类型的错误，可以很容易地通过 `instanceof` 进行检查。

JavaScript 提供了以面向对象的方式组织代码的能力，但是否并如何使用它完全取决于程序员的判断。

* « 上一页
* 下一页 »


文本格式化 - JavaScript

文本格式化
=====

* « 上一页
* 下一页 »

本章介绍在 Javascript 中如何使用字符串与文本内容。

字符串
---

JavaScript 中的 String 类型用于表示文本型的数据。它是由无符号整数值（16bit）作为元素而组成的集合。字符串中的每个元素在字符串中占据一个位置。第一个元素的 index 值是 0，下一个元素的 index 值是 1，以此类推。字符串的长度就是字符串中所含的元素个数。你可以通过 String 字面值或者 String 对象两种方式创建一个字符串。

### String 字面量

可以使用单引号或双引号创建简单的字符串：

```js
"foo";
"bar";
```

可以使用转义序列来创建更复杂的字符串：

#### 16 进制转义序列

\x 之后的数值将被认为是一个 16 进制数。

```js
"\xA9"; // "©"
```

#### Unicode 转义序列

Unicode 转义序列在\u 之后需要至少 4 个字符。

```js
"\u00A9"; // "©"
```

#### Unicode 字元逸出

这是 ECMAScript 6 中的新特性。有了 Unicode 字元逸出，任何字符都可以用 16 进制数转义，这使得通过 Unicode 转义表示大于`0x10FFFF`的字符成为可能。使用简单的 Unicode 转义时通常需要分别写字符相应的两个部分（译注：大于 0x10FFFF 的字符需要拆分为相应的两个小于 0x10FFFF 的部分）来达到同样的效果。

请参阅 `String.fromCodePoint()` 或 `String.prototype.codePointAt()`。

```js
"\u{2F804}";

// the same with simple Unicode escapes
"\uD87E\uDC04";
```

### 字符串对象

`String` 对象是对原始 string 类型的封装 .

```js
const foo = new String("foo"); // 创建一个 String 对象
console.log(foo); // 输出：[String: 'foo']
typeof foo; // 返回 'object'
```

你可以在 String 字面值上使用 String 对象的任何方法—JavaScript 自动把 String 字面值转换为一个临时的 String 对象，然后调用其相应方法，最后丢弃此临时对象。在 String 字面值上也可以使用 String.length 属性。

除非必要，应该尽量使用 String 字面值，因为 String 对象的某些行为可能并不与直觉一致。举例：

```js
const firstString = "2 + 2"; //创建一个字符串字面量
const secondString = new String("2 + 2"); // 创建一个字符串对象
eval(firstString); // 返回数字 4
eval(secondString); // 返回包含 "2 + 2" 的字符串对象
```

`String` 对象有一个属性 `length`，标识了字符串中 UTF-16 的码点个数。举例，下面的代码把 13 赋值给了`helloLength`，因为 "Hello, World!" 包含 13 个字符，每个字符用一个 UTF-16 码点表示。你可以通过数组的方式访问每一个码点，但你不能修改每个字符，因为字符串是不变的类数组对象：

```js
const hello = "Hello, World!";
const helloLength = hello.length;
hello[0] = "L"; // 无效，因为字符串是不变的
hello[0]; // 返回 "H"
```

Unicode 标量值（Unicode scalar value）大于 U+FFFF 的字符（比如中文、日文、韩文、越南语中的一些不常用的文字或者 emoji 表情）使用 2 个代理码点存储在 UTF-16 中。比如，一个包含 U+20BB7“𠮷”字符的字符串，其长度为 2。使用括号访问单个码点的时候，可能不会得到你所期望的结果，你得到的可能是一个不匹配字符串位置的代理码点，并且其违反了 Unicode 规范。请参阅 `String.fromCodePoint()` 或 `String.prototype.codePointAt()`。

`String`对象有许多方法：举例来说有些方法返回字符串本身的变体，如 `substring` 和`toUpperCase`.

下表总结了 `String` 对象的方法。

| 方法 | 描述 |
| --- | --- |
| `charAt`, `charCodeAt`, `codePointAt` | 返回字符串指定位置的字符或者字符编码。 |
| `indexOf`, `lastIndexOf` | 分别返回字符串中指定子串的位置或最后位置。 |
| `startsWith`, `endsWith`, `includes` | 返回字符串是否以指定字符串开始、结束或包含指定字符串。 |
| `concat` | 连接两个字符串并返回新的字符串。 |
| `fromCharCode`, `fromCodePoint` | 从指定的 Unicode 值序列构造一个字符串。这是一个 String 类方法，不是实例方法。 |
| `split` | 通过将字符串分离成一个个子串来把一个 String 对象分裂到一个字符串数组中。 |
| `slice` | 从一个字符串提取片段并作为新字符串返回。 |
| `substring`, `substr` | 分别通过指定起始和结束位置，起始位置和长度来返回字符串的指定子集。 |
| `match`, `replace`, `search` | 通过正则表达式来工作。 |
| `toLowerCase`, `toUpperCase` | 分别返回字符串的小写表示和大写表示。 |
| `normalize` | 按照指定的一种 Unicode 正规形式将当前字符串正规化。 |
| `repeat` | 将字符串内容重复指定次数后返回。 |
| `trim` | 去掉字符串开头和结尾的空白字符。 |

### 多行模板字符串

模板字符串是一种允许内嵌表达式的 String 字面值。可以用它实现多行字符串或者字符串内插等特性。

模板字符串使用反勾号 (` `) (grave accent) 包裹内容而不是单引号或双引号。模板字符串可以包含占位符。占位符用美元符号和花括号标识 (`${expression}`).

#### 多行

源代码中插入的任何新行开始字符都作为模板字符串的内容。使用一般的字符串时，为了创建多行的字符串不得不用如下语法：

```js
console.log(
  "string text line 1\n\
string text line 2",
);
// "string text line 1
// string text line 2"
```

为了实现同样效果的多行字符串，现在可以写成如下形式：

```js
console.log(`string text line 1
string text line 2`);
// "string text line 1
// string text line 2"
```

#### 嵌入表达式

为了在一般的字符串中嵌入表达式，需要使用如下语法：

```js
const five = 5;
const ten = 10;
console.log(
  "Fifteen is " + (five + ten) + " and not " + (2 \* five + ten) + ".",
);
// "Fifteen is 15 and not 20."
```

现在，使用模板字符串，可以使用语法糖让类似功能的实现代码更具可读性：

```js
const five = 5;
const ten = 10;
console.log(`Fifteen is ${five + ten} and not ${2 \* five + ten}.`);
// "Fifteen is 15 and not 20."
```

更多信息，请阅读 JavaScript reference 中的 Template literals。

国际化
---

`Intl` 对象是 ECMAScript 国际化 API 的命名空间，它提供了语言敏感的字符串比较，数字格式化和日期时间格式化功能。`Collator`, `NumberFormat`, 和 `DateTimeFormat` 对象的构造函数是`Intl`对象的属性。

### 日期和时间格式化

`DateTimeFormat` 对象在日期和时间的格式化方面很有用。下面的代码把一个日期格式化为美式英语格式。(不同时区结果不同.)

```js
// July 17, 2014 00:00:00 UTC:
const july172014 = new Date("2014-07-17");

const options = {
  year: "2-digit",
  month: "2-digit",
  day: "2-digit",
  hour: "2-digit",
  minute: "2-digit",
  timeZoneName: "short",
};
const americanDateTime = new Intl.DateTimeFormat("en-US", options).format;

// 本地时区会根据你的设置而有所不同。
// 在 CEST（中欧夏令时）中，输出：07/17/14, 02:00 AM GMT+2。
// 在 PDT（太平洋夏令时）中，输出：07/16/14, 05:00 PM GMT-7。
console.log(americanDateTime(july172014));
```

### 数字格式化

`NumberFormat` 对象在数字的格式化方面很有用，比如货币数量值。

```js
var gasPrice = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
  minimumFractionDigits: 3,
});

console.log(gasPrice.format(5.259)); // $5.259

var hanDecimalRMBInChina = new Intl.NumberFormat("zh-CN-u-nu-hanidec", {
  style: "currency",
  currency: "CNY",
});

console.log(hanDecimalRMBInChina.format(1314.25)); // ￥ 一，三一四。二五
```

### 定序

`Collator` 对象在字符串比较和排序方面很有用。

举例，德语中*有两种不同的排序方式 电话本（phonebook）* 和 字典（*dictionary）*. 电话本排序强调发音，比如在排序前“ä”, “ö”等被扩展为“ae”, “oe”等发音。

```js
var names = ["Hochberg", "Hönigswald", "Holzman"];

var germanPhonebook = new Intl.Collator("de-DE-u-co-phonebk");

// as if sorting ["Hochberg", "Hoenigswald", "Holzman"]:
console.log(names.sort(germanPhonebook.compare).join(", "));
// logs "Hochberg, Hönigswald, Holzman"
```

有些德语词包含变音，所以在字典中忽略变音进行排序是合理的 (除非待排序的单词只有变音部分不同：*schon* 先于 *schön*).

```js
var germanDictionary = new Intl.Collator("de-DE-u-co-dict");

// as if sorting ["Hochberg", "Honigswald", "Holzman"]:
console.log(names.sort(germanDictionary.compare).join(", "));
// logs "Hochberg, Holzman, Hönigswald"
```

关于`Intl` API 的更多信息，请参考 Introducing the JavaScript Internationalization API。

* « 上一页
* 下一页 »


元编程 - JavaScript

元编程
===

* « 上一页
* 下一页 »

`Proxy` 和 `Reflect` 对象允许你拦截并自定义基本语言操作（例如属性查找、赋值、枚举和函数调用等）。借助这两个对象，你可以在 JavaScript 进行元级别的编程。

代理
--

`Proxy` 对象可以拦截某些操作并实现自定义行为。

例如获取一个对象上的属性：

```js
let handler = {
  get(target, name) {
    return name in target ? target[name] : 42;
  },
};

let p = new Proxy({}, handler);
p.a = 1;

console.log(p.a, p.b); // 1, 42
```

`Proxy` 对象定义了一个 `target`（这里是一个空对象）和一个实现了 `get` *陷阱*的 `handler` 对象。这里，代理的对象在获取未定义的属性时不会返回 `undefined`，而是返回 `42`。

更多例子参见 `Proxy` 页面。

### 术语

在讨论代理的功能时会用到以下术语：

`handler`

包含陷阱的占位符对象（下译作“处理器”）。

陷阱

提供属性访问的方法（这类似于操作系统中*陷阱*的概念）。

`target`

代理所虚拟化的对象（下译作“目标”）。它通常用作代理的存储后端。JavaScript 会验证与不可扩展性或不可配置属性相关的不变式。

不变式

实现自定义操作时保持不变的语义称为*不变式*。如果你破坏了处理器的不变式，则会引发 `TypeError` 异常。

处理器和陷阱
------

以下表格中总结了 `Proxy` 对象可用的陷阱。详细的解释和例子请看参考页。

| 处理器 / 陷阱 | 拦截的操作 | 不变式 |
| --- | --- | --- |
| `handler.getPrototypeOf()` | `Object.getPrototypeOf()``Reflect.getPrototypeOf()``__proto__``Object.prototype.isPrototypeOf()``instanceof` | * `getPrototypeOf` 方法必须返回一个对象或 `null`。
* 如果 `target` 不可扩展，`Object.getPrototypeOf(proxy)`
 必须返回和 `Object.getPrototypeOf(target)`
 一样的值。
 |
| `handler.setPrototypeOf()` | `Object.setPrototypeOf()``Reflect.setPrototypeOf()` |
 如果 `target` 不可扩展，参数 `prototype`
 必须与 `Object.getPrototypeOf(target)` 的值相同。
  |
| `handler.isExtensible()` | `Object.isExtensible()``Reflect.isExtensible()` | `Object.isExtensible(proxy)` 必须返回和
 `Object.isExtensible(target)` 一样的值。
  |
| `handler.preventExtensions()` | `Object.preventExtensions()``Reflect.preventExtensions()` |
 如果 `Object.isExtensible(proxy)`
 值为 `false`，那么
 `Object.preventExtensions(proxy)` 只可能返回
 `true`。
  |
| `handler.getOwnPropertyDescriptor()` | `Object.getOwnPropertyDescriptor()``Reflect.getOwnPropertyDescriptor()` | * `getOwnPropertyDescriptor` 必须返回对象或者
 `undefined`。
* 如果存在一个对应于 `target`
 的属性是不可配置的自有属性，那么该属性不能被报告为不存在的。
* 如果存在一个对应于 `target`
 的属性是自有属性，且该
 `target` 不可扩展，那么该属性不能被报告为不存在的。
* 如果并不存在一个对应于 `target`
 的属性是自有属性，且该
 `target` 不可扩展，那么该属性不能被报告为存在的。
* 如果并不存在一个对应于 `target`
 的属性是自有属性，或存在一个对应于 `target`
 的属性是可配置的自有属性，那么它不能被报告为不可配置的。
* `Object.getOwnPropertyDescriptor(target)`
 的结果可以通过 `Object.defineProperty` 应用到
 `target` 上，且不会抛出异常。
 |
| `handler.defineProperty()` | `Object.defineProperty()``Reflect.defineProperty()` | * 如果 `target` 不可扩展，那么就不能添加属性。
* 如果并不存在一个对应于 `target`
 的属性是不可配置的自有属性，那么就不能添加（或修改）该属性为不可配置的。
* 如果存在一个对应于 `target`
 的属性是可配置的，那么这个属性未必是不可配置的。
* 如果存在一个对应于 `target` 的属性，那么
 `Object.defineProperty(target, prop,
descriptor)`
 将不会抛出异常。
* 在严格模式下，如果 `defineProperty` 处理器返回
 `false`，则会抛出 `TypeError` 异常。
 |
| `handler.has()` |
属性查询
`foo in proxy`
继承属性查询
`foo in Object.create(proxy)``Reflect.has()`
 | * 如果存在一个对应于 `target`
 的属性是不可配置的自有属性，那么该属性不能被报告为不存在的。
* 如果存在一个对应于 `target`
 的属性是自有属性，且 `target`
 不可扩展，那么该属性不能被报告为不存在的。
 |
| `handler.get()` |
属性访问
`proxy[foo]``proxy.bar`
继承属性访问
`Object.create(proxy)[foo]``Reflect.get()`
 | * 如果对应于 `target`
 的属性是不可写且不可配置的数据属性，那么该属性值必须与其相同。
* 如果对应于 `target`
 的属性是不可配置的访问器属性，且其 `[[Get]]`
 属性为 `undefined`，那么该属性值必须为
 `undefined`。
 |
| `handler.set()` |
属性赋值
`proxy[foo] = bar``proxy.foo = bar`
继承属性赋值
`Object.create(proxy)[foo] = bar``Reflect.set()`
 | * 如果对应于 `target`
 的属性是不可写且不可配置的数据属性，那么就不能修改该属性的值使其不同于
 `target` 上对应属性的值。
* 如果对应于 `target`
 的属性是不可配置的访问器属性，且其 `[[Set]]`
 属性为 `undefined`，那么就不能设置该属性的值。
* 在严格模式下，如果 `set` 处理器返回
 `false`，则会抛出 `TypeError` 异常。
 |
| `handler.deleteProperty()` |
属性删除
`delete proxy[foo]``delete proxy.foo``Reflect.deleteProperty()`
 |
 如果存在一个对应于 `target`
 的属性是不可配置的自有属性，那么该属性不能被删除。
  |
| `handler.ownKeys()` | `Object.getOwnPropertyNames()``Object.getOwnPropertySymbols()``Object.keys()``Reflect.ownKeys()` | * `ownKeys` 的返回值是一个数组。
* 返回值中的每个元素类型为
 `String` 或 `Symbol`。
* 返回值中必须包含 `target`
 的所有不可配置自有属性的键名。
* 如果 `target` 不可扩展，那么返回值中必须有且仅有
 `target` 的所有自有属性的键名。
 |
| `handler.apply()` | `proxy(..args)``Function.prototype.apply()``Function.prototype.call()``Reflect.apply()` | 不存在关于 `handler.apply` 方法的不变式。 |
| `handler.construct()` | `new proxy(...args)``Reflect.construct()` | 返回值必须是一个 `Object`。 |

可撤销的 `Proxy`
------------

可以用 `Proxy.revocable()` 方法来创建可撤销的 `Proxy` 对象。这意味着可以通过 `revoke` 函数来撤销并关闭一个代理。

此后，对代理进行的任意的操作都会导致 `TypeError`。

```js
const revocable = Proxy.revocable(
  {},
  {
    get(target, name) {
      return `[[${name}]]`;
    },
  },
);
const proxy = revocable.proxy;
console.log(proxy.foo); // "[[foo]]"

revocable.revoke();

console.log(proxy.foo); // TypeError: Cannot perform 'get' on a proxy that has been revoked
proxy.foo = 1; // TypeError: Cannot perform 'set' on a proxy that has been revoked
delete proxy.foo; // TypeError: Cannot perform 'deleteProperty' on a proxy that has been revoked
console.log(typeof proxy); // "object", `typeof` 不会触发任何陷阱
```

反射
--

`Reflect` 是一个内置对象，它为可拦截的 JavaScript 操作提供了方法。这些方法与代理处理器所提供的方法类似。

`Reflect` 并不是一个函数对象。

`Reflect` 将默认操作从处理器转发到 `target`。

以 `Reflect.has()` 为例，你可以将 `in` 运算符作为函数：

```js
Reflect.has(Object, "assign"); // true
```

### 更好的 `apply` 函数

在不借助 `Reflect` 的情况下，我们通常使用 `Function.prototype.apply()` 方法调用一个具有给定 `this` 值和 `arguments` 数组（或类数组对象）的函数。

```js
Function.prototype.apply.call(Math.floor, undefined, [1.75]);
```

借助 `Reflect.apply`，这些操作将变得更加简洁：

```js
Reflect.apply(Math.floor, undefined, [1.75]);
// 1;

Reflect.apply(String.fromCharCode, undefined, [104, 101, 108, 108, 111]);
// "hello"

Reflect.apply(RegExp.prototype.exec, /ab/, ["confabulation"]).index;
// 4

Reflect.apply("".charAt, "ponies", [3]);
// "i"
```

### 检查属性定义是否成功

使用 `Object.defineProperty`，如果成功则返回一个对象，否则抛出一个 `TypeError`，你可使用 `try...catch` 块来捕获定义属性时发生的任何错误。因为 `Reflect.defineProperty` 返回一个布尔值表示的成功状态，你可以在这里使用 `if...else` 块：

```js
if (Reflect.defineProperty(target, property, attributes)) {
  // success
} else {
  // failure
}
```

* « 上一页
* 下一页 »


迭代器和生成器 - JavaScript

迭代器和生成器
=======

* « 上一页
* 下一页 »

迭代器和生成器将迭代的概念直接带入核心语言，并提供了一种机制来自定义 `for...of` 循环的行为。

若想了解更多详情，请参考：

* 迭代协议
* `for...of`
* `function*` 和 `Generator`
* `yield` 和 `yield*`
迭代器
---

在 JavaScript 中，**迭代器**是一个对象，它定义一个序列，并在终止时可能附带一个返回值。

更具体地说，迭代器是通过使用 `next()` 方法实现了迭代器协议的任何一个对象，该方法返回具有两个属性的对象：

`value`

迭代序列的下一个值。

`done`

如果已经迭代到序列中的最后一个值，则它为 `true`。如果 `value` 和 `done` 一起出现，则它就是迭代器的返回值。

一旦创建，迭代器对象可以通过重复调用 `next()` 显式地迭代。迭代一个迭代器被称为消耗了这个迭代器，因为它通常只能执行一次：在产生终值后，对 `next()` 的额外调用应该继续返回 `{done：true}`。

Javascript 中最常见的迭代器是数组迭代器，它按顺序返回关联数组中的每个值。

虽然很容易想象所有迭代器都可以表示为数组，但事实并非如此。数组必须完整分配，而迭代器则是按需分配。因此，迭代器可以表示无限大小的序列，例如 0 和 `Infinity` 之间的整数范围。

下面的例子展示了具体做法。它允许你创建一个简单的范围迭代器，以定义一个从 `start`（闭）到 `end`（开），以 `step` 为步长的整数序列。它的最终返回值是它创建的序列的大小，由变量 `iterationCount` 跟踪。

```js
function makeRangeIterator(start = 0, end = Infinity, step = 1) {
  let nextIndex = start;
  let iterationCount = 0;

  const rangeIterator = {
    next() {
      let result;
      if (nextIndex < end) {
        result = { value: nextIndex, done: false };
        nextIndex += step;
        iterationCount++;
        return result;
      }
      return { value: iterationCount, done: true };
    },
  };
  return rangeIterator;
}
```

使用这个迭代器看起来像这样：

```js
let it = makeRangeIterator(1, 10, 2);

let result = it.next();
while (!result.done) {
  console.log(result.value); // 1 3 5 7 9
  result = it.next();
}

console.log(`已迭代序列的大小：${result.value}`); // 5
```

**备注：** 反射性地知道特定对象是否是迭代器是不可能的。如果你需要这样做，请使用可迭代对象。

生成器函数
-----

虽然自定义迭代器是一个有用的工具，但由于需要显式地维护其内部状态，因此创建时要格外谨慎。**生成器函数**（Generator 函数）提供了一个强大的替代选择：它允许你定义一个非连续执行的函数作为迭代算法。生成器函数使用 `function*` 语法编写。

最初调用时，生成器函数不执行任何代码，而是返回一种称为**生成器**的特殊迭代器。通过调用 `next()` 方法消耗该生成器时，生成器函数将执行，直至遇到 `yield` 关键字。

可以根据需要多次调用该函数，并且每次都返回一个新的生成器，但每个生成器只能迭代一次。

我们现在可以调整上面的例子了。此代码的行为并没有改变，但更容易编写和阅读。

```js
function\* makeRangeIterator(start = 0, end = Infinity, step = 1) {
  let iterationCount = 0;
  for (let i = start; i < end; i += step) {
    iterationCount++;
    yield i;
  }
  return iterationCount;
}
```

可迭代对象
-----

若一个对象拥有迭代行为，比如在 `for...of` 中会循环一些值，那么那个对象便是一个可迭代对象。一些内置类型，如 `Array` 或 `Map` 拥有默认的迭代行为，而其他类型（比如 `Object`）则没有。

为了实现**可迭代**，一个对象必须实现 **@@iterator** 方法，这意味着这个对象（或其原型链中的任意一个对象）必须具有一个键值为 `Symbol.iterator` 的属性。

程序员应知道一个可迭代对象可以多次迭代，还是只能迭代一次。

只能迭代一次的可迭代对象（例如 Generator）通常从它们的 **@@iterator** 方法中返回 `this`，而那些可以多次迭代的方法必须在每次调用 **@@iterator** 时返回一个新的迭代器。

```js
function\* makeIterator() {
  yield 1;
  yield 2;
}

const it = makeIterator();

for (const itItem of it) {
  console.log(itItem);
}

console.log(it[Symbol.iterator]() === it); // true

// 这个例子向我们展示了生成器（迭代器）是可迭代对象，
// 它有一个 @@iterator 方法返回 it（它自己），
// 因此，it 对象只能迭代\*一次\*。

// 如果我们将它的 @@iterator 方法改为一个返回新的迭代器/生成器对象的函数/生成器，
// 它（it）就可以迭代多次了。

it[Symbol.iterator] = function\* () {
  yield 2;
  yield 1;
};
```

### 自定义的可迭代对象

我们可以像这样实现自己的可迭代对象：

```js
var myIterable = {
  \*[Symbol.iterator]() {
    yield 1;
    yield 2;
    yield 3;
  },
};
```

自定义的可迭代对象可用 `for...of` 循环或者展开语法进行迭代。

```js
for (let value of myIterable) {
  console.log(value);
}
// 1
// 2
// 3

[...myIterable]; // [1, 2, 3]
```

### 内置可迭代对象

`String`、`Array`、`TypedArray`、`Map` 和 `Set` 都是内置可迭代对象，因为它们的原型对象都拥有一个 `Symbol.iterator` 方法。

### 用于可迭代对象的语法

一些语句和表达式专用于可迭代对象，例如 `for...of` 循环、展开语法、`yield*` 和解构语法。

```js
for (let value of ["a", "b", "c"]) {
  console.log(value);
}
// "a"
// "b"
// "c"

[..."abc"]; // ["a", "b", "c"]

function\* gen() {
  yield\* ["a", "b", "c"];
}

gen().next(); // { value: "a", done: false }

[a, b, c] = new Set(["a", "b", "c"]);
a; // "a"
```

高级生成器
-----

生成器会*按需*计算它们 `yield` 的值，这使得它们能够高效地表示一个计算成本很高的序列，甚至是前文所示的一个无限序列。

`next()` 方法也接受一个参数用于修改生成器内部状态。传递给 `next()` 的参数值会被 `yield` 接收。

**备注：** 传给*第一个* `next()` 的值会被忽略。

下面的是斐波那契数列生成器，它使用了 `next(x)` 来重启序列：

```js
function\* fibonacci() {
  let current = 0;
  let next = 1;
  while (true) {
    const reset = yield current;
    [current, next] = [next, next + current];
    if (reset) {
      current = 0;
      next = 1;
    }
  }
}

const sequence = fibonacci();
console.log(sequence.next().value); // 0
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2
console.log(sequence.next().value); // 3
console.log(sequence.next().value); // 5
console.log(sequence.next().value); // 8
console.log(sequence.next(true).value); // 0
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2
```

你可以通过调用其 `throw()` 方法强制生成器抛出异常，并传递应该抛出的异常值。这个异常将从当前挂起的生成器的上下文中抛出，就好像当前挂起的 `yield` 是一个 `throw value` 语句。

如果该异常没有在生成器内部被捕获，则它将通过 `throw()` 的调用向上传播，对 `next()` 的后续调用将导致 `done` 属性为 `true`。

生成器的 `return()` 方法可返回给定的值并终结这个生成器。

* « 上一页
* 下一页 »


JavaScript 类型化数组 - JavaScript

JavaScript 类型化数组
================

* « 上一页
* 下一页 »

JavaScript 类型化数组是一种类似数组的对象，并提供了一种用于在内存缓冲中访问原始二进制数据的机制。

引入类型化数组并非是为了取代 JavaScript 中数组的任何一种功能。相反，它为开发者提供了一个操作二进制数据的接口。这在操作与平台相关的特性时会很有用，例如：音频视频编辑和访问 WebSocket 原始数据等。JavaScript 类型化数组中的每一个元素都是以某种格式表示的原始二进制值，JavaScript 支持从 8 位整数到 64 位浮点数的多种二进制格式。

类型化数组拥有许多与数组相同的方法，语义也相似。但是，类型化数组并不是普通数组，因为在类型化数组上调用 `Array.isArray()` 会返回 `false`。此外，并不是所有可用于普通数组的方法都能被类型化数组所支持（如 push 和 pop）。

为了最大程度的灵活性和效率，JavaScript 将类型化数组的实现拆分为*缓冲*和*视图*两部分。缓冲是一种表示了数据块的对象，它没有格式可言，也没有提供访问其内容的机制。为了访问缓冲中的内存，你需要使用视图。视图提供了*上下文*——即数据类型、起始偏移量和元素数量。

![同一缓冲上的不同类型化数组。每种类型化数组的元素数量与元素宽度都不同。](/zh-CN/docs/Web/JavaScript/Guide/Typed_arrays/typed_arrays.png)

缓冲
--

有两种类型的缓冲：`ArrayBuffer` 和 `SharedArrayBuffer`。它们都是内存块的低级表示。它们名字中都含有“array”，但是它们与数组并没有太多关系——你不能直接读写它们。相反，缓冲是通用的对象，它们只包含原始数据。为了访问缓冲所表示的内存，你需要使用视图。

缓冲支持以下操作：

* *分配*：创建一个新的缓冲时，会分配一个新的内存块，并且初始化为 `0`。
* *复制*：使用 `slice()` 方法，你可以高效地复制缓冲中的一部分数据，而不需要创建视图来手动复制每一个字节。
* *转移*：使用 `transfer()` 和 `transferToFixedLength()` 方法，可以将内存块的所有权转移给一个新的缓冲对象。若你想要在不同的执行上下文间转移数据，又不想复制，这些方法就很有用。转移后，原始缓冲将不再可用。`SharedArrayBuffer` 不能被转移（因为缓冲已经被所有执行上下文共享）。
* *调整大小*：使用 `resize()` 方法，可以调整内存块的大小（只要不超过预设的 `maxByteLength` 限制）。`SharedArrayBuffer` 只能增长，不能缩小。

`ArrayBuffer` 与 `SharedArrayBuffer` 之间的区别是，前者在同一时刻只能所属于单个执行上下文。如果你将 `ArrayBuffer` 传递给另一个执行上下文，它会被*转移*，原始的 `ArrayBuffer` 将不再可用。这确保了同一时刻只有一个执行上下文可以访问内存。`SharedArrayBuffer` 在传递给另一个执行上下文时不会被转移，因此可以被多个执行上下文同时访问。当多个线程同时访问同一内存块时，可能会出现竞争条件，这时候 `Atomics` 方法就很有用了。

视图
--

目前主要有两种视图：类型化数组视图和 `DataView`。类型化数组提供了实用方法，可以方便地转换二进制数据。`DataView` 更底层，可以精确控制数据的访问方式。使用这两种视图读写数据的方式是非常不同的。

两种视图都会使 `ArrayBuffer.isView()` 返回 `true`。它们都有以下属性：

`buffer`

视图所引用的底层缓冲。

`byteOffset`

视图相对于缓冲起始位置的偏移量（以字节为单位）。

`byteLength`

视图的长度（以字节为单位）。

两者的构造函数都接受上述三个分离的参数。类型化数组构造函数还接受 `length` 作为元素数量，而不是字节长度。

### 类型化数组视图

类型化数组视图有自描述的名称，并且提供了所有常见数值类型的视图，如 `Int8`、`Uint32` 和 `Float64` 等等。还有一种特殊的类型化数组视图，`Uint8ClampedArray`，它会将值钳制（clamp）到 `0` 到 `255` 之间，这在 Canvas 数据处理等场景中很有用。

| 类型 | 值范围 | 字节数 | 描述 | 对应的 Web IDL 类型 | 等效的 C 类型 |
| --- | --- | --- | --- | --- | --- |
| `Int8Array` | -128~127 | 1 | 8 位有符号整数（补码） | `byte` | `int8_t` |
| `Uint8Array` | 0~255 | 1 | 8 位无符号整数 | `octet` | `uint8_t` |
| `Uint8ClampedArray` | 0~255 | 1 | 8 位无符号整数（值会被裁剪） | `octet` | `uint8_t` |
| `Int16Array` | -32768~32767 | 2 | 16 位有符号整数（补码） | `short` | `int16_t` |
| `Uint16Array` | 0~65535 | 2 | 16 位无符号整数 | `unsigned short` | `uint16_t` |
| `Int32Array` | -2147483648~2147483647 | 4 | 32 位有符号整数（补码） | `long` | `int32_t` |
| `Uint32Array` | 0~4294967295 | 4 | 32 位无符号整数 | `unsigned long` | `uint32_t` |
| `Float32Array` | `-3.4E38`~`3.4E38` 以及 `1.2E-38`（最小正数） | 4 | 32 位 IEEE 浮点数（7 位有效数字，例如 `1.123456`） | `unrestricted float` | `float` |
| `Float64Array` | `-1.8E308`~`1.8E308` 以及 `5E-324`（最小正数） | 8 | 64 位 IEEE 浮点数（16 位有效数字，例如 `1.123...15`） | `unrestricted double` | `double` |
| `BigInt64Array` | -263~263 - 1 | 8 | 64 位有符号整数（补码） | `bigint` | `int64_t (signed long long)` |
| `BigUint64Array` (en-US) | 0~264 - 1 | 8 | 64 位无符号整数 | `bigint` | `uint64_t (unsigned long long)` |

所有类型化数组类型都有相同的方法与属性，这些方法与属性由 `TypedArray` 类定义。它们的唯一区别在于底层数据类型和字节数的不同。这在值编码与标准化中有详细讨论。

类型化数组原则上是固定长度的，因此并不存在可以改变数组长度的方法，如 `pop`、`push`、`shift`、`splice` 和 `unshift`。此外，`flat` 也不可用，因为类型化数组无法嵌套；`concat` 和 `flatMap` 则是由于应用场景不大，亦不可用。由于 `splice` 不可用，因此 `toSpliced` 也不可用。所有其他数组方法都是 `Array` 和 `TypedArray` 共享的。

另一方面，类型化数组有额外的 `set` 和 `subarray` 方法，可以用来优化“同缓冲多视图”的场景。`set()` 方法允许你使用一个数组或类型化数组的数据，来对另一个类型化数组的多个索引同时进行设置。如果两个类型化数组共享同一个底层缓冲，那么这个操作可能会更加高效，因为这是一个快速的内存移动。`subarray()` 方法创建一个新的类型化数组视图，它引用与原始类型化数组相同的缓冲，但是范围更窄。

在不改变底层缓冲的前提下，无法直接改变类型化数组的长度。但是，当类型化数组的底层是可调整大小的缓冲，且没有固定的 `byteLength` 时，它就会*跟踪长度*，即它的长度会随着缓冲的大小而自动调整。详情请参阅底层为可变大小缓冲时的行为。

类似于普通数组，你可以使用方括号表示法来访问类型化数组的元素。底层缓冲中对应的字节会被解析为一个数字。任何使用数字（或数字的字符串表示，因为数字总是在访问属性时被转换为字符串）的属性访问都会被类型化数组代理——它们永远不会与对象本身交互。这意味着，例如：

* 超越边界索引的访问总是返回 `undefined`，而不会实际访问对象上的属性。
* 任何尝试写入超越边界的属性的行为都不会生效：它既不会抛出错误，也不会改变缓冲或类型化数组。
* 类型化数组的索引看起来是可配置的和可写的，但是任何改变它们的属性的尝试都会失败。

```js
const uint8 = new Uint8Array([1, 2, 3]);
console.log(uint8[0]); // 1

// 仅用作示例。不要在生产代码中使用。
uint8[-1] = 0;
uint8[2.5] = 0;
uint8[NaN] = 0;
console.log(Object.keys(uint8)); // ["0", "1", "2"]
console.log(uint8[NaN]); // undefined

// 非数字访问仍然有效
uint8[true] = 0;
console.log(uint8[true]); // 0

Object.freeze(uint8); // TypeError：无法冻结非空缓冲的视图
```

### DataView

`DataView` 是一种底层接口，它提供可以操作缓冲中任意数据的 getter/setter API。这对操作不同类型数据的场景很有帮助，例如：类型化数组视图都是运行在本地字节序模式（参考字节序），可以通过使用 `DataView` 来控制字节序。默认是大端字节序（big-endian），但可以调用 getter/setter 方法改为小端字节序（little-endian）。

`DataView` 不需要对齐，多字节读写可以从任意指定的偏移量开始。setter 方法也是如此。

下面的例子使用 `DataView` 来获取任意数字的二进制表示：

```js
function toBinary(
  x,
  { type = "Float64", littleEndian = false, separator = " ", radix = 16 } = {},
) {
  const bytesNeeded = globalThis[`${type}Array`].BYTES\_PER\_ELEMENT;
  const dv = new DataView(new ArrayBuffer(bytesNeeded));
  dv[`set${type}`](0, x, littleEndian);
  const bytes = Array.from({ length: bytesNeeded }, (\_, i) =>
    dv
      .getUint8(i)
      .toString(radix)
      .padStart(8 / Math.log2(radix), "0"),
  );
  return bytes.join(separator);
}

console.log(toBinary(1.1)); // 3f f1 99 99 99 99 99 9a
console.log(toBinary(1.1, { littleEndian: true })); // 9a 99 99 99 99 99 f1 3f
console.log(toBinary(20, { type: "Int8", radix: 2 })); // 00010100
```

使用类型化数组的 Web API
----------------

下面是一些使用类型化数组的 API 示例，其并不完整，我们会在未来添加更多示例。

`FileReader.prototype.readAsArrayBuffer()`

`FileReader.prototype.readAsArrayBuffer()` 读取对应的 `Blob` 或 `File` 的内容

`XMLHttpRequest.prototype.send()`

`XMLHttpRequest` 实例的 `send()` 方法现在支持使用类型化数组和 `ArrayBuffer` 对象作为参数。

`ImageData.data`

是一个 `Uint8ClampedArray` 对象，用来描述包含按照 RGBA 序列的颜色数据的一维数组，其值的范围在 `0` 到 `255` 之间。

示例
--

### 使用视图和缓冲

首先，我们需要创建一个 16 字节固定长度的缓冲：

```js
const buffer = new ArrayBuffer(16);
```

现在我们有了一段初始化为 0 的内存，目前还做不了什么太多操作。让我们确认一下数据的字节长度：

```js
if (buffer.byteLength === 16) {
  console.log("是 16 字节。");
} else {
  console.log("大小有问题！");
}
```

在实际开始操作这个缓冲之前，还需要一个视图。现在，我们就创建一个视图，该视图将把缓冲内的数据视为一个 32 位有符号整数数组：

```js
const int32View = new Int32Array(buffer);
```

现在我们可以像普通数组一样访问该数组中的元素：

```js
for (let i = 0; i < int32View.length; i++) {
  int32View[i] = i \* 2;
}
```

该代码会将数组以 `0`、`2`、`4` 和 `6` 填充（一共 4 个 4 字节元素，所以总长度为 16 字节）。

### 相同数据上的多个视图

更有意思的是，你可以在同一数据上创建多个视图。例如：基于上文的代码，我们可以像这样继续操作：

```js
const int16View = new Int16Array(buffer);

for (let i = 0; i < int16View.length; i++) {
  console.log(`索引 ${i}：${int16View[i]}`);
}
```

这里我们创建了一个 16 位整数视图，该视图共享上文的 32 位整数视图的缓冲，然后以 16 位整数打印出缓冲里的数据，这次我们会得到 `0`、`0`、`2`、`0`、`4`、`0`、`6`、`0` 这样的输出（假设是小端序编码）。
```

Int16Array  |   0  |  0   |   2  |  0   |   4  |  0   |   6  |  0   |
Int32Array  |      0      |      2      |      4      |      6      |
ArrayBuffer | 00 00 00 00 | 02 00 00 00 | 04 00 00 00 | 06 00 00 00 |
```

进一步地：

```js
int16View[0] = 32;
console.log(`32 位数组的索引 0 处数据是：${int32View[0]}`);
```

这次的输出是 `"32 位数组的索引 0 处数据是：32"`。也就是，这 2 个数组都是同一数据的以不同格式展示出来的视图。
```

Int16Array  |  32  |  0   |   2  |  0   |   4  |  0   |   6  |  0   |
Int32Array  |     32      |      2      |      4      |      6      |
ArrayBuffer | 00 02 00 00 | 02 00 00 00 | 04 00 00 00 | 06 00 00 00 |
```

你可以对任意视图类型进行这样的操作，尽管如果你设置一个整数，然后以浮点数的形式读取它，你可能会得到一个奇怪的结果，因为位的解释方式不同。

```js
const float32View = new Float32Array(buffer);
console.log(float32View[0]); // 4.484155085839415e-44
```

### 从缓冲读取文本

缓冲并不总是表示数字。例如，读取文件可以给你一个文本数据缓冲。你可以使用类型化数组从缓冲中读取这些数据。

下面的代码使用 `TextDecoder` web API 读取 UTF-8 文本：

```js
const buffer = new ArrayBuffer(8);
const uint8 = new Uint8Array(buffer);
// 我们手动写入数据，假装它已经在缓冲区中了
uint8.set([228, 189, 160, 229, 165, 189]);
const text = new TextDecoder().decode(uint8);
console.log(text); // "你好"
```

下面的代码使用 `String.fromCharCode()` 方法读取 UTF-16 文本：

```js
const buffer = new ArrayBuffer(8);
const uint16 = new Uint16Array(buffer);
// 我们手动写入数据，假装它已经在缓冲区中了
uint16.set([0x4f60, 0x597d]);
const text = String.fromCharCode(...uint16);
console.log(text); // "你好"
```

### 使用复杂的数据结构

通过将缓冲与不同类型视图组合，以及修改内存访问的偏移量，你可以操作包含更多更复杂数据结构的数据。例如，你可以操作诸如 WebGL 或数据文件这些复杂的数据结构。

请看如下的 C 语言结构体：

```cpp
struct someStruct {
  unsigned long id;
  char username[16];
  float amountDue;
};
```

你可以采用如下代码访问一个包含此类结构体的缓冲：

```js
const buffer = new ArrayBuffer(24);

// …将数据读入缓冲…

const idView = new Uint32Array(buffer, 0, 1);
const usernameView = new Uint8Array(buffer, 4, 16);
const amountDueView = new Float32Array(buffer, 20, 1);
```

这样一来，你就得到了一个类似的数据结构，例如，`amountDueView[0]` 对应了 C 的 `amountDue` 字段。

>
> **备注**：C 语言结构体的数据对齐与平台相关。因此需要防范和考虑不同平台字节填充对齐的差异。
>
>
>

### 转换为普通数组

在处理完一个类型化数组后，有时需要把它转为普通数组，以便可以从 `Array` 原型中受益。可以通过使用 `Array.from()` 实现：

```js
const typedArray = new Uint8Array([1, 2, 3, 4]);
const normalArray = Array.from(typedArray);
```

也可以使用展开语法。

```js
const typedArray = new Uint8Array([1, 2, 3, 4]);
const normalArray = [...typedArray];
```

参见
--

* 使用类型化数组实现快速的 Canvas 像素操作
* 类型化数组：浏览器中的二进制数据
* 字节序
* `ArrayBuffer`
* `DataView`
* `TypedArray`
* `SharedArrayBuffer`
* « 上一页
* 下一页 »


Groups and ranges - JavaScript

Groups and ranges
=================

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

尝试一下
----

类型
--

| 字符集 | 含义 |
| --- | --- |
| `*x*|*y*` |
 匹配 "x" 或 "y" 任意一个字符。例如， `/green|red/` 在
 "green apple" 里匹配 "green"，且在 "red apple" 里匹配 "red" 。
  |
| `[xyz][a-c]` |
 字符集。
 匹配任何一个包含的字符。你可以使用连字符来指定字符范围，但如果连字符显示为方括号中的第一个或最后一个字符，则它将被视为作为普通字符包含在字符集中的文字连字符。也可以在字符集中包含字符类。

 例如，`[abcd]` 是与`[a-d]`.一样的，它们会
 在"brisket" 中匹配 "b",在 "chop" 中匹配 "c" .

 例如，`[abcd-]` 和`[-abcd]` 将会在 "brisket"
 匹配 "b" , 在 "chop" 匹配 "c" , 并且匹配 "non-profit" 中的 "-"
 (连字符)

 例如，`[\w-]` 是字符集 \w 和
 “-”（连字符）的并集，与这种写法一样：
 `[A-Za-z0-9_-]`.。他们都会 在 "brisket"中匹配“b”, 在
 "chop"中匹配“c”, 在 "non-profit" 中匹配 "n"。
  |
| `[^xyz][^a-c]` |
 一个否定的或被补充的字符集。也就是说，它匹配任何没有包含在括号中的字符。可以通过使用连字符来指定字符范围，但是如果连字符作为方括号中的第一个或最后一个字符出现，那么它将被视为作为普通字符包含在字符集中。例如，[^abc]
 和 [^a-c] 一样。它们最初匹配“bacon”中的“o”和“chop”中的“h”。

**备注：** ^ 字符也可以表示
 输入的起始 (en-US)

 |
| `(*x*)` |
**捕获组：**匹配 x
 并记住匹配项。例如，/(foo)/匹配并记住“foo bar”中的“foo”

 正则表达式可以有多个捕获组。结果，匹配通常在数组中捕获的组，该数组的成员与捕获组中左括号的顺序相同。这通常只是捕获组本身的顺序。当捕获组被嵌套时，这一点非常重要。使用结果元素的索引
 (`[1], ..., [n]`) 或从预定义的
 `RegExp` 对象的属性 (`$1, ..., $9`).

 捕获组会带来性能损失。如果不需要收回匹配的子字符串，请选择非捕获括号
 (见下面)。

`String.match()`
 不会返回组，如果设置了 `/.../g` 标志。但是，你仍然可以使用
 `String.matchAll()`
 to get all matches.

 match() 不会返回组，如果/…但是，你仍然可以使用 String.matchAll()
 来获取所有匹配项。
  |
| `\*n*` |
 其中 n 是一个正整数。对正则表达式中与 n
 括号匹配的最后一个子字符串的反向引用 (计算左括号)。例如，`/apple(,)\sorange\1/`
 匹配“apple, orange, cherry, peach”中的 "apple, orange,"，其中
 `\1` 引用了 之前使用 `()` 捕获的
 `,`
 |
| `(?<Name>x)` |
**具名捕获组：**匹配"x"并将其存储在返回的匹配项的 groups
 属性中，该属性位于`<Name>`指定的名称下。尖括号 (`<`
 和 `>`) 用于组名。

 例如，使用正则 `/-(?<customName>\w)/` 匹配“web-doc”
 中的“d”

`'web-doc'.match(/-(?<customName>\w)/).groups //{customName:
"d"}` |
| `(?:*x*)` | **非捕获组：**匹配
 “x”，但不记得匹配。不能从结果数组的元素中收回匹配的子字符串 (`[1], ..., [n]`) or from the predefined `RegExp` object's properties (`$1, ..., $9`).
  |

示例
--

### 使用组

```js
let personList = `First\_Name: John, Last\_Name: Doe
First\_Name: Jane, Last\_Name: Smith`;

let regexpNames = /First\_Name: (\w+), Last\_Name: (\w+)/gm;
let match = regexpNames.exec(personList);
do {
  console.log(`Hello ${match[1]} ${match[2]}`);
} while ((match = regexpNames.exec(personList)) !== null);
```

### 使用命名组

```js
let users = `姓氏：李，名字：雷
姓氏：韩，名字：梅梅`;

let regexpNames = /姓氏：(?<first>.+)，名字：(?<last>.+)/gm;
let match = regexpNames.exec(users);

do {
  console.log(`Hello ${match.groups.first} ${match.groups.last}`);
} while ((match = regexpNames.exec(users)) !== null);

// Hellow 李 雷
// Hellow 韩 梅梅
```

参见
--

* 正则表达式指南
    + 字符类
    + 断言
    + 量词
    + Unicode 属性转义
* 正则构造函数


量词 - JavaScript

量词
==

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

尝试一下
----

类型
--

| Characters | Meaning |
| --- | --- |
| `*x**` |
 将前面的项“x”匹配 0 次或更多次。例如，/bo\*/匹配“A ghost
 booooed”中的“boooo”和“A bird warbled”中的“b”，但在“A goat
 grunt”中没有匹配。
  |
| `*x*+` |
 将前一项“x”匹配 1
 次或更多次。等价于 `{1,}`。例如，`/a+/` 匹配“candy”中的“a”和“caaaaaaandy”中的所有“a”。
  |
| `*x*?` |
 将前面的项“x”匹配 0 或 1 次。例如，/e?le?/匹配 angel 中的 el 和 angle
 中的 le。

 如果立即在任何量词\*、+、?或{}之后使用，则使量词是非贪婪的
 (匹配最小次数)，而不是默认的贪婪的 (匹配最大次数)。
  |
| `*x*{*n*}` | 其中“n”是一个正整数，与前一项“x”的 n 次匹配。例如，`/a{2}/` 不匹配“candy”中的“a”，但它匹配“caandy”中的所有“a”，以及“caaandy”中的前两个“a”。 |
| `*x*{*n*,}` |
 其中，“n”是一个正整数，与前一项“x”至少匹配“n”次。例如，`/a{2，}/`不匹配“candy”中的“a”，但匹配“caandy”和“caaaaaaandy”中的所有
 a。
  |
| `*x*{*n*,*m*}` |
 其中，“n”是 0 或一个正整数，“m”是一个正整数，而 m > n
 至少与前一项“x”匹配，最多与“m”匹配。例如，/a{1,3}/不匹配“cndy”中的“a”，“candy”中的“a”，“caandy”中的两个“a”，以及“caaaaaaandy”中的前三个“a”。注意，当匹配“caaaaaaandy”时，匹配的是“aaa”，即使原始字符串中有更多的“a”。
  |
| `*x**?``*x*+?``*x*??``*x*{n}?``*x*{n,}?``*x*{n,m}?` |
 默认情况下，像 `*` 和
 `+` 这样的量词是“贪婪的”，这意味着它们试图匹配尽可能多的字符串。?量词后面的字符使量词“非贪婪”：意思是它一旦找到匹配就会停止。例如，给定一个字符串“some
 <foo> <bar> new </bar> </foo> thing”:
 * `/<.*>/` will match "<foo> <bar> new
 </bar> </foo>"
* `/<.*?>/` will match "<foo>"
 |

举例说明
----

### 重复模式

```js
var wordEndingWithAs = /\w+a+/;
var delicateMessage = "This is Spartaaaaaaa";

console.table(delicateMessage.match(wordEndingWithAs)); // [ "Spartaaaaaaa" ]
```

### 计算字符集

```js
var singleLetterWord = /\b\w\b/g;
var notSoLongWord = /\b\w{1,6}\b/g;
var loooongWord = /\b\w{13,}\b/g;

var sentence = "Why do I have to learn multiplication table?";

console.table(sentence.match(singleLetterWord)); // ["I"]
console.table(sentence.match(notSoLongWord)); // [ "Why", "do", "I", "have", "to", "learn", "table" ]
console.table(sentence.match(loooongWord)); // ["multiplication"] 可选可选字符
```

### 可选字符

```js
var britishText = "He asked his neighbour a favour.";
var americanText = "He asked his neighbor a favor.";

var regexpEnding = /\w+ou?r/g;
// \w+ One or several letters
// o followed by an "o",
// u? optionally followed by a "u"
// r followed by an "r"

console.table(britishText.match(regexpEnding));
// ["neighbour", "favour"]

console.table(americanText.match(regexpEnding));
// ["neighbor", "favor"]
```

### 贪婪 与 非贪婪的

```js
var text = "I must be getting somewhere near the centre of the earth.";
var greedyRegexp = /[\w ]+/;
// [\w ] a letter of the latin alphabet or a whitespace
// + one or several times

console.log(text.match(greedyRegexp)[0]);
// "I must be getting somewhere near the centre of the earth"
// almost all of the text matches (leaves out the dot character)

var nonGreedyRegexp = /[\w ]+?/; // Notice the question mark
console.log(text.match(nonGreedyRegexp));
// "I"
// The match is the smallest one possible
```

参见
--

* Regular expressions guide
    + Character classes
    + Assertions
    + Unicode property escapes
    + Groups and ranges
* The `RegExp()` constructor


Assertions - JavaScript

Assertions
==========

断言的组成之一是边界。对于文本、词或模式，边界可以用来表明它们的起始或终止部分（如先行断言，后行断言以及条件表达式）。

尝试一下
----

类型
--

### 边界类断言

| 字符 | 含义 |
| --- | --- |
| `^` |
 匹配输入的开头。如果多行模式设为 true，`^`
 在换行符后也能立即匹配，比如 `/^A/` 匹配不了 "an A" 里面的
 "A"，但是可以匹配 "An A" 里面第一个 "A"。

**备注：**`^` 出现在集合或范围开头时的含义与此不同（参见
 group）。

 |
| `$` |
 匹配输入的结束。如果多行模式设为 true，`$`
 在换行符前也能立即匹配，比如 `/t$/` 不能匹配 "eater" 中的
 "t"，但是可以匹配 "eat" 中的 "t"。
  |
| `\b` | 匹配一个单词的边界，这是一个字的字符前后没有另一个字的字符位置，例如在字母和空格之间。需要注意的是匹配的单词边界不包括在匹配中。换句话说，匹配字边界的长度为零。
一些例子：* `/\bm/` 在 "moon" 中匹配到 "m"
* `/oo\b/` 在 "moon" 中不会匹配到 "oo", 因为 "oo" 后面跟着
 "n" 这个单词字符。
* `/oon\b/` 在 "moon" 中匹配 "oon"，因为 "oon"
 是这个字符串的结尾，因此后面没有单词字符
* `/\w\b\w/`
 将永远不会匹配任何东西，因为一个单词字符后面永远不会同时有一个非单词字符和一个单词字符。

 匹配退格字符 (`[\b]`), 查看
 字符类
 |
| `\B` |
 匹配非单词边界。这是上一个字符和下一个字符属于同一类型的位置：要么两者都必须是单词，要么两者都必须是非单词，例如在两个字母之间或两个空格之间。字符串的开头和结尾被视为非单词。与匹配的词边界相同，匹配的非词边界也不包含在匹配中。例如，`/\Bon/`
 在“at noon”中匹配“on” ，`/ye\B/` 在 "possibly
 yesterday"中匹配"ye" 。
  |

### 其他断言

**备注：** `？`字符也可用作量词

| `字符` | `含义` |
| --- | --- |
| `x(?=y)` |

|  |
| --- |
| **先行断言：** x 被 y 跟随时匹配
 x。例如，对于/`Jack(?=Sprat)`/，“Jack”在跟有“Sprat”的情况下才会得到匹配．`/Jack(?=Sprat|Frost)/`
 “Jack”后跟有“Sprat”或“Frost”的情况下才会得到匹配。不过，
 匹配结果不包括“Sprat”或“Frost”。
  |

 |
| x(?!y) |

|  |
| --- |
| **先行否定断言：** x 没有被 y 紧随时匹配
 x。例如，对于`/\d+(?!\.)/`，数字后没有跟随小数点的情况下才会得到匹配。对于`/\d+(?!\.)/.exec(3.141)`，匹配‘141’而不是‘3’。
  |

 |
| (?<=y)x |

|  |
| --- |
| **后行断言：** x 跟随 y 的情况下匹配
 x。例如，对于`/(?<=Jack)Sprat/`，“Sprat”紧随“Jack”时才会得到匹配。对于`/(?<=Jack|Tom)Sprat`，“Sprat”在紧随“Jack”或“Tom”的情况下才会得到匹配。不过，匹配结果中不包括“Jack”或“Tom”。
  |

 |
| (?<!y)x |

|  |
| --- |
| **后行否定断言：** x 不跟随 y 时匹配
 x。例如，对于`/(?<!-)\d+/`，数字不紧随 -
 符号的情况下才会得到匹配。对于`/(?<!-)\d+/.exec(3)`
 ，“3”得到匹配。
 而`/(?<!-)\d+/.exec(-3)`的结果无匹配，这是由于数字之前有
 - 符号。
  |

 |

示例
--

### 一般边界类型概述示例
```

// 使用 正则表达式边界修复错误字符串
buggyMultiline = `tey, ihe light-greon apple
tangs on ihe greon traa`;

// 1) 使用 ^ 修正字符串开始处和换行后的匹配。
buggyMultiline = buggyMultiline.replace(/^t/gim,'h');
console.log(1, buggyMultiline); // 修复 'tey'=>'hey'(字符串开始) , 'tangs'=>'hangs'(换行后)

// 2) 使用 $ 修正字符串结尾处的匹配。
buggyMultiline = buggyMultiline.replace(/aa$/gim,'ee.');
console.log(2, buggyMultiline); // 修复 'traa' => 'tree'.

// 3) 使用 \b 修正单词和空格边界上的字符。
buggyMultiline = buggyMultiline.replace(/\bi/gim,'t');
console.log(3, buggyMultiline); // 修复 'ihe' => 'the'  不影响 'light'.

// 4) 使用 \B 匹配实体边界内的字符。
fixedMultiline = buggyMultiline.replace(/\Bo/gim,'e');
console.log(4, fixedMultiline); // 修复  'greon'  不影响'on'.
```

### 使用 ^（控制字符）匹配输入的开头

使用 `^`匹配输入的开头。在这个例子中，我们可以通过 /^A/ 正则表达式得到以 A 开头的水果。为了选择合适的水果，我们可以使用带有箭头函数的过滤方法。
```

let fruits = ["Apple", "Watermelon", "Orange", "Avocado", "Strawberry"];

// 使用正则 /^A/ 选择以'A'开头的水果。
// 这里的 '^' 只有一种含义：匹配输入的开头。

let fruitsStartsWithA = fruits.filter(fruit => /^A/.test(fruit));
console.log(fruitsStartsWithA); // [ 'Apple', 'Avocado' ]
```

在第二个示例中，^用于在输入的开始处匹配，以及在内部使用时用于创建否定或被补充的字符集组和范围。
```

let fruits = ["Apple", "Watermelon", "Orange", "Avocado", "Strawberry"];

// 使用正则 /^[^A]/ 选择 不是以‘A’开头的水果
// 在这个例子中，“^”控件符号表示两种含义：
// 1) 匹配输入的开头
// 2) 一个否定的字符集：[^A] ，意思是匹配不是‘A’的字符

let fruitsStartsWithNotA = fruits.filter(fruit => /^[^A]/.test(fruit));

console.log(fruitsStartsWithNotA); // [ 'Watermelon', 'Orange', 'Strawberry' ]
```

### 匹配字边界
```

let fruitsWithDescription = ["Red apple", "Orange orange", "Green Avocado"];

// 选择包含以“en”或“ed”结尾的单词的描述：
let enEdSelection = fruitsWithDescription.filter(descr => /(en|ed)\b/.test(descr));

console.log(enEdSelection); // [ 'Red apple', 'Green Avocado' ]
```

### 先行断言
```

// JS 先行断言 x(?=y) 匹配被 y 跟随的 x

let regex = /First(?= test)/g;

console.log('First test'.match(regex)); // [ 'First' ]
console.log('test First peach'.match(regex)); // null
console.log('This is a First test in a year.'.match(regex)); // [ 'First' ]
console.log('This is a First peach in a month.'.match(regex)); // null
```

### 先行否定断言

例如， `/\d+(?!\.)/` 匹配没有被小数点跟随且至少有一位的数字。 `/\d+(?!\.)/.exec('3.141')` 匹配 "141" 而不是 "3"
```

console.log(/\d+(?!\.)/g.exec('3.141')); // [ '141', index: 2, input: '3.141' ]
```

### 不同含义的'?!'：断言和范围的组合用法

不同含义的 `?!` 结合使用断言 `/x(?!y)/` 和范围 `[^?!]`。
```

let orangeNotLemon = "Do you want to have an orange? Yes, I do not want to have a lemon!";

let selectNotLemonRegex = /[^?!]+have(?! a lemon)[^?!]+[?!]/gi
console.log(orangeNotLemon.match(selectNotLemonRegex)); // [ 'Do you want to have an orange?' ]

let selectNotOrangeRegex = /[^?!]+have(?! an orange)[^?!]+[?!]/gi
console.log(orangeNotLemon.match(selectNotOrangeRegex)); // [ ' Yes, I do not want to have a lemon!' ]
```

### 后行断言
```

let oranges = ['ripe orange A ', 'green orange B', 'ripe orange C',];

let ripe_oranges = oranges.filter( fruit => fruit.match(/(?<=ripe )orange/));
console.log(ripe_oranges); // [ 'ripe orange A ', 'ripe orange C' ]
```

参见
--

* 正则表达式指南
    + 字符类
    + 量词
    + Unicode 属性转义
    + 组和范围
* RegExp() 构造函数


字符类 - JavaScript

字符类
===

字符类可以区分各种字符，例如区分字母和数字。

尝试一下
----

类型
--

| 字符 | 含义 |
| --- | --- |
| `[xyz][a-c]` | 一个字符类。匹配包含在方括号中的任何字符。你可以使用连字符指定字符范围，但如果连字符出现在方括号中的第一个或最后一个字符，则将其视为字面连字符，作为普通字符包含在字符类中。
例如，`[abcd]` 与 `[a-d]` 相同。它们匹配“brisket”中的“b”和“chop”中的“c”。
例如，`[abcd-]` 和 `[-abcd]` 匹配“brisket”中的“b”、“chop”中的“c”和“non-profit”中的“-”（连字符）。
例如，`[\w-]` 与 `[A-Za-z0-9_-]` 相同。它们都匹配“brisket”中的“b”、“chop”中的“c”和“non-profit”中的“n”。 |
| `[^xyz][^a-c]` | 一个否定或补充的字符类。也就是说，它匹配未包含在方括号中的任何字符。你可以使用连字符指定字符范围，但如果连字符出现在`^`后的第一个字符或方括号中的或最后一个字符，则将其视为字面连字符，作为普通字符包含在字符类中。例如，`[^abc]` 与 `[^a-c]` 相同。它们首先匹配“bacon”中的“o”和“chop”中的“h”。

**备注：** ^ 字符也可以表示输入的开始。
 |
| `.` | 有下列含义之一：* 匹配除行终止符之外的任何单个字符：`\n`, `\r`, `\u2028` or `\u2029`. 例如，`/.y/` 在“yes make my day”中匹配“my”和“ay”，而不是“yes”。
* 在字符集内，点失去了它的特殊意义，并与文字点匹配。

需要注意的是，`m` multiline 标志不会改变点的行为。因此，要跨多行匹配一个模式，可以使用字符集`[^]`—它将匹配任何字符，包括新行。
`s`“dotAll”标志允许点也匹配行终止符。 |
| `\d` | 匹配任何数字 (阿拉伯数字)。相当于 `[0-9]`。例如，`/\d/` 或 `/[0-9]/` 匹配“B2is the suite number”中的“2”。 |
| `\D` | 匹配任何非数字 (阿拉伯数字) 的字符。相当于`[^0-9]`。例如，`/\D/` 或者 `/[^0-9]/` 匹配“B2 is the suite number”中的“B”。 |
| `\w` | 匹配基本拉丁字母中的任何字母数字字符，包括下划线。相当于 `[A-Za-z0-9_]`。例如，`/\w/` 匹配“apple”中的“a”，匹配“$5.28”中的“5”，匹配“3D”中的“3”，以及匹配“Émanuel”中的“m”。 |
| `\W` | 匹配任何不是来自基本拉丁字母的单词字符。相当于 `[^A-Za-z0-9_]`。例如，`/\W/` 或者 `/[^A-Za-z0-9_]/` 匹配“50%”中的“%”，以及匹配“Émanuel”中的“É”。 |
| `\s` |
 匹配单个空白字符，包括空格、制表符、换页符、换行符和其他 Unicode 空格。相当于
 `[\f\n\r\t\v\u0020\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]`。例如，`\s\w*/` 匹配“foo bar”中的“bar”。
  |
| `\S` |
 匹配除空格以外的单个字符。相当于
 `[^\f\n\r\t\v\u0020\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]`。例如，`/\S\w*/` 匹配“foo bar”中的“foo”。
  |
| `\t` | 匹配水平制表符。 |
| `\r` | 匹配回车符。 |
| `\n` | 匹配换行符。 |
| `\v` | 匹配垂直制表符。 |
| `\f` | 匹配换页符。 |
| `[\b]` | 匹配退格键。如果你正在寻找单词边界字符（`\b`），请参阅断言。 |
| `\0` | 匹配一个 NUL 字符。不要在此后面加上另一个数字。 |
| `\c*X*` | 使用插入符号匹配控制字符，其中“X”是 A–Z 中的一个字母（对应于代码点 `U+0001`*–*`U+001F`）。例如，`/\cM\cJ/`匹配“\r\n”。 |
| `\x*hh*` | 匹配与代码 `*hh*`（两个十六进制数字）对应的字符。 |
| `\u*hhhh*` | 匹配与值 `*hhhh*`（四个十六进制数字）对应的 UTF-16 代码单元。 |
| `\u*{hhhh}* 或 *\u{hhhhh}*` | （仅当设置了 `u` 标志时。）匹配与 Unicode 值 `U+*hhhh*` 或 `U+*hhhhh*`（十六进制数字）对应的字符。 |
| `\p{*UnicodeProperty*}`，`\P{*UnicodeProperty*}` | 根据字符的 Unicode 字符属性匹配字符（例如，仅匹配表情符号字符、日文*片假名*字符、中文汉字字符或日文汉字字符等）。 |
| `\` | 指示应特殊处理或“转义”后面的字符。它表现为两种方式之一。* 对于通常按字面处理的字符，表示下一个字符是特殊的，不能按字面解释。例如，`/b/` 匹配字符“b”。通过在“b”前面放置反斜杠，即使用 `/\b/`，字符变得特殊以表示匹配单词边界。
* 对于通常被特殊对待的字符，表示下一个字符不是特殊的，应该按字面意思解释。例如，“\*”是一个特殊字符，表示应该匹配前面的字符出现 0 次或多次；例如，`/a*/` 示匹配 0 个或多个“a”。要从字面上匹配 `*` 需在其前面加上反斜杠；例如，`/a\*/` 匹配“a\*”。

**备注：**要从字面上匹配此字符，请将其转义。换句话说就是搜索 `\` 需要使用 `/\\/`。
 |
| `*x*|*y*` | **析取：**匹配“x”或“y”。每个由管道符 (`|`) 分隔的部分称为一个*可选项*。例如，`/green|red/` 匹配“green apple”中的“green”和“red apple”中的“red”。

**备注：** 析取是指定“一组选择”的另一种方式，但它不是字符类。析取不是原子的——你需要使用组使其成为一个更大的模式的一部分。`[abc]` 在功能上等同于 `(?:a|b|c)`。
 |

示例
--

### 寻找一系列数字

```js
const randomData = "015 354 8787 687351 3512 8735";
const regexpFourDigits = /\b\d{4}\b/g;
// \b 表示边界（即不要在单词中间开始匹配）
// \d{4} 表示一个数字，四次
// \b 表示另一个边界（即不要在单词中间结束匹配）

console.table(randomData.match(regexpFourDigits));
// ['8787', '3512', '8735']
```

### 寻找以 A 开头的拉丁字母单词

```js
const aliceExcerpt =
  "I'm sure I'm not Ada,' she said, 'for her hair goes in such long ringlets, and mine doesn't go in ringlets at all.";
const regexpWordStartingWithA = /\b[aA]\w+/g;
// \b 表示边界（即不要在单词中间开始匹配）
// [aA] 表示字母 a 或 A
// \w+ 表示任何\*拉丁字母\*字符，多次

console.table(aliceExcerpt.match(regexpWordStartingWithA));
// ['Ada', 'and', 'at', 'all']
```

### 寻找一个 Unicode 字符的单词

代替拉丁字母，我们可以使用一系列 Unicode 字符来识别一个单词（从而能够处理其他语言的文本，如中文、俄语或阿拉伯语）。Unicode 的“基本多文种平面（Basic Multilingual Plane）”包含世界各地使用的大部分字符，我们可以使用字符类和范围来匹配用这些字符编写的单词。

```js
const nonEnglishText = "爱丽丝 梦游 仙境";
const regexpBMPWord = /([\u0000-\u0019\u0021-\uFFFF])+/gu;
// 基本多文种平面范围是 U+0000 到 U+FFFF 但空格是 U+0020

console.table(nonEnglishText.match(regexpBMPWord));
["爱丽丝", "梦游", "仙境"];
```

### 计算元音个数

```js
const aliceExcerpt =
  "There was a long silence after this, and Alice could only hear whispers now and then.";
const regexpVowels = /[AEIOUYaeiouy]/g;

console.log("元音数：", aliceExcerpt.match(regexpVowels).length);
// 元音数：26
```

参见
--

* 正则表达式
    + 断言
    + 量词
    + Unicode 属性转义
    + 组和后向引用
* `RegExp()` 构造函数
* CharacterClass in the ECMAScript specification


模板字符串 - JavaScript

模板字符串
=====

**模板字面量**是用反引号（```）分隔的字面量，允许多行字符串、带嵌入表达式的字符串插值和一种叫带标签的模板的特殊结构。

模板字面量有时被非正式地叫作*模板字符串*，因为它们最常被用作字符串插值（通过替换占位符来创建字符串）。然而，带标签的模板字面量可能不会产生字符串——它可以与自定义标签函数一起使用，来对模板字面量的不同部分执行任何操作。

语法
--

```js
`string text`

`string text line 1
 string text line 2`

`string text ${expression} string text`

tagFunction`string text ${expression} string text`
```

### 参数

`string text`

将成为模板字面量的一部分的字符串文本。几乎允许所有字符，包括换行符和其他空白字符。但是，除非使用了标签函数，否则无效的转义序列将导致语法错误。

`expression`

要插入当前位置的表达式，其值被转换为字符串或传递给 `tagFunction`。

`tagFunction`

如果指定，将使用模板字符串数组和替换表达式调用它，返回值将成为模板字面量的值。见带标签的模板。

描述
--

 模板字面量用反引号（```）括起来，而不是双引号（`"`）或单引号（`'`）。
 除了普通字符串外，模板字面量还可以包含*占位符*——一种由美元符号和大括号分隔的嵌入式表达式：`${expression}`。字符串和占位符被传递给一个函数（要么是默认函数，要么是自定义函数）。默认函数（当未提供自定义函数时）只执行字符串插值来替换占位符，然后将这些部分拼接到一个字符串中。

若要提供自定义函数，需在模板字面量之前加上函数名（结果被称为**带标签的模板**）。此时，模板字面量被传递给你的标签函数，然后就可以在那里对模板文本的不同部分执行任何操作。

若要转义模板字面量中的反引号（```），需在反引号之前加一个反斜杠（`\`）。

```js
`\`` === "`"; // true
```

美元符号 `$` 也可以被转义，来阻止插值。

```js
`\${1}` === "${1}"; // true
```

### 多行字符串

在源码中插入的任何换行符都是模板字面量的一部分。

使用普通字符串，可以通过下面的方式得到多行字符串：

```js
console.log("string text line 1\n" + "string text line 2");
// "string text line 1
// string text line 2"
```

使用模板字面量，下面的代码同样可以做到：

```js
console.log(`string text line 1
string text line 2`);
// "string text line 1
// string text line 2"
```

### 字符串插值

如果没有模板字面量，当你想组合表达式的输出与字符串时，可以使用加法运算符 `+` 连接它们：

```js
const a = 5;
const b = 10;
console.log("Fifteen is " + (a + b) + " and\nnot " + (2 \* a + b) + ".");
// "Fifteen is 15 and
// not 20."
```

这可能很难阅读，尤其是当存在多个表达式时。

有了模板字面量，就可以通过使用占位符 `${expression}` 嵌入待替换的表达式，从而避免串联运算符，并提高代码的可读性：

```js
const a = 5;
const b = 10;
console.log(`Fifteen is ${a + b} and
not ${2 \* a + b}.`);
// "Fifteen is 15 and
// not 20."
```

注意，这两种语法有一点小区别：模板字面量直接将其表达式强制转换为字符串，而加法则会先强制转换为原语类型。更多相关信息，参见加法（`+`）运算符。

### 嵌套模板

在某些情况下，嵌套模板是具有可配置字符串的最简单的（也许还是更可读的）方法。在反引号分隔的模板中，允许在占位符 `${expression}` 中使用内层的反引号。

例如，不用模板字面量的情况下，如果你想根据特定条件返回某个值，可以执行以下操作：

```js
let classes = "header";
classes += isLargeScreen()
  ? ""
  : item.isCollapsed
    ? " icon-expander"
    : " icon-collapser";
```

用模板字面量但不嵌套时，你可以这么做：

```js
const classes = `header ${
 isLargeScreen() ? "" : item.isCollapsed ? "icon-expander" : "icon-collapser"
}`;
```

用嵌套模板字面量时，你可以这么做：

```js
const classes = `header ${
 isLargeScreen() ? "" : `icon-${item.isCollapsed ? "expander" : "collapser"}`
}`;
```

### 带标签的模板

*带标签的*模板是模板字面量的一种更高级的形式，它允许你使用函数解析模板字面量。标签函数的第一个参数包含一个字符串数组，其余的参数与表达式相关。你可以用标签函数对这些参数执行任何操作，并返回被操作过的字符串（或者，也可返回完全不同的内容，见下面的示例）。用作标签的函数名没有限制。

```js
const person = "Mike";
const age = 28;

function myTag(strings, personExp, ageExp) {
  const str0 = strings[0]; // "That "
  const str1 = strings[1]; // " is a "
  const str2 = strings[2]; // "."

  const ageStr = ageExp > 99 ? "centenarian" : "youngster";

  // 我们甚至可以返回使用模板字面量构建的字符串
  return `${str0}${personExp}${str1}${ageStr}${str2}`;
}

const output = myTag`That ${person} is a ${age}.`;

console.log(output);
// That Mike is a youngster.
```

标签不必是普通的标识符，你可以使用任何优先级大于 16 的表达式，包括属性访问、函数调用、new 表达式，甚至其他带标签的模板字面量。

```js
console.log`Hello`; // [ 'Hello' ]
console.log.bind(1, 2)`Hello`; // 2 [ 'Hello' ]
new Function("console.log(arguments)")`Hello`; // [Arguments] { '0': [ 'Hello' ] }

function recursive(strings, ...values) {
  console.log(strings, values);
  return recursive;
}
recursive`Hello``World`;
// [ 'Hello' ] []
// [ 'World' ] []
```

虽然语法从技术上允许这么做，但*不带标签的*模板字面量是字符串，并且在链式调用时会抛出 `TypeError`。

```js
console.log(`Hello``World`); // TypeError: "Hello" is not a function
```

唯一的例外是可选链，这将抛出语法错误。

```js
console.log?.`Hello`; // SyntaxError: Invalid tagged template on optional chain
console?.log`Hello`; // SyntaxError: Invalid tagged template on optional chain
```

请注意，这两个表达式仍然是可解析的。这意味着它们将不受自动分号补全的影响，其只会插入分号来修复无法解析的代码。

```js
// 仍是语法错误
const a = console?.log
`Hello`
```

标签函数甚至不需要返回字符串！

```js
function template(strings, ...keys) {
  return (...values) => {
    const dict = values[values.length - 1] || {};
    const result = [strings[0]];
    keys.forEach((key, i) => {
      const value = Number.isInteger(key) ? values[key] : dict[key];
      result.push(value, strings[i + 1]);
    });
    return result.join("");
  };
}

const t1Closure = template`${0}${1}${0}!`;
// const t1Closure = template(["","","","!"],0,1,0);
t1Closure("Y", "A"); // "YAY!"

const t2Closure = template`${0} ${"foo"}!`;
// const t2Closure = template([""," ","!"],0,"foo");
t2Closure("Hello", { foo: "World" }); // "Hello World!"

const t3Closure = template`I'm ${"name"}. I'm almost ${"age"} years old.`;
// const t3Closure = template(["I'm ", ". I'm almost ", " years old."], "name", "age");
t3Closure("foo", { name: "MDN", age: 30 }); // "I'm MDN. I'm almost 30 years old."
t3Closure({ name: "MDN", age: 30 }); // "I'm MDN. I'm almost 30 years old."
```

标签函数接收到的第一个参数是一个字符串数组。对于任何模板字面量，其长度等于替换次数（`${…}` 出现次数）加一，因此总是非空的。对于任何特定的带标签的模板字面量表达式，无论对字面量求值多少次，都将始终使用完全相同的字面量数组调用标签函数。

```js
const callHistory = [];

function tag(strings, ...values) {
  callHistory.push(strings);
  // Return a freshly made object
  return {};
}

function evaluateLiteral() {
  return tag`Hello, ${"world"}!`;
}

console.log(evaluateLiteral() === evaluateLiteral()); // false; each time `tag` is called, it returns a new object
console.log(callHistory[0] === callHistory[1]); // true; all evaluations of the same tagged literal would pass in the same strings array
```

这允许标签函数以其第一个参数作为标识来缓存结果。为了进一步确保数组值不变，第一个参数及其 `raw` 属性都会被冻结，因此你将无法改变它们。

### 原始字符串

在标签函数的第一个参数中，存在一个特殊的属性 `raw` ，我们可以通过它来访问模板字符串的原始字符串，而无需转义特殊字符。

```js
function tag(strings) {
  console.log(strings.raw[0]);
}

tag`string text line 1 \n string text line 2`;
// logs "string text line 1 \n string text line 2" ,
// including the two characters '\' and 'n'
```

另外，使用 `String.raw()` 方法创建原始字符串和使用默认模板函数和字符串连接创建是一样的。

```js
let str = String.raw`Hi\n${2+3}!`;
// "Hi\\n5!"

str.length;.
// 6

str.split('').join(',');.
// "H,i,\\,n,5,!"
```

如果字面量不包含任何转义序列，`String.raw` 函数就像一个“identity”标签。如果你想要一个始终像不带标签的字面量那样的实际标识标签，可以用自定义函数，将“cooked”（例如，经转义序列处理过的）字面量数组传递给 `String.raw`，将它们当成原始字符串。

```js
const identity = (strings, ...values) =>
  String.raw({ raw: strings }, ...values);
console.log(identity`Hi\n${2 + 3}!`);
// Hi
// 5!
```

这对于许多工具来说很有用，它们要对以特定名称为标签的字面量作特殊处理。

```js
const html = (strings, ...values) => String.raw({ raw: strings }, ...values);
// 一些格式化程序会将此字面量的内容格式化为 HTML
const doc = html`<!doctype html>

 <title>Hello</title>

Hello world!

 `;
```

### 带标签的模板字面量及转义序列

在普通模板字面量中，字符串字面量中的转义序列都是允许的，任何其他格式不正确的转义序列都是语法错误，包括：

* `\` 后跟 `0` 以外的任何十进制数字，或 `\0` 后跟一个十进制数字，例如 `\9` 和 `\07`（这是一种已弃用的语法）
* `\x` 后跟两位以下十六进制数字，例如`\xz`
* `\u` 后不跟 `{`，并且后跟四个以下十六进制数字，例如 `\uz`
* `\u{}` 包含无效的 Unicode 码点——包含一个非十六进制数字，或者它的值大于 10FFFF，例如 `\u{110000}` 和 `\u{z}`

**备注：** `\` 后面跟着其他字符，虽然它们可能没有用，因为没有转义，但它们不是语法错误。

然而，这对于带标签的模板来说是有问题的，除了“cooked”字面量外，这些模板还可以访问原始字面量（转义序列按原样保留）。带标签的模板应该允许嵌入语言（例如 DSL 或 LaTeX），在这些语言里其他转义序列是常见的。因此，从带标签的模板中删除了转义序列诸多格式的语法限制。

不过，非法转义序列在“cooked”当中仍然会体现出来。它们将以 `undefined` 元素的形式存在于“cooked”数组之中：

```js
function latex(str) {
  return { cooked: str[0], raw: str.raw[0] };
}

latex`\unicode`;

// { cooked: undefined, raw: "\\unicode" }
```

值得注意的是，这一转义序列限制只对带标签的模板字面量移除，而不包括不带标签的模板字面量：

```js
const bad = `bad escape sequence: \unicode`;
```

规范
--

| Specification |
| --- |
| ECMAScript Language Specification # sec-template-literals |

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

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

参见
--

* `String`
* `String.raw()`
* 词法文法
* ES3 中的类模板字符串
* hacks.mozilla.org 上的《深度学习 ES6 之模板字面量》