mirror of
https://github.com/apachecn/eloquent-js-3e-zh.git
synced 2025-05-23 20:02:20 +00:00
6
This commit is contained in:
parent
a8deced354
commit
f304d920b4
162
6.md
162
6.md
@ -1,9 +1,21 @@
|
||||
## 六、对象的秘密
|
||||
|
||||
> 原文:[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》
|
||||
|
||||

|
||||
|
||||
第 4 章介绍了 JavaScript 的对象(object)。 在编程文化中,我们有一个名为面向对象编程(OOP)的东西,这是一组技术,使用对象(和相关概念)作为程序组织的中心原则。
|
||||
|
||||
虽然没有人真正同意其精确定义,但面向对象编程已经成为了许多编程语言的设计,包括 JavaScript 在内。 本章将描述这些想法在 JavaScript 中的应用方式。
|
||||
@ -28,7 +40,7 @@
|
||||
|
||||
方法不过是持有函数值的属性。 这是一个简单的方法:
|
||||
|
||||
```
|
||||
```js
|
||||
let rabbit = {};
|
||||
rabbit.speak = function(line) {
|
||||
console.log(`The rabbit says '${line}'`);
|
||||
@ -40,7 +52,7 @@ rabbit.speak("I'm alive.");
|
||||
|
||||
方法通常会在对象被调用时执行一些操作。将函数作为对象的方法调用时,会找到对象中对应的属性并直接调用。当函数作为方法调用时,函数体内叫做`this`的绑定自动指向在它上面调用的对象。
|
||||
|
||||
```
|
||||
```js
|
||||
function speak(line) {
|
||||
console.log(`The ${this.type} rabbit says '${line}'`);
|
||||
}
|
||||
@ -57,18 +69,18 @@ hungryRabbit.speak("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`来输出正在说话的兔子的种类。我们回想一下`apply`和`bind`方法,这两个方法接受的第一个参数可以用来模拟对象中方法的调用。这两个方法会把第一个参数复制给`this`。
|
||||
|
||||
由于每个函数都有自己的`this`绑定,它的值依赖于它的调用方式,所以在用`function`关键字定义的常规函数中,不能引用外层作用域的`this`。
|
||||
|
||||
箭头函数是不同的 - 它们不绑定他们自己的`this`,但可以看到他们周围(定义位置)作用域的`this`绑定。 因此,你可以像下面的代码那样,在局部函数中引用`this`:
|
||||
|
||||
```
|
||||
```js
|
||||
function normalize() {
|
||||
console.log(this.coords.map(n => n / this.length));
|
||||
}
|
||||
@ -82,7 +94,7 @@ normalize.call({coords: [0, 2, 3], length: 5});
|
||||
|
||||
我们来仔细看看以下这段代码。
|
||||
|
||||
```
|
||||
```js
|
||||
let empty = {};
|
||||
console.log(empty.toString);
|
||||
// → function toString(){…}
|
||||
@ -92,11 +104,11 @@ console.log(empty.toString());
|
||||
|
||||
我从一个空对象中取出了一个属性。 好神奇!
|
||||
|
||||
实际上并非如此。我只是掩盖了一些JavaScript对象的内部工作细节罢了。每个对象除了拥有自己的属性外,都包含一个原型(prototype)。原型是另一个对象,是对象的一个属性来源。当开发人员访问一个对象不包含的属性时,就会从对象原型中搜索属性,接着是原型的原型,依此类推。
|
||||
实际上并非如此。我只是掩盖了一些 JavaScript 对象的内部工作细节罢了。每个对象除了拥有自己的属性外,都包含一个原型(prototype)。原型是另一个对象,是对象的一个属性来源。当开发人员访问一个对象不包含的属性时,就会从对象原型中搜索属性,接着是原型的原型,依此类推。
|
||||
|
||||
那么空对象的原型是什么呢?是Object.prototype,它是所有对象中原型的父原型。
|
||||
那么空对象的原型是什么呢?是`Object.prototype`,它是所有对象中原型的父原型。
|
||||
|
||||
```
|
||||
```js
|
||||
console.log(Object.getPrototypeOf({}) ==
|
||||
Object.prototype);
|
||||
// → true
|
||||
@ -106,11 +118,11 @@ console.log(Object.getPrototypeOf(Object.prototype));
|
||||
|
||||
正如你的猜测,`Object.getPrototypeOf`返回一个对象的原型。
|
||||
|
||||
JavaScript对象原型的关系是一种树形结构,整个树形结构的根部就是Object.prototype。Object.prototype提供了一些可以在所有对象中使用的方法。比如说,toString方法可以将一个对象转换成其字符串表示形式。
|
||||
JavaScript 对象原型的关系是一种树形结构,整个树形结构的根部就是`Object.prototype`。`Object.prototype`提供了一些可以在所有对象中使用的方法。比如说,`toString`方法可以将一个对象转换成其字符串表示形式。
|
||||
|
||||
许多对象并不直接将Object.prototype作为其原型,而会使用另一个原型对象,用于提供一系列不同的默认属性。函数继承自Function.prototype,而数组继承自Array.prototype。
|
||||
许多对象并不直接将`Object.prototype`作为其原型,而会使用另一个原型对象,用于提供一系列不同的默认属性。函数继承自`Function.prototype`,而数组继承自`Array.prototype`。
|
||||
|
||||
```
|
||||
```js
|
||||
console.log(Object.getPrototypeOf(Math.max) ==
|
||||
Function.prototype);
|
||||
// → true
|
||||
@ -119,11 +131,11 @@ console.log(Object.getPrototypeOf([]) ==
|
||||
// → true
|
||||
```
|
||||
|
||||
对于这样的原型对象来说,其自身也包含了一个原型对象,通常情况下是Object.prototype,所以说,这些原型对象可以间接提供toString这样的方法。
|
||||
对于这样的原型对象来说,其自身也包含了一个原型对象,通常情况下是`Object.prototype`,所以说,这些原型对象可以间接提供`toString`这样的方法。
|
||||
|
||||
你可以使用`Object.create`来创建一个具有特定原型的对象。
|
||||
|
||||
```
|
||||
```js
|
||||
let protoRabbit = {
|
||||
speak(line) {
|
||||
console.log(`The ${this.type} rabbit says '${line}'`);
|
||||
@ -137,7 +149,7 @@ killerRabbit.speak("SKREEEE!");
|
||||
|
||||
像对象表达式中的`speak(line)`这样的属性是定义方法的简写。 它创建了一个名为`speak`的属性,并向其提供函数作为它的值。
|
||||
|
||||
原型对象protoRabbit是一个容器,用于包含所有兔子对象的公有属性。每个独立的兔子对象(比如killerRabbit)可以包含其自身属性(比如本例中的type属性),也可以派生其原型对象中公有的属性。
|
||||
原型对象`protoRabbit`是一个容器,用于包含所有兔子对象的公有属性。每个独立的兔子对象(比如`killerRabbit`)可以包含其自身属性(比如本例中的`type`属性),也可以派生其原型对象中公有的属性。
|
||||
|
||||
### 类
|
||||
|
||||
@ -147,7 +159,7 @@ JavaScript 的原型系统可以解释为对一种面向对象的概念(称为
|
||||
|
||||
所以为了创建一个给定类的实例,你必须使对象从正确的原型派生,但是你也必须确保,它本身具有这个类的实例应该具有的属性。 这是构造器(constructor)函数的作用。
|
||||
|
||||
```
|
||||
```js
|
||||
function makeRabbit(type) {
|
||||
let rabbit = Object.create(protoRabbit);
|
||||
rabbit.type = type;
|
||||
@ -159,7 +171,7 @@ JavaScript 提供了一种方法,来使得更容易定义这种类型的功能
|
||||
|
||||
构造对象时使用的原型对象,可以通过构造器的`prototype`属性来查找。
|
||||
|
||||
```
|
||||
```js
|
||||
function Rabbit(type) {
|
||||
this.type = type;
|
||||
}
|
||||
@ -170,13 +182,13 @@ Rabbit.prototype.speak = function(line) {
|
||||
let weirdRabbit = new Rabbit("weird");
|
||||
```
|
||||
|
||||
构造器(实际上是所有函数)都会自动获得一个名为`prototype`的属性,默认情况下它包含一个普通的,来自`Object.prototype`的空对象。 如果需要,可以用新对象覆盖它。 或者,您可以将属性添加到现有对象,如示例所示。
|
||||
构造器(实际上是所有函数)都会自动获得一个名为`prototype`的属性,默认情况下它包含一个普通的,来自`Object.prototype`的空对象。 如果需要,可以用新对象覆盖它。 或者,你可以将属性添加到现有对象,如示例所示。
|
||||
|
||||
按照惯例,构造器的名字是大写的,这样它们可以很容易地与其他函数区分开来。
|
||||
|
||||
重要的是,理解原型与构造器关联的方式(通过其`prototype`属性),与对象拥有原型(可以通过`Object.getPrototypeOf`查找)的方式之间的区别。 构造器的实际原型是`Function.prototype`,因为构造器是函数。 它的`prototype`属性拥有原型,用于通过它创建的实例。
|
||||
|
||||
```
|
||||
```js
|
||||
console.log(Object.getPrototypeOf(Rabbit) ==
|
||||
Function.prototype);
|
||||
// → true
|
||||
@ -189,7 +201,7 @@ console.log(Object.getPrototypeOf(weirdRabbit) ==
|
||||
|
||||
所以 JavaScript 类是带有原型属性的构造器。 这就是他们的工作方式,直到 2015 年,这就是你编写他们的方式。 最近,我们有了一个不太笨拙的表示法。
|
||||
|
||||
```
|
||||
```js
|
||||
class Rabbit {
|
||||
constructor(type) {
|
||||
this.type = type;
|
||||
@ -205,11 +217,11 @@ let blackRabbit = new Rabbit("black");
|
||||
|
||||
`class`关键字是类声明的开始,它允许我们在一个地方定义一个构造器和一组方法。 可以在声明的大括号内写入任意数量的方法。 一个名为`constructor`的对象受到特别处理。 它提供了实际的构造器,它将绑定到名称`"Rabbit"`。 其他函数被打包到该构造器的原型中。 因此,上面的类声明等同于上一节中的构造器定义。 它看起来更好。
|
||||
|
||||
类声明目前只允许方法 - 持有函数的属性 - 添加到原型中。 当你想在那里保存一个非函数值时,这可能会有点不方便。 该语言的下一个版本可能会改善这一点。 现在,您可以在定义该类后直接操作原型来创建这些属性。
|
||||
类声明目前只允许方法 - 持有函数的属性 - 添加到原型中。 当你想在那里保存一个非函数值时,这可能会有点不方便。 该语言的下一个版本可能会改善这一点。 现在,你可以在定义该类后直接操作原型来创建这些属性。
|
||||
|
||||
像`function`一样,`class`可以在语句和表达式中使用。 当用作表达式时,它没有定义绑定,而只是将构造函数作为一个值生成。 您可以在类表达式中省略类名称。
|
||||
像`function`一样,`class`可以在语句和表达式中使用。 当用作表达式时,它没有定义绑定,而只是将构造器作为一个值生成。 你可以在类表达式中省略类名称。
|
||||
|
||||
```
|
||||
```js
|
||||
let object = new class { getWord() { return "hello"; } };
|
||||
console.log(object.getWord());
|
||||
// → hello
|
||||
@ -219,7 +231,7 @@ console.log(object.getWord());
|
||||
|
||||
将属性添加到对象时,无论它是否存在于原型中,该属性都会添加到对象本身中。 如果原型中已经有一个同名的属性,该属性将不再影响对象,因为它现在隐藏在对象自己的属性后面。
|
||||
|
||||
```
|
||||
```js
|
||||
Rabbit.prototype.teeth = "small";
|
||||
console.log(killerRabbit.teeth);
|
||||
// → small
|
||||
@ -232,15 +244,15 @@ console.log(Rabbit.prototype.teeth);
|
||||
// → small
|
||||
```
|
||||
|
||||
下图简单地描述了代码执行后的情况。其中Rabbit和Object原型画在了killerRabbit之下,我们可以从原型中找到对象中没有的属性。
|
||||
下图简单地描述了代码执行后的情况。其中`Rabbit`和`Object`原型画在了`killerRabbit`之下,我们可以从原型中找到对象中没有的属性。
|
||||
|
||||

|
||||

|
||||
|
||||
覆盖原型中存在的属性是很有用的特性。就像示例展示的那样,我们覆盖了killerRabbit的teeth属性,这可以用来描述实例(对象中更为泛化的类的实例)的特殊属性,同时又可以让简单对象从原型中获取标准的值。
|
||||
覆盖原型中存在的属性是很有用的特性。就像示例展示的那样,我们覆盖了`killerRabbit`的`teeth`属性,这可以用来描述实例(对象中更为泛化的类的实例)的特殊属性,同时又可以让简单对象从原型中获取标准的值。
|
||||
|
||||
覆盖也用于向标准函数和数组原型提供`toString`方法,与基本对象的原型不同。
|
||||
|
||||
```
|
||||
```js
|
||||
console.log(Array.prototype.toString ==
|
||||
Object.prototype.toString);
|
||||
// → false
|
||||
@ -248,9 +260,9 @@ console.log([1, 2].toString());
|
||||
// → 1,2
|
||||
```
|
||||
|
||||
调用数组的toString方法后得到的结果与调用.join(“,”)的结果十分类似,即在数组的每个值之间插入一个逗号。而直接使用数组调用Object.prototype.toString则会产生一个完全不同的字符串。由于Object原型提供的toString方法并不了解数组结构,因此只会简单地输出一对方括号,并在方括号中间输出单词“object”和类型的名称。
|
||||
调用数组的`toString`方法后得到的结果与调用`.join(",")`的结果十分类似,即在数组的每个值之间插入一个逗号。而直接使用数组调用`Object.prototype.toString`则会产生一个完全不同的字符串。由于`Object`原型提供的`toString`方法并不了解数组结构,因此只会简单地输出一对方括号,并在方括号中间输出单词`"object"`和类型的名称。
|
||||
|
||||
```
|
||||
```js
|
||||
console.log(Object.prototype.toString.call([1, 2]));
|
||||
// → [object Array]
|
||||
```
|
||||
@ -259,9 +271,9 @@ console.log(Object.prototype.toString.call([1, 2]));
|
||||
|
||||
我们在上一章中看到了映射(map)这个词,用于一个操作,通过对元素应用函数来转换数据结构。 令人困惑的是,在编程时,同一个词也被用于相关而不同的事物。
|
||||
|
||||
映射(名词)是将值(键)与其他值相关联的数据结构。 例如,您可能想要将姓名映射到年龄。 为此可以使用对象。
|
||||
映射(名词)是将值(键)与其他值相关联的数据结构。 例如,你可能想要将姓名映射到年龄。 为此可以使用对象。
|
||||
|
||||
```
|
||||
```js
|
||||
let ages = {
|
||||
Boris: 39,
|
||||
Liang: 22,
|
||||
@ -280,7 +292,7 @@ console.log("Is toString's age known?", "toString" in ages);
|
||||
|
||||
因此,使用简单对象作为映射是危险的。 有几种可能的方法来避免这个问题。 首先,可以使用`null`原型创建对象。 如果将`null`传递给`Object.create`,那么所得到的对象将不会从`Object.prototype`派生,并且可以安全地用作映射。
|
||||
|
||||
```
|
||||
```js
|
||||
console.log("toString" in Object.create(null));
|
||||
// → false
|
||||
```
|
||||
@ -289,7 +301,7 @@ console.log("toString" in Object.create(null));
|
||||
|
||||
幸运的是,JavaScript 带有一个叫做`Map`的类,它正是为了这个目的而编写。 它存储映射并允许任何类型的键。
|
||||
|
||||
```
|
||||
```js
|
||||
let ages = new Map();
|
||||
ages.set("Boris", 39);
|
||||
ages.set("Liang", 22);
|
||||
@ -304,9 +316,9 @@ console.log(ages.has("toString"));
|
||||
|
||||
`set`,`get`和`has`方法是`Map`对象的接口的一部分。 编写一个可以快速更新和搜索大量值的数据结构并不容易,但我们不必担心这一点。 其他人为我们实现,我们可以通过这个简单的接口来使用他们的工作。
|
||||
|
||||
如果您确实有一个简单对象,出于某种原因需要将它视为一个映射,那么了解`Object.keys`只返回对象的自己的键,而不是原型中的那些键,会很有用。 作为`in`运算符的替代方法,您可以使用`hasOwnProperty`方法,该方法会忽略对象的原型。
|
||||
如果你确实有一个简单对象,出于某种原因需要将它视为一个映射,那么了解`Object.keys`只返回对象的自己的键,而不是原型中的那些键,会很有用。 作为`in`运算符的替代方法,你可以使用`hasOwnProperty`方法,该方法会忽略对象的原型。
|
||||
|
||||
```
|
||||
```js
|
||||
console.log({x: 1}.hasOwnProperty("x"));
|
||||
// → true
|
||||
console.log({x: 1}.hasOwnProperty("toString"));
|
||||
@ -317,7 +329,7 @@ console.log({x: 1}.hasOwnProperty("toString"));
|
||||
|
||||
当你调用一个对象的`String`函数(将一个值转换为一个字符串)时,它会调用该对象的`toString`方法来尝试从它创建一个有意义的字符串。 我提到一些标准原型定义了自己的`toString`版本,因此它们可以创建一个包含比`"[object Object]"`有用信息更多的字符串。 你也可以自己实现。
|
||||
|
||||
```
|
||||
```js
|
||||
Rabbit.prototype.toString = function() {
|
||||
return `a ${this.type} rabbit`;
|
||||
};
|
||||
@ -338,9 +350,9 @@ console.log(String(blackRabbit));
|
||||
|
||||
这是一个坏主意,这个问题并不常见。 大多数 JavaScript 程序员根本就不会去想它。 但是,语言设计师们正在思考这个问题,无论如何都为我们提供了解决方案。
|
||||
|
||||
当我声称属性名称是字符串时,这并不完全准确。 他们通常是,但他们也可以是符号(symbol)。 符号是使用`Symbol`函数创建的值。 与字符串不同,新创建的符号是唯一的 - 您不能两次创建相同的符号。
|
||||
当我声称属性名称是字符串时,这并不完全准确。 他们通常是,但他们也可以是符号(symbol)。 符号是使用`Symbol`函数创建的值。 与字符串不同,新创建的符号是唯一的 - 你不能两次创建相同的符号。
|
||||
|
||||
```
|
||||
```js
|
||||
let sym = Symbol("name");
|
||||
console.log(sym == Symbol("name"));
|
||||
// → false
|
||||
@ -353,7 +365,7 @@ console.log(blackRabbit[sym]);
|
||||
|
||||
由于符号既独特又可用于属性名称,因此符号适合定义可以和其他属性共生的接口,无论它们的名称是什么。
|
||||
|
||||
```
|
||||
```js
|
||||
const toStringSymbol = Symbol("toString");
|
||||
Array.prototype[toStringSymbol] = function() {
|
||||
return `${this.length} cm of blue yarn`;
|
||||
@ -366,7 +378,7 @@ console.log([1, 2][toStringSymbol]());
|
||||
|
||||
通过在属性名称周围使用方括号,可以在对象表达式和类中包含符号属性。 这会导致属性名称的求值,就像方括号属性访问表示法一样,这允许我们引用一个持有该符号的绑定。
|
||||
|
||||
```
|
||||
```js
|
||||
let stringObject = {
|
||||
[toStringSymbol]() { return "a jute rope"; }
|
||||
};
|
||||
@ -384,7 +396,7 @@ console.log(stringObject[toStringSymbol]());
|
||||
|
||||
我们可以直接使用这个接口。
|
||||
|
||||
```
|
||||
```js
|
||||
let okIterator = "OK"[Symbol.iterator]();
|
||||
console.log(okIterator.next());
|
||||
// → {value: "O", done: false}
|
||||
@ -396,7 +408,7 @@ console.log(okIterator.next());
|
||||
|
||||
我们来实现一个可迭代的数据结构。 我们将构建一个`matrix`类,充当一个二维数组。
|
||||
|
||||
```
|
||||
```js
|
||||
class Matrix {
|
||||
constructor(width, height, element = (x, y) => undefined) {
|
||||
this.width = width;
|
||||
@ -419,12 +431,12 @@ class Matrix {
|
||||
|
||||
该类将其内容存储在`width × height`个元素的单个数组中。 元素是按行存储的,因此,例如,第五行中的第三个元素存储在位置`4 × width + 2`中(使用基于零的索引)。
|
||||
|
||||
构造函数需要宽度,高度和一个可选的内容函数,用来填充初始值。 `get`和`set`方法用于检索和更新矩阵中的元素。
|
||||
构造器需要宽度,高度和一个可选的内容函数,用来填充初始值。 `get`和`set`方法用于检索和更新矩阵中的元素。
|
||||
|
||||
遍历矩阵时,通常对元素的位置以及元素本身感兴趣,所以我们会让迭代器产生具有`x`,`y`和`value`属性的对象。
|
||||
|
||||
|
||||
```
|
||||
```js
|
||||
class MatrixIterator {
|
||||
constructor(matrix) {
|
||||
this.x = 0;
|
||||
@ -451,7 +463,7 @@ class MatrixIterator {
|
||||
|
||||
让我们使`Matrix`类可迭代。 在本书中,我会偶尔使用事后的原型操作来为类添加方法,以便单个代码段保持较小且独立。 在一个正常的程序中,不需要将代码分成小块,而是直接在`class`中声明这些方法。
|
||||
|
||||
```
|
||||
```js
|
||||
Matrix.prototype[Symbol.iterator] = function() {
|
||||
return new MatrixIterator(this);
|
||||
};
|
||||
@ -459,7 +471,7 @@ Matrix.prototype[Symbol.iterator] = function() {
|
||||
|
||||
现在我们可以用`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);
|
||||
@ -476,7 +488,7 @@ for (let {x, y, value} of matrix) {
|
||||
|
||||
这样的对象甚至不需要直接在实例中计算和存储这样的属性。 即使直接访问的属性也可能隐藏了方法调用。 这种方法称为读取器(getter),它们通过在方法名称前面编写`get`来定义。
|
||||
|
||||
```
|
||||
```js
|
||||
let varyingSize = {
|
||||
get size() {
|
||||
return Math.floor(Math.random() * 100);
|
||||
@ -491,7 +503,7 @@ console.log(varyingSize.size);
|
||||
每当有人读取此对象的`size`属性时,就会调用相关的方法。 当使用写入器(setter)写入属性时,可以做类似的事情。
|
||||
|
||||
|
||||
```
|
||||
```js
|
||||
class Temperature {
|
||||
constructor(celsius) {
|
||||
this.celsius = celsius;
|
||||
@ -515,11 +527,11 @@ console.log(temp.celsius);
|
||||
// → 30
|
||||
```
|
||||
|
||||
`Temperature`类允许您以摄氏度或华氏度读取和写入温度,但内部仅存储摄氏度,并在`fahrenheit`读写器中自动转换为摄氏度。
|
||||
`Temperature`类允许你以摄氏度或华氏度读取和写入温度,但内部仅存储摄氏度,并在`fahrenheit`读写器中自动转换为摄氏度。
|
||||
|
||||
有时候你想直接向你的构造函数附加一些属性,而不是原型。 这样的方法将无法访问类实例,但可以用来提供额外方法来创建实例。
|
||||
有时候你想直接向你的构造器附加一些属性,而不是原型。 这样的方法将无法访问类实例,但可以用来提供额外方法来创建实例。
|
||||
|
||||
在类声明内部,名称前面写有`static`的方法,存储在构造函数中。 所以`Temperature`类可以让你写出`Temperature.fromFahrenheit(100)`,来使用华氏温度创建一个温度。
|
||||
在类声明内部,名称前面写有`static`的方法,存储在构造器中。 所以`Temperature`类可以让你写出`Temperature.fromFahrenheit(100)`,来使用华氏温度创建一个温度。
|
||||
|
||||
## 继承
|
||||
|
||||
@ -531,7 +543,7 @@ JavaScript 的原型系统可以创建一个新类,就像旧类一样,但是
|
||||
|
||||
在面向对象的编程术语中,这称为继承(inheritance)。 新类继承旧类的属性和行为。
|
||||
|
||||
```
|
||||
```js
|
||||
class SymmetricMatrix extends Matrix {
|
||||
constructor(size, element = (x, y) => undefined) {
|
||||
super(size, size, (x, y) => {
|
||||
@ -554,19 +566,19 @@ console.log(matrix.get(2, 3));
|
||||
|
||||
`extends`这个词用于表示,这个类不应该直接基于默认的`Object`原型,而应该基于其他类。 这被称为超类(superclass)。 派生类是子类(subclass)。
|
||||
|
||||
为了初始化`SymmetricMatrix`实例,构造函数通过`super`关键字调用其超类的构造函数。 这是必要的,因为如果这个新对象的行为(大致)像`Matrix`,它需要矩阵具有的实例属性。 为了确保矩阵是对称的,构造函数包装了`content`方法,来交换对角线以下的值的坐标。
|
||||
为了初始化`SymmetricMatrix`实例,构造器通过`super`关键字调用其超类的构造器。 这是必要的,因为如果这个新对象的行为(大致)像`Matrix`,它需要矩阵具有的实例属性。 为了确保矩阵是对称的,构造器包装了`content`方法,来交换对角线以下的值的坐标。
|
||||
|
||||
`set`方法再次使用`super`,但这次不是调用构造函数,而是从超类的一组方法中调用特定的方法。 我们正在重新定义`set`,但是想要使用原来的行为。 因为`this.set`引用新的`set`方法,所以调用这个方法是行不通的。 在类方法内部,`super`提供了一种方法,来调用超类中定义的方法。
|
||||
`set`方法再次使用`super`,但这次不是调用构造器,而是从超类的一组方法中调用特定的方法。 我们正在重新定义`set`,但是想要使用原来的行为。 因为`this.set`引用新的`set`方法,所以调用这个方法是行不通的。 在类方法内部,`super`提供了一种方法,来调用超类中定义的方法。
|
||||
|
||||
继承允许我们用相对较少的工作,从现有数据类型构建稍微不同的数据类型。 它是面向对象传统的基础部分,与封装和多态一样。 尽管后两者现在普遍被认为是伟大的想法,但继承更具争议性。
|
||||
|
||||
尽管封装和多态可用于将代码彼此分离,从而减少整个程序的耦合,但继承从根本上将类连接在一起,从而产生更多的耦合。 继承一个类时,比起单纯使用它,你通常必须更加了解它如何工作。 继承可能是一个有用的工具,并且我现在在自己的程序中使用它,但它不应该成为你的第一个工具,你可能不应该积极寻找机会来构建类层次结构(类的家族树)。
|
||||
|
||||
### 6.12 instanceof运算符
|
||||
### 6.12 `instanceof`运算符
|
||||
|
||||
在有些时候,了解某个对象是否继承自某个特定类,也是十分有用的。JavaScript 为此提供了一个二元运算符,名为instanceof。
|
||||
在有些时候,了解某个对象是否继承自某个特定类,也是十分有用的。JavaScript 为此提供了一个二元运算符,名为`instanceof`。
|
||||
|
||||
```
|
||||
```js
|
||||
console.log(
|
||||
new SymmetricMatrix(2) instanceof SymmetricMatrix);
|
||||
// → true
|
||||
@ -578,17 +590,17 @@ console.log([1] instanceof Array);
|
||||
// → true
|
||||
```
|
||||
|
||||
该运算符会浏览所有继承类型。所以`SymmetricMatrix`是`Matrix`的一个实例。 该运算符也可以应用于像`Array`这样的标准构造函数。 几乎每个对象都是`Object`的一个实例。
|
||||
该运算符会浏览所有继承类型。所以`SymmetricMatrix`是`Matrix`的一个实例。 该运算符也可以应用于像`Array`这样的标准构造器。 几乎每个对象都是`Object`的一个实例。
|
||||
|
||||
### 本章小结
|
||||
|
||||
对象不仅仅持有它们自己的属性。对象中有另一个对象:原型,只要原型中包含了属性,那么根据原型构造出来的对象也就可以看成包含了相应的属性。简单对象直接以Object.prototype作为原型。
|
||||
对象不仅仅持有它们自己的属性。对象中有另一个对象:原型,只要原型中包含了属性,那么根据原型构造出来的对象也就可以看成包含了相应的属性。简单对象直接以`Object.prototype`作为原型。
|
||||
|
||||
构造函数是名称通常以大写字母开头的函数,可以与`new`运算符一起使用来创建新对象。 新对象的原型是构造函数的`prototype`属性中的对象。 通过将属性放到它们的原型中,可以充分利用这一点,给定类型的所有值在原型中分享它们的属性。 `class`表示法提供了一个显式方法,来定义一个构造函数及其原型。
|
||||
构造器是名称通常以大写字母开头的函数,可以与`new`运算符一起使用来创建新对象。 新对象的原型是构造器的`prototype`属性中的对象。 通过将属性放到它们的原型中,可以充分利用这一点,给定类型的所有值在原型中分享它们的属性。 `class`表示法提供了一个显式方法,来定义一个构造器及其原型。
|
||||
|
||||
您可以定义读写器,在每次访问对象的属性时秘密地调用方法。 静态方法是存储在类的构造函数,而不是其原型中的方法。
|
||||
你可以定义读写器,在每次访问对象的属性时秘密地调用方法。 静态方法是存储在类的构造器,而不是其原型中的方法。
|
||||
|
||||
给定一个对象和一个构造函数,`instanceof`运算符可以告诉你该对象是否是该构造函数的一个实例。
|
||||
给定一个对象和一个构造器,`instanceof`运算符可以告诉你该对象是否是该构造器的一个实例。
|
||||
|
||||
可以使用对象的来做一个有用的事情是,为它们指定一个接口,告诉每个人他们只能通过该接口与对象通信。 构成对象的其余细节,现在被封装在接口后面。
|
||||
|
||||
@ -600,13 +612,13 @@ console.log([1] instanceof Array);
|
||||
|
||||
#### 6.14.1 向量类型
|
||||
|
||||
编写一个构造器Vec,在二维空间中表示数组。该函数接受两个数字参数x和y,并将其保存到对象的同名属性中。
|
||||
编写一个构造器`Vec`,在二维空间中表示数组。该函数接受两个数字参数`x`和`y`,并将其保存到对象的同名属性中。
|
||||
|
||||
向Vec原型添加两个方法:plus和minus,它们接受另一个向量作为参数,分别返回两个向量(一个是this,另一个是参数)的和向量与差向量。
|
||||
向`Vec`原型添加两个方法:`plus`和`minus`,它们接受另一个向量作为参数,分别返回两个向量(一个是`this`,另一个是参数)的和向量与差向量。
|
||||
|
||||
向原型添加一个getter属性length,用于计算向量长度,即点(x,y)与原点(0,0)之间的距离。
|
||||
向原型添加一个`getter`属性`length`,用于计算向量长度,即点`(x,y)`与原点`(0,0)`之间的距离。
|
||||
|
||||
```
|
||||
```js
|
||||
// Your code here.
|
||||
|
||||
console.log(new Vec(1, 2).plus(new Vec(2, 3)));
|
||||
@ -621,13 +633,13 @@ console.log(new Vec(3, 4).length);
|
||||
|
||||
标准的 JavaScript 环境提供了另一个名为`Set`的数据结构。 像`Map`的实例一样,集合包含一组值。 与`Map`不同,它不会将其他值与这些值相关联 - 它只会跟踪哪些值是该集合的一部分。 一个值只能是一个集合的一部分 - 再次添加它没有任何作用。
|
||||
|
||||
写一个名为`Group`的类(因为`Set`已被占用)。 像`Set`一样,它具有`add`,`delete`和`has`方法。 它的构造函数创建一个空的分组,`add`给分组添加一个值(但仅当它不是成员时),`delete`从组中删除它的参数(如果它是成员),`has` 返回一个布尔值,表明其参数是否为分组的成员。
|
||||
写一个名为`Group`的类(因为`Set`已被占用)。 像`Set`一样,它具有`add`,`delete`和`has`方法。 它的构造器创建一个空的分组,`add`给分组添加一个值(但仅当它不是成员时),`delete`从组中删除它的参数(如果它是成员),`has` 返回一个布尔值,表明其参数是否为分组的成员。
|
||||
|
||||
使用`===`运算符或类似于`indexOf`的东西来确定两个值是否相同。
|
||||
|
||||
为该类提供一个静态的`from`方法,该方法接受一个可迭代的对象作为参数,并创建一个分组,包含遍历它产生的所有值。
|
||||
|
||||
```
|
||||
```js
|
||||
// Your code here.
|
||||
|
||||
class Group {
|
||||
@ -646,13 +658,13 @@ console.log(group.has(10));
|
||||
|
||||
#### 可迭代分组
|
||||
|
||||
使上一个练习中的`Group`类可迭代。 如果您不清楚接口的确切形式,请参阅本章前面迭代器接口的章节。
|
||||
使上一个练习中的`Group`类可迭代。 如果你不清楚接口的确切形式,请参阅本章前面迭代器接口的章节。
|
||||
|
||||
如果您使用数组来表示分组的成员,则不要仅仅通过调用数组中的`Symbol.iterator`方法来返回迭代器。 这会起作用,但它会破坏这个练习的目的。
|
||||
如果你使用数组来表示分组的成员,则不要仅仅通过调用数组中的`Symbol.iterator`方法来返回迭代器。 这会起作用,但它会破坏这个练习的目的。
|
||||
|
||||
如果分组被修改时,您的迭代器在迭代过程中出现奇怪的行为,那也没问题。
|
||||
如果分组被修改时,你的迭代器在迭代过程中出现奇怪的行为,那也没问题。
|
||||
|
||||
```
|
||||
```js
|
||||
// Your code here (and the code from the previous exercise)
|
||||
|
||||
for (let value of Group.from(["a", "b", "c"])) {
|
||||
@ -665,11 +677,11 @@ for (let value of Group.from(["a", "b", "c"])) {
|
||||
|
||||
## 借鉴方法
|
||||
|
||||
在本章前面我提到,当你想忽略原型的属性时,对象的`hasOwnProperty`可以用作`in`运算符的更强大的替代方法。 但是如果你的映射需要包含`hasOwnProperty`这个词呢? 您将无法再调用该方法,因为对象的属性隐藏了方法值。
|
||||
在本章前面我提到,当你想忽略原型的属性时,对象的`hasOwnProperty`可以用作`in`运算符的更强大的替代方法。 但是如果你的映射需要包含`hasOwnProperty`这个词呢? 你将无法再调用该方法,因为对象的属性隐藏了方法值。
|
||||
|
||||
你能想到一种方法,对拥有自己的同名属性的对象,调用`hasOwnProperty`吗?
|
||||
|
||||
```
|
||||
```js
|
||||
let map = {one: true, two: true, hasOwnProperty: true};
|
||||
// Fix this call
|
||||
console.log(map.hasOwnProperty("one"));
|
||||
|
BIN
img/6-0.jpg
Normal file
BIN
img/6-0.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
13
img/6-1.svg
Normal file
13
img/6-1.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="621" height="274" viewBox="-4 -82 621 274"><style>
|
||||
@font-face {
|
||||
font-family: 'PT Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('PT Mono'), local('PTMono-Regular'), url(http://themes.googleusercontent.com/static/fonts/ptmono/v1/jmle3kzCPnW8O7_gZGRDlQ.woff) format('woff');
|
||||
}
|
||||
.objtext { font-family: "PT Mono"; font-size: 14px; stroke: none; }
|
||||
.objbox { border-radius: 2px; fill: white; stroke: black }
|
||||
.sep { stroke: #666 }
|
||||
</style>
|
||||
<g><g><rect x="210.5" y="133.5" width="202" height="55" class="objbox"></rect><text x="221.5" y="154.5" class="objtext">toString: <function></text><text x="221.5" y="176.5" class="objtext">...</text></g><g><rect x="121.5" y="83.5" width="178" height="55" class="objbox"></rect><text x="132.5" y="104.5" class="objtext">teeth: "small"</text><text x="132.5" y="126.5" class="objtext">speak: <function></text></g><g><rect x="0.5" y="0.5" width="242" height="88" class="objbox"></rect><text x="11" y="21" class="objtext">killerRabbit</text><text x="11" y="54" class="objtext">teeth: "long, sharp, ..."</text><text x="11" y="76" class="objtext">type: "killer"</text><path d="M 0.5 33.5 L 242.5 33.5" class="sep"></path></g><g><rect x="282.5" y="-79.5" width="114" height="66" class="objbox"></rect><text x="293.5" y="-58.5" class="objtext">Rabbit</text><text x="293.5" y="-25.5" class="objtext">prototype</text><path d="M 282.5 -46.5 L 396.5 -46.5" class="sep"></path></g><path d="M 329.5 -19.5 L 259.5 83.5" class="sep"></path><path d="M 0 0 L 3 8 L 0 7 L -3 8 Z" stroke="none" fill="rgb(102, 102, 102)" transform="translate(259.5 83.5) rotate(214.20048413026853) scale(1)"></path><g><rect x="426.5" y="-9.5" width="186" height="110" class="objbox"></rect><text x="437.5" y="11.5" class="objtext">Object</text><text x="437.5" y="44.5" class="objtext">create: <function></text><text x="437.5" y="66.5" class="objtext">prototype</text><text x="437.5" y="88.5" class="objtext">...</text><path d="M 426.5 23.5 L 612.5 23.5" class="sep"></path></g><path d="M 432.5 70.5 L 331.5 133.5" class="sep"></path><path d="M 0 0 L 3 8 L 0 7 L -3 8 Z" stroke="none" fill="rgb(102, 102, 102)" transform="translate(331.5 133.5) rotate(238.0456370629486) scale(1)"></path></g></svg>
|
After Width: | Height: | Size: 2.3 KiB |
Loading…
x
Reference in New Issue
Block a user