diff --git a/6.md b/6.md index 11bd076..86cf70c 100644 --- a/6.md +++ b/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》 +![](img/6-0.jpg) + 第 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`之下,我们可以从原型中找到对象中没有的属性。 -![](../Images/00204.jpeg) +![](img/6-1.svg) -覆盖原型中存在的属性是很有用的特性。就像示例展示的那样,我们覆盖了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")); diff --git a/img/6-0.jpg b/img/6-0.jpg new file mode 100644 index 0000000..8af17db Binary files /dev/null and b/img/6-0.jpg differ diff --git a/img/6-1.svg b/img/6-1.svg new file mode 100644 index 0000000..5ee2dac --- /dev/null +++ b/img/6-1.svg @@ -0,0 +1,13 @@ + + +toString: <function>...teeth: "small"speak: <function>killerRabbitteeth: "long, sharp, ..."type: "killer"RabbitprototypeObjectcreate: <function>prototype...