重新介绍 JavaScript（JS 教程） - JavaScript

重新介绍 JavaScript（JS 教程）
======================

为什么会有这一篇“重新介绍”呢？因为 JavaScript 堪称世界上被人误解最深的编程语言。虽然常被嘲为“玩具语言”，但在它看似简洁的外衣下，还隐藏着强大的语言特性。JavaScript 目前广泛应用于众多知名应用中，对于网页和移动开发者来说，深入理解 JavaScript 就尤为必要。

我们有必要先从这门语言的历史谈起。在 1995 年 Netscape 一位名为 Brendan Eich 的工程师创造了 JavaScript，随后在 1996 年初，JavaScript 首先被应用于 Netscape 2 浏览器上。最初的 JavaScript 名为 LiveScript，但是因为一个糟糕的营销策略而被重新命名，该策略企图利用 Sun Microsystem 的 Java 语言的流行性，将它的名字从最初的 LiveScript 更改为 JavaScript——尽管两者之间并没有什么共同点。这便是之后混淆产生的根源。

几个月后，Microsoft 随 IE 3 发布推出了一个与之基本兼容的语言 JScript。又过了几个月，Netscape 将 JavaScript 提交至 Ecma International（一个欧洲标准化组织），ECMAScript 标准第一版便在 1997 年诞生了，随后在 1999 年以 ECMAScript 第三版的形式进行了更新，从那之后这个标准没有发生过大的改动。由于委员会在语言特性的讨论上发生分歧，ECMAScript 第四版尚未推出便被废除，但随后于 2009 年 12 月发布的 ECMAScript 第五版引入了第四版草案加入的许多特性。第六版标准已经于 2015 年 6 月发布。

**备注：** 由于这种用法更常见，从这里开始，我们将使用 JavaScript 来指代 ECMAScript。

与大多数编程语言不同，JavaScript 没有输入或输出的概念。它是一个在宿主环境（host environment）下运行的脚本语言，任何与外界沟通的机制都是由宿主环境提供的。浏览器是最常见的宿主环境，但在非常多的其他程序中也包含 JavaScript 解释器，如 Adobe Acrobat、Adobe Photoshop、SVG 图像、Yahoo！的 Widget 引擎，Node.js 之类的服务器端环境，NoSQL 数据库（如开源的 Apache CouchDB）、嵌入式计算机，以及包括 GNOME（注：GNU/Linux 上最流行的 GUI 之一）在内的桌面环境等等。

概览
--

JavaScript 是一种多范式的动态语言，它包含类型、运算符、标准内置（built-in）对象和方法。它的语法来源于 Java 和 C，所以这两种语言的许多语法特性同样适用于 JavaScript。JavaScript 通过原型链而不是类来支持面向对象编程（有关 ES6 类的内容参考这里`Classes`，有关对象原型参考见此继承与原型链）。JavaScript 同样支持函数式编程——因为它们也是对象，函数也可以被保存在变量中，并且像其他对象一样被传递。

先从任何编程语言都不可缺少的组成部分——“类型”开始。JavaScript 程序可以修改值（value），这些值都有各自的类型。JavaScript 中的类型包括：

* `Number`（数字）
* `String`（字符串）
* `Boolean`（布尔）
* `Function`（函数）
* `Object`（对象）
* `Symbol`（ES2015 新增）

…哦，还有看上去有些…奇怪的 `undefined`（未定义）类型和 `null`（空）类型。此外还有 `Array`（数组）类型，以及分别用于表示日期和正则表达式的 `Date`（日期）和 `RegExp`（正则表达式），这三种类型都是特殊的对象。严格意义上说，Function（函数）也是一种特殊的对象。所以准确来说，JavaScript 中的类型应该包括这些：

* `Number`（数字）
* `String`（字符串）
* `Boolean`（布尔）
* `Symbol`（符号）（ES2015 新增）
* `Object`（对象）
    + `Function`（函数）
    + `Array`（数组）
    + `Date`（日期）
    + `RegExp`（正则表达式）
* `null`（空）
* `undefined`（未定义）

JavaScript 还有一种内置的 `Error`（错误）类型。但是，如果我们继续使用上面的分类，事情便容易得多；所以，现在，我们先讨论上面这些类型。

数字
--

根据语言规范，JavaScript 采用“遵循 IEEE 754 标准的双精度 64 位格式”（"double-precision 64-bit format IEEE 754 values"）表示数字。——在 JavaScript（除了`BigInt`）当中，\*\*并不存在整数/整型 (Integer)。\*\*因此在处理如下的场景时候，你一定要小心：
```

console.log(3 / 2);             // 1.5,not 1
console.log(Math.floor(3 / 2)); // 1
```

一个看上去是整数的东西，其实都是浮点数。

当然，你也需要小心这种情况：

```js
0.1 + 0.2 = 0.30000000000000004
```

在具体实现时，整数值通常被视为 32 位整型变量，在个别实现（如某些浏览器）中也以 32 位整型变量的形式进行存储，直到它被用于执行某些 32 位整型不支持的操作，这是为了便于进行位操作。

JavaScript 支持标准的算术运算符 (en-US)，包括加法、减法、取模（或取余）等等。还有一个之前没有提及的内置对象 `Math`（数学对象），用以处理更多的高级数学函数和常数：

```js
Math.sin(3.5);
var circumference = 2 \* Math.PI \* r;
```

你可以使用内置函数 `parseInt()` 将字符串转换为整型。该函数的第二个可选参数表示字符串所表示数字的基（进制）：

```js
parseInt("123", 10); // 123
parseInt("010", 10); // 10
```

一些老版本的浏览器会将首字符为“0”的字符串当做八进制数字，2013 年以前的 JavaScript 实现会返回一个意外的结果：

```js
parseInt("010"); // 8
parseInt("0x10"); // 16
```

这是因为字符串以数字 0 开头，`parseInt()`函数会把这样的字符串视作八进制数字；同理，0x 开头的字符串则视为十六进制数字。

如果想把一个二进制数字字符串转换成整数值，只要把第二个参数设置为 2 就可以了：

```js
parseInt("11", 2); // 3
```

JavaScript 还有一个类似的内置函数 `parseFloat()`，用以解析浮点数字符串，与`parseInt()`不同的地方是，`parseFloat()` 只应用于解析十进制数字。

一元运算符 + 也可以把数字字符串转换成数值：

```js
+"42"; // 42
+"010"; // 10
+"0x10"; // 16
```

如果给定的字符串不存在数值形式，函数会返回一个特殊的值 `NaN`（Not a Number 的缩写）：

```js
parseInt("hello", 10); // NaN
```

要小心 NaN：如果把 `NaN` 作为参数进行任何数学运算，结果也会是 `NaN`：

```js
NaN + 5; //NaN
```

可以使用内置函数 `isNaN()` 来判断一个变量是否为 `NaN`：

```js
isNaN(NaN); // true
```

JavaScript 还有两个特殊值：`Infinity`（正无穷）和 `-Infinity`（负无穷）：

```js
1 / 0; // Infinity
-1 / 0; // -Infinity
```

可以使用内置函数 `isFinite()` 来判断一个变量是否是一个有穷数，如果类型为`Infinity`, `-Infinity` 或 `NaN 则返回 false`：

```js
isFinite(1 / 0); // false
isFinite(Infinity); // false
isFinite(-Infinity); // false
isFinite(NaN); // false

isFinite(0); // true
isFinite(2e64); // true

isFinite("0"); // true
// 如果是纯数值类型的检测，则返回 false：
Number.isFinite("0"); // false
```

**备注：** `parseInt()` 和 `parseFloat()` 函数会尝试逐个解析字符串中的字符，直到遇上一个无法被解析成数字的字符，然后返回该字符前所有数字字符组成的数字。但是运算符 "+"对字符串的转换方式与之不同，只要字符串含有无法被解析成数字的字符，该字符串就将被转换成 `NaN`。可分别使用这两种方法解析“10.2abc”这一字符串，并比较得到的结果，来理解这两种方法的区别。

字符串
---

JavaScript 中的字符串是一串Unicode 字符序列。这对于那些需要和多语种网页打交道的开发者来说是个好消息。更准确地说，它们是一串 UTF-16 编码单元的序列，每一个编码单元由一个 16 位二进制数表示。每一个 Unicode 字符由一个或两个编码单元来表示。

如果想表示一个单独的字符，只需使用长度为 1 的字符串。

通过访问字符串的 `length`（编码单元的个数）属性，可以得到它的长度。

```js
"hello".length; // 5
```

这是我们第一次碰到 JavaScript 对象。我们有没有提过你可以像 object 一样使用字符串？是的，字符串也有 methods（方法）能让你操作字符串和获取字符串的信息。

```js
"hello".charAt(0); // "h"
"hello, world".replace("world", "mars"); // "hello, mars"
"hello".toUpperCase(); // "HELLO"
```

其他类型
----

与其他类型不同，JavaScript 中的 `null` 表示一个空值（non-value），必须使用 null 关键字才能访问，`undefined` 是一个“undefined（未定义）”类型的对象，表示一个未初始化的值，也就是还没有被分配的值。我们之后再具体讨论变量，但有一点可以先简单说明一下，JavaScript 允许声明变量但不对其赋值，一个未被赋值的变量就是 `undefined` 类型。还有一点需要说明的是，`undefined` 实际上是一个不允许修改的常量。

JavaScript 包含布尔类型，这个类型的变量有两个可能的值，分别是 `true` 和 `false`（两者都是关键字）。根据具体需要，JavaScript 按照如下规则将变量转换成布尔类型：

1. `false`、`0`、空字符串（`""`）、`NaN`、`null` 和 `undefined` 被转换为 `false`
2. 所有其他值被转换为 `true`

也可以使用 `Boolean()` 函数进行显式转换：

```js
Boolean(""); // false
Boolean(234); // true
```

不过一般没必要这么做，因为 JavaScript 会在需要一个布尔变量时隐式完成这个转换操作（比如在 `if` 条件语句中）。所以，有时我们可以把转换成布尔值后的变量分别称为 真值（true values）——即值为 true 和 假值（false values）——即值为 false；也可以分别称为“真的”（truthy）和“假的”（falsy）。

JavaScript 支持包括 `&&`（逻辑与）、`||` （逻辑或）和`!`（逻辑非）在内的一些逻辑运算符。下面会有所提到。

变量
--

在 JavaScript 中声明一个新变量的方法是使用关键字 `let` 、`const` 和 `var`：

**`let`** 语句声明一个块级作用域的本地变量，并且可选的将其初始化为一个值。

```js
let a;
let name = "Simon";
```

下面是使用 **`let`** 声明变量作用域的例子：

```js
// myLetVariable 在这里 \*不能\* 被引用

for (let myLetVariable = 0; myLetVariable < 5; myLetVariable++) {
  // myLetVariable 只能在这里引用
}

// myLetVariable 在这里 \*不能\* 被引用
```

**`const`** 允许声明一个不可变的常量。这个常量在定义域内总是可见的。

```js
const Pi = 3.14; // 设置 Pi 的值
Pi = 1; // 将会抛出一个错误因为你改变了一个常量的值。
```

**`var`** 是最常见的声明变量的关键字。它没有其他两个关键字的种种限制。这是因为它是传统上在 JavaScript 声明变量的唯一方法。使用 **`var`** 声明的变量在它所声明的整个函数都是可见的。

```js
var a;
var name = "simon";
```

一个使用 **`var`** 声明变量的语句块的例子：

```js
// myVarVariable 在这里 \*能\* 被引用

for (var myVarVariable = 0; myVarVariable < 5; myVarVariable++) {
  // myVarVariable 整个函数中都能被引用
}

// myVarVariable 在这里 \*能\* 被引用
```

如果声明了一个变量却没有对其赋值，那么这个变量的类型就是 `undefined`。

JavaScript 与其他语言的（如 Java）的重要区别是在 JavaScript 中语句块（blocks）是没有作用域的，只有函数有作用域。因此如果在一个复合语句中（如 if 控制结构中）使用 var 声明一个变量，那么它的作用域是整个函数（复合语句在函数中）。但是从 ECMAScript Edition 6 开始将有所不同的， `let` 和 `const` 关键字允许你创建块作用域的变量。

运算符
---

JavaScript 的算术操作符包括 `+`、`-`、`*`、`/` 和 `%`——求余（与模运算相同）。赋值使用 `=` 运算符，此外还有一些复合运算符，如 `+=` 和 `-=`，它们等价于 `x = x operator y`。

```js
x += 5; // 等价于 x = x + 5;
```

可以使用 `++` 和 `--` 分别实现变量的自增和自减。两者都可以作为前缀或后缀操作符使用。

`+` 操作符 (en-US) 还可以用来连接字符串：

```js
"hello" + " world"; // hello world
```

如果你用一个字符串加上一个数字（或其他值），那么操作数都会被首先转换为字符串。如下所示：

```js
"3" + 4 + 5; // 345
3 + 4 + "5"; // 75
```

这里不难看出一个实用的技巧——通过与空字符串相加，可以将某个变量快速转换成字符串类型。

JavaScript 中的比较操作 (en-US)使用 `<`、`>`、`<=` 和 `>=`，这些运算符对于数字和字符串都通用。相等的比较稍微复杂一些。由两个“`=`（等号）”组成的相等运算符有类型自适应的功能，具体例子如下：

```js
123 == "123"; // true
1 == true; // true
```

如果在比较前不需要自动类型转换，应该使用由三个“`=`（等号）”组成的相等运算符：

```js
1 === true; //false
123 === "123"; // false
```

JavaScript 还支持 `!=` 和 `!==` 两种不等运算符，具体区别与两种相等运算符的区别类似。

JavaScript 还提供了 位操作符 (en-US)。

控制结构
----

JavaScript 的控制结构与其他类 C 语言类似。可以使用 `if` 和 `else` 来定义条件语句，还可以连起来使用：

```js
var name = "kittens";
if (name == "puppies") {
  name += "!";
} else if (name == "kittens") {
  name += "!!";
} else {
  name = "!" + name;
}
name == "kittens!!"; // true
```

JavaScript 支持 `while` 循环和 `do-while` 循环。前者适合常见的基本循环操作，如果需要循环体至少被执行一次则可以使用 `do-while`：

```js
while (true) {
  // 一个无限循环！
}

var input;
do {
  input = get\_input();
} while (inputIsNotValid(input));
```

JavaScript 的 `for` 循环与 C 和 Java 中的相同：使用时可以在一行代码中提供控制信息。

```js
for (var i = 0; i < 5; i++) {
  // 将会执行五次
}
```

JavaScript 也还包括其他两种重要的 for 循环： `for`...`of`

```js
for (let value of array) {
  // do something with value
}
```

和 `for`...`in` ：

```js
for (let property in object) {
  // do something with object property
}
```

`&&` 和 `||` 运算符使用短路逻辑（short-circuit logic），是否会执行第二个语句（操作数）取决于第一个操作数的结果。在需要访问某个对象的属性时，使用这个特性可以事先检测该对象是否为空：

```js
var name = o && o.getName();
```

或用于缓存值（当错误值无效时）：

```js
var name = cachedName || (cachedName = getName());
```

类似地，JavaScript 也有一个用于条件表达式的三元操作符：

```js
var allowed = age > 18 ? "yes" : "no";
```

在需要多重分支时可以使用基于一个数字或字符串的 `switch` 语句：

```js
switch (action) {
  case "draw":
    drawIt();
    break;
  case "eat":
    eatIt();
    break;
  default:
    doNothing();
}
```

如果你不使用 `break` 语句，JavaScript 解释器将会执行之后 `case` 中的代码。除非是为了调试，一般你并不需要这个特性，所以大多数时候不要忘了加上 `break`。

```js
switch (a) {
  case 1: // 继续向下
  case 2:
    eatIt();
    break;
  default:
    doNothing();
}
```

`default` 语句是可选的。`switch` 和 `case` 都可以使用需要运算才能得到结果的表达式；在 `switch` 的表达式和 `case` 的表达式是使用 `===` 严格相等运算符进行比较的：

```js
switch (1 + 3) {
  case 2 + 2:
    yay();
    break;
  default:
    neverhappens();
}
```

对象
--

JavaScript 中的对象，Object，可以简单理解成“名称 - 值”对（而不是键值对：现在，ES 2015 的映射表（Map），比对象更接近键值对），不难联想 JavaScript 中的对象与下面这些概念类似：

* Python 中的字典（Dictionary）
* Perl 和 Ruby 中的散列/哈希（Hash）
* C/C++ 中的散列表（Hash table）
* Java 中的散列映射表（HashMap）
* PHP 中的关联数组（Associative array）

这样的数据结构设计合理，能应付各类复杂需求，所以被各类编程语言广泛采用。正因为 JavaScript 中的一切（除了核心类型，core object）都是对象，所以 JavaScript 程序必然与大量的散列表查找操作有着千丝万缕的联系，而散列表擅长的正是高速查找。

“名称”部分是一个 JavaScript 字符串，“值”部分可以是任何 JavaScript 的数据类型——包括对象。这使用户可以根据具体需求，创建出相当复杂的数据结构。

有两种简单方法可以创建一个空对象：

```js
var obj = new Object();
```

和：

```js
var obj = {};
```

这两种方法在语义上是相同的。第二种更方便的方法叫作“对象字面量（object literal）”法。这种也是 JSON 格式的核心语法，一般我们优先选择第二种方法。

“对象字面量”也可以用来在对象实例中定义一个对象：

```js
var obj = {
  name: "Carrot",
  \_for: "Max", //'for' 是保留字之一，使用'\_for'代替
  details: {
    color: "orange",
    size: 12,
  },
};
```

对象的属性可以通过链式（chain）表示方法进行访问：

```js
obj.details.color; // orange
obj["details"]["size"]; // 12
```

下面的例子创建了一个对象原型，**`Person`**，和这个原型的实例，**`You`**。

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

// 定义一个对象
var You = new Person("You", 24);
// 我们创建了一个新的 Person，名称是 "You"
// ("You" 是第一个参数，24 是第二个参数..)
```

完成创建后，对象属性可以通过如下两种方式进行赋值和访问：

```js
// 点表示法 (dot notation)
obj.name = "Simon";
var name = obj.name;
```

和：

```js
// 括号表示法 (bracket notation)
obj["name"] = "Simon";
var name = obj["name"];
// can use a variable to define a key
var user = prompt("what is your key?");
obj[user] = prompt("what is its value?");
```

这两种方法在语义上也是相同的。第二种方法的优点在于属性的名称被看作一个字符串，这就意味着它可以在运行时被计算，缺点在于这样的代码有可能无法在后期被解释器优化。它也可以被用来访问某些以预留关键字作为名称的属性的值：

```js
obj.for = "Simon"; // 语法错误，因为 for 是一个预留关键字
obj["for"] = "Simon"; // 工作正常
```

**备注：** 从 ECMAScript 5 开始，预留关键字可以作为对象的属性名（reserved words may be used as object property names "in the buff"）。这意味着当定义对象字面量时不需要用双引号了。参见 ES5 Spec.

关于对象和原型的详情参见： Object.prototype (en-US). 解释对象原型和对象原型链可以参见：继承与原型链。

**备注：** 从 ECMAScript 2015 开始，对象键可以在创建时使用括号表示法由变量定义。{[phoneType]: 12345} 可以用来替换 `var userPhone = {}; userPhone[phoneType] = 12345` .

数组
--

JavaScript 中的数组是一种特殊的对象。它的工作原理与普通对象类似（以数字为属性名，但只能通过`[]` 来访问），但数组还有一个特殊的属性——`length`（长度）属性。这个属性的值通常比数组最大索引大 1。

创建数组的传统方法是：

```js
var a = new Array();
a[0] = "dog";
a[1] = "cat";
a[2] = "hen";
a.length; // 3
```

使用数组字面量（array literal）法更加方便：

```js
var a = ["dog", "cat", "hen"];
a.length; // 3
```

注意，`Array.length` 并不总是等于数组中元素的个数，如下所示：

```js
var a = ["dog", "cat", "hen"];
a[100] = "fox";
a.length; // 101
```

记住：数组的长度是比数组最大索引值多一的数。

如果试图访问一个不存在的数组索引，会得到 `undefined`：

```js
typeof a[90]; // undefined
```

可以通过如下方式遍历一个数组：

```js
for (var i = 0; i < a.length; i++) {
  // Do something with a[i]
}
```

ES2015 引入了更加简洁的 `for`...`of` 循环，可以用它来遍历可迭代对象，例如数组：

```js
for (const currentValue of a) {
  // Do something with currentValue
}
```

遍历数组的另一种方法是使用 `for...in` 循环，然而这并不是遍历数组元素而是数组的索引。注意，如果哪个家伙直接向 `Array.prototype` 添加了新的属性，使用这样的循环这些属性也同样会被遍历。所以并不推荐使用这种方法遍历数组：

```js
for (var i in a) {
  // 操作 a[i]
}
```

ECMAScript 5 增加了另一个遍历数组的方法，`forEach()`：

```js
["dog", "cat", "hen"].forEach(function (currentValue, index, array) {
  // 操作 currentValue 或者 array[index]
});
```

如果想在数组后追加元素，只需要：

```js
a.push(item);
```

除了 `forEach()` 和 `push()`，Array（数组）类还自带了许多方法。建议查看 Array 方法的完整文档。

| 方法名称 | 描述 |
| --- | --- |
| `a.toString()` | 返回一个包含数组中所有元素的字符串，每个元素通过逗号分隔。 |
| `a.toLocaleString()` | 根据宿主环境的区域设置，返回一个包含数组中所有元素的字符串，每个元素通过逗号分隔。 |
| `a.concat(item1[, item2[, ...[, itemN]]])` | 返回一个数组，这个数组包含原先 `a` 和 `item1、item2、……、itemN` 中的所有元素。 |
| `a.join(sep)` | 返回一个包含数组中所有元素的字符串，每个元素通过指定的 `sep` 分隔。 |
| `a.pop()` | 删除并返回数组中的最后一个元素。 |
| `a.push(item1, ..., itemN)` | 将 `item1、item2、……、itemN` 追加至数组 `a`。 |
| `a.reverse()` | 数组逆序（会更改原数组 `a`）。 |
| `a.shift()` | 删除并返回数组中第一个元素。 |
| `a.slice(start, end)` | 返回子数组，以 `a[start]` 开头，以 `a[end]` 前一个元素结尾。 |
| `a.sort([cmpfn])` | 依据可选的比较函数 `cmpfn` 进行排序，如果未指定比较函数，则按字符顺序比较（即使被比较元素是数字）。 |
| `a.splice(start, delcount[, item1[, ...[, itemN]]])` | 从 `start` 开始，删除 `delcount` 个元素，然后插入所有的 `item`。 |
| `a.unshift(item1[, item2[, ...[, itemN]]])` | 将 `item` 插入数组头部，返回数组新长度（考虑 `undefined`）。 |

函数
--

学习 JavaScript 最重要的就是要理解对象和函数两个部分。最简单的函数就像下面这个这么简单：

```js
function add(x, y) {
  var total = x + y;
  return total;
}
```

这个例子包括你需要了解的关于基本函数的所有部分。一个 JavaScript 函数可以包含 0 个或多个已命名的变量。函数体中的表达式数量也没有限制。你可以声明函数自己的局部变量。`return` 语句在返回一个值并结束函数。如果没有使用 `return` 语句，或者一个没有值的 `return` 语句，JavaScript 会返回 `undefined`。

已命名的参数更像是一个指示而没有其他作用。如果调用函数时没有提供足够的参数，缺少的参数会被 `undefined` 替代。

```js
add(); // NaN
// 不能在 undefined 对象上进行加法操作
```

你还可以传入多于函数本身需要参数个数的参数：

```js
add(2, 3, 4); // 5
// 将前两个值相加，4 被忽略了
```

这看上去有点蠢。函数实际上是访问了函数体中一个名为 `arguments` 的内部对象，这个对象就如同一个类似于数组的对象一样，包括了所有被传入的参数。让我们重写一下上面的函数，使它可以接收任意个数的参数：

```js
function add() {
  var sum = 0;
  for (var i = 0, j = arguments.length; i < j; i++) {
    sum += arguments[i];
  }
  return sum;
}

add(2, 3, 4, 5); // 14
```

这跟直接写成 `2 + 3 + 4 + 5` 也没什么区别。我们还是创建一个求平均数的函数吧：

```js
function avg() {
  var sum = 0;
  for (var i = 0, j = arguments.length; i < j; i++) {
    sum += arguments[i];
  }
  return sum / arguments.length;
}
avg(2, 3, 4, 5); // 3.5
```

这个就有用多了，但是却有些冗长。为了使代码变短一些，我们可以使用剩余参数来替换 arguments 的使用。在这方法中，我们可以传递任意数量的参数到函数中同时尽量减少我们的代码。这个**剩余参数操作符**在函数中以：**...variable** 的形式被使用。在调用函数时，它将包含所有未被捕获的参数。

```js
function avg(...args) {
  var sum = 0;
  for (let value of args) {
    sum += value;
  }
  return sum / args.length;
}

avg(2, 3, 4, 5); // 3.5
```

**备注：** 在上面这段代码中，所有被传入该函数的参数都被变量 **args** 所持有。需要注意的是，无论“剩余参数操作符”被放置到函数声明的哪里，它都会把除了自己之前的所有参数存储起来。比如函数：function avg(**firstValue**, ...args) 会把传入函数的第一个值存入 **firstValue**，其他的参数存入 **args**。虽然这是一个很有用的语言特性，却也会带来新的问题。`avg()` 函数只接受逗号分开的参数列表——但是如果你想要获取一个数组的平均值怎么办？一种方法是将函数按照如下方式重写：

```js
function avgArray(arr) {
  var sum = 0;
  for (var i = 0, j = arr.length; i < j; i++) {
    sum += arr[i];
  }
  return sum / arr.length;
}
avgArray([2, 3, 4, 5]); // 3.5
```

但如果能重用我们已经创建的那个函数不是更好吗？幸运的是 JavaScript 允许你通过任意函数对象的 `apply()` 方法来传递给它一个数组作为参数列表。

```js
avg.apply(null, [2, 3, 4, 5]); // 3.5
```

传给 `apply()` 的第二个参数是一个数组，它将被当作 `avg()` 的参数列表使用，至于第一个参数 `null`，我们将在后面讨论。这也正说明了一个事实——函数也是对象。

**备注：** 通过使用展开语法，你也可以获得同样的效果。比如说：`avg(...numbers)`

JavaScript 允许你创建匿名函数：

```js
var avg = function () {
  var sum = 0;
  for (var i = 0, j = arguments.length; i < j; i++) {
    sum += arguments[i];
  }
  return sum / arguments.length;
};
```

这个函数在语义上与 `function avg()` 相同。你可以在代码中的任何地方定义这个函数，就像写普通的表达式一样。基于这个特性，有人发明出一些有趣的技巧。与 C 中的块级作用域类似，下面这个例子隐藏了局部变量：

```js
var a = 1;
var b = 2;
(function () {
  var b = 3;
  a += b;
})();

a; // 4
b; // 2
```

JavaScript 允许以递归方式调用函数。递归在处理树形结构（比如浏览器 DOM）时非常有用。

```js
function countChars(elm) {
  if (elm.nodeType == 3) {
    // 文本节点
    return elm.nodeValue.length;
  }
  var count = 0;
  for (var i = 0, child; (child = elm.childNodes[i]); i++) {
    count += countChars(child);
  }
  return count;
}
```

这里需要说明一个潜在问题——既然匿名函数没有名字，那该怎么递归调用它呢？在这一点上，JavaScript 允许你命名这个函数表达式。你可以命名立即调用的函数表达式（IIFE——Immediately Invoked Function Expression），如下所示：

```js
var charsInBody = (function counter(elm) {
  if (elm.nodeType == 3) {
    // 文本节点
    return elm.nodeValue.length;
  }
  var count = 0;
  for (var i = 0, child; (child = elm.childNodes[i]); i++) {
    count += counter(child);
  }
  return count;
})(document.body);
```

如上所提供的函数表达式的名称的作用域仅仅是该函数自身。这允许引擎去做更多的优化，并且这种实现更可读、友好。该名称也显示在调试器和一些堆栈跟踪中，节省了调试时的时间。

需要注意的是 JavaScript 函数是它们本身的对象——就和 JavaScript 其他一切一样——你可以给它们添加属性或者更改它们的属性，这与前面的对象部分一样。

自定义对象
-----

**备注：** 关于 JavaScript 中面向对象编程更详细的信息，请参考 JavaScript 面向对象简介 (en-US)。

在经典的面向对象语言中，对象是指数据和在这些数据上进行的操作的集合。与 C++ 和 Java 不同，JavaScript 是一种基于原型的编程语言，并没有 class 语句，而是把函数用作类。那么让我们来定义一个人名对象，这个对象包括人的姓和名两个域（field）。名字的表示有两种方法：“名 姓（First Last）”或“姓，名（Last, First）”。使用我们前面讨论过的函数和对象概念，可以像这样完成定义：

```js
function makePerson(first, last) {
  return {
    first: first,
    last: last,
  };
}
function personFullName(person) {
  return person.first + " " + person.last;
}
function personFullNameReversed(person) {
  return person.last + ", " + person.first;
}

var s = makePerson("Simon", "Willison");
personFullName(s); // "Simon Willison"
personFullNameReversed(s); // "Willison, Simon"
```

上面的写法虽然可以满足要求，但是看起来很麻烦，因为需要在全局命名空间中写很多函数。既然函数本身就是对象，如果需要使一个函数隶属于一个对象，那么不难得到：

```js
function makePerson(first, last) {
  return {
    first: first,
    last: last,
    fullName: function () {
      return this.first + " " + this.last;
    },
    fullNameReversed: function () {
      return this.last + ", " + this.first;
    },
  };
}
s = makePerson("Simon", "Willison");
s.fullName(); // "Simon Willison"
s.fullNameReversed(); // Willison, Simon
```

上面的代码里有一些我们之前没有见过的东西：关键字 `this`。当使用在函数中时，`this` 指代当前的对象，也就是调用了函数的对象。如果在一个对象上使用点或者方括号来访问属性或方法，这个对象就成了 `this`。如果并没有使用“点”运算符调用某个对象，那么 `this` 将指向全局对象（global object）。这是一个经常出错的地方。例如：

```js
s = makePerson("Simon", "Willison");
var fullName = s.fullName;
fullName(); // undefined undefined
```

当我们调用 `fullName()` 时，`this` 实际上是指向全局对象的，并没有名为 `first` 或 `last` 的全局变量，所以它们两个的返回值都会是 `undefined`。

下面使用关键字 `this` 改进已有的 `makePerson`函数：

```js
function Person(first, last) {
  this.first = first;
  this.last = last;
  this.fullName = function () {
    return this.first + " " + this.last;
  };
  this.fullNameReversed = function () {
    return this.last + ", " + this.first;
  };
}
var s = new Person("Simon", "Willison");
```

我们引入了另外一个关键字：`new`，它和 `this` 密切相关。它的作用是创建一个崭新的空对象，然后使用指向那个对象的 `this` 调用特定的函数。注意，含有 `this` 的特定函数不会返回任何值，只会修改 `this` 对象本身。`new` 关键字将生成的 `this` 对象返回给调用方，而被 `new` 调用的函数称为构造函数。习惯的做法是将这些函数的首字母大写，这样用 `new` 调用他们的时候就容易识别了。

不过，这个改进的函数还是和上一个例子一样，在单独调用`fullName()` 时，会产生相同的问题。

我们的 Person 对象现在已经相当完善了，但还有一些不太好的地方。每次我们创建一个 Person 对象的时候，我们都在其中创建了两个新的函数对象——如果这个代码可以共享不是更好吗？

```js
function personFullName() {
  return this.first + " " + this.last;
}
function personFullNameReversed() {
  return this.last + ", " + this.first;
}
function Person(first, last) {
  this.first = first;
  this.last = last;
  this.fullName = personFullName;
  this.fullNameReversed = personFullNameReversed;
}
```

这种写法的好处是，我们只需要创建一次方法函数，在构造函数中引用它们。那是否还有更好的方法呢？答案是肯定的。

```js
function Person(first, last) {
  this.first = first;
  this.last = last;
}
Person.prototype.fullName = function () {
  return this.first + " " + this.last;
};
Person.prototype.fullNameReversed = function () {
  return this.last + ", " + this.first;
};
```

`Person.prototype` 是一个可以被 `Person` 的所有实例共享的对象。它是一个名叫原型链（prototype chain）的查询链的一部分：当你试图访问 `Person` 某个实例（例如上个例子中的 s）一个没有定义的属性时，解释器会首先检查这个 `Person.prototype` 来判断是否存在这样一个属性。所以，任何分配给 `Person.prototype` 的东西对通过 `this` 对象构造的实例都是可用的。

这个特性功能十分强大，JavaScript 允许你在程序中的任何时候修改原型（prototype）中的一些东西，也就是说你可以在运行时 (runtime) 给已存在的对象添加额外的方法：

```js
s = new Person("Simon", "Willison");
s.firstNameCaps(); // TypeError on line 1: s.firstNameCaps is not a function

Person.prototype.firstNameCaps = function () {
  return this.first.toUpperCase();
};
s.firstNameCaps(); // SIMON
```

有趣的是，你还可以给 JavaScript 的内置函数原型（prototype）添加东西。让我们给 `String` 添加一个方法用来返回逆序的字符串：

```js
var s = "Simon";
s.reversed(); // TypeError on line 1: s.reversed is not a function

String.prototype.reversed = function () {
  var r = "";
  for (var i = this.length - 1; i >= 0; i--) {
    r += this[i];
  }
  return r;
};
s.reversed(); // nomiS
```

定义新方法也可以在字符串字面量上用（string literal）。

```js
"This can now be reversed".reversed(); // desrever eb won nac sihT
```

正如我前面提到的，原型组成链的一部分。那条链的根节点是 `Object.prototype`，它包括 `toString()` 方法——将对象转换成字符串时调用的方法。这对于调试我们的 `Person` 对象很有用：

```js
var s = new Person("Simon", "Willison");
s; // [object Object]

Person.prototype.toString = function () {
  return "<Person: " + this.fullName() + ">";
};
s.toString(); // <Person: Simon Willison>
```

你是否还记得之前我们说的 `avg.apply()` 中的第一个参数 `null`？现在我们可以回头看看这个东西了。`apply()` 的第一个参数应该是一个被当作 `this` 来看待的对象。下面是一个 `new` 方法的简单实现：

```js
function trivialNew(constructor, ...args) {
  var o = {}; // 创建一个对象
  constructor.apply(o, args);
  return o;
}
```

这并不是 `new` 的完整实现，因为它没有创建原型（prototype）链。想举例说明 new 的实现有些困难，因为你不会经常用到这个，但是适当了解一下还是很有用的。在这一小段代码里，`...args`（包括省略号）叫作剩余参数（rest arguments）。如名所示，这个东西包含了剩下的参数。

因此，调用

```js
var bill = trivialNew(Person, "William", "Orange");
```

可认为和调用如下语句是等效的

```js
var bill = new Person("William", "Orange");
```

`apply()` 有一个姐妹函数，名叫 `call`，它也可以允许你设置 `this`，但它带有一个扩展的参数列表而不是一个数组。

```js
function lastNameCaps() {
  return this.last.toUpperCase();
}
var s = new Person("Simon", "Willison");
lastNameCaps.call(s);
// 和以下方式等价
s.lastNameCaps = lastNameCaps;
s.lastNameCaps();
```

### 内部函数

JavaScript 允许在一个函数内部定义函数，这一点我们在之前的 `makePerson()` 例子中也见过。关于 JavaScript 中的嵌套函数，一个很重要的细节是，它们可以访问父函数作用域中的变量：

```js
function parentFunc() {
  var a = 1;

  function nestedFunc() {
    var b = 4; // parentFunc 无法访问 b
    return a + b;
  }
  return nestedFunc(); // 5
}
```

如果某个函数依赖于其他的一两个函数，而这一两个函数对你其余的代码没有用处，你可以将它们嵌套在会被调用的那个函数内部，这样做可以减少全局作用域下的函数的数量，这有利于编写易于维护的代码。

这也是一个减少使用全局变量的好方法。当编写复杂代码时，程序员往往试图使用全局变量，将值共享给多个函数，但这样做会使代码很难维护。内部函数可以共享父函数的变量，所以你可以使用这个特性把一些函数捆绑在一起，这样可以有效地防止“污染”你的全局命名空间——你可以称它为“局部全局（local global）”。虽然这种方法应该谨慎使用，但它确实很有用，应该掌握。

闭包
--

闭包是 JavaScript 中最强大的抽象概念之一——但它也是最容易造成困惑的。它究竟是做什么的呢？

```js
function makeAdder(a) {
  return function (b) {
    return a + b;
  };
}
var add5 = makeAdder(5);
var add20 = makeAdder(20);
add5(6); // ?
add20(7); // ?
```

`makeAdder` 这个名字本身，便应该能说明函数是用来做什么的：它会用一个参数来创建一个新的“adder”函数，再用另一个参数来调用被创建的函数时，`makeAdder` 会将一前一后两个参数相加。

从被创建的函数的视角来看的话，这两个参数的来源问题会更显而易见：新函数自带一个参数——在新函数被创建时，便钦定、钦点了前一个参数（如上方代码中的 a、5 和 20，参考 `makeAdder` 的结构，它应当位于新函数外部）；新函数被调用时，又接收了后一个参数（如上方代码中的 b、6 和 7，位于新函数内部）。最终，新函数被调用的时候，前一个参数便会和由外层函数传入的后一个参数相加。

这里发生的事情和前面介绍过的内嵌函数十分相似：一个函数被定义在了另外一个函数的内部，内部函数可以访问外部函数的变量。唯一的不同是，外部函数已经返回了，那么常识告诉我们局部变量“应该”不再存在。但是它们却仍然存在——否则 `adder` 函数将不能工作。也就是说，这里存在 `makeAdder` 的局部变量的两个不同的“副本”——一个是 `a` 等于 5，另一个是 `a` 等于 20。那些函数的运行结果就如下所示：

```js
add5(6); // 返回 11
add20(7); // 返回 27
```

下面来说说，到底发生了什么了不得的事情。每当 JavaScript 执行一个函数时，都会创建一个作用域对象（scope object），用来保存在这个函数中创建的局部变量。它使用一切被传入函数的变量进行初始化（初始化后，它包含一切被传入函数的变量）。这与那些保存的所有全局变量和函数的全局对象（global object）相类似，但仍有一些很重要的区别：第一，每次函数被执行的时候，就会创建一个新的，特定的作用域对象；第二，与全局对象（如浏览器的 `window` 对象）不同的是，你不能从 JavaScript 代码中直接访问作用域对象，也没有 可以遍历当前作用域对象中的属性 的方法。

所以，当调用 `makeAdder` 时，解释器创建了一个作用域对象，它带有一个属性：`a`，这个属性被当作参数传入 `makeAdder` 函数。然后 `makeAdder` 返回一个新创建的函数（暂记为 `adder`）。通常，JavaScript 的垃圾回收器会在这时回收 `makeAdder` 创建的作用域对象（暂记为 `b`），但是，`makeAdder` 的返回值，新函数 `adder`，拥有一个指向作用域对象 `b` 的引用。最终，作用域对象 `b` 不会被垃圾回收器回收，直到没有任何引用指向新函数 `adder`。

作用域对象组成了一个名为作用域链（scope chain）的（调用）链。它和 JavaScript 的对象系统使用的原型（prototype）链相类似。

一个**闭包**，就是 一个函数 与其 被创建时所带有的作用域对象 的组合。闭包允许你保存状态——所以，它们可以用来代替对象。这个 StackOverflow 帖子里有一些关于闭包的详细介绍。


继承与原型链 - JavaScript

继承与原型链
======

对于使用过基于类的语言（如 Java 或 C++）的开发者来说，JavaScript 实在是有些令人困惑——JavaScript 是动态的且没有静态类型。

当谈到继承时，JavaScript 只有一种结构：对象。每个对象（object）都有一个私有属性指向另一个名为**原型**（prototype）的对象。原型对象也有一个自己的原型，层层向上直到一个对象的原型为 `null`。根据定义，`null` 没有原型，并作为这个**原型链**（prototype chain）中的最后一个环节。可以改变原型链中的任何成员，甚至可以在运行时换出原型，因此 JavaScript 中不存在静态分派的概念。

尽管这种混杂通常被认为是 JavaScript 的弱点之一，但是原型继承模型本身实际上比类式模型更强大。例如，在原型模型的基础上构建类式模型（即类的实现方式）相当简单。

尽管类现在被广泛采用并成为 JavaScript 中新的范式，但类并没有带来新的继承模式。虽然类为大部分原型的机制提供了抽象，但了解原型在底层是如何工作的仍然十分有用。

基于原型链的继承
--------

### 继承属性

JavaScript 对象是动态的属性（指**其自有属性**）“包”。JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时，它不仅仅在该对象上搜寻，还会搜寻该对象的原型，以及原型的原型，依次层层向上搜索，直到找到一个名字匹配的属性或到达原型链的末尾。

**备注：** 遵循 ECMAScript 标准，符号 `someObject.[[Prototype]]` 用于标识 `someObject` 的原型。内部插槽 `[[Prototype]]` 可以通过 `Object.getPrototypeOf()` 和 `Object.setPrototypeOf()` 函数来访问。这个等同于 JavaScript 的非标准但被许多 JavaScript 引擎实现的属性 `__proto__` 访问器。为在保持简洁的同时避免混淆，在我们的符号中会避免使用 `obj.__proto__`，而是使用 `obj.[[Prototype]]` 作为代替。其对应于 `Object.getPrototypeOf(obj)`。

它不应与函数的 `func.prototype` 属性混淆，后者指定在给定函数被用作构造函数时分配给所有对象*实例*的 `[[Prototype]]`。我们将在后面的小节中讨论构造函数的原型属性。

有几种可以指定对象的 `[[Prototype]]` 的方法，这些方法将在后面的小节中列出。现在，我们将使用 `__proto__` 语法进行说明。值得注意的是，`{ __proto__: ... }` 语法与 `obj.__proto__` 访问器不同：前者是标准且未被弃用的。

在像 `{ a: 1, b: 2, __proto__: c }` 这样的对象字面量中，`c` 值（必须为 `null` 或另一个对象）将变成字面量所表示的对象的 `[[Prototype]]`，而其他键（如 `a` 和 `b`）将变成对象的*自有属性*。这种语法读起来非常自然，因为 `[[Prototype]]` 只是对象的“内部属性”。

下面演示当尝试访问属性时会发生什么：

```js
const o = {
  a: 1,
  b: 2,
  // \_\_proto\_\_ 设置了 [[Prototype]]。它在这里被指定为另一个对象字面量。
  \_\_proto\_\_: {
    b: 3,
    c: 4,
  },
};

// o.[[Prototype]] 具有属性 b 和 c。
// o.[[Prototype]].[[Prototype]] 是 Object.prototype（我们会在下文解释其含义）。
// 最后，o.[[Prototype]].[[Prototype]].[[Prototype]] 是 null。
// 这是原型链的末尾，值为 null，
// 根据定义，其没有 [[Prototype]]。
// 因此，完整的原型链看起来像这样：
// { a: 1, b: 2 } ---> { b: 3, c: 4 } ---> Object.prototype ---> null

console.log(o.a); // 1
// o 上有自有属性“a”吗？有，且其值为 1。

console.log(o.b); // 2
// o 上有自有属性“b”吗？有，且其值为 2。
// 原型也有“b”属性，但其没有被访问。
// 这被称为属性遮蔽（Property Shadowing）

console.log(o.c); // 4
// o 上有自有属性“c”吗？没有，检查其原型。
// o.[[Prototype]] 上有自有属性“c”吗？有，其值为 4。

console.log(o.d); // undefined
// o 上有自有属性“d”吗？没有，检查其原型。
// o.[[Prototype]] 上有自有属性“d”吗？没有，检查其原型。
// o.[[Prototype]].[[Prototype]] 是 Object.prototype 且
// 其默认没有“d”属性，检查其原型。
// o.[[Prototype]].[[Prototype]].[[Prototype]] 为 null，停止搜索，
// 未找到该属性，返回 undefined。
```

给对象设置属性会创建自有属性。获取和设置行为规则的唯一例外是当它被 getter 或 setter 拦截时。

同理，你可以创建更长的原型链，并在原型链上查找一个属性。

```js
const o = {
  a: 1,
  b: 2,
  // \_\_proto\_\_ 设置了 [[Prototype]]。它在这里被指定为另一个对象字面量。
  \_\_proto\_\_: {
    b: 3,
    c: 4,
    \_\_proto\_\_: {
      d: 5,
    },
  },
};

// { a: 1, b: 2 } ---> { b: 3, c: 4 } ---> { d: 5 } ---> Object.prototype ---> null

console.log(o.d); // 5
```

### 继承“方法”

JavaScript 并没有其他基于类的语言所定义的“方法”。在 JavaScript 中，任何函数都被可以添加到对象上作为其属性。函数的继承与其他属性的继承没有差别，包括上面的“属性遮蔽”（这种情况相当于其他语言的*方法重写*）。

当继承的函数被调用时，`this` 值指向的是当前继承的对象，而不是拥有该函数属性的原型对象。

```js
const parent = {
  value: 2,
  method() {
    return this.value + 1;
  },
};

console.log(parent.method()); // 3
// 当调用 parent.method 时，“this”指向了 parent

// child 是一个继承了 parent 的对象
const child = {
  \_\_proto\_\_: parent,
};
console.log(child.method()); // 3
// 调用 child.method 时，“this”指向了 child。
// 又因为 child 继承的是 parent 的方法，
// 首先在 child 上寻找“value”属性。但由于 child 本身
// 没有名为“value”的自有属性，该属性会在
// [[Prototype]] 上被找到，即 parent.value。

child.value = 4; // 在 child，将“value”属性赋值为 4。
// 这会遮蔽 parent 上的“value”属性。
// child 对象现在看起来是这样的：
// { value: 4, \_\_proto\_\_: { value: 2, method: [Function] } }
console.log(child.method()); // 5
// 因为 child 现在拥有“value”属性，“this.value”现在表示
// child.value
```

构造函数
----

原型的强大之处在于，如果一组属性应该出现在每一个实例上，那我们就可以重用它们——尤其是对于方法。假设我们要创建多个盒子，其中每一个盒子都是一个对象，包含一个可以通过 `getValue` 函数访问的值。一个简单的实现可能是：

```js
const boxes = [
  { value: 1, getValue() { return this.value; } },
  { value: 2, getValue() { return this.value; } },
  { value: 3, getValue() { return this.value; } },
];
```

这是不够好的，因为每一个实例都有自己的，做相同事情的函数属性，这是冗余且不必要的。相反，我们可以将 `getValue` 移动到所有盒子的 `[[Prototype]]` 上：

```js
const boxPrototype = {
  getValue() {
    return this.value;
  },
};

const boxes = [
  { value: 1, \_\_proto\_\_: boxPrototype },
  { value: 2, \_\_proto\_\_: boxPrototype },
  { value: 3, \_\_proto\_\_: boxPrototype },
];
```

这样，所有盒子的 `getValue` 方法都会引用相同的函数，降低了内存使用率。但是，手动绑定每个对象创建的 `__proto__` 仍旧非常不方便。这时，我们就可以使用*构造函数*，它会自动为每个构造的对象设置 `[[Prototype]]`。构造函数是使用 `new` 调用的函数。

```js
// 一个构造函数
function Box(value) {
  this.value = value;
}

// 使用 Box() 构造函数创建的所有盒子都将具有的属性
Box.prototype.getValue = function () {
  return this.value;
};

const boxes = [new Box(1), new Box(2), new Box(3)];
```

我们说 `new Box(1)` 是通过 `Box` 构造函数创建的一个*实例*。`Box.prototype` 与我们之前创建的 `boxPrototype` 并无太大区别——它只是一个普通的对象。通过构造函数创建的每一个实例都会自动将构造函数的 `prototype` 属性作为其 `[[Prototype]]`。即，`Object.getPrototypeOf(new Box()) === Box.prototype`。`Constructor.prototype` 默认具有一个自有属性：`constructor`，它引用了构造函数本身。即，`Box.prototype.constructor === Box`。这允许我们在任何实例中访问原始构造函数。

**备注：** 如果构造函数返回非原始值，则该值将成为 `new` 表达式的结果。在这种情况下，`[[Prototype]]` 可能无法正确绑定——但在实践中应该很少发生。

上面的构造函数可以重写为类：

```js
class Box {
  constructor(value) {
    this.value = value;
  }

  // 在 Box.prototype 上创建方法
  getValue() {
    return this.value;
  }
}
```

类是构造函数的语法糖，这意味着你仍然可以修改 `Box.prototype` 来改变所有实例的行为。然而，由于类被设计为对底层原型机制的抽象，我们将在本教程中使用更轻量级的构造函数语法，以充分展示原型的工作原理。

因为 `Box.prototype` 引用了（作为所有实例的 `[[Prototype]]` 的）相同的对象，所以我们可以通过改变 `Box.prototype` 来改变所有实例的行为。

```js
function Box(value) {
  this.value = value;
}
Box.prototype.getValue = function () {
  return this.value;
};
const box = new Box(1);

// 在创建实例后修改 Box.prototype
Box.prototype.getValue = function () {
  return this.value + 1;
};
box.getValue(); // 2
```

有个推论是：*重新赋值* `Constructor.prototype`（`Constructor.prototype = ...`）是一个不好的主意，原因有两点：

* 在重新赋值之前创建的实例的 `[[Prototype]]` 现在引用的是与重新赋值之后创建的实例的 `[[Prototype]]` 不同的对象——改变一个的 `[[Prototype]]` 不再改变另一个的 `[[Prototype]]`。
* 除非你手动重新设置 `constructor` 属性，否则无法再通过 `instance.constructor` 追踪到构造函数，这可能会破坏用户期望的行为。一些内置操作也会读取 `constructor` 属性，如果没有设置，它们可能无法按预期工作。

`Constructor.prototype` 仅在构造实例时有用。它与 `Constructor.[[Prototype]]` 无关，后者是构造函数的*自有*原型，即 `Function.prototype`。也就是说，`Object.getPrototypeOf(Constructor) === Function.prototype`。

### 字面量的隐式构造函数

JavaScript 中的一些字面量语法会创建隐式设置 `[[Prototype]]` 的实例。例如：

```js
// 对象字面量（没有 `\_\_proto\_\_` 键）自动将
// `Object.prototype` 作为它们的 `[[Prototype]]`
const object = { a: 1 };
Object.getPrototypeOf(object) === Object.prototype; // true

// 数组字面量自动将 `Array.prototype` 作为它们的 `[[Prototype]]`
const array = [1, 2, 3];
Object.getPrototypeOf(array) === Array.prototype; // true

// 正则表达式字面量自动将 `RegExp.prototype` 作为它们的 `[[Prototype]]`
const regexp = /abc/;
Object.getPrototypeOf(regexp) === RegExp.prototype; // true
```

我们可以将它们“解糖（de-sugar）”为构造函数形式。

```js
const array = new Array(1, 2, 3);
const regexp = new RegExp("abc");
```

例如，像 `map()` 这样的“数组方法”只是在 `Array.prototype` 上定义的方法，而它们又自动在所有数组实例上可用，就是因为这个原因。

**警告：** 有一个常见的错误实践（misfeature）：扩展 `Object.prototype` 或其它内置原型。这种不良特性例子是，定义 `Array.prototype.myMethod = function () {...}`，然后在所有数组实例上使用 `myMethod`。

这种错误实践被称为*猴子修补*（monkey patching）。使用猴子修补存在向前兼容的风险，因为如果语言在未来添加了此方法但具有不同的签名，你的代码将会出错。它已经导致了类似于 SmooshGate 这样的事件，并且由于 JavaScript 致力于“不破坏 web”，因此这可能会对语言的发展造成极大的麻烦。

扩展内置原型的**唯一**理由是向后移植新的 JavaScript 引擎的特性，比如 `Array.prototype.forEach`。

有趣的是，由于历史原因，一些内置构造函数的 `prototype` 属性本身就是其自身的实例。例如，`Number.prototype` 是数字 0，`Array.prototype` 是一个空数组，`RegExp.prototype` 是 `/(?:)/`。

```js
Number.prototype + 1; // 1
Array.prototype.map((x) => x + 1); // []
String.prototype + "a"; // "a"
RegExp.prototype.source; // "(?:)"
Function.prototype(); // Function.prototype 本身就是一个无操作函数
```

然而，对于用户定义的构造函数，以及 `Map` 等现代的构造函数，则并非如此。

```js
Map.prototype.get(1);
// Uncaught TypeError: get method called on incompatible Map.prototype
```

### 构建更长的继承链

`Constructor.prototype` 属性将成为构造函数实例的 `[[Prototype]]`，包括 `Constructor.prototype` 自身的 `[[Prototype]]`。默认情况下，`Constructor.prototype` 是一个*普通对象*——即 `Object.getPrototypeOf(Constructor.prototype) === Object.prototype`。唯一的例外是 `Object.prototype` 本身，其 `[[Prototype]]` 是 `null`——即 `Object.getPrototypeOf(Object.prototype) === null`。因此，一个典型的构造函数将构建以下原型链：

```js
function Constructor() {}

const obj = new Constructor();
// obj ---> Constructor.prototype ---> Object.prototype ---> null
```

要构建更长的原型链，我们可用通过 `Object.setPrototypeOf()` 函数设置 `Constructor.prototype` 的 `[[Prototype]]`。

```js
function Base() {}
function Derived() {}
// 将 `Derived.prototype` 的 `[[Prototype]]`
// 设置为 `Base.prototype`
Object.setPrototypeOf(Derived.prototype, Base.prototype);

const obj = new Derived();
// obj ---> Derived.prototype ---> Base.prototype ---> Object.prototype ---> null
```

在类的术语中，这等同于使用 `extends` 语法。

```js
class Base {}
class Derived extends Base {}

const obj = new Derived();
// obj ---> Derived.prototype ---> Base.prototype ---> Object.prototype ---> null
```

你可能还会看到一些使用 `Object.create()` 来构建继承链的旧代码。然而，因为这会重新为 `prototype` 属性赋值并删除 `constructor` 属性，所以更容易出错，而且如果构造函数还没有创建任何实例，性能提升可能并不明显。

```js
function Base() {}
function Derived() {}
// 将 `Derived.prototype` 重新赋值为 `Base.prototype`，
// 以作为其 `[[Prototype]]` 的新对象
// 请不要这样做——使用 Object.setPrototypeOf 来修改它
Derived.prototype = Object.create(Base.prototype);
```

检查原型：更深入的研究
-----------

让我们来仔细看看幕后发生了什么。

如上所述，在 JavaScript 中，函数可以拥有属性。所有函数都有一个名为 `prototype` 的特殊属性。请注意，下面的代码是独立的（出于严谨，假设页面没有其他的 JavaScript 代码）。为获得最佳的学习体验，强烈建议你打开控制台，进入“console”标签页，复制并粘贴以下 JavaScript 代码，然后按回车键运行。（大多数 web 浏览器的开发者工具中都包含控制台。请参阅 Firefox 开发者工具、Chrome 开发者工具和 Edge 开发者工具，以了解详情。）

```js
function doSomething() {}
console.log(doSomething.prototype);
// 你如何声明函数并不重要；
// JavaScript 中的函数总有一个默认的
// 原型属性——有一个例外：
// 箭头函数没有默认的原型属性：
const doSomethingFromArrowFunction = () => {};
console.log(doSomethingFromArrowFunction.prototype);
```

如上所示，`doSomething()` 有一个默认的 `prototype` 属性（正如控制台所示）。运行这段代码后，控制台应该显示一个类似于下面的对象。
```

{
  constructor: ƒ doSomething(),
  [[Prototype]]: {
    constructor: ƒ Object(),
    hasOwnProperty: ƒ hasOwnProperty(),
    isPrototypeOf: ƒ isPrototypeOf(),
    propertyIsEnumerable: ƒ propertyIsEnumerable(),
    toLocaleString: ƒ toLocaleString(),
    toString: ƒ toString(),
    valueOf: ƒ valueOf()
  }
}
```

**备注：** Chrome 控制台使用 `[[Prototype]]` 来表示对象的原型，遵循规范的术语；Firefox 使用 `<prototype>`。为了保持一致性，我们将使用 `[[Prototype]]`。

我们可以像下面这样，向 `doSomething()` 的原型添加属性。

```js
function doSomething() {}
doSomething.prototype.foo = "bar";
console.log(doSomething.prototype);
```

其结果为：
```

{
  foo: "bar",
  constructor: ƒ doSomething(),
  [[Prototype]]: {
    constructor: ƒ Object(),
    hasOwnProperty: ƒ hasOwnProperty(),
    isPrototypeOf: ƒ isPrototypeOf(),
    propertyIsEnumerable: ƒ propertyIsEnumerable(),
    toLocaleString: ƒ toLocaleString(),
    toString: ƒ toString(),
    valueOf: ƒ valueOf()
  }
}
```

我们现在可以使用 `new` 运算符来创建基于该原型的 `doSomething()` 的实例。要使用 new 运算符，只需像往常一样调用函数，只是要在前面加上 `new`。使用 `new` 运算符调用函数会返回一个函数的实例对象。然后可以在该对象上添加属性。

尝试以下代码：

```js
function doSomething() {}
doSomething.prototype.foo = "bar"; // 向原型上添加一个属性
const doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value"; // 向该对象添加一个属性
console.log(doSomeInstancing);
```

这会产生类似于下面的输出：
```

{
  prop: "some value",
  [[Prototype]]: {
    foo: "bar",
    constructor: ƒ doSomething(),
    [[Prototype]]: {
      constructor: ƒ Object(),
      hasOwnProperty: ƒ hasOwnProperty(),
      isPrototypeOf: ƒ isPrototypeOf(),
      propertyIsEnumerable: ƒ propertyIsEnumerable(),
      toLocaleString: ƒ toLocaleString(),
      toString: ƒ toString(),
      valueOf: ƒ valueOf()
    }
  }
}
```

如上所示，`doSomeInstancing` 的 `[[Prototype]]` 是 `doSomething.prototype`。但是，这是做什么的呢？当你访问 `doSomeInstancing` 的属性时，运行时首先会查找 `doSomeInstancing` 是否有该属性。

如果 `doSomeInstancing` 没有该属性，那么运行时会在 `doSomeInstancing.[[Prototype]]`（也就是 `doSomething.prototype`）中查找该属性。如果 `doSomeInstancing.[[Prototype]]` 有该属性，那么就会使用 `doSomeInstancing.[[Prototype]]` 上的该属性。

否则，如果 `doSomeInstancing.[[Prototype]]` 没有该属性，那么就会在 `doSomeInstancing.[[Prototype]].[[Prototype]]` 中查找该属性。默认情况下，任何函数的 `prototype` 属性的 `[[Prototype]]` 都是 `Object.prototype`。因此会在 `doSomeInstancing.[[Prototype]].[[Prototype]]`（也就是 `doSomething.prototype.[[Prototype]]`（也就是 `Object.prototype`））上查找该属性。

如果在 `doSomeInstancing.[[Prototype]].[[Prototype]]` 中没有找到该属性，那么就会在 `doSomeInstancing.[[Prototype]].[[Prototype]].[[Prototype]]` 中查找该属性。但是，这里有一个问题：`doSomeInstancing.[[Prototype]].[[Prototype]].[[Prototype]]` 不存在，因为 `Object.prototype.[[Prototype]]` 是 `null`。然后，只有在查找完整个 `[[Prototype]]` 链之后，运行时才会断言该属性不存在，并得出该属性的值为 `undefined`。

让我们在控制台中输入更多的代码：

```js
function doSomething() {}
doSomething.prototype.foo = "bar";
const doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value";
console.log("doSomeInstancing.prop: ", doSomeInstancing.prop);
console.log("doSomeInstancing.foo: ", doSomeInstancing.foo);
console.log("doSomething.prop: ", doSomething.prop);
console.log("doSomething.foo: ", doSomething.foo);
console.log("doSomething.prototype.prop:", doSomething.prototype.prop);
console.log("doSomething.prototype.foo: ", doSomething.prototype.foo);
```

其结果如下：
```

doSomeInstancing.prop:      some value
doSomeInstancing.foo:       bar
doSomething.prop:           undefined
doSomething.foo:            undefined
doSomething.prototype.prop: undefined
doSomething.prototype.foo:  bar
```

使用不同的方法来创建对象和改变原型链
------------------

我们碰到过很多创建对象和改变其原型链的方法。我们将系统地总结不同的方法，并比较每种方法的优缺点。

### 使用语法结构创建对象

```js
const o = { a: 1 };
// 新创建的对象 o 以 Object.prototype 作为它的 [[Prototype]]
// Object.prototype 的原型为 null。
// o ---> Object.prototype ---> null

const b = ["yo", "whadup", "?"];
// 数组继承了 Array.prototype（具有 indexOf、forEach 等方法）
// 其原型链如下所示：
// b ---> Array.prototype ---> Object.prototype ---> null

function f() {
  return 2;
}
// 函数继承了 Function.prototype（具有 call、bind 等方法）
// f ---> Function.prototype ---> Object.prototype ---> null

const p = { b: 2, \_\_proto\_\_: o };
// 可以通过 \_\_proto\_\_ 字面量属性将新创建对象的
// [[Prototype]] 指向另一个对象。
// （不要与 Object.prototype.\_\_proto\_\_ 访问器混淆）
// p ---> o ---> Object.prototype ---> null
```

在对象初始化器中使用 `__proto__` 键的优缺点| 优点 |
 被所有的现代引擎所支持。将 `__proto__`
 属性指向非对象的值只会被忽略，而非抛出异常。与
 `Object.prototype.__proto__` setter 相反，对象字面量初始化器中的 `__proto__`
 是标准化，被优化的。甚至可以比 `Object.create` 更高效。在创建对象时声明额外的自有属性比 `Object.create`
 更符合习惯。
  |
| 缺点 |
 不支持 IE10 及以下的版本。对于不了解其与
 `Object.prototype.__proto__`
 访问器差异的人可能会将两者混淆。
  |

### 使用构造函数

```js
function Graph() {
  this.vertices = [];
  this.edges = [];
}

Graph.prototype.addVertex = function (v) {
  this.vertices.push(v);
};

const g = new Graph();
// g 是一个带有自有属性“vertices”和“edges”的对象。
// 在执行 new Graph() 时，g.[[Prototype]] 是 Graph.prototype 的值。
```

使用构造函数的优缺点| 优点 | 所有引擎都支持——一直到 IE 5.5。此外，其速度很快、非常标准，且极易被 JIT 优化。 |
| 缺点 | * 要使用这个方法，必须初始化该函数。在初始化过程中，构造函数可能会存储每一个对象都必须生成的唯一信息。这些唯一信息只会生成一次，可能会导致问题。
* 构造函数的初始化过程可能会将不需要的方法放到对象上。

这两者在实践中通常都不是问题。 |

### 使用 Object.create()

调用 `Object.create()` 来创建一个新对象。该对象的 `[[Prototype]]` 是该函数的第一个参数：

```js
const a = { a: 1 };
// a ---> Object.prototype ---> null

const b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (inherited)

const c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null

const d = Object.create(null);
// d ---> null（d 是一个直接以 null 为原型的对象）
console.log(d.hasOwnProperty);
// undefined，因为 d 没有继承 Object.prototype
```

`Object.create` 的优缺点| 优点 |
 被所有现代引擎所支持。允许在创建时直接设置对象的
 `[[Prototype]]`，这允许运行时进一步优化对象。还允许使用
 `Object.create(null)` 创建没有原型的对象。
  |
| 缺点 | 不支持 IE8 及以下版本。但是，由于微软已经停止了对运行 IE8 及以下版本的系统的扩展支持，这对大多数应用程序而言应该不是问题。此外，如果使用了第二个参数，慢对象的初始化可能会成为性能瓶颈，因为每个对象描述符属性都有自己单独的描述符对象。当处理上万个对象描述符时，这种延时可能会成为一个严重的问题。 |

### 使用类

```js
class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

class Square extends Polygon {
  constructor(sideLength) {
    super(sideLength, sideLength);
  }

  get area() {
    return this.height \* this.width;
  }

  set sideLength(newLength) {
    this.height = newLength;
    this.width = newLength;
  }
}

const square = new Square(2);
// square ---> Square.prototype ---> Polygon.prototype ---> Object.prototype ---> null
```

类的优缺点| 优点 | 被所有现代引擎所支持。非常高的可读性和可维护性。私有属性是原型继承中没有简单替代方案的特性。 |
| 缺点 | 类，尤其是带有私有属性的类，比传统的类的性能要差（尽管引擎实现者正在努力改进这一点）。不支持旧环境，通常需要转译器才能在生产中使用类。 |

### 使用 Object.setPrototypeOf()

虽然上面的所有方法都会在对象创建时设置原型链，但是 `Object.setPrototypeOf()` 允许修改现有对象的 `[[Prototype]]` 内部属性。

```js
const obj = { a: 1 };
const anotherObj = { b: 2 };
Object.setPrototypeOf(obj, anotherObj);
// obj ---> anotherObj ---> Object.prototype ---> null
```

`Object.setPrototypeOf` 的优缺点| 优点 |
 被所有现代引擎所支持。允许动态地修改对象的原型，甚至可以强制为使用
 `Object.create(null)` 创建的无原型对象设置原型。
  |
| 缺点 | 性能不佳。如果可以在创建对象时设置原型，则应避免此方法。许多引擎会优化原型，并在调用实例时会尝试提前猜测方法在内存中的位置；但是动态设置原型会破坏这些优化。它可能会导致某些引擎重新编译你的代码以进行反优化，以使其按照规范工作。不支持 IE8 及以下版本。 |

### 使用 \_\_proto\_\_ 访问器

所有对象都继承了 `Object.prototype.__proto__` 访问器，它可以用来设置现有对象的 `[[Prototype]]`（如果对象没有覆盖 `__proto__` 属性）。

**警告：** `Object.prototype.__proto__` 访问器是**非标准**的，且已被弃用。你几乎总是应该使用 `Object.setPrototypeOf` 来代替。

```js
const obj = {};
// 请不要使用该方法：仅作为示例。
obj.__proto__ = { barProp: "bar val" };
obj.__proto__.__proto__ = { fooProp: "foo val" };
console.log(obj.fooProp);
console.log(obj.barProp);
```

设置 `__proto__` 属性的优缺点| 优点 |
 被所有现代引擎所支持。将 `__proto__`
 设置为非对象的值只会被忽略，而非抛出异常。
  |
| 缺点 | 性能不佳且已被弃用。许多引擎会优化原型，并在调用实例时会尝试提前猜测方法在内存中的位置；但是动态设置原型会破坏这些优化，甚至可能会导致某些引擎重新编译你的代码以进行反优化，以使其按照规范工作。不支持 IE10 及以下版本。`__proto__` 访问器是规范中可选的特性，因此可能无法在所有平台上使用。你几乎总是应该使用 `Object.setPrototypeOf` 代替。 |

性能
--

原型链上较深层的属性的查找时间可能会对性能产生负面影响，这在性能至关重要的代码中可能会非常明显。此外，尝试访问不存在的属性始终会遍历整个原型链。

此外，在遍历对象的属性时，原型链中的**每个**可枚举属性都将被枚举。要检查对象是否具有在其*自身*上定义的属性，而不是在其原型链上的某个地方，则有必要使用 `hasOwnProperty` 或 `Object.hasOwn` 方法。除 `[[Prototype]]` 为 `null` 的对象外，所有对象都从 `Object.prototype` 继承 `hasOwnProperty`——除非它已经在原型链的更深处被覆盖。我们将使用上面的图示例代码来说明它，具体如下：

```js
function Graph() {
  this.vertices = [];
  this.edges = [];
}

Graph.prototype.addVertex = function (v) {
  this.vertices.push(v);
};

const g = new Graph();
// g ---> Graph.prototype ---> Object.prototype ---> null

g.hasOwnProperty("vertices"); // true
Object.hasOwn(g, "vertices"); // true

g.hasOwnProperty("nope"); // false
Object.hasOwn(g, "nope"); // false

g.hasOwnProperty("addVertex"); // false
Object.hasOwn(g, "addVertex"); // false

Object.getPrototypeOf(g).hasOwnProperty("addVertex"); // true
```

注意：仅检查属性是否为 `undefined` 是**不够的**。该属性很可能存在，但其值恰好设置为 `undefined`。

结论
--

对于 Java 或 C++ 的开发者来说，JavaScript 可能有点令人困惑，因为它是完全动态、完全是在执行期间确定的，而且根本没有静态类型。一切都是对象（实例）或函数（构造函数），甚至函数本身也是 `Function` 构造函数的实例。即使是语法结构中的“类”也只是运行时的构造函数。

JavaScript 中的所有构造函数都有一个被称为 `prototype` 的特殊属性，它与 `new` 运算符一起使用。对原型对象的引用被复制到新实例的内部属性 `[[Prototype]]` 中。例如，当你执行 `const a1 = new A()` 时，JavaScript（在内存中创建对象之后，为其定义 `this` 并执行 `A()` 之前）设置 `a1.[[Prototype]] = A.prototype`。然后，当你访问实例的属性时，JavaScript 首先检查它们是否直接存在于该对象上，如果不存在，则在 `[[Prototype]]` 中查找。会*递归*查询 `[[Prototype]]`，即 `a1.doSomething`、`Object.getPrototypeOf(a1).doSomething`、`Object.getPrototypeOf(Object.getPrototypeOf(a1)).doSomething`，以此类推，直至找到或 `Object.getPrototypeOf` 返回 `null`。这意味着在 `prototype` 上定义的所有属性实际上都由所有实例共享，并且甚至可以更改 `prototype` 的部分内容，使得更改被应用到所有现有的实例中。

在上面的示例中，如果你执行 `const a1 = new A(); const a2 = new A();`，那么 `a1.doSomething` 实际上会引用 `Object.getPrototypeOf(a1).doSomething`——这与你定义的 `A.prototype.doSomething` 相同，即 `Object.getPrototypeOf(a1).doSomething === Object.getPrototypeOf(a2).doSomething === A.prototype.doSomething`。

了解原型继承模型是使用它编写复杂代码的重要基础。此外，要注意代码中原型链的长度，在必要时可以将其分解，以避免潜在的性能问题。此外，除非是为了与新的 JavaScript 特性兼容，否则**永远不应**扩展原生原型。


JavaScript 指南 - JavaScript

JavaScript 指南
=============

JavaScript 指南向你介绍如何使用 JavaScript，并且给出了语言概述。如果你需要了解某些语言特性的详细信息，请参阅 JavaScript 参考。

介绍
--

概述：介绍

* 关于这份指南
* 关于 JavaScript
* JavaScript 与 Java
* ECMAScript
* 工具
* Hello World
语法与数据类型
-------

概述：语法与数据类型

* 基本语法与注释
* 声明
* 变量作用域
* 变量提升
* 数据结构和类型
* 字面量
控制流与错误处理
--------

概述：控制流与错误处理

* `if...else`
* `switch`
* `try`/`catch`/`throw`
* Error 对象
循环与迭代
-----

概述：循环与迭代

* `for`
* `while`
* `do...while`
* `break`/`continue`
* `for..in`
* `for..of`
函数
--

概述：函数

* 定义函数
* 调用函数
* 函数作用域
* 闭包
* arguments 对象 和 参数
* 箭头函数
表达式和运算符
-------

概述：表达式和运算符

* 赋值
* 比较
* 算术运算符
* 位运算
* 逻辑运算符
* 条件（三元）运算符
数字与日期
-----

概述：数字与日期

* `Number` 字面量
* `Number` 对象
* `Math` 对象
* `Date` 对象
文本格式化
-----

概述：文本格式化

* 字符串字面量
* 字符串对象
* 模版字面量
* 国际化
* 正则表达式
索引集合
----

概述：索引集合

* 数组
* 类型数组
带键集合
----

概述：带键集合

* `Map`
* `WeakMap`
* `Set`
* `WeakSet`
处理对象
----

概述：处理对象

* 对象和属性
* 创建对象
* 定义方法
* getter 和 setter
对象模型的细节
-------

概述：对象模型的细节

* 基于原型的面向对象编程
* 创建对象层次结构
* 继承
Promises
--------

概述：Promises

* 约定
* 链式调用
* 错误传递
* 组合
* 时序
迭代器与生成器
-------

概述：迭代器与生成器

* 迭代器
* 可迭代对象
* 生成器
元编程
---

概述：元编程

* `Proxy`
* Handlers 和 traps
* 撤销 Proxy
* `Reflect`
JavaScript 模块
-------------

概述：JavaScript 模块

* 导出模块
* 导入模块
* 默认导出
* 重命名模块
* 合并模块
* 动态加载模块
* 下一页 »


闭包 - JavaScript

闭包
==

**闭包**（closure）是一个函数以及其捆绑的周边环境状态（**lexical environment**，**词法环境**）的引用的组合。换而言之，闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中，闭包会随着函数的创建而被同时创建。

词法作用域
-----

请看下面的代码：

```js
function init() {
  var name = "Mozilla"; // name 是一个被 init 创建的局部变量
  function displayName() {
    // displayName() 是内部函数，一个闭包
    alert(name); // 使用了父函数中声明的变量
  }
  displayName();
}
init();
```

`init()` 创建了一个局部变量 `name` 和一个名为 `displayName()` 的函数。`displayName()` 是定义在 `init()` 里的内部函数，并且仅在 `init()` 函数体内可用。请注意，`displayName()` 没有自己的局部变量。然而，因为它可以访问到外部函数的变量，所以 `displayName()` 可以使用父函数 `init()` 中声明的变量 `name` 。

使用这个 JSFiddle 链接运行该代码后发现， `displayName()` 函数内的 `alert()` 语句成功显示出了变量 `name` 的值（该变量在其父函数中声明）。这个*词法作用域*的例子描述了分析器如何在函数嵌套的情况下解析变量名。词法（lexical）一词指的是，词法作用域根据源代码中声明变量的位置来确定该变量在何处可用。嵌套函数可访问声明于它们外部作用域的变量。

闭包
--

现在来考虑以下例子：

```js
function makeFunc() {
  var name = "Mozilla";
  function displayName() {
    alert(name);
  }
  return displayName;
}

var myFunc = makeFunc();
myFunc();
```

运行这段代码的效果和之前 `init()` 函数的示例完全一样。其中不同的地方（也是有意思的地方）在于内部函数 `displayName()` *在执行前*，从外部函数返回。

第一眼看上去，也许不能直观地看出这段代码能够正常运行。在一些编程语言中，一个函数中的局部变量仅存在于此函数的执行期间。一旦 `makeFunc()` 执行完毕，你可能会认为 `name` 变量将不能再被访问。然而，因为代码仍按预期运行，所以在 JavaScript 中情况显然与此不同。

原因在于，JavaScript 中的函数会形成了闭包。 *闭包*是由函数以及声明该函数的词法环境组合而成的。该环境包含了这个闭包创建时作用域内的任何局部变量。在本例子中，`myFunc` 是执行 `makeFunc` 时创建的 `displayName` 函数实例的引用。`displayName` 的实例维持了一个对它的词法环境（变量 `name` 存在于其中）的引用。因此，当 `myFunc` 被调用时，变量 `name` 仍然可用，其值 `Mozilla` 就被传递到`alert`中。

下面是一个更有意思的示例 — 一个 `makeAdder` 函数：

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

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2)); // 7
console.log(add10(2)); // 12
```

在这个示例中，我们定义了 `makeAdder(x)` 函数，它接受一个参数 `x` ，并返回一个新的函数。返回的函数接受一个参数 `y`，并返回`x+y`的值。

从本质上讲，`makeAdder` 是一个函数工厂 — 他创建了将指定的值和它的参数相加求和的函数。在上面的示例中，我们使用函数工厂创建了两个新函数 — 一个将其参数和 5 求和，另一个和 10 求和。

`add5` 和 `add10` 都是闭包。它们共享相同的函数定义，但是保存了不同的词法环境。在 `add5` 的环境中，`x` 为 5。而在 `add10` 中，`x` 则为 10。

实用的闭包
-----

闭包很有用，因为它允许将函数与其所操作的某些数据（环境）关联起来。这显然类似于面向对象编程。在面向对象编程中，对象允许我们将某些数据（对象的属性）与一个或者多个方法相关联。

因此，通常你使用只有一个方法的对象的地方，都可以使用闭包。

在 Web 中，你想要这样做的情况特别常见。大部分我们所写的 JavaScript 代码都是基于事件的 — 定义某种行为，然后将其添加到用户触发的事件之上（比如点击或者按键）。我们的代码通常作为回调：为响应事件而执行的函数。

假如，我们想在页面上添加一些可以调整字号的按钮。一种方法是以像素为单位指定 `body` 元素的 `font-size`，然后通过相对的 `em` 单位设置页面中其他元素（例如`header`）的字号：

```css
body {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 12px;
}

h1 {
  font-size: 1.5em;
}

h2 {
  font-size: 1.2em;
}
```

我们的文本尺寸调整按钮可以修改 `body` 元素的 `font-size` 属性，由于我们使用相对单位，页面中的其他元素也会相应地调整。

以下是 JavaScript：

```js
function makeSizer(size) {
  return function () {
    document.body.style.fontSize = size + "px";
  };
}

var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
```

`size12`，`size14` 和 `size16` 三个函数将分别把 `body` 文本调整为 12，14，16 像素。我们可以将它们分别添加到按钮的点击事件上。如下所示：

```js
document.getElementById("size-12").onclick = size12;
document.getElementById("size-14").onclick = size14;
document.getElementById("size-16").onclick = size16;
```

```html
<a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a>
```

用闭包模拟私有方法
---------

编程语言中，比如 Java，是支持将方法声明为私有的，即它们只能被同一个类中的其他方法所调用。

而 JavaScript 没有这种原生支持，但我们可以使用闭包来模拟私有方法。私有方法不仅仅有利于限制对代码的访问：还提供了管理全局命名空间的强大能力，避免非核心的方法弄乱了代码的公共接口部分。

下面的示例展现了如何使用闭包来定义公共函数，并令其可以访问私有函数和变量。这个方式也称为模块模式（module pattern）：

```js
var Counter = (function () {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function () {
      changeBy(1);
    },
    decrement: function () {
      changeBy(-1);
    },
    value: function () {
      return privateCounter;
    },
  };
})();

console.log(Counter.value()); /\* logs 0 \*/
Counter.increment();
Counter.increment();
console.log(Counter.value()); /\* logs 2 \*/
Counter.decrement();
console.log(Counter.value()); /\* logs 1 \*/
```

在之前的示例中，每个闭包都有它自己的词法环境；而这次我们只创建了一个词法环境，为三个函数所共享：`Counter.increment`，`Counter.decrement` 和 `Counter.value`。

该共享环境创建于一个立即执行的匿名函数体内。这个环境中包含两个私有项：名为 `privateCounter` 的变量和名为 `changeBy` 的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问。

这三个公共函数是共享同一个环境的闭包。多亏 JavaScript 的词法作用域，它们都可以访问 `privateCounter` 变量和 `changeBy` 函数。

