## 六、对象的秘密

> 原文：[The Secret Life of Objects](http://eloquentjavascript.net/06_object.html)
> 
> 译者：[飞龙](https://github.com/wizardforcel)
> 
> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
> 
> 自豪地采用[谷歌翻译](https://translate.google.cn/)
> 
> 部分参考了[《JavaScript 编程精解（第 2 版）》](https://book.douban.com/subject/26707144/)

> 抽象数据类型是通过编写一种特殊的程序来实现的，该程序根据可在其上执行的操作来定义类型。
> 
> Barbara Liskov，《Programming with Abstract Data Types》

![](img/6-0.jpg)

第 4 章介绍了 JavaScript 的对象（object）。 在编程文化中，我们有一个名为面向对象编程（OOP）的东西，这是一组技术，使用对象（和相关概念）作为程序组织的中心原则。

虽然没有人真正同意其精确定义，但面向对象编程已经成为了许多编程语言的设计，包括 JavaScript 在内。 本章将描述这些想法在 JavaScript 中的应用方式。

## 封装

面向对象编程的核心思想是将程序分成小型片段，并让每个片段负责管理自己的状态。

通过这种方式，一些程序片段的工作方式的知识可以局部保留。 从事其他方面的工作的人，不必记住甚至不知道这些知识。 无论什么时候这些局部细节发生变化，只需要直接更新其周围的代码。

这种程序的不同片段通过接口（interface），函数或绑定的有限集合交互，它以更抽象的级别提供有用的功能，并隐藏它的精确实现。

这些程序片段使用对象建模。 它们的接口由一组特定的方法（method）和属性（property）组成。 接口的一部分的属性称为公共的（public）。 其他外部代码不应该接触属性的称为私有的（private）。

许多语言提供了区分公共和私有属性的方法，并且完全防止外部代码访问私有属性。 JavaScript 再次采用极简主义的方式，没有。 至少目前还没有 - 有个正在开展的工作，将其添加到该语言中。

即使这种语言没有内置这种区别，JavaScript 程序员也成功地使用了这种想法。 通常，可用的接口在文档或数字一中描述。 在属性名称的的开头经常会放置一个下划线（`_`）字符，来表明这些属性是私有的。

将接口与实现分离是一个好主意。 它通常被称为封装（encapsulation）。

## 方法

方法不过是持有函数值的属性。 这是一个简单的方法：

```js
let rabbit = {};
rabbit.speak = function(line) {
  console.log(`The rabbit says '${line}'`);
};

rabbit.speak("I'm alive.");
// → The rabbit says 'I'm alive.'
```

方法通常会在对象被调用时执行一些操作。将函数作为对象的方法调用时，会找到对象中对应的属性并直接调用。当函数作为方法调用时，函数体内叫做`this`的绑定自动指向在它上面调用的对象。

```js
function speak(line) {
  console.log(`The ${this.type} rabbit says '${line}'`);
}
let whiteRabbit = {type: "white", speak: speak};
let fatRabbit = {type: "fat", speak: speak};

whiteRabbit.speak("Oh my ears and whiskers, " +
                  "how late it's getting!");
// → The white rabbit says 'Oh my ears and whiskers, how
//   late it's getting!'
hungryRabbit.speak("I could use a carrot right now.");
// → The hungry rabbit says 'I could use a carrot right now.'
```

你可以把`this`看作是以不同方式传递的额外参数。 如果你想显式传递它，你可以使用函数的`call`方法，它接受`this`值作为第一个参数，并将其它处理为看做普通参数。

```js
speak.call(hungryRabbit, "Burp!");
// → The hungry rabbit says 'Burp!'
```

这段代码使用了关键字`this`来输出正在说话的兔子的种类。我们回想一下`apply`和`bind`方法，这两个方法接受的第一个参数可以用来模拟对象中方法的调用。这两个方法会把第一个参数复制给`this`。

由于每个函数都有自己的`this`绑定，它的值依赖于它的调用方式，所以在用`function`关键字定义的常规函数中，不能引用外层作用域的`this`。

箭头函数是不同的 - 它们不绑定他们自己的`this`，但可以看到他们周围（定义位置）作用域的`this`绑定。 因此，你可以像下面的代码那样，在局部函数中引用`this`：

```js
function normalize() {
  console.log(this.coords.map(n => n / this.length));
}
normalize.call({coords: [0, 2, 3], length: 5});
// → [0, 0.4, 0.6]
```

如果我使用`function`关键字将参数写入`map`，则代码将不起作用。

## 原型

我们来仔细看看以下这段代码。

```js
let empty = {};
console.log(empty.toString);
// → function toString(){…}
console.log(empty.toString());
// → [object Object]
```

我从一个空对象中取出了一个属性。 好神奇！

实际上并非如此。我只是掩盖了一些 JavaScript 对象的内部工作细节罢了。每个对象除了拥有自己的属性外，都包含一个原型（prototype）。原型是另一个对象，是对象的一个属性来源。当开发人员访问一个对象不包含的属性时，就会从对象原型中搜索属性，接着是原型的原型，依此类推。

那么空对象的原型是什么呢？是`Object.prototype`，它是所有对象中原型的父原型。

```js
console.log(Object.getPrototypeOf({}) ==
            Object.prototype);
// → true
console.log(Object.getPrototypeOf(Object.prototype));
// → null
```

正如你的猜测，`Object.getPrototypeOf`返回一个对象的原型。

JavaScript 对象原型的关系是一种树形结构，整个树形结构的根部就是`Object.prototype`。`Object.prototype`提供了一些可以在所有对象中使用的方法。比如说，`toString`方法可以将一个对象转换成其字符串表示形式。

许多对象并不直接将`Object.prototype`作为其原型，而会使用另一个原型对象，用于提供一系列不同的默认属性。函数继承自`Function.prototype`，而数组继承自`Array.prototype`。

```js
console.log(Object.getPrototypeOf(Math.max) ==
            Function.prototype);
// → true
console.log(Object.getPrototypeOf([]) ==
            Array.prototype);
// → true
```

对于这样的原型对象来说，其自身也包含了一个原型对象，通常情况下是`Object.prototype`，所以说，这些原型对象可以间接提供`toString`这样的方法。

你可以使用`Object.create`来创建一个具有特定原型的对象。

```js
let protoRabbit = {
  speak(line) {
    console.log(`The ${this.type} rabbit says '${line}'`);
  }
};
let killerRabbit = Object.create(protoRabbit);
killerRabbit.type = "killer";
killerRabbit.speak("SKREEEE!");
// → The killer rabbit says 'SKREEEE!'
```

像对象表达式中的`speak(line)`这样的属性是定义方法的简写。 它创建了一个名为`speak`的属性，并向其提供函数作为它的值。

原型对象`protoRabbit`是一个容器，用于包含所有兔子对象的公有属性。每个独立的兔子对象（比如`killerRabbit`）可以包含其自身属性（比如本例中的`type`属性），也可以派生其原型对象中公有的属性。

## 类

JavaScript 的原型系统可以解释为对一种面向对象的概念（称为类（class））的某种非正式实现。 类定义了对象的类型的形状 - 它具有什么方法和属性。 这样的对象被称为类的实例（instance）。

原型对于属性来说很实用。一个类的所有实例共享相同的属性值，例如方法。 每个实例上的不同属性，比如我们的兔子的`type`属性，需要直接存储在对象本身中。

所以为了创建一个给定类的实例，你必须使对象从正确的原型派生，但是你也必须确保，它本身具有这个类的实例应该具有的属性。 这是构造器（constructor）函数的作用。

```js
function makeRabbit(type) {
  let rabbit = Object.create(protoRabbit);
  rabbit.type = type;
  return rabbit;
}
```

JavaScript 提供了一种方法，来使得更容易定义这种类型的功能。 如果将关键字`new`放在函数调用之前，则该函数将被视为构造器。 这意味着具有正确原型的对象会自动创建，绑定到函数中的`this`，并在函数结束时返回。

构造对象时使用的原型对象，可以通过构造器的`prototype`属性来查找。

```js
function Rabbit(type) {
  this.type = type;
}

Rabbit.prototype.speak = function(line) {
  console.log(`The ${this.type} rabbit says '${line}'`);
};
let weirdRabbit = new Rabbit("weird");
```

构造器（实际上是所有函数）都会自动获得一个名为`prototype`的属性，默认情况下它包含一个普通的，来自`Object.prototype`的空对象。 如果需要，可以用新对象覆盖它。 或者，你可以将属性添加到现有对象，如示例所示。

按照惯例，构造器的名字是大写的，这样它们可以很容易地与其他函数区分开来。

重要的是，理解原型与构造器关联的方式（通过其`prototype`属性），与对象拥有原型（可以通过`Object.getPrototypeOf`查找）的方式之间的区别。 构造器的实际原型是`Function.prototype`，因为构造器是函数。 它的`prototype`属性拥有原型，用于通过它创建的实例。

```js
console.log(Object.getPrototypeOf(Rabbit) ==
            Function.prototype);
// → true
console.log(Object.getPrototypeOf(weirdRabbit) ==
            Rabbit.prototype);
// → true
```

## 类的表示法

所以 JavaScript 类是带有原型属性的构造器。 这就是他们的工作方式，直到 2015 年，这就是你编写他们的方式。 最近，我们有了一个不太笨拙的表示法。

```js
class Rabbit {
  constructor(type) {
    this.type = type;
  }
  speak(line) {
    console.log(`The ${this.type} rabbit says '${line}'`);
  }
}

let killerRabbit = new Rabbit("killer");
let blackRabbit = new Rabbit("black");
```

`class`关键字是类声明的开始，它允许我们在一个地方定义一个构造器和一组方法。 可以在声明的大括号内写入任意数量的方法。 一个名为`constructor`的对象受到特别处理。 它提供了实际的构造器，它将绑定到名称`"Rabbit"`。 其他函数被打包到该构造器的原型中。 因此，上面的类声明等同于上一节中的构造器定义。 它看起来更好。

类声明目前只允许方法 - 持有函数的属性 - 添加到原型中。 当你想在那里保存一个非函数值时，这可能会有点不方便。 该语言的下一个版本可能会改善这一点。 现在，你可以在定义该类后直接操作原型来创建这些属性。

像`function`一样，`class`可以在语句和表达式中使用。 当用作表达式时，它没有定义绑定，而只是将构造器作为一个值生成。 你可以在类表达式中省略类名称。

```js
let object = new class { getWord() { return "hello"; } };
console.log(object.getWord());
// → hello
```

## 覆盖派生的属性

将属性添加到对象时，无论它是否存在于原型中，该属性都会添加到对象本身中。 如果原型中已经有一个同名的属性，该属性将不再影响对象，因为它现在隐藏在对象自己的属性后面。

```js
Rabbit.prototype.teeth = "small";
console.log(killerRabbit.teeth);
// → small
killerRabbit.teeth = "long, sharp, and bloody";
console.log(killerRabbit.teeth);
// → long, sharp, and bloody
console.log(blackRabbit.teeth);
// → small
console.log(Rabbit.prototype.teeth);
// → small
```

下图简单地描述了代码执行后的情况。其中`Rabbit`和`Object`原型画在了`killerRabbit`之下，我们可以从原型中找到对象中没有的属性。

![](img/6-1.svg)

覆盖原型中存在的属性是很有用的特性。就像示例展示的那样，我们覆盖了`killerRabbit`的`teeth`属性，这可以用来描述实例（对象中更为泛化的类的实例）的特殊属性，同时又可以让简单对象从原型中获取标准的值。

覆盖也用于向标准函数和数组原型提供`toString`方法，与基本对象的原型不同。

```js
console.log(Array.prototype.toString ==
            Object.prototype.toString);
// → false
console.log([1, 2].toString());
// → 1,2
```

调用数组的`toString`方法后得到的结果与调用`.join(",")`的结果十分类似，即在数组的每个值之间插入一个逗号。而直接使用数组调用`Object.prototype.toString`则会产生一个完全不同的字符串。由于`Object`原型提供的`toString`方法并不了解数组结构，因此只会简单地输出一对方括号，并在方括号中间输出单词`"object"`和类型的名称。

```js
console.log(Object.prototype.toString.call([1, 2]));
// → [object Array]
```

## 映射

我们在上一章中看到了映射（map）这个词，用于一个操作，通过对元素应用函数来转换数据结构。 令人困惑的是，在编程时，同一个词也被用于相关而不同的事物。

映射（名词）是将值（键）与其他值相关联的数据结构。 例如，你可能想要将姓名映射到年龄。 为此可以使用对象。

```js
let ages = {
  Boris: 39,
  Liang: 22,
  Júlia: 62
};

console.log(`Júlia is ${ages["Júlia"]}`);
// → Júlia is 62
console.log("Is Jack's age known?", "Jack" in ages);
// → Is Jack's age known? false
console.log("Is toString's age known?", "toString" in ages);
// → Is toString's age known? true
```

在这里，对象的属性名称是人们的姓名，并且该属性的值为他们的年龄。 但是我们当然没有在我们的映射中列出任何名为`toString`的人。 似的，因为简单对象是从`Object.prototype`派生的，所以它看起来就像拥有这个属性。

因此，使用简单对象作为映射是危险的。 有几种可能的方法来避免这个问题。 首先，可以使用`null`原型创建对象。 如果将`null`传递给`Object.create`，那么所得到的对象将不会从`Object.prototype`派生，并且可以安全地用作映射。

```js
console.log("toString" in Object.create(null));
// → false
```

对象属性名称必须是字符串。 如果你需要一个映射，它的键不能轻易转换为字符串 - 比如对象 - 你不能使用对象作为你的映射。

幸运的是，JavaScript 带有一个叫做`Map`的类，它正是为了这个目的而编写。 它存储映射并允许任何类型的键。

```js
let ages = new Map();
ages.set("Boris", 39);
ages.set("Liang", 22);
ages.set("Júlia", 62);
console.log(`Júlia is ${ages.get("Júlia")}`);
// → Júlia is 62
console.log("Is Jack's age known?", ages.has("Jack"));
// → Is Jack's age known? false
console.log(ages.has("toString"));
 // → false
```

`set`，`get`和`has`方法是`Map`对象的接口的一部分。 编写一个可以快速更新和搜索大量值的数据结构并不容易，但我们不必担心这一点。 其他人为我们实现，我们可以通过这个简单的接口来使用他们的工作。

如果你确实有一个简单对象，出于某种原因需要将它视为一个映射，那么了解`Object.keys`只返回对象的自己的键，而不是原型中的那些键，会很有用。 作为`in`运算符的替代方法，你可以使用`hasOwnProperty`方法，该方法会忽略对象的原型。

```js
console.log({x: 1}.hasOwnProperty("x"));
// → true
console.log({x: 1}.hasOwnProperty("toString"));
// → false
```

## 多态

当你调用一个对象的`String`函数（将一个值转换为一个字符串）时，它会调用该对象的`toString`方法来尝试从它创建一个有意义的字符串。 我提到一些标准原型定义了自己的`toString`版本，因此它们可以创建一个包含比`"[object Object]"`有用信息更多的字符串。 你也可以自己实现。

```js
Rabbit.prototype.toString = function() {
  return `a ${this.type} rabbit`;
};

console.log(String(blackRabbit));
// → a black rabbit
```

这是一个强大的想法的简单实例。 当一段代码为了与某些对象协作而编写，这些对象具有特定接口时（在本例中为`toString`方法），任何类型的支持此接口的对象都可以插入到代码中，并且它将正常工作。

这种技术被称为多态（polymorphism）。 多态代码可以处理不同形状的值，只要它们支持它所期望的接口即可。

我在第四章中提到`for/of`循环可以遍历几种数据结构。 这是多态性的另一种情况 - 这样的循环期望数据结构公开的特定接口，数组和字符串是这样。 你也可以将这个接口添加到你自己的对象中！ 但在我们实现它之前，我们需要知道什么是符号。

## 符号

多个接口可能为不同的事物使用相同的属性名称。 例如，我可以定义一个接口，其中`toString`方法应该将对象转换为一段纱线。 一个对象不可能同时满足这个接口和`toString`的标准用法。

这是一个坏主意，这个问题并不常见。 大多数 JavaScript 程序员根本就不会去想它。 但是，语言设计师们正在思考这个问题，无论如何都为我们提供了解决方案。

当我声称属性名称是字符串时，这并不完全准确。 他们通常是，但他们也可以是符号（symbol）。 符号是使用`Symbol`函数创建的值。 与字符串不同，新创建的符号是唯一的 - 你不能两次创建相同的符号。

```js
let sym = Symbol("name");
console.log(sym == Symbol("name"));
// → false
Rabbit.prototype[sym] = 55;
console.log(blackRabbit[sym]);
// → 55
```

将`Symbol`转换为字符串时，会得到传递给它的字符串，例如，在控制台中显示时，符号可以更容易识别。 但除此之外没有任何意义 - 多个符号可能具有相同的名称。

由于符号既独特又可用于属性名称，因此符号适合定义可以和其他属性共生的接口，无论它们的名称是什么。

```js
const toStringSymbol = Symbol("toString");
Array.prototype[toStringSymbol] = function() {
  return `${this.length} cm of blue yarn`;
};
console.log([1, 2].toString());
// → 1,2
console.log([1, 2][toStringSymbol]());
// → 2 cm of blue yarn
```

通过在属性名称周围使用方括号，可以在对象表达式和类中包含符号属性。 这会导致属性名称的求值，就像方括号属性访问表示法一样，这允许我们引用一个持有该符号的绑定。

```js
let stringObject = {
  [toStringSymbol]() { return "a jute rope"; }
};
console.log(stringObject[toStringSymbol]());
// → a jute rope
```

## 迭代器接口

提供给`for/of`循环的对象预计为可迭代对象（iterable）。 这意味着它有一个以`Symbol.iterator`符号命名的方法（由语言定义的符号值，存储为`Symbol`符号的一个属性）。

当被调用时，该方法应该返回一个对象，它提供第二个接口迭代器（iterator）。 这是执行迭代的实际事物。 它拥有返回下一个结果的`next`方法。 这个结果应该是一个对象，如果有下一个值，`value`属性会提供它；没有更多结果时，`done`属性应该为`true`，否则为`false`。

请注意，`next`，`value`和`done`属性名称是纯字符串，而不是符号。 只有`Symbol.iterator`是一个实际的符号，它可能被添加到不同的大量对象中。

我们可以直接使用这个接口。

```js
let okIterator = "OK"[Symbol.iterator]();
console.log(okIterator.next());
// → {value: "O", done: false}
console.log(okIterator.next());
// → {value: "K", done: false}
console.log(okIterator.next());
// → {value: undefined, done: true}
```

我们来实现一个可迭代的数据结构。 我们将构建一个`matrix`类，充当一个二维数组。

```js
class Matrix {
  constructor(width, height, element = (x, y) => undefined) {
    this.width = width;
    this.height = height;
    this.content = [];
    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        this.content[y * width + x] = element(x, y);
      }
    }
  }
  get(x, y) {
    return this.content[y * this.width + x];
  }
  set(x, y, value) {
    this.content[y * this.width + x] = value;
  }
}
```

该类将其内容存储在`width × height`个元素的单个数组中。 元素是按行存储的，因此，例如，第五行中的第三个元素存储在位置`4 × width + 2`中（使用基于零的索引）。

构造器需要宽度，高度和一个可选的内容函数，用来填充初始值。 `get`和`set`方法用于检索和更新矩阵中的元素。

遍历矩阵时，通常对元素的位置以及元素本身感兴趣，所以我们会让迭代器产生具有`x`，`y`和`value`属性的对象。


```js
class MatrixIterator {
  constructor(matrix) {
    this.x = 0;
    this.y = 0;
    this.matrix = matrix;
  }
  next() {
    if (this.y == this.matrix.height) return {done: true};

    let value = {x: this.x,
                 y: this.y,
                 value: this.matrix.get(this.x, this.y)};
    this.x++;
    if (this.x == this.matrix.width) {
      this.x = 0;
      this.y++;
    }
    return {value, done: false};
  }
}
```

这个类在其`x`和`y`属性中跟踪遍历矩阵的进度。 `next`方法最开始检查是否到达矩阵的底部。 如果没有，则首先创建保存当前值的对象，之后更新其位置，如有必要则移至下一行。

让我们使`Matrix`类可迭代。 在本书中，我会偶尔使用事后的原型操作来为类添加方法，以便单个代码段保持较小且独立。 在一个正常的程序中，不需要将代码分成小块，而是直接在`class`中声明这些方法。

```js
Matrix.prototype[Symbol.iterator] = function() {
  return new MatrixIterator(this);
};
```

现在我们可以用`for/of`来遍历一个矩阵。

```js
let matrix = new Matrix(2, 2, (x, y) => `value ${x},${y}`);
for (let {x, y, value} of matrix) {
  console.log(x, y, value);
}
// → 0 0 value 0,0
// → 1 0 value 1,0
// → 0 1 value 0,1
// → 1 1 value 1,1
```

## 读写器和静态

接口通常主要由方法组成，但也可以持有非函数值的属性。 例如，`Map`对象有`size`属性，告诉你有多少个键存储在它们中。

这样的对象甚至不需要直接在实例中计算和存储这样的属性。 即使直接访问的属性也可能隐藏了方法调用。 这种方法称为读取器（getter），它们通过在方法名称前面编写`get`来定义。

```js
let varyingSize = {
  get size() {
    return Math.floor(Math.random() * 100);
  }
};
console.log(varyingSize.size);
// → 73
console.log(varyingSize.size);
// → 49
```

每当有人读取此对象的`size`属性时，就会调用相关的方法。 当使用写入器（setter）写入属性时，可以做类似的事情。


```js
class Temperature {
  constructor(celsius) {
    this.celsius = celsius;
  }
  get fahrenheit() {
    return this.celsius * 1.8 + 32;
  }
  set fahrenheit(value) {
    this.celsius = (value - 32) / 1.8;
  }

  static fromFahrenheit(value) {
    return new Temperature((value - 32) / 1.8);
  }
}
let temp = new Temperature(22);
console.log(temp.fahrenheit);
// → 71.6
temp.fahrenheit = 86;
console.log(temp.celsius);
// → 30
```

`Temperature`类允许你以摄氏度或华氏度读取和写入温度，但内部仅存储摄氏度，并在`fahrenheit`读写器中自动转换为摄氏度。

有时候你想直接向你的构造器附加一些属性，而不是原型。 这样的方法将无法访问类实例，但可以用来提供额外方法来创建实例。

在类声明内部，名称前面写有`static`的方法，存储在构造器中。 所以`Temperature`类可以让你写出`Temperature.fromFahrenheit(100)`，来使用华氏温度创建一个温度。

## 继承

已知一些矩阵是对称的。 如果沿左上角到右下角的对角线翻转对称矩阵，它保持不变。 换句话说，存储在`x,y`的值总是与`y,x`相同。

想象一下，我们需要一个像`Matrix`这样的数据结构，但是它必需保证一个事实，矩阵是对称的。 我们可以从头开始编写它，但这需要重复一些代码，与我们已经写过的代码很相似。

JavaScript 的原型系统可以创建一个新类，就像旧类一样，但是它的一些属性有了新的定义。 新类派生自旧类的原型，但为`set`方法增加了一个新的定义。

在面向对象的编程术语中，这称为继承（inheritance）。 新类继承旧类的属性和行为。

```js
class SymmetricMatrix extends Matrix {
  constructor(size, element = (x, y) => undefined) {
    super(size, size, (x, y) => {
      if (x < y) return element(y, x);
      else return element(x, y);
    });
  }

  set(x, y, value) {
    super.set(x, y, value);
    if (x != y) {
      super.set(y, x, value);
    }
  }
}
let matrix = new SymmetricMatrix(5, (x, y) => `${x},${y}`);
console.log(matrix.get(2, 3));
// → 3,2
```

`extends`这个词用于表示，这个类不应该直接基于默认的`Object`原型，而应该基于其他类。 这被称为超类（superclass）。 派生类是子类（subclass）。

为了初始化`SymmetricMatrix`实例，构造器通过`super`关键字调用其超类的构造器。 这是必要的，因为如果这个新对象的行为（大致）像`Matrix`，它需要矩阵具有的实例属性。 为了确保矩阵是对称的，构造器包装了`content`方法，来交换对角线以下的值的坐标。

`set`方法再次使用`super`，但这次不是调用构造器，而是从超类的一组方法中调用特定的方法。 我们正在重新定义`set`，但是想要使用原来的行为。 因为`this.set`引用新的`set`方法，所以调用这个方法是行不通的。 在类方法内部，`super`提供了一种方法，来调用超类中定义的方法。

继承允许我们用相对较少的工作，从现有数据类型构建稍微不同的数据类型。 它是面向对象传统的基础部分，与封装和多态一样。 尽管后两者现在普遍被认为是伟大的想法，但继承更具争议性。

尽管封装和多态可用于将代码彼此分离，从而减少整个程序的耦合，但继承从根本上将类连接在一起，从而产生更多的耦合。 继承一个类时，比起单纯使用它，你通常必须更加了解它如何工作。 继承可能是一个有用的工具，并且我现在在自己的程序中使用它，但它不应该成为你的第一个工具，你可能不应该积极寻找机会来构建类层次结构（类的家族树）。

## `instanceof`运算符

在有些时候，了解某个对象是否继承自某个特定类，也是十分有用的。JavaScript 为此提供了一个二元运算符，名为`instanceof`。

```js
console.log(
  new SymmetricMatrix(2) instanceof SymmetricMatrix);
// → true
console.log(new SymmetricMatrix(2) instanceof Matrix);
// → true
console.log(new Matrix(2, 2) instanceof SymmetricMatrix);
// → false
console.log([1] instanceof Array);
// → true
```

该运算符会浏览所有继承类型。所以`SymmetricMatrix`是`Matrix`的一个实例。 该运算符也可以应用于像`Array`这样的标准构造器。 几乎每个对象都是`Object`的一个实例。

## 本章小结

对象不仅仅持有它们自己的属性。对象中有另一个对象：原型，只要原型中包含了属性，那么根据原型构造出来的对象也就可以看成包含了相应的属性。简单对象直接以`Object.prototype`作为原型。

构造器是名称通常以大写字母开头的函数，可以与`new`运算符一起使用来创建新对象。 新对象的原型是构造器的`prototype`属性中的对象。 通过将属性放到它们的原型中，可以充分利用这一点，给定类型的所有值在原型中分享它们的属性。 `class`表示法提供了一个显式方法，来定义一个构造器及其原型。

你可以定义读写器，在每次访问对象的属性时秘密地调用方法。 静态方法是存储在类的构造器，而不是其原型中的方法。

给定一个对象和一个构造器，`instanceof`运算符可以告诉你该对象是否是该构造器的一个实例。

可以使用对象的来做一个有用的事情是，为它们指定一个接口，告诉每个人他们只能通过该接口与对象通信。 构成对象的其余细节，现在被封装在接口后面。

不止一种类型可以实现相同的接口。 为使用接口而编写的代码，自动知道如何使用提供接口的任意数量的不同对象。 这被称为多态。

实现多个类，它们仅在一些细节上有所不同的时，将新类编写为现有类的子类，继承其一部分行为会很有帮助。

## 习题

### 向量类型

编写一个`Vec` 类，它表示二维空间中的一个向量。它接受`x`和`y`参数(数字)，并将其保存到对象的同名属性中。

向`Vec`原型添加两个方法：`plus`和`minus`，它们接受另一个向量作为参数，分别返回两个向量（一个是`this`，另一个是参数）的和向量与差向量。

向原型添加一个`getter`属性`length`，用于计算向量长度，即点`(x,y)`与原点`(0,0)`之间的距离。

```js
// Your code here.

console.log(new Vec(1, 2).plus(new Vec(2, 3)));
// → Vec{x: 3, y: 5}
console.log(new Vec(1, 2).minus(new Vec(2, 3)));
// → Vec{x: -1, y: -1}
console.log(new Vec(3, 4).length);
// → 5
```

### 分组

标准的 JavaScript 环境提供了另一个名为`Set`的数据结构。 像`Map`的实例一样，集合包含一组值。 与`Map`不同，它不会将其他值与这些值相关联 - 它只会跟踪哪些值是该集合的一部分。 一个值只能是一个集合的一部分 - 再次添加它没有任何作用。

写一个名为`Group`的类（因为`Set`已被占用）。 像`Set`一样，它具有`add`，`delete`和`has`方法。 它的构造器创建一个空的分组，`add`给分组添加一个值（但仅当它不是成员时），`delete`从组中删除它的参数（如果它是成员），`has` 返回一个布尔值，表明其参数是否为分组的成员。

使用`===`运算符或类似于`indexOf`的东西来确定两个值是否相同。

为该类提供一个静态的`from`方法，该方法接受一个可迭代的对象作为参数，并创建一个分组，包含遍历它产生的所有值。

```js
// Your code here.

class Group {
  // Your code here.
}
let group = Group.from([10, 20]);
console.log(group.has(10));
// → true
console.log(group.has(30));
// → false
group.add(10);
group.delete(10);
console.log(group.has(10));
// → false
```

### 可迭代分组

使上一个练习中的`Group`类可迭代。 如果你不清楚接口的确切形式，请参阅本章前面迭代器接口的章节。

如果你使用数组来表示分组的成员，则不要仅仅通过调用数组中的`Symbol.iterator`方法来返回迭代器。 这会起作用，但它会破坏这个练习的目的。

如果分组被修改时，你的迭代器在迭代过程中出现奇怪的行为，那也没问题。

```js
// Your code here (and the code from the previous exercise)

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

## 借鉴方法

在本章前面我提到，当你想忽略原型的属性时，对象的`hasOwnProperty`可以用作`in`运算符的更强大的替代方法。 但是如果你的映射需要包含`hasOwnProperty`这个词呢？ 你将无法再调用该方法，因为对象的属性隐藏了方法值。

你能想到一种方法，对拥有自己的同名属性的对象，调用`hasOwnProperty`吗？

```js
let map = {one: true, two: true, hasOwnProperty: true};
// Fix this call
console.log(map.hasOwnProperty("one"));
// → true
```