**备注：** 你应该注意到我们定义了一个匿名函数，用于创建一个计数器。我们立即执行了这个匿名函数，并将他的值赋给了变量`Counter`。我们可以把这个函数储存在另外一个变量`makeCounter`中，并用他来创建多个计数器。

```js
var makeCounter = function () {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function () {
      changeBy(1);
    },
    decrement: function () {
      changeBy(-1);
    },
    value: function () {
      return privateCounter;
    },
  };
};

var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /\* logs 0 \*/
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /\* logs 2 \*/
Counter1.decrement();
console.log(Counter1.value()); /\* logs 1 \*/
console.log(Counter2.value()); /\* logs 0 \*/
```

请注意两个计数器 `Counter1` 和 `Counter2` 是如何维护它们各自的独立性的。每个闭包都是引用自己词法作用域内的变量 `privateCounter` 。

每次调用其中一个计数器时，通过改变这个变量的值，会改变这个闭包的词法环境。然而在一个闭包内对变量的修改，不会影响到另外一个闭包中的变量。

**备注：** 以这种方式使用闭包，提供了许多与面向对象编程相关的好处——特别是数据隐藏和封装。

在循环中创建闭包：一个常见错误
---------------

在 ECMAScript 2015 引入 `let` 关键字 之前，在循环中有一个常见的闭包创建问题。参考下面的示例：

```html
<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email" /></p>
<p>Name: <input type="text" id="name" name="name" /></p>
<p>Age: <input type="text" id="age" name="age" /></p>
```

```js
function showHelp(help) {
  document.getElementById("help").innerHTML = help;
}

function setupHelp() {
  var helpText = [
    { id: "email", help: "Your e-mail address" },
    { id: "name", help: "Your full name" },
    { id: "age", help: "Your age (you must be over 16)" },
  ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function () {
      showHelp(item.help);
    };
  }
}

setupHelp();
```

数组 `helpText` 中定义了三个有用的提示信息，每一个都关联于对应的文档中的 `input` 的 ID。通过循环这三项定义，依次为相应 `input` 添加了一个 `onfocus` 事件处理函数，以便显示帮助信息。

运行这段代码后，你会发现它没有达到想要的效果。无论焦点在哪个 `input` 上，显示的都是关于年龄的信息。

原因是赋值给 `onfocus` 的是闭包。这些闭包是由他们的函数定义和在 `setupHelp` 作用域中捕获的环境所组成的。这三个闭包在循环中被创建，但他们共享了同一个词法作用域，在这个作用域中存在一个变量 item。这是因为变量 `item` 使用 `var` 进行声明，由于变量提升，所以具有函数作用域。当 `onfocus` 的回调执行时，`item.help` 的值被决定。由于循环在事件触发之前早已执行完毕，变量对象 `item`（被三个闭包所共享）已经指向了 `helpText` 的最后一项。

解决这个问题的一种方案是使用更多的闭包：特别是使用前面所述的函数工厂：

```js
function showHelp(help) {
  document.getElementById("help").innerHTML = help;
}

function makeHelpCallback(help) {
  return function () {
    showHelp(help);
  };
}

function setupHelp() {
  var helpText = [
    { id: "email", help: "Your e-mail address" },
    { id: "name", help: "Your full name" },
    { id: "age", help: "Your age (you must be over 16)" },
  ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
  }
}

setupHelp();
```

这段代码可以如我们所期望的那样工作。所有的回调不再共享同一个环境， `makeHelpCallback` 函数为每一个回调创建一个新的词法环境。在这些环境中，`help` 指向 `helpText` 数组中对应的字符串。

另一种方法使用了匿名闭包：

```js
function showHelp(help) {
  document.getElementById("help").innerHTML = help;
}

function setupHelp() {
  var helpText = [
    { id: "email", help: "Your e-mail address" },
    { id: "name", help: "Your full name" },
    { id: "age", help: "Your age (you must be over 16)" },
  ];

  for (var i = 0; i < helpText.length; i++) {
    (function () {
      var item = helpText[i];
      document.getElementById(item.id).onfocus = function () {
        showHelp(item.help);
      };
    })(); // 马上把当前循环项的 item 与事件回调相关联起来
  }
}

setupHelp();
```

如果不想使用过多的闭包，你可以用 ES2015 引入的 `let` 或 `const` 关键词：

```js
function showHelp(help) {
  document.getElementById("help").textContent = help;
}

function setupHelp() {
  const helpText = [
    { id: "email", help: "Your e-mail address" },
    { id: "name", help: "Your full name" },
    { id: "age", help: "Your age (you must be over 16)" },
  ];

  for (let i = 0; i < helpText.length; i++) {
    const item = helpText[i];
    document.getElementById(item.id).onfocus = () => {
      showHelp(item.help);
    };
  }
}

setupHelp();
```

这个例子使用 `const` 而不是 `var`，因此每个闭包都绑定了块作用域的变量，这意味着不再需要额外的闭包。

另一个可选方案是使用 `forEach()` 来遍历 `helpText` 数组并给每一个 `<input>` 添加一个监听器，如下所示：

```js
function showHelp(help) {
  document.getElementById("help").textContent = help;
}

function setupHelp() {
  var helpText = [
    { id: "email", help: "Your e-mail address" },
    { id: "name", help: "Your full name" },
    { id: "age", help: "Your age (you must be over 16)" },
  ];

  helpText.forEach(function (text) {
    document.getElementById(text.id).onfocus = function () {
      showHelp(text.help);
    };
  });
}

setupHelp();
```

性能考量
----

如果不是某些特定任务需要使用闭包，在其他函数中创建函数是不明智的，因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。

例如，在创建新的对象或者类时，方法通常应该关联于对象的原型，而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时，方法都会被重新赋值一次（也就是说，对于每个对象的创建，方法都会被重新赋值）。

考虑以下示例：

```js
function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function () {
    return this.name;
  };

  this.getMessage = function () {
    return this.message;
  };
}
```

在上面的代码中，我们并没有利用到闭包的好处，因此可以避免使用闭包。修改成如下：

```js
function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype = {
  getName() {
    return this.name;
  },
  getMessage() {
    return this.message;
  },
};
```

但我们不建议重新定义原型。可改成如下例子：

```js
function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype.getName = function () {
  return this.name;
};
MyObject.prototype.getMessage = function () {
  return this.message;
};
```

在前面的两个示例中，继承的原型可以为所有对象共享，不必在每一次创建对象时定义方法。参见 对象模型的细节 一章可以了解更为详细的信息。


JavaScript 参考 - JavaScript

JavaScript 参考
=============

JavaScript 参考提供了有关 JavaScript 语言的内容资料库。这里详细描述了整个语言。在编写 JavaScript 代码时，你经常会参考这些页面（因此，标题为“JavaScript 参考”）。

JavaScript 语言旨在用于更加广泛的环境，不论是浏览器、服务端脚本还是其他类似的环境。在大多数情况下，本参考与环境无关，且不针对 web 浏览器环境。

如果你对 JavaScript 不熟悉，请先阅读指南。一旦你牢牢地掌握了基础知识，就可以使用参考资料，来获取有关各个对象和语言结构的更加详细的知识。

内置对象
----

JavaScript 标准内置对象，以及它们的方法和属性。

### 值属性

* `Infinity`
* `NaN`
* `undefined`
* `globalThis`
### Function 属性

* `eval()`
* `isFinite()`
* `isNaN()`
* `parseFloat()`
* `parseInt()`
* `decodeURI()`
* `decodeURIComponent()`
* `encodeURI()`
* `encodeURIComponent()`
### 基本对象

* `Object`
* `Function`
* `Boolean`
* `Symbol`
### 错误对象

* `Error`
* `AggregateError`
* `EvalError`
* `InternalError`
* `RangeError`
* `ReferenceError`
* `SyntaxError`
* `TypeError`
* `URIError`
### 数字和日期

* `Number`
* `BigInt`
* `Math`
* `Date`
### 文本处理

* `String`
* `RegExp`
### 索引集合类

* `Array`
* `Int8Array`
* `Uint8Array`
* `Uint8ClampedArray`
* `Int16Array`
* `Uint16Array`
* `Int32Array`
* `Uint32Array`
* `Float32Array`
* `Float64Array`
* `BigInt64Array`
* `BigUint64Array` (en-US)
### Keyed collections

* `Map`
* `Set`
* `WeakMap`
* `WeakSet`
### 结构化数据

* `ArrayBuffer`
* `SharedArrayBuffer`
* `Atomics`
* `DataView`
* `JSON`
### 控制抽象化

* `Promise`
* `Generator`
* `GeneratorFunction`
* `AsyncFunction`
### 反射

* `Reflect`
* `Proxy`
### 国际化

* `Intl`
* `Intl.Collator`
* `Intl.DateTimeFormat`
* `Intl.DisplayNames`
* `Intl.ListFormat`
* `Intl.Locale`
* `Intl.NumberFormat`
* `Intl.PluralRules`
* `Intl.RelativeTimeFormat`
### WebAssembly

* `WebAssembly`
* `WebAssembly.Module`
* `WebAssembly.Instance`
* `WebAssembly.Memory`
* `WebAssembly.Table`
* `WebAssembly.CompileError`
* `WebAssembly.LinkError` (en-US)
* `WebAssembly.RuntimeError`
语句
--

`语句和声明`### 控制流

* `Block`
* `break`
* `continue`
* `Empty`
* `if...else`
* `switch`
* `throw`
* `try...catch`
### 声明

* `var`
* `let`
* `const`
### 函数和类

* `function`
* `function*`
* `async function`
* `return`
* `class`
### 迭代

* `do...while`
* `for`
* `for each...in`
* `for...in`
* `for...of`
* `for await...of`
* `while`
### 其他

* `debugger`
* `import`
* `label`
* `with`
表达式和运算符
-------

`表达式和运算符`.

### Primary expressions

* `this`
* `function`
* `class`
* `function*`
* `yield`
* `yield*`
* `async function`
* `await`
* `[]`
* `{}`
* `/ab+c/i`
* `( )`
* `null`
### Left-hand-side expressions

* Property accessors
* `new`
* `new.target`
* `super`
* `...obj`
### Increment & decrement

* `A++` (en-US)
* `A--` (en-US)
* `++A` (en-US)
* `--A` (en-US)
### 一元运算符

* `delete`
* `void`
* `typeof`
* `+` (en-US)
* `-` (en-US)
* `~` (en-US)
* `!` (en-US)
### 算术运算符

* `+` (en-US)
* `-` (en-US)
* `/` (en-US)
* `*` (en-US)
* `%` (en-US)
* `**` (en-US)
### 关系运算符

* `in`
* `instanceof`
* `<` (en-US)
* `>` (en-US)
* `<=` (en-US)
* `>=` (en-US)
### 相等运算符

* `==` (en-US)
* `!=` (en-US)
* `===` (en-US)
* `!==` (en-US)
### 位移运算符

* `<<` (en-US)
* `>>` (en-US)
* `>>>` (en-US)
### 位运算

* `&` (en-US)
* `|` (en-US)
* `^` (en-US)
### 逻辑运算

* `&&` (en-US)
* `||` (en-US)
### 条件运算符

* `(condition ? ifTrue : ifFalse)`
### 赋值操作符

* `=` (en-US)
* `*=` (en-US)
* `/=` (en-US)
* `%=` (en-US)
* `+=` (en-US)
* `-=` (en-US)
* `<<=` (en-US)
* `>>=` (en-US)
* `>>>=` (en-US)
* `&=` (en-US)
* `^=` (en-US)
* `|=` (en-US)
* `[a, b] = [1, 2]`
* `{a, b} = {a:1, b:2}`
函数
--

本章介绍如何使用 JavaScript 函数 来开发应用程序。

* `arguments`
* 箭头函数
* 默认参数
* 剩余参数
附加参考页面
------

* 词法文法
* 数据类型和数据结构
* 严格模式
* 过时的特性


并发模型与事件循环 - JavaScript

并发模型与事件循环
=========

JavaScript 有一个基于**事件循环**的并发模型，事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。这个模型与其他语言中的模型截然不同，比如 C 和 Java。

运行时概念
-----

接下来的内容解释了这个理论模型。现代 JavaScript 引擎实现并着重优化了以下描述的这些语义。

### 可视化描述

![Stack, heap, queue](/zh-CN/docs/Web/JavaScript/Event_loop/the_javascript_runtime_environment_example.svg)

### 栈

函数调用形成了一个由若干帧组成的栈。

```js
function foo(b) {
  let a = 10;
  return a + b + 11;
}

function bar(x) {
  let y = 3;
  return foo(x \* y);
}

console.log(bar(7)); // 返回 42
```

当调用 `bar` 时，第一个帧被创建并压入栈中，帧中包含了 `bar` 的参数和局部变量。当 `bar` 调用 `foo` 时，第二个帧被创建并被压入栈中，放在第一个帧之上，帧中包含 `foo` 的参数和局部变量。当 `foo` 执行完毕然后返回时，第二个帧就被弹出栈（剩下 `bar` 函数的调用帧）。当 `bar` 也执行完毕然后返回时，第一个帧也被弹出，栈就被清空了。

### 堆

对象被分配在堆中，堆是一个用来表示一大块（通常是非结构化的）内存区域的计算机术语。

### 队列

一个 JavaScript 运行时包含了一个待处理消息的消息队列。每一个消息都关联着一个用以处理这个消息的回调函数。

在 事件循环 期间的某个时刻，运行时会从最先进入队列的消息开始处理队列中的消息。被处理的消息会被移出队列，并作为输入参数来调用与之关联的函数。正如前面所提到的，调用一个函数总是会为其创造一个新的栈帧。

函数的处理会一直进行到执行栈再次为空为止；然后事件循环将会处理队列中的下一个消息（如果还有的话）。

事件循环
----

之所以称之为 **事件循环**，是因为它经常按照类似如下的方式来被实现：

```js
while (queue.waitForMessage()) {
  queue.processNextMessage();
}
```

`queue.waitForMessage()` 会同步地等待消息到达 (如果当前没有任何消息等待被处理)。

### "执行至完成"

每一个消息完整地执行后，其他消息才会被执行。这为程序的分析提供了一些优秀的特性，包括：当一个函数执行时，它不会被抢占，只有在它运行完毕之后才会去运行任何其他的代码，才能修改这个函数操作的数据。这与 C 语言不同，例如，如果函数在线程中运行，它可能在任何位置被终止，然后在另一个线程中运行其他代码。

这个模型的一个缺点在于当一个消息需要太长时间才能处理完毕时，Web 应用程序就无法处理与用户的交互，例如点击或滚动。为了缓解这个问题，浏览器一般会弹出一个“这个脚本运行时间过长”的对话框。一个良好的习惯是缩短单个消息处理时间，并在可能的情况下将一个消息裁剪成多个消息。

### 添加消息

在浏览器里，每当一个事件发生并且有一个事件监听器绑定在该事件上时，一个消息就会被添加进消息队列。如果没有事件监听器，这个事件将会丢失。所以当一个带有点击事件处理器的元素被点击时，就会像其他事件一样产生一个类似的消息。

函数 `setTimeout` 接受两个参数：待加入队列的消息和一个时间值（可选，默认为 0）。这个时间值代表了消息被实际加入到队列的最小延迟时间。如果队列中没有其他消息并且栈为空，在这段延迟时间过去之后，消息会被马上处理。但是，如果有其他消息，`setTimeout` 消息必须等待其他消息处理完。因此第二个参数仅仅表示最少延迟时间，而非确切的等待时间。

下面的例子演示了这个概念（`setTimeout` 并不会在计时器到期之后直接执行）：

```js
const s = new Date().getSeconds();

setTimeout(function () {
  // 输出 "2"，表示回调函数并没有在 500 毫秒之后立即执行
  console.log("Ran after " + (new Date().getSeconds() - s) + " seconds");
}, 500);

while (true) {
  if (new Date().getSeconds() - s >= 2) {
    console.log("Good, looped for 2 seconds");
    break;
  }
}
```

### 零延迟

零延迟并不意味着回调会立即执行。以 0 为第二参数调用 `setTimeout` 并不表示在 0 毫秒后就立即调用回调函数。

其等待的时间取决于队列里待处理的消息数量。在下面的例子中，`"这是一条消息"` 将会在回调获得处理之前输出到控制台，这是因为延迟参数是运行时处理请求所需的最小等待时间，但并不保证是准确的等待时间。

基本上，`setTimeout` 需要等待当前队列中所有的消息都处理完毕之后才能执行，即使已经超出了由第二参数所指定的时间。

```js
(function () {
  console.log("这是开始");

  setTimeout(function cb() {
    console.log("这是来自第一个回调的消息");
  });

  console.log("这是一条消息");

  setTimeout(function cb1() {
    console.log("这是来自第二个回调的消息");
  }, 0);

  console.log("这是结束");
})();

// "这是开始"
// "这是一条消息"
// "这是结束"
// "这是来自第一个回调的消息"
// "这是来自第二个回调的消息"
```

### 多个运行时互相通信

一个 web worker 或者一个跨域的 `iframe` 都有自己的栈、堆和消息队列。两个不同的运行时只能通过 `postMessage` 方法进行通信。如果另一个运行时侦听 `message` 事件，则此方法会向该运行时添加消息。

永不阻塞
----

JavaScript 的事件循环模型与许多其他语言不同的一个非常有趣的特性是，它永不阻塞。处理 I/O 通常通过事件和回调来执行，所以当一个应用正等待一个 IndexedDB 查询返回或者一个 XHR 请求返回时，它仍然可以处理其他事情，比如用户输入。

由于历史原因有一些例外，如 `alert` 或者同步 XHR，但应该尽量避免使用它们。注意，例外的例外也是存在的（但通常是实现错误而非其他原因）。


JavaScript 技术概览 - JavaScript

JavaScript 技术概览
===============

HTML 定义网页的结构与内容，CSS 定义其格式与样式，而 JavaScript 则为网页增加可交互性，创作功能丰富的 Web 应用。

但是，如果从浏览器的范畴去理解“JavaScript”这个术语，它包含了截然不同的两个方面。一方面是 JavaScript 的核心语言（ECMAScript），另一方面是大量的 Web API (en-US)，包括 DOM（文档对象模型）。

JavaScript 核心语言（ECMAScript）
---------------------------

JavaScript 的核心语言是 ECMAScript，是一门由 ECMA TC39 委员会标准化的编程语言。“ECMAScript”是语言标准的术语，但“ECMAScript”和“JavaScript”是可以互换使用的。

该核心语言同样可以被用在非浏览器环境之中，例如 Node.js。

### 哪些内容被纳入 ECMAScript 范畴之中？

除却一些其他元素，ECMAScript 定义了：

* 语法（解析规则、关键词、流程控制、对象初始化，等等）
* 错误处理机制（`throw`、`try...catch`，以及创建用户自定义错误类型的能力）
* 类型（布尔值、数字、字符串、函数、对象，等等）
* 基于原型的继承机制
* 内置对象和函数（`JSON`、`Math`、Array 方法、`parseInt`、`decodeURI`，等等）
* 严格模式
* 模块系统
* 基本内存模型
### 标准化流程

ECMAScript 版本由每年的 ECMA 大会批准并作为标准发布。所有的进展都在 Ecma TC39 GitHub 组织上公开，该组织托管提案、官方规范文本和会议记录。

在 ECMAScript 第 6 版（称为 ES6）之前，规范是几年发布一次，通常用它们的主要版本号来指代（ES3、ES5 等）。在 ES6 之后，规范以发布年份命名（ES2017、ES2018 等）。ES6 是 ES2015 的代名词。*ESNext* 是一个动态名称，指的是撰写本文时的下一个版本。ESNext 中的特性更准确地称为提案，因为根据定义，规范尚未最终确定。

目前委员会批准的 ECMA-262 的快照有 PDF 版本和 HTML 版本。ECMA-262 和 ECMA-402 正处于维护状态，仍在由规范编辑者更新；TC39 网站托管了最新版本的 ECMA-262 和 ECMA-402。

新的语言特性，包括新的语法和 API 的引入以及现有行为的修订，都以提案的形式进行讨论。每个提案都需要经过 4 个阶段的过程，通常在第 3 或 第 4 阶段时，JavaScript 引擎会实现这些提案，以供公众使用。

有关 ECMAScript 历史的更多信息，请参阅维基百科上的 ECMAScript 条目。

国际化 API
-------

由 ECMA TC39 进行标准化的 ECMAScript 国际化 API 规范是在 ECMAScript 语言规范之上额外增加的。国际化 API 为 JavaScript 提供了国际化的规则排序（字符串比较）、数字格式化、日期时间格式化等功能，能够让应用选择语言，并根据实际需要选用功能。本标准在 2012 年 12 月审批通过，可以在 `Intl` 对象页面查看各个浏览器对其的实现情况。如今，国际化规范也每年批准一次，浏览器不断改进其实现。

### 相关资源

你可以通过多种方式参与或跟踪 ECMAScript 语言规范和 ECMAScript 国际化 API 规范和相关资源的目前的工作：

* ECMAScript 语言规范仓库
* ECMAScript 国际化 API 规范仓库
* ECMAScript 提案仓库
* ECMAScript 一致性测试套件仓库
* TC39 会议记录
* ECMAScript 规范讨论；当前邮件列表
* ECMAScript 规范讨论；历史邮件列表档案（至 2021 年 3 月）
DOM API
-------

### WebIDL

WebIDL 规范定义了 ECMAScript 和 DOM 技术之间的交互规范。

### DOM 的核心

文档对象模型（DOM）是用来表达 HTML、XHTML 及 XML 文档中的对象或与其进行交互的**约定**，它是跨平台的，并且与**编程语言无关**。通过调用**DOM 树**上对象的方法可以操纵这些对象。文档对象模型核心是由 W3C (en-US) 进行标准化的，它将 HTML 和 XML 文档抽象成对象，并在其上定义接口以及操纵这些对象的机制，DOM 定义的元素有：

* DOM 核心中定义了文档结构、树模型，以及 DOM 事件架构，包括：`Node`、`Element`、`DocumentFragment`、`Document`、`DOMImplementation`、`Event`、`EventTarget`，等等
* DOM 事件中包括对于 DOM 事件架构的一个不太严格的定义，以及一些特殊事件。
* DOM 元素遍历以及 DOM 范围对象等其他内容。

从 ECMAScript 的角度来看，DOM 规范中定义的对象被称作“宿主对象”。

### HTML DOM

HTML，Web 的标记语言，是根据 DOM 定义的。位于 DOM 核心抽象概念之上，HTML 还定义了元素的意义。比如元素的 `className` 属性以及例如 `document.body` 这样的 API。

HTML 规范同时还约束了元素之间的关系，例如无序列表 `<ul>` 元素中，只能以 `<li>` 元素作为子元素来表达列表项。还有就是禁止使用标准中未定义的元素和属性。

想了解更多关于 `Document` 对象、`Window` 对象以及其他 DOM 元素的信息？请访问 MDN 的 DOM 文档。

其他值得关注的 API
-----------

* `setTimeout` 和 `setInterval` 函数最早被定义在 HTML 标准的 `Window` 接口下。
* XMLHttpRequest 使得发起异步 HTTP 请求成为可能。
* Fetch API 为网络请求提供了更符合人体工程学的抽象。
* CSS 对象模型 将 CSS 规则抽象成对象。
* WebWorker 允许进行并行计算。
* WebSocket 允许进行底层双向通信。
* Canvas 2D Context 是 `<canvas>` 元素的绘图 API。
* WebAssembly 接口 提供了 JavaScript 代码和 WebAssembly 模块之间的通信工具。

非浏览器环境（如 Node.js）通常不提供 DOM API——因为它们不与文档进行交互，但它们仍然会实现很多 web API，例如 `fetch()` 和 `setTimeout()`。

有哪些 JavaScript 的实现？
-------------------

mozilla.org 上托管了两个 JavaScript 实现。**首个** JavaScript 由网景公司的 Brendan Eich 创建，并不断地更新以符合 ECMA-262 Edition 5 及其之后的标准版本。这个引擎，代号 SpiderMonkey ，是由 C/C++ 语言开发的。而 Rhino 引擎，主要由 Norris Boyd（同样也是在网景公司）创建，则是一个 Java 语言开发的 JavaScript 实现。与 SpiderMonkey 类似，Rhino 符合 ECMA-262 Edition 5 标准。

有很多优化技术如 TraceMonkey (Firefox 3.5)、JägerMonkey (Firefox 4) 和 IonMonkey 被不断添加到了 SpiderMonkey JavaScript 引擎。并且提升 JavaScript 执行效率的工作一直在进行。

除了以上实现，还有其他一些流行的 JavaScript 引擎，如：

* Google 的 V8，在 Google Chrome 浏览器和较新的 Opera 浏览器中使用。这同时也是Node.js使用的引擎。
* JavaScriptCore (SquirrelFish/Nitro)，被用在了一些 WebKit 浏览器如 Apple Safari。
* Carakan，用在旧版本 Opera 中。
* Chakra 引擎，用在 Internet Explorer 中（尽管它实现的语言被正式地称作为“JScript”来避免商标问题）。

每个 mozilla.org 的 JavaScript 引擎都提供了 公用 API 使程序开发者能将其 JavaScript 嵌入自己的软件中。目前最常见的 JavaScript 宿主环境是网页浏览器。浏览器一般通过 API 创建“宿主对象”来负责将DOM 反射到 JavaScript 中。

另一个常见的 JavaScript 应用是作为服务端脚本语言。JavaScript 服务器提供宿主对象代表 HTTP 请求和响应，随后可以通过 JavaScript 程序来动态的生成 Web 页面。Node.js便是一个流行的例子。

Shell
-----

JavaScript shell 允许你快速测试代码片段或 JavaScript 代码，而无需重载 web 页面。其在开发和代码调试中非常有用。

### 独立的 JavaScript shell

以下 JavaScript shell 是与 Perl 或 Python 类似的独立的环境：

* Node.js——Node.js 是用于构建快速、可扩展的网络应用的平台。
* ShellJS——用于 Node.js 的便携式 Unix shell 命令。
### 基于浏览器的 JavaScript shell

以下 JavaScript shell 是通过浏览器的 JavaScript 引擎运行代码的：

* Firefox 有一个内置的 JavaScript 控制台，支持多行编辑。
* Babel 交互式解释器——一个基于浏览器的、用于试验下一代 JavaScript 特性的交互式解释器。
* TypeScript playground——一个基于浏览器的在线运行环境，用于试验新的 JavaScript 特性（通过 tsc 编译器）和 TypeScript 语法。
工具和资源
-----

用于编写和调试 JavaScript 代码的实用工具。

Firefox 开发者工具

包括 Web Console、JavaScript Profiler、Debugger 等。

JavaScript Shell

Javascript Shell 使你可以即时测试 JavaScript 代码片段。

TogetherJS

添加 TogetherJS 到你的网站，让用户实时互助，协作更简单。

Stack Overflow

你可以在 StackOverflow 查看或者发布带有 JavaScript 标签的问题。

JSFiddle

编辑 JavaScript、CSS 和 HTML 并获得实时结果。使用外置资源，并同你的团队在线合作。

Plunker

Plunker 是一个在线社区，用于创建、协作和共享你的 Web 开发创意。编辑你的 JavaScript、CSS 和 HTML 文件并获取实时结果和文件结构。

JSBin

JS Bin 是一个开源的协作式的 web 开发调试工具。

Codepen

Codepen 也是一个 Web 协作工具，可用作展示实时结果的 playground。

StackBlitz

StackBlitz 同样是一个支持调试的在线 playground，它支持部署使用了 React、Angular 等技术的全栈应用

RunJS

RunJS 是一个桌面端便笺式 playground，可提供实时执行结果并支持调用 Node 与浏览器 API。


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

JavaScript 数据类型和数据结构
====================

编程语言都具有内建的数据结构，但各种编程语言的数据结构常有不同之处。本文尝试列出 JavaScript 语言中内建的数据结构及其属性，它们可以用来构建其他的数据结构。

JavaScript 语言概述提供了对常见数据类型的类似总结，但是更倾向于和其他语言进行比较。

动态和弱类型
------

JavaScript 是一种有着动态类型的动态语言。JavaScript 中的变量与任何特定值类型没有任何关联，任何变量都可以被赋予（和重新赋予）各种类型的值：

```js
let foo = 42; // foo 现在是一个数值
foo = "bar"; // foo 现在是一个字符串
foo = true; // foo 现在是一个布尔值
```

JavaScript 也是一个弱类型语言，这意味着当操作涉及不匹配的类型时，它允许隐式类型转换，而不是抛出类型错误。

```js
const foo = 42; // foo 现在是一个数值
const result = foo + "1"; // JavaScript 将 foo 强制转换为字符串，因此可以将其与另一个操作数连接起来
console.log(result); // 421
```

隐式强制转换是非常方便的，但当转换发生在预期之外的地方，或发生在预期的另一个方向（例如，字符串转换为数值，而不是数值转换为字符串）时，就会产生一些微妙的错误。对于 symbol 和 BigInt，JavaScript 有意禁止了某些隐式类型转换。

原始值
---

除了 Object 以外，所有类型都定义了表示在语言最低层面的不可变值。我们将这些值称为*原始值*。

除了 `null`，所有原始类型都可以使用 `typeof` 运算符进行测试。`typeof null` 返回 `"object"`，因此必须使用 `=== null` 来测试 `null`。

除了 `null` 和 `undefined`，所有原始类型都有它们相应的对象包装类型，这为处理原始值提供可用的方法。例如，`Number` 对象提供像 `toExponential()` 这样的方法。当在原始值上访问属性时，JavaScript 会自动将值包装到相应的包装对象中，并访问对象上的属性。然而，在 `null` 或 `undefined` 上访问属性时，会抛出 `TypeError` 异常，这需要采用可选链运算符。

| 类型 | `typeof` 返回值 | 对象包装器 |
| --- | --- | --- |
| Null | `"object"` | 不适用 |
| Undefined | `"undefined"` | 不适用 |
| Boolean | `"boolean"` | `Boolean` |
| Number | `"number"` | `Number` |
| BigInt | `"bigint"` | `BigInt` |
| String | `"string"` | `String` |
| Symbol | `"symbol"` | `Symbol` |

对象包装器类的参考页面包含关于每个类型可用方法和属性类型的更多用法，以及原始类型本身的详细描述。

### Null 类型

Null 类型只有一个值：`null`。

### undefined 类型

Undefined 类型只有一个值：`undefined`。

从概念上讲，`undefined` 表示*值*的缺失，`null` 表示*对象*的缺失（这也可以说明 `typeof null === "object"` 的原因）。当某些东西没有值时，该语言通常默认为 `undefined`：

* 没有值（`return;`）的 `return` 语句，隐式返回 `undefined`。
* 访问不存在的对象属性（`obj.iDontExist`），返回 `undefined`。
* 变量声明时没有初始化（`let x;`），隐式初始化为 `undefined`。
* 许多像 `Array.prototype.find()` 和 `Map.prototype.get()` 的方法，当没有找到元素时，返回 `undefined`。

`null` 在核心语言中使用频率少得多。最重要的地方是原型链的末端，其次是与原型交互的方法，如 `Object.getPrototypeOf()`、`Object.create()` 等，接受或返回 `null` 而不是 `undefined`。

`null` 是一个关键字，但是 `undefined` 是一个普通的标识符，恰好是一个全局属性。在实践中，这两个差异很小，因为 `undefined` 不应该被重新定义或者遮蔽。

### Boolean 类型

`Boolean` 类型表示一个逻辑实体并且包括两个值：`true` 和 `false`。

布尔值通常用于条件运算，包括三元运算符、`if...else`、`while` 等。

### Number 类型

`Number` 类型是一种基于 IEEE 754 标准的双精度 64 位二进制格式的值。它能够存储 2-1074（`Number.MIN_VALUE`）和 21024（`Number.MAX_VALUE`）之间的正浮点数，以及 -2-1074 和 -21024 之间的负浮点数，但是它仅能安全地存储在 -(253 − 1)（`Number.MIN_SAFE_INTEGER`）到 253 − 1（`Number.MAX_SAFE_INTEGER`）范围内的整数。超出这个范围，JavaScript 将不能安全地表示整数；相反，它们将由双精度浮点近似表示。你可以使用 `Number.isSafeInteger()` 检查一个数是否在安全的整数范围内。

±(2-1074 ~ 21024) 范围之外的值会自动转换：

* 大于 `Number.MAX_VALUE` 的正值被转换为 `+Infinity`。
* 小于 `Number.MIN_VALUE` 的正值被转换为 `+0`。
* 小于 -`Number.MAX_VALUE` 的负值被转换为 `-Infinity`。
* 大于 -`Number.MIN_VALUE` 的负值被转换为 `-0`。

`+Infinity` 和 `-Infinity` 行为类似于数学上的无穷大，但是有一些细微的区别；更多细节，参见 `Number.POSITIVE_INFINITY` 和 `Number.NEGATIVE_INFINITY`。

Number 类型仅有一个具有多个表现形式的值：`0` 同时表示为 `-0` 和 `+0`（其中 `0` 是 `+0` 的别名）。实际上，这两者之间几乎没有区别；例如，`+0 === -0` 是 `true`。然而，当你除以 0 的时候，你要注意到这一点：

```js
console.log(42 / +0); // Infinity
console.log(42 / -0); // -Infinity
```

`NaN`（“**N**ot **a** **N**umber”）是一个特殊种类的数值，当算术运算的结果不表示数值时，通常会遇到它。它也是 JavaScript 中唯一不等于自身的值。

虽然 number 在概念上是一个“数学的值”，并且总是隐式的编码为浮点类型，但是 JavaScript 提供了位运算符。当应用位运算符时，number 首先转换为 32 位整数。

**备注：** 尽管位运算符*可以*使用位掩码来表示单个数值中的几个布尔值，但通常这不是一个好的做法。JavaScript 提供了表示一组布尔值的其他方法（如布尔数组，或将布尔值分配给命名属性的对象）。位掩码也往往会使代码更难读取、理解和维护。

可能有必要在非常受限的环境中使用此类技术，例如在试图应对本地存储的限制时，或在极端情况下（例如当网络上的每个位计数时）。只有当这项技术是优化尺寸的最后一项措施时，才应考虑这项技术。

### BigInt 类型

`BigInt` 类型在 Javascript 中是一个数字的原始值，它可以表示任意大小的整数。使用 BigInt，你可以安全地存储和操作巨大的整数，甚至超过 Number 的安全整数限制（`Number.MAX_SAFE_INTEGER`）。

BigInt 是通过将 `n` 附加到整数末尾或调用 `BigInt()` 函数来创建的。

本例演示了递增 `Number.MAX_SAFE_INTEGER` 会返回预期结果：

```js
// BigInt
const x = BigInt(Number.MAX\_SAFE\_INTEGER); // 9007199254740991n
x + 1n === x + 2n; // false，因为 9007199254740992n 和 9007199254740993n 不相等

// Number
Number.MAX\_SAFE\_INTEGER + 1 === Number.MAX\_SAFE\_INTEGER + 2; // true，因为都等于 9007199254740992
```

你可以使用大多数运算符处理 BigInt，包括 `+`、`*`、`-`、`**` 和 `%`。——唯一被禁止的是 `>>>`。BigInt 并不是严格等于有着相同数学值的 Number，而是宽松的相等。

BigInt 值并不总是更精确的，也不总是比 number 精确，因为 BigInt 不能表示小数，但可以更精确地表示大整数。这两种类型都不能相互替代。如果 BigInt 值在算术表达式中与常规 number 值混合，或者它们相互隐式转换，则抛出 `TypeError`。

### String 类型

`String` 类型表示文本数据并编码为 UTF-16 码元的 16 位无符号整数值序列。字符串中的每个元素在字符串中占据一个位置。第一个元素的索引为 `0`，下一个是索引 `1`，依此类推。字符串的长度是它的元素的数量。字符串的长度是其中的 UTF-16 代码单元的数量，这可能与 Unicode 字符的实际数量不符；更多细节参见 `String` 参考页面。

JavaScript 字符串是不可变的。这意味着一旦字符串被创建，就不可能修改它。字符串方法基于当前字符串的内容创建一个新的字符串——例如：

* 使用 `substring()` 获取原始的子字符串。
* 使用串联运算符（`+`）或 `concat()` 将两个字符串串联。

#### 注意代码中的“字符串类型”！

使用字符串来表示复杂的数据可能很诱人。这样做会带来短期的好处：

* 容易通过字符串拼接来构造复杂的字符串。
* 字符串容易被调试（你看到的打印的内容始终是字符串）。
* 字符串通常是许多 API 的常见标准（input 字段、local storage 中的值，以及 `XMLHttpRequest` 使用 `responseText` 等作为响应，等等）而且这些 API 可能只能与字符串一同使用。

使用约定，字符串一般可以用来表示任何数据结构。但这并不总是一个好主意。例如，使用一个分隔符，可以模拟一个列表（而 JavaScript 数组可能更适合）。不幸的是，当分隔符用于列表中的元素时，列表就会被破坏。这时可以选择转义字符，等等。所有这些都需要约定，并造成不必要的维护负担。

表示文本数据时候推荐使用字符串。当需要表示复杂的数据时，使用字符串*解析*并使用适当的抽象。

### Symbol 类型

`Symbol` 是**唯一**并且**不可变**的原始值并且可以用来作为对象属性的键（如下）。在某些程序语言当中，Symbol 也被称作“原子（atom）类型”。symbol 的目的是去创建一个唯一属性键，保证不会与其他代码中的键产生冲突。

Object
------

在计算机科学中，对象（object）是指内存中的可以被标识符引用的一块区域。在 JavaScript 中，对象是唯一可变的值。事实上，函数也是具有额外可*调用*能力的对象。

### 属性

在 JavaScript 中，对象可以被看作是一组属性的集合。用对象字面量语法来定义一个对象时，会自动初始化一组有限的属性；然后，这些属性还可以被添加和移除。对象属性等价于键值对。属性键要么是字符串类型，要么是 symbol。属性值可以是任何类型的值，包括其他对象，从而可以构建复杂的数据结构。

有两种对象属性的类型：*数据*属性和*访问器*属性。每个属性都有对应的*特性*（attribute）。JavaScript 引擎可在内部访问每个属性，但是你可以通过 `Object.defineProperty()` 设置它们，或通过 `Object.getOwnPropertyDescriptor()` 读取它们。你可以在 `Object.defineProperty()` 页面阅读更多有关各种细微差别的信息。

#### 数据属性

数据属性将键与值相关联。它可以通过以下属性来描述：

`value`

通过属性访问器获取值。可以是任意的 JavaScript 值。

`writable`

一个布尔值，表示是否可以通过赋值来改变属性。

`enumerable`

一个布尔值，表示是否可以通过 `for...in` 循环来枚举属性。另请参阅枚举性和属性所有权，以了解枚举属性如何与其他函数和语法交互。

`configurable`

一个布尔值，表示该属性是否可以删除，是否可以更改为访问器属性，并可以更改其特性。

#### 访问器属性

将键与两个访问器函数（`get` 和 `set`）相关联，以获取或者存储值。

**备注：** 重要的是，意识到它是访问器*属性*——而不是访问器*方法*。我们可以将函数作为值来提供给 JavaScript 对象的访问器，使得对象表现得像一个类——但这并不能使该对象成为类。

一个访问器属性有着以下的特性：

`get`

该函数使用一个空的参数列表，以便有权对值执行访问时，获取属性值。参见 getter。可能是 `undefined`。

`set`

使用包含分配值的参数调用的函数。每当尝试更改指定属性时执行。参见 setter。可能是 `undefined`。

`enumerable`

一个布尔值，表示是否可以通过 `for...in` 循环来枚举属性。另请参阅枚举性和属性所有权，以了解枚举属性如何与其他函数和语法交互。

`configurable`

一个布尔值，表示该属性是否可以删除，是否可以更改为访问器属性，并可以更改其特性。

对象的原型（prototype）指向另一个对象或者 `null`——从概念上讲，它是对象的隐藏属性，通常表示为 `[[Prototype]]`。对象的 `[[Prototype]]` 属性也可以在对象自身访问。

对象是临时的键值对，因此经常被用作映射。不过，这可能存在人体工程学、安全性和性能方面的问题。请使用 `Map` 来存储任意数据。`Map` 参考页面更详细地讨论了使用普通对象和使用 map 存储键值之间的利弊。

### Date

当表示日期时，最好的选择是使用在 JavaScript 内置的 `Date` 工具类。

### 索引类集合：数组和类型化数组

数组是一种以整数为键（integer-keyed）的属性并与长度（`length`）属性关联的常规对象。

此外，数组对象还继承了 `Array.prototype` 的一些操作数组的便捷方法。例如，`indexOf()`（搜索数组中的一个值）或 `push()`（向数组中添加一个元素），等等。这使得数组成为表示有序列表的理想选择。

类型化数组表示底层二进制缓冲区的类数组视图，并且提供了与数组相对应的类似语义的方法。“类型化数组”是一系列数据结构的总话术语，包括 `Int8Array`、`Float32Array` 等等。获取更多细节，请查看类型化数组页。类型化数组通常与 `ArrayBuffer` 和 `DataView` 一起使用。

### 带键的集合：Map、Set、WeakMap、WeakSet

这些数据结构把对象的引用当作键。`Set` 和 `WeakSet` 表示唯一值的集合，而 `Map` 和 `WeakMap` 表示键值相关联的集合。

你也可以自己实现 `Map` 和 `Set`。然而，因为对象不能被比较（例如，在 `<`“小于”的意义上），另一方面，引擎也没有暴露出它的哈希函数，因此查找性能必定是线性的。它们的原生实现（包括 `WeakMap`）可以达到近似对数到常数时间的查找性能。

通常，要将数据绑定到 DOM 节点，可以直接在对象上设置属性，或使用 `data-*` 属性。这样做的缺点是，在同一上下文中运行的任何脚本都可以使用这些数据。而 `Map` 和 `WeakMap` 则可以轻松地将数据*隐蔽地*绑定到对象上。

`WeakMap` 和 `WeakSet` 只允许将可垃圾回收的值作为键，这些键要么是对象，要么是未注册的 symbol，即使键仍在集合中，也可能被收集。它们专门用于优化内存使用。

### 结构化数据：JSON

JSON（**J**ava**S**cript **O**bject **N**otation）是一种轻量级的数据交换格式，来源于 JavaScript，同时也被多种语言所使用。JSON 构建了通用数据结构，可以在不同环境之间传输，甚至可以跨语言传输。更多细节，请参见 `JSON`。

### 标准库中的更多对象

JavaScript 有一个内置对象的标准库。请阅读参考页面，了解有关内置对象的更多信息。

强制类型转换
------

如上所述，JavaScript 是一个弱类型语言。这意味着你经常可以使用与预期类型不同类型的值，并且该语言将为你转换它为正确的类型。为此，JavaScript 定义了少数强制转换规则。

### 原始值强制转换

原始值强制转换用于得到一个期望的原始值，但对实际类型应该是什么并没有强烈的偏好。通常情况下可以接受字符串、数值或 BigInt。例如：

* `Date()` 构造函数，当它收到一个不是 `Date` 实例的参数时——字符串表示日期字符串，而数值表示时间戳。
* `+` 运算符——如果运算对象是字符串，执行字符串串联；否则，执行数值相加。
* `==` 运算符——如果一个运算对象是原始值，而另一个运算对象是对象（object），则该对象将转换为没有首选类型的原始值。

如果值已经是原始值，则此操作不会进行任何转换。对象将依次调用它的 `[@@toPrimitive]()`（将 `default` 作为 hint 值）、`valueOf()` 和 `toString()` 方法，将其转换为原始值。注意，原始值转换会在 `toString()` 方法之前调用 `valueOf()` 方法，这与数字类型强制转换的行为相似，但与字符串类型强制转换不同。

`[@@toPrimitive]()` 方法，如果存在，则必须返回原始值——返回对象，会导致 `TypeError`。对于 `valueOf()` 和 `toString()`，如果其中一个返回对象，则忽略其返回值，从而使用另一个的返回值；如果两者都不存在，或者两者都没有返回一个原始值，则抛出 `TypeError`。例如，以下代码：

```js
console.log({} + []); // "[object Object]"
```

`{}` 和 `[]` 都没有 `[@@toPrimitive]()` 方法。`{}` 和 `[]` 都从 `Object.prototype.valueOf` 继承 `valueOf()`，其返回对象自身。因为返回值是一个对象，因此它被忽略。因此，调用 `toString()` 方法。`{}.toString()` 返回 `"[object Object]"`，而 `[].toString()` 返回 `""`，因此这个结果是它们的串联：`"[object Object]"`。

在强制转换为任意的原始类型时，`[@@toPrimitive]()` 方法总是优先调用。原始值的强制转换的行为通常与 number 类型的强制转换类似，因为优先调用了 `valueOf()`；然而，有着自定义 `[@@toPrimitive]()` 方法的对象可以选择返回任意的原始值。`Date` 和 `Symbol` 对象是唯一重写 `[@@toPrimitive]()` 方法的对象。`Date.prototype[@@toPrimitive]()` 将 `"string"` 视为 `"default"` hint，而 `Symbol.prototype[@@toPrimitive]()` 忽略 hint 并始终返回一个 symbol。

### 数字类型强制转换

有两种数字类型：number 和 BigInt。有时候，该语言期望使用 number 或 BigInt（例如 `Array.prototype.slice()`，其中索引必须是一个数字）；其他时候，它可能容忍并且根据运算对象的类型不同执行不同的运算。有关不允许从其他类型隐式转换的严格强制转换过程，请参阅 number 强制转换和 BigInt 强制转换。

数字类型强制转换与 number 类型强制转换几乎相同，只是 BigInt 会按原样返回，而不是引起 `TypeError`。强制数字类型转换用于所有算术运算，因为它们重载了 number 和 BigInt 类型。唯一例外的是一元加，它总是实施 number 强制类型转换。

### 其他类型强制转换

所有除了 null、undefined 以及 Symbol 的数据类型，都有它们各自的强制转换过程。更多细节，请参见字符串强制转换、布尔值强制转换以及对象强制转换。

你可能已经注意到，有三种不同的路径可以将对象转换为原始值：

* 原始值强制转换：`[@@toPrimitive]("default")` → `valueOf()` → `toString()`
* 数字类型强制转换、number 类型强制转换、BigInt 类型强制转换：`[@@toPrimitive]("number")` → `valueOf()` → `toString()`
* 字符串类型强制转换：`[@@toPrimitive]("string")` → `toString()` → `valueOf()`

在所有情况下，`[@@toPrimitive]()` 如果存在，必须可调用并返回原始值，而如果它们不可调用或返回对象，`valueOf` 或 `toString` 将被忽略。在过程结束时，如果成功，结果保证是原始值。然后，由此产生的原始值会进一步强制类型转换，具体取决于上下文。

参见
--

* JavaScript 数据类型和数据结构，由 Oleksii Trekhleb 撰写
* JavaScript 中的计算机科学，由 Nicholas C. Zakas 撰写


内存管理 - JavaScript

内存管理
====

简介
--

像 C 语言这样的底层语言一般都有底层的内存管理接口，比如 `malloc()`和`free()`。相反，JavaScript 是在创建变量（对象，字符串等）时自动进行了分配内存，并且在不使用它们时“自动”释放。释放的过程称为垃圾回收。这个“自动”是混乱的根源，并让 JavaScript（和其他高级语言）开发者错误的感觉他们可以不关心内存管理。

内存生命周期
------

不管什么程序语言，内存生命周期基本是一致的：

1. 分配你所需要的内存
2. 使用分配到的内存（读、写）
3. 不需要时将其释放\归还

所有语言第二部分都是明确的。第一和第三部分在底层语言中是明确的，但在像 JavaScript 这些高级语言中，大部分都是隐含的。

### JavaScript 的内存分配

#### 值的初始化

为了不让程序员费心分配内存，JavaScript 在定义变量时就完成了内存分配。

```js
var n = 123; // 给数值变量分配内存
var s = "azerty"; // 给字符串分配内存

var o = {
  a: 1,
  b: null,
}; // 给对象及其包含的值分配内存

// 给数组及其包含的值分配内存（就像对象一样）
var a = [1, null, "abra"];

function f(a) {
  return a + 2;
} // 给函数（可调用的对象）分配内存

// 函数表达式也能分配一个对象
someElement.addEventListener(
  "click",
  function () {
    someElement.style.backgroundColor = "blue";
  },
  false,
);
```

#### 通过函数调用分配内存

有些函数调用结果是分配对象内存：

```js
var d = new Date(); // 分配一个 Date 对象

var e = document.createElement("div"); // 分配一个 DOM 元素
```

有些方法分配新变量或者新对象：

```js
var s = "azerty";
var s2 = s.substr(0, 3); // s2 是一个新的字符串
// 因为字符串是不变量，
// JavaScript 可能决定不分配内存，
// 只是存储了 [0-3] 的范围。

var a = ["ouais ouais", "nan nan"];
var a2 = ["generation", "nan nan"];
var a3 = a.concat(a2);
// 新数组有四个元素，是 a 连接 a2 的结果
```

### 使用值

使用值的过程实际上是对分配内存进行读取与写入的操作。读取与写入可能是写入一个变量或者一个对象的属性值，甚至传递函数的参数。

### 当内存不再需要使用时释放

大多数内存管理的问题都在这个阶段。在这里最艰难的任务是找到“哪些被分配的内存确实已经不再需要了”。它往往要求开发人员来确定在程序中哪一块内存不再需要并且释放它。

高级语言解释器嵌入了“垃圾回收器”，它的主要工作是跟踪内存的分配和使用，以便当分配的内存不再使用时，自动释放它。这只能是一个近似的过程，因为要知道是否仍然需要某块内存是无法判定的（无法通过某种算法解决）。

垃圾回收
----

如上文所述自动寻找是否一些内存“不再需要”的问题是无法判定的。因此，垃圾回收实现只能有限制的解决一般问题。本节将解释必要的概念，了解主要的垃圾回收算法和它们的局限性。

### 引用

垃圾回收算法主要依赖于引用的概念。在内存管理的环境中，一个对象如果有访问另一个对象的权限（隐式或者显式），叫做一个对象引用另一个对象。例如，一个 Javascript 对象具有对它原型的引用（隐式引用）和对它属性的引用（显式引用）。

在这里，“对象”的概念不仅特指 JavaScript 对象，还包括函数作用域（或者全局词法作用域）。

### 引用计数垃圾收集

这是最初级的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象（零引用），对象将被垃圾回收机制回收。

#### 示例

```js
var o = {
  a: {
    b: 2,
  },
};
// 两个对象被创建，一个作为另一个的属性被引用，另一个被分配给变量 o
// 很显然，没有一个可以被垃圾收集

var o2 = o; // o2 变量是第二个对“这个对象”的引用

o = 1; // 现在，“这个对象”只有一个 o2 变量的引用了，“这个对象”的原始引用 o 已经没有

var oa = o2.a; // 引用“这个对象”的 a 属性
// 现在，“这个对象”有两个引用了，一个是 o2，一个是 oa

o2 = "yo"; // 虽然最初的对象现在已经是零引用了，可以被垃圾回收了
// 但是它的属性 a 的对象还在被 oa 引用，所以还不能回收

oa = null; // a 属性的那个对象现在也是零引用了
// 它可以被垃圾回收了
```

#### 限制：循环引用

该算法有个限制：无法处理循环引用的事例。在下面的例子中，两个对象被创建，并互相引用，形成了一个循环。它们被调用之后会离开函数作用域，所以它们已经没有用了，可以被回收了。然而，引用计数算法考虑到它们互相都有至少一次引用，所以它们不会被回收。

```js
function f() {
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o

  return "azerty";
}

f();
```

#### 实际例子

IE 6, 7 使用引用计数方式对 DOM 对象进行垃圾回收。该方式常常造成对象被循环引用时内存发生泄漏：

```js
var div;
window.onload = function () {
  div = document.getElementById("myDivElement");
  div.circularReference = div;
  div.lotsOfData = new Array(10000).join("\*");
};
```

在上面的例子里，`myDivElement` 这个 DOM 元素里的 `circularReference 属性`引用了 `myDivElement`，造成了循环引用。如果该属性没有显示移除或者设为 null，引用计数式垃圾收集器将总是且至少有一个引用，并将一直保持在内存里的 DOM 元素，即使其从 DOM 树中删去了。如果这个 DOM 元素拥有大量的数据 (如上的 `lotsOfData` 属性)，而这个数据占用的内存将永远不会被释放。

### 标记 - 清除算法

这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。

这个算法假定设置一个叫做根（root）的对象（在 Javascript 里，根是全局对象）。垃圾回收器将定期从根开始，找所有从根开始引用的对象，然后找这些对象引用的对象……从根开始，垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。

这个算法比前一个要好，因为“有零引用的对象”总是不可获得的，但是相反却不一定，参考“循环引用”。

从 2012 年起，所有现代浏览器都使用了标记 - 清除垃圾回收算法。所有对 JavaScript 垃圾回收算法的改进都是基于标记 - 清除算法的改进，并没有改进标记 - 清除算法本身和它对“对象是否不再需要”的简化定义。

#### 循环引用不再是问题了

在上面的示例中，函数调用返回之后，两个对象从全局对象出发无法获取。因此，他们将会被垃圾回收器回收。第二个示例同样，一旦 div 和其事件处理无法从根获取到，他们将会被垃圾回收器回收。

#### 限制：那些无法从根对象查询到的对象都将被清除

尽管这是一个限制，但实践中我们很少会碰到类似的情况，所以开发者不太会去关心垃圾回收机制。

参考
--

* IBM article on "Memory leak patterns in JavaScript" (2007)
* Kangax article on how to register event handler and avoid memory leaks (2010)
* Performance


表达式与运算符 - JavaScript

表达式与运算符
=======

* « 上一页
* 下一页 »

本章描述了 JavaScript 的表达式和运算符，包括了赋值、比较、算数、位运算、逻辑、字符串、三元，等等。

一个完整详细的运算符列表和表达式可以参见参考。

运算符
---

JavaScript 拥有如下类型的运算符。本节描述了运算符和运算符的优先级。

* 赋值运算符（Assignment operators）
* 比较运算符（Comparison operators）
* 算数运算符（Arithmetic operators）
* 位运算符（Bitwise operators）
* 逻辑运算符（Logical operators）
* 字符串运算符（String operators）
* 条件（三元）运算符（Conditional operator）
* 逗号运算符（Comma operator）
* 一元运算符（Unary operators）
* 关系运算符（Relational operator）

JavaScript 拥有二元和一元运算符和一个特殊的三元运算符（条件运算符）。一个二元运算符需要两个操作数，分别在运算符的前面和后面：
```

操作数 1 运算符 操作数 2
```

例如，`3+4` 或 `x*y`。

一个一元运算符需要一个操作数，在运算符前面或后面：
```

运算符 操作数
```

或
```

操作数 运算符
```

例如，`x++` 或 `++x`。

### 赋值运算符

一个 赋值运算符 (assignment operator) (en-US) 将它右边操作数的值赋给它左边的操作数。最简单的赋值运算符是等于（`=`），它将右边的操作数值赋给左边的操作数。那么 `x = y` 就是将 y 的值赋给 x。

还有一些复合赋值操作符，它们是下表列出的这些操作的缩写：

| 名字 | 简写的操作符 | 含义 |
| --- | --- | --- |
| 赋值 (Assignment) (en-US) | `x = y` | `x = y` |
| 加法赋值 (Addition assignment) (en-US) | `x += y` | `x = x + y` |
| 减法赋值 (Subtraction assignment) (en-US) | `x -= y` | `x = x - y` |
| 乘法赋值 (Multiplication assignment) (en-US) | `x *= y` | `x = x * y` |
| 除法赋值 (Division assignment) (en-US) | `x /= y` | `x = x / y` |
| 求余赋值 (Remainder assignment) (en-US) | `x %= y` | `x = x % y` |
| 求幂赋值 (Exponentiation assignment) (en-US) | `x **= y` | `x = x ** y` |
| 左移位赋值 (Left shift assignment) (en-US) | `x <<= y` | `x = x << y` |
| 右移位赋值 (Right shift assignment) (en-US) | `x >>= y` | `x = x >> y` |
| 无符号右移位赋值 (Unsigned right shift assignment) (en-US) | `x >>>= y` | `x = x >>> y` |
| 按位与赋值 (Bitwise AND assignment) (en-US) | `x &= y` | `x = x & y` |
| 按位异或赋值 (Bitwise XOR assignment) (en-US) | `x ^= y` | `x = x ^ y` |
| 按位或赋值 (Bitwise OR assignment) (en-US) | `x |= y` | `x = x | y` |

#### 解构

对于更复杂的赋值，解构赋值语法是一个能从数组或对象对应的数组结构或对象字面量里提取数据的 Javascript 表达式。

```js
var foo = ["one", "two", "three"];

// 不使用解构
var one = foo[0];
var two = foo[1];
var three = foo[2];

// 使用解构
var [one, two, three] = foo;
```

### 比较运算符

比较运算符 (en-US)比较它的操作数并返回一个基于表达式是否为真的逻辑值。操作数可以是数字，字符串，逻辑，对象值。字符串比较是基于标准的字典顺序，使用 Unicode 值。在多数情况下，如果两个操作数不是相同的类型，JavaScript 会尝试转换它们为恰当的类型来比较。这种行为通常发生在数字作为操作数的比较。类型转换的例外是使用 `===` 和 `!==` 操作符，它们会执行严格的相等和不相等比较。这些运算符不会在检查相等之前转换操作数的类型。下面的表格描述了该示例代码中的各比较运算符

```js
var var1 = 3;
var var2 = 4;
```

| 运算符 | 描述 | 返回 true 的示例 |
| --- | --- | --- |
| 等于 Equal (en-US) (`==`) | 如果两边操作数相等时返回 true。 | `3 == var1` `"3" == var1` `3 == '3'` |
| 不等于 Not equal (en-US) (`!=`) | 如果两边操作数不相等时返回 true | `var1 != 4 var2 != "3"` |
| 全等 Strict equal (en-US) (`===`) | 两边操作数相等且类型相同时返回 true。参见 `Object.is` and sameness in JS. | `3 === var1` |
| 不全等 Strict not equal (en-US) (`!==`) | 两边操作数不相等或类型不同时返回 true。 | `var1 !== "3" 3 !== '3'` |
| 大于 Greater than (en-US) (`>`) | 左边的操作数大于右边的操作数返回 true | `var2 > var1 "12" > 2` |
| 大于等于 Greater than or equal (en-US) (`>=`) | 左边的操作数大于或等于右边的操作数返回 true | `var2 >= var1 var1 >= 3` |
| 小于 Less than (en-US) (`<`) | 左边的操作数小于右边的操作数返回 true | `var1 < var2 "2" < 12` |
| 小于等于 Less than or equal (en-US) (`<=`) | 左边的操作数小于或等于右边的操作数返回 true | `var1 <= var2 var2 <= 5` |

**备注：** （**=>**）不是运算符，而是箭头函数的标记符号。

### 算术运算符

算术运算符使用数值 (字面量或者变量) 作为操作数并返回一个数值。标准的算术运算符就是加减乘除 (+ - \* /)。当操作数是浮点数时，这些运算符表现得跟它们在大多数编程语言中一样（特殊要注意的是，除零会产生`Infinity`）。例如：

```js
1 / 2; // 0.5
1 / 2 == 1.0 / 2.0; // true
```

除了标准的算术运算符（+， - ，\* /），JavaScript 还提供了下表中的算术运算符。

| 运算符 | 描述 | 示例 |
| --- | --- | --- |
| 求余（`%`） | 二元运算符。返回相除之后的余数。 | 12 % 5 返回 2。 |
| 自增（`++`） | 一元运算符。将操作数的值加一。如果放在操作数前面（`++x`），则返回加一后的值；如果放在操作数后面（`x++`），则返回操作数原值，然后再将操作数加一。 | `var x=3;` `console.log(++x); //4` `console.log(x); //4` `var y=3;` `console.log(y++); //3` `console.log(y); //4` |
| 自减（`--`） | 一元运算符。将操作数的值减一。前后缀两种用法的返回值类似自增运算符。 | var x=3; console.log(--x); //输入 2,x=2var y=3;console.log(y--);//输出 3,x=2; |
| 一元负值符（`-`） | 一元运算符，返回操作数的负值。 | var x=3; console.log(-x); //输入 -3 |
| 一元正值符（+） | 一元运算符，如果操作数在之前不是数值，试图将其转换为数值。 | `console.log( +'3' ); // 3` `console.log( '3' ); // '3'` `console.log(+true); // 1` |
| 指数运算符（\*\*） | 计算底数（`base`）的指数（`exponent`）次方，表示为 `base^exponent`。 | `2 ** 3` 返回 `8`。`10 ** -1` 返回 `0.1`。 |

### 位运算符

位运算符将它的操作数视为 32 位元的二进制串（0 和 1 组成）而非十进制八进制或十六进制数。例如：十进制数字 9 用二进制表示为 1001，位运算符就是在这个二进制表示上执行运算，但是返回结果是标准的 JavaScript 数值。

下表总结了 JavaScript 的位运算符。

| Operator | Usage | Description |
| --- | --- | --- |
| 按位与 AND (en-US) | `a & b` | 在 a,b 的位表示中，每一个对应的位都为 1 则返回 1，否则返回 0. |
| 按位或 OR (en-US) | `a | b` | 在 a,b 的位表示中，每一个对应的位，只要有一个为 1 则返回 1，否则返回 0. |
| 按位异或 XOR (en-US) | `a ^ b` | 在 a,b 的位表示中，每一个对应的位，两个不相同则返回 1，相同则返回 0. |
| 按位非 NOT (en-US) | `~ a` | 反转被操作数的位。 |
| 左移 shift (en-US) | `a << b` | 将 a 的二进制串向左移动 b 位，右边移入 0. |
| 算术右移 | `a >> b` | 把 a 的二进制表示向右移动 b 位，丢弃被移出的所有位。(译注：算术右移左边空出的位是根据最高位是 0 和 1 来进行填充的) |
| 无符号右移 (左边空出位用 0 填充) | `a >>> b` | 把 a 的二进制表示向右移动 b 位，丢弃被移出的所有位，并把左边空出的位都填充为 0 |

#### 位逻辑运算符

概念上来讲，位逻辑运算符工作流程如下：

* 操作数被转换为 32bit 整數，以位序列（0 和 1 组成）表示。若超過 32bits，則取低位 32bit，如下所示：
```

  Before: 11100110111110100000000000000110000000000001
  After:              10100000000000000110000000000001
```

* 第一个操作数的每一位都与第二个操作数的对应位组对：第一位对应第一位，第二位对应第二位，以此类推。
* 运算符被应用到每一对"位"上，最终的运算结果由每一对“位”的运算结果组合起来。

例如，十进制数 9 的二进制表示是 1001，十进制数 15 的二进制表示是 1111.因此，当位运算符应用到这两个值时，结果如下：

| 表达式 | 结果 | 二进制描述 |
| --- | --- | --- |
| `15 & 9` | `9` | `1111 & 1001 = 1001` |
| `15 | 9` | `15` | `1111 | 1001 = 1111` |
| `15 ^ 9` | `6` | `1111 ^ 1001 = 0110` |
| `~15` | `-16` | `~ 0000 0000 … 0000 1111 = 1111 1111 … 1111 0000` |
| `~9` | `-10` | `~ 0000 0000 … 0000 1001 = 1111 1111 … 1111 0110` |

注意位运算符“非”将所有的 32 位取反，而值的最高位 (最左边的一位) 为 1 则表示负数 (2-补码表示法)。

#### 移位运算符

移位运算符带两个操作数：第一个是待移位的数，第二个是指定第一个数要被移多少位的数。移位的方向由运算符来控制。

移位运算符把操作数转为 32bit 整数，然后得出一个与待移位数相同种类的值。

移位运算符列表如下。

| 运算符 | 描述 | 示例 |
| --- | --- | --- |
| `<<`（左移位） | 将第一个操作数向左移动指定数量的位。左边移出位被抛弃。左边移出的几位被丢弃。右边多出的空位由 0 补齐。 | `9<<2` 产生 36，因为 1001 移位 2 比特向左变为 100100，它是 36。 |
| `>>`（带符号右移） | 将第一个操作数向右移动指定数量的位。右边移出位被抛弃。左边多出的空位由原值的最左边数字补齐。 | `9>>2` 产生 2，因为 1001 移位 2 位向右变为 10，其是 2。同样，`-9>>2` 产生 -3，由于符号被保留。 |
| `>>>`（补零右移） | 将第一个操作数向右移动指定数量的位。右边移出位被抛弃。左边多出的空位由 0 补齐。 | `19>>>2`产生 4，因为 10011 移位 2 位向右变为 100，它是 4。对非负数值，补零右移和带符号右移产生相同结果。 |

### 逻辑运算符

逻辑运算符常用于布尔（逻辑）值之间; 当操作数都是布尔值时，返回值也是布尔值。不过实际上`&&`和`||`返回的是一个特定的操作数的值，所以当它用于非布尔值的时候，返回值就可能是非布尔值。逻辑运算符的描述如下。

| 运算符 | 范例 | 描述 |
| --- | --- | --- |
| 逻辑与 (en-US) `(&&`) | `expr1 && expr2` | (逻辑与) 如果 expr1 能被转换为 false，那么返回 expr1；否则，返回`expr2`。因此`，&&`用于布尔值时，当操作数都为 true 时返回 true；否则返回 false. |
| 逻辑或 (en-US) (`||`) | `expr1 || expr2` | (逻辑或) 如果 expr1 能被转换为 true，那么返回 expr1；否则，返回`expr2`。因此，|| 用于布尔值时，当任何一个操作数为 true 则返回 true；如果操作数都是 false 则返回 false。 |
| 逻辑非 (en-US) `(!)` | `!expr` | (逻辑非) 如果操作数能够转换为 true 则返回 false；否则返回 true。 |

能被转换为`false`的值有`null`, `0`, `NaN`, 空字符串 ("") 和`undefined`。（译者注：也可以称作”falsy“）

下面是&&（逻辑"与"）操作符的示例。

```js
var a1 = true && true; // t && t returns true
var a2 = true && false; // t && f returns false
var a3 = false && true; // f && t returns false
var a4 = false && 3 == 4; // f && f returns false
var a5 = "Cat" && "Dog"; // t && t returns Dog
var a6 = false && "Cat"; // f && t returns false
var a7 = "Cat" && false; // t && f returns false
```

下面是 ||（逻辑"或"）操作符的示例。

```js
var o1 = true || true; // t || t returns true
var o2 = false || true; // f || t returns true
var o3 = true || false; // t || f returns true
var o4 = false || 3 == 4; // f || f returns false
var o5 = "Cat" || "Dog"; // t || t returns Cat
var o6 = false || "Cat"; // f || t returns Cat
var o7 = "Cat" || false; // t || f returns Cat
```

下面是！（逻辑"非"）操作符的示例。

```js
var n1 = !true; // !t returns false
var n2 = !false; // !f returns true
var n3 = !"Cat"; // !t returns false
```

#### 短路求值

作为逻辑表达式进行求值是从左到右，它们是为可能的“短路”的出现而使用以下规则进行测试：

* `false` && *anything* // 被短路求值为 false
* `true` || *anything* // 被短路求值为 true

逻辑的规则，保证这些评估是总是正确的。请注意，上述表达式的`anything`部分不会被求值，所以这样做不会产生任何副作用。

### 字符串运算符

除了比较操作符，它可以在字符串值中使用，连接操作符（+）连接两个字符串值相连接，返回另一个字符串，它是两个操作数串的结合。

例如，

```js
console.log("my " + "string"); // console logs the string "my string".
```

简写操作符 `+=` 也可以用来拼接字符串，例如：

```js
var myString = "alpha";

myString += "bet"; // 返回 "alphabet"
```

### 条件（三元）运算符

条件运算符是 JavaScript 中唯一需要三个操作数的运算符。运算的结果根据给定条件在两个值中取其一。语法为：
```

条件 ? 值 1 : 值 2
```

如果`条件`为真，则结果取`值 1`。否则为`值 2`。你能够在任何允许使用标准运算符的地方使用条件运算符。

例如，

```js
var status = age >= 18 ? "adult" : "minor";
```

当 `age` 大于等于 18 的时候，将“adult”赋值给 `status`；否则将“minor”赋值给 `status`。

### 逗号操作符

逗号操作符（`,`）对两个操作数进行求值并返回最终操作数的值。它常常用在 `for` 循环中，在每次循环时对多个变量进行更新。

例如，假如 `a` 是一个二维数组，每个维度各有 10 个元素，以下代码利用逗号操作符来同时改变两个变量的值。这段代码的功能是打印出该二维数组的对角线元素的值：

```js
var x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
var a = [x, x, x, x, x];

for (var i = 0, j = 9; i <= j; i++, j--)
  console.log("a[" + i + "][" + j + "]= " + a[i][j]);
```

### 一元操作符

一元操作符仅对应一个操作数。

#### `delete`

`delete`操作符，删除一个对象的属性或者一个数组中某一个键值。语法如下：

```js
delete objectName.property;
delete objectName[index];
delete property; // legal only within a with statement
```

`objectName`是一个对象名，`property` 是一个已经存在的属性，`index`是数组中的一个已经存在的键值的索引值。

第三行的形式只在`with`声明的状态下是合法的，从对象中删除一个属性。

你能使用 `delete` 删除各种各样的隐式声明，但是被`var`声明的除外。

如果 `delete` 操作成功，属性或者元素会变成 `undefined`。如果 `delete`可行会返回`true`，如果不成功返回`false`。

```js
x = 42;
var y = 43;
myobj = new Number();
myobj.h = 4; // create property h
delete x; // returns true (can delete if declared implicitly)
delete y; // returns false (cannot delete if declared with var)
delete Math.PI; // returns false (cannot delete predefined properties)
delete myobj.h; // returns true (can delete user-defined properties)
delete myobj; // returns true (can delete if declared implicitly)
```

##### 删除数组元素

删除数组中的元素时，数组的长度是不变的，例如删除 `a[3]`, `a[4]`，`a[4]` 和 `a[3]` 仍然存在变成了 `undefined`。

`delete` 删除数组中的一个元素，这个元素就不在数组中了。例如，`trees[3]`被删除，`trees[3]` 仍然可寻址并返回 `undefined`。

```js
var trees = new Array("redwood", "bay", "cedar", "oak", "maple");
delete trees[3];
if (3 in trees) {
  // 不会被执行
}
```

如果想让数组中存在一个元素但是是`undefined`值，使用`undefined`关键字而不是`delete`操作。如下： `trees[3] 分配一个 undefined`,但是这个数组元素仍然存在：

```js
var trees = new Array("redwood", "bay", "cedar", "oak", "maple");
trees[3] = undefined;
if (3 in trees) {
  // this gets executed（会被执行）
}
```

#### `typeof`

typeof 操作符 可通过下面 2 种方式使用：

```js
typeof operand;
typeof (operand);
```

typeof 操作符返回一个表示 operand 类型的字符串值。operand 可为字符串、变量、关键词或对象，其类型将被返回。operand 两侧的括号为可选。

假设你定义了如下的变量：

```js
var myFun = new Function("5 + 2");
var shape = "round";
var size = 1;
var today = new Date();
```

typeof 操作符将会返回如下的结果：

```js
typeof myFun; // returns "function"
typeof shape; // returns "string"
typeof size; // returns "number"
typeof today; // returns "object"
typeof dontExist; // returns "undefined"
```

对于关键词 `true` 和 `null`， `typeof` 操作符将会返回如下结果：

```js
typeof true; // returns "boolean"
typeof null; // returns "object"
```

对于一个数值或字符串， `typeof` 操作符将会返回如下结果：

```js
typeof 62; // returns "number"
typeof "Hello world"; // returns "string"
```

对于属性值，typeof 操作符将会返回属性所包含值的类型：

```js
typeof document.lastModified; // returns "string"
typeof window.length; // returns "number"
typeof Math.LN2; // returns "number"
```

对于方法和函数，typeof 操作符将会返回如下结果：

```js
typeof blur; // returns "function"
typeof eval; // returns "function"
typeof parseInt; // returns "function"
typeof shape.split; // returns "function"
```

对于预定义的对象，typeof 操作符将会返回如下结果：

```js
typeof Date; // returns "function"
typeof Function; // returns "function"
typeof Math; // returns "object"
typeof Option; // returns "function"
typeof String; // returns "function"
```

#### `void`

void 运算符运用方法如下：

```js
void expression;
void (expression);
```

void 运算符，表明一个运算没有返回值。expression 是 javaScript 表达式，括号中的表达式是一个可选项，当然使用该方式是一种好的形式。

你可以使用 void 运算符指明一个超文本链接。该表达式是有效的，但是并不会在当前文档中进行加载。

如下创建了一个超链接文本，当用户单击该文本时，不会有任何效果。

```html
<a href="javascript:void(0)">Click here to do nothing</a>
```

下面的代码创建了一个超链接，当用户单击它时，提交一个表单。

```html
<a href="javascript:void(document.form.submit())">Click here to submit</a>
```

### 关系运算符

关系运算符对操作数进行比较，根据比较结果真或假，返回相应的布尔值。

#### `in`

`in`操作符，如果所指定的**属性**确实存在于所指定的对象中，则会返回`true`，语法如下：

```js
propNameOrNumber in objectName;
```

在这里 `propNameOrNumber`可以是一个代表着属性名的字符串或者是一个代表着数组索引的数值表达式，而`objectName`则是一个对象名。

下面的例子是 `in` 操作的常见用法。

```js
// Arrays
var trees = new Array("redwood", "bay", "cedar", "oak", "maple");
0 in trees; // returns true
3 in trees; // returns true
6 in trees; // returns false
"bay" in trees; // returns false (you must specify the index number,
// not the value at that index)
"length" in trees; // returns true (length is an Array property)

// Predefined objects
"PI" in Math; // returns true
var myString = new String("coral");
"length" in myString; // returns true

// Custom objects
var mycar = { make: "Honda", model: "Accord", year: 1998 };
"make" in mycar; // returns true
"model" in mycar; // returns true
```

#### `instanceof`

如果所判别的对象确实是所指定的类型，则返回`true`。其语法如下：

```js
objectName instanceof objectType;
```

`objectName` 是需要做判别的对象的名称，而`objectType`是假定的对象的类型，例如`Date`或 `Array`.

当你需要确认一个对象在运行时的类型时，可使用`instanceof`. 例如，需要 catch 异常时，你可以针对抛出异常的类型，来做不同的异常处理。

例如，下面的代码使用 `instanceof` 去判断 `theDay` 是否是一个 `Date` 对象。因为 `theDay` 是一个 `Date` 对象，所以 `if` 中的代码会执行。

```js
var theDay = new Date(1995, 12, 17);
if (theDay instanceof Date) {
  // statements to execute
}
```

### 运算符优先级

运算符的优先级，用于确定一个表达式的计算顺序。在你不能确定优先级时，可以通过使用括号显式声明运算符的优先级。

下表列出了描述符的优先级，从最高到最低。

| Operator type | Individual operators |
| --- | --- |
| member | `. []` |
| call / create instance | `() new` |
| negation/increment | `! ~ - + ++ -- typeof void delete` |
| multiply/divide | `* / %` |
| addition/subtraction | `+ -` |
| bitwise shift | `<< >> >>>` |
| relational | `< <= > >= in instanceof` |
| equality | `== != === !==` |
| bitwise-and | `&` |
| bitwise-xor | `^` |
| bitwise-or | `|` |
| logical-and | `&&` |
| logical-or | `||` |
| conditional | `?:` |
| assignment | `= += -= *= /= %= <<= >>= >>>= &= ^= |=` |
| comma | `,` |

上表有一个更详细的版本，它包含了各操作符更详细的说明，可在 JavaScript 参考手册中找到。

表达式
---

表达式是一组代码的集合，它返回一个值。（译注：定义比较不好理解，看下面的举例就很好懂了。）

每一个合法的表达式都能计算成某个值，但从概念上讲，有两种类型的表达式：有副作用的（比如赋值）和单纯计算求值的。

表达式 x=7 是第一类型的一个例子。该表达式使用=运算符将值 7 赋予变量 x。这个表达式自己的值等于 7。

代码 3 + 4 是第二个表达式类型的一个例子。该表达式使用 + 运算符把 3 和 4 加到一起但并没有把结果（7）赋值给一个变量。

JavaScript 有以下表达式类型：

* 算数：得出一个数字，例如 3.14159。（通常使用算数运算符）
* 字符串：得出一个字符串，例如，"Fred" 或 "234"。（通常使用字符串运算符。）
* 逻辑值：得出 true 或者 false。（经常涉及到逻辑运算符。）
* 基本表达式：javascript 中基本的关键字和一般表达式。
* 左值表达式：分配给左值。
### 基本表达式

#### `this`

`this`关键字被用于指代当前的对象，通常，`this`指代的是方法中正在被调用的对象。用法如下：

```js
this["propertyName"];
this.propertyName;
```

假设一个用于验证对象`value`属性的`validate`函数，传参有对象，最高值和最低值。

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

你可以在任何表单元素的`onchange`事件处理中调用`validat`函数，用`this`来指代当前的表单元素，用例如下：

```html
<p>Enter a number between 18 and 99:</p>
<input type="text" name="age" size="3" onChange="validate(this, 18, 99);" />
```

#### 分组操作符

分组操作符（）控制了表达式中计算的优先级。举例来说，你可以改变先乘除后加减的顺序，转而先计算加法。

```js
var a = 1;
var b = 2;
var c = 3;

// 默认优先级
a + b \* c; // 7
// 默认是这样计算的
a + b \* c; // 7

// 现在使加法优先于乘法
(a + b) \* c; // 9

// 这等价于
a \* c + b \* c; // 9
```

##### 数值推导

Comprehensions 是一个带有实验性质的 JavaScript 特性，计划将在未来的 ECMAScript 版本中加入该特性。有两种类型的 comprehensions:

实验性 `[for (x of y) x]` (en-US)

数列数值推导（非标准用法）

实验性 `(for (x of y) y)` (en-US)

生成器数值推导（译者注：生成器数值推导标准化可能不大，推荐使用 生成器函数 (en-US)）

Comprehensions 特性被许多编程语言所采用，该特性能够使你快速地通过一个已有的数组来创建出一个新的数组，比如：

```js
[for (i of [ 1, 2, 3 ]) i\*i ];
// [ 1, 4, 9 ]

var abc = [ "A", "B", "C" ];
[for (letters of abc) letters.toLowerCase()];
// [ "a", "b", "c" ]
```

### 左值表达式

左值可以作为赋值的目标。

#### `new`

你可以使用`new` 运算符创建一个自定义类型或者是预置类型的对象实例。用法如下：

```js
var objectName = new objectType([param1, param2, ..., paramN]);
```

#### super

super 关键字可以用来调用一个对象父类的函数，它在用来调用一个类的父类的构造函数时非常有用，比如：
```

super([arguments]); // calls the parent constructor. super.functionOnParent([arguments]);
```

* « 上一页
* 下一页 »