diff --git a/6.md b/6.md index e08ec37..11bd076 100644 --- a/6.md +++ b/6.md @@ -523,144 +523,155 @@ console.log(temp.celsius); ## 继承 -我们还差一些工作才能完成绘制表格的程序。我们希望将数字列右对齐以增强表格的可读性。我们应该创建另一个类似于TextCell的单元格类型,只不过不是右侧填补空格,而是在左侧填补空格,这样就可以将内容右对齐。 +已知一些矩阵是对称的。 如果沿左上角到右下角的对角线翻转对称矩阵,它保持不变。 换句话说,存储在`x,y`的值总是与`y,x`相同。 -我们可以简单地编写一个新的构造器,并在原型中添加三个方法。但由于原型自身可以包含原型的,因此我们可以采取更为巧妙的方法来完成任务。 +想象一下,我们需要一个像`Matrix`这样的数据结构,但是它必需保证一个事实,矩阵是对称的。 我们可以从头开始编写它,但这需要重复一些代码,与我们已经写过的代码很相似。 + +JavaScript 的原型系统可以创建一个新类,就像旧类一样,但是它的一些属性有了新的定义。 新类派生自旧类的原型,但为`set`方法增加了一个新的定义。 + +在面向对象的编程术语中,这称为继承(inheritance)。 新类继承旧类的属性和行为。 ``` -function RTextCell(text) { - TextCell.call(this, text); -} -RTextCell.prototype = Object.create(TextCell.prototype); -RTextCell.prototype.draw = function(width, height) { - var result = []; - for (var i = 0; i < height; i++) { - var line = this.text[i] || ""; - result.push(repeat(" ", width - line.length) + line); - } - return result; -}; -``` - -我们重用了TextCell的构造器、minHeight和minWidth属性。RTextCell基本上就是TextCell,只不过draw方法中包含了不同的函数。 - -我们将这种模式称之为继承。我们可以使用继承来花很少的力气构造出与当前类型相似的数据类型,其中两个数据结构只有细微差别。新的构造器通常会调用旧的构造器(使用call方法将新对象作为旧构造器的this值)。当调用构造器时,我们可以认为所有旧的对象类型中包含的字段都已经添加到了新对象中。我们让构造器的原型继承旧的原型对象,因此所有新类型实例都可以访问旧原型中的属性。最后,我们将一些属性添加到新的原型中并覆盖这些属性。 - -现在,如果我们稍微调整一下dataTable函数,使用RTextCall来处理数字,最终得到的表格就可以满足我们的要求了。 - -``` -function dataTable(data) { - var keys = Object.keys(data[0]); - var headers = keys.map(function(name) { - return new UnderlinedCell(new TextCell(name)); - }); - var body = data.map(function(row) { - return keys.map(function(name) { - var value = row[name]; - // This was changed: - if (typeof value == "number") - return new RTextCell(String(value)); - else - return new TextCell(String(value)); +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); }); - }); - return [headers].concat(body); -} + } -console.log(drawTable(dataTable(MOUNTAINS))); -// → … beautifully aligned table + 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`方法,来交换对角线以下的值的坐标。 -正如我们所见,你可以在使用多态时不使用继承。我并不建议你完全不使用继承这种特性,我常常就在自己的程序中使用继承。不过你应该视其为一种在定义新类型时减少代码的技巧,而非编写代码的绝对原则。更好的扩展类型的方式是组合,比如基于另一个对象构建UnderlinedCell时,只是简单地将该对象存储为其属性并在UnderlinedCell的方法中,将函数调用转发给内部存储的对象,而这种方法就是组合。 +`set`方法再次使用`super`,但这次不是调用构造函数,而是从超类的一组方法中调用特定的方法。 我们正在重新定义`set`,但是想要使用原来的行为。 因为`this.set`引用新的`set`方法,所以调用这个方法是行不通的。 在类方法内部,`super`提供了一种方法,来调用超类中定义的方法。 + +继承允许我们用相对较少的工作,从现有数据类型构建稍微不同的数据类型。 它是面向对象传统的基础部分,与封装和多态一样。 尽管后两者现在普遍被认为是伟大的想法,但继承更具争议性。 + +尽管封装和多态可用于将代码彼此分离,从而减少整个程序的耦合,但继承从根本上将类连接在一起,从而产生更多的耦合。 继承一个类时,比起单纯使用它,你通常必须更加了解它如何工作。 继承可能是一个有用的工具,并且我现在在自己的程序中使用它,但它不应该成为你的第一个工具,你可能不应该积极寻找机会来构建类层次结构(类的家族树)。 ### 6.12 instanceof运算符 -在有些时候,了解某个对象是否继承自某个特定构造器也是十分有用的。JavaScript为此提供了一个二元运算符,名为instanceof。 +在有些时候,了解某个对象是否继承自某个特定类,也是十分有用的。JavaScript 为此提供了一个二元运算符,名为instanceof。 ``` -console.log(new RTextCell("A") instanceof RTextCell); +console.log( + new SymmetricMatrix(2) instanceof SymmetricMatrix); // → true -console.log(new RTextCell("A") instanceof TextCell); +console.log(new SymmetricMatrix(2) instanceof Matrix); // → true -console.log(new TextCell("A") instanceof RTextCell); +console.log(new Matrix(2, 2) instanceof SymmetricMatrix); // → false console.log([1] instanceof Array); // → true ``` -该运算符会查遍所有继承类型。RTextCell对象是TextCell的实例,因为RTextCell.prototype派生自TextCell.prototype。该运算符也可以用于标准构造器(比如Array)。几乎所有对象都是Object的实例。 +该运算符会浏览所有继承类型。所以`SymmetricMatrix`是`Matrix`的一个实例。 该运算符也可以应用于像`Array`这样的标准构造函数。 几乎每个对象都是`Object`的一个实例。 -### 6.13 本章小结 +### 本章小结 -对象比我们之前了解到的复杂许多。对象中有另一个对象:原型,只要原型中包含了属性,那么根据原型构造出来的对象也就可以看成包含了相应的属性。简单对象直接以Object.prototype作为原型。 +对象不仅仅持有它们自己的属性。对象中有另一个对象:原型,只要原型中包含了属性,那么根据原型构造出来的对象也就可以看成包含了相应的属性。简单对象直接以Object.prototype作为原型。 -构造器的名称通常以大写字母开头,可以和new运算符一起使用创建新的对象。新对象的原型是构造器的prototype属性。你可以充分利用原型将特定类型的所有属性放在原型中共享。instanceof运算符可以判断特定对象是否是特定构造器的实例。 +构造函数是名称通常以大写字母开头的函数,可以与`new`运算符一起使用来创建新对象。 新对象的原型是构造函数的`prototype`属性中的对象。 通过将属性放到它们的原型中,可以充分利用这一点,给定类型的所有值在原型中分享它们的属性。 `class`表示法提供了一个显式方法,来定义一个构造函数及其原型。 -我们可以为对象添加接口,用户只需通过接口来使用对象即可。你的对象中的其他细节则被封装了起来,隐藏在接口之后为用户提供所需功能。 +您可以定义读写器,在每次访问对象的属性时秘密地调用方法。 静态方法是存储在类的构造函数,而不是其原型中的方法。 -当我们谈论接口的时候,其实不同的对象也可以实现相同的接口,只不过不同的对象提供了不同的内部实现细节罢了,我们把这种特性称之为多态。我们会在编程过程中经常使用到多态这个特性。 +给定一个对象和一个构造函数,`instanceof`运算符可以告诉你该对象是否是该构造函数的一个实例。 -如果我们实现的对象之间的差别微乎其微,那么我们可以直接使用原型来创建新的类型,而新类型则通过继承原有类型的原型来实现,并使用新的构造器来调用原有类型中的构造器。这样可以得到一个类似于旧类型的新类型,而且我们还可以在原有类型当中添加属性或覆盖属性。 +可以使用对象的来做一个有用的事情是,为它们指定一个接口,告诉每个人他们只能通过该接口与对象通信。 构成对象的其余细节,现在被封装在接口后面。 + +不止一种类型可以实现相同的接口。 为使用接口而编写的代码,自动知道如何使用提供接口的任意数量的不同对象。 这被称为多态。 + +实现多个类,它们仅在一些细节上有所不同的时,将新类编写为现有类的子类,继承其一部分行为会很有帮助。 ### 6.14 习题 #### 6.14.1 向量类型 -编写一个构造器Vector,以二维空间表示数组。该函数接受两个数字参数x和y,并将其保存到对象的同名属性中。 +编写一个构造器Vec,在二维空间中表示数组。该函数接受两个数字参数x和y,并将其保存到对象的同名属性中。 -向Vector原型添加两个方法:plus和minus,它们接受另一个向量作为参数,分别返回两个向量(一个是this,另一个是参数)的和向量与差向量。 +向Vec原型添加两个方法:plus和minus,它们接受另一个向量作为参数,分别返回两个向量(一个是this,另一个是参数)的和向量与差向量。 向原型添加一个getter属性length,用于计算向量长度,即点(x,y)与原点(0,0)之间的距离。 ``` // Your code here. -console.log(new Vector(1, 2).plus(new Vector(2, 3))); -// → Vector{x: 3, y: 5} -console.log(new Vector(1, 2).minus(new Vector(2, 3))); -// → Vector{x: -1, y: -1} -console.log(new Vector(3, 4).length); +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 ``` -#### 6.14.2 另一种单元格 +#### 分组 -实现一个单元格类型,将其命名为StretchCell(inner,width,height),对应本章介绍的表格单元格接口。该类型将另一个单元格对象(比如UnderlinedCell)存储在内部,并确保产生的单元格长度与高度必须大于等于width和height值。 +标准的 JavaScript 环境提供了另一个名为`Set`的数据结构。 像`Map`的实例一样,集合包含一组值。 与`Map`不同,它不会将其他值与这些值相关联 - 它只会跟踪哪些值是该集合的一部分。 一个值只能是一个集合的一部分 - 再次添加它没有任何作用。 + +写一个名为`Group`的类(因为`Set`已被占用)。 像`Set`一样,它具有`add`,`delete`和`has`方法。 它的构造函数创建一个空的分组,`add`给分组添加一个值(但仅当它不是成员时),`delete`从组中删除它的参数(如果它是成员),`has` 返回一个布尔值,表明其参数是否为分组的成员。 + +使用`===`运算符或类似于`indexOf`的东西来确定两个值是否相同。 + +为该类提供一个静态的`from`方法,该方法接受一个可迭代的对象作为参数,并创建一个分组,包含遍历它产生的所有值。 ``` // Your code here. -var sc = new StretchCell(new TextCell("abc"), 1, 2); -console.log(sc.minWidth()); -// → 3 -console.log(sc.minHeight()); -// → 2 -console.log(sc.draw(3, 2)); -// → ["abc", " "] +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 ``` -#### 6.14.3 序列接口 +#### 可迭代分组 -设计一个接口,用于抽象集合的元素迭代过程。对象提供该接口,将自身展现成一种序列形式,代码中可以使用这类对象迭代序列,查看组成序列的每一个元素,并能发现序列的结束位置。 +使上一个练习中的`Group`类可迭代。 如果您不清楚接口的确切形式,请参阅本章前面迭代器接口的章节。 -指定了接口后,编写一个logFive函数,该函数接受一个序列对象,并调用console.log返回其中前五个元素,如果序列中元素数量少于5个,则输出所有元素。 +如果您使用数组来表示分组的成员,则不要仅仅通过调用数组中的`Symbol.iterator`方法来返回迭代器。 这会起作用,但它会破坏这个练习的目的。 -接着实现一个对象类型ArraySeq,并提供一个你设计的接口来迭代存储在内部的数组。然后实现另一个对象类型RangeSeq,提供一个迭代一定范围内(使用from和to两个参数来指定迭代范围)整数的接口。 +如果分组被修改时,您的迭代器在迭代过程中出现奇怪的行为,那也没问题。 ``` -// Your code here. +// Your code here (and the code from the previous exercise) -logFive(new ArraySeq([1, 2])); -// → 1 -// → 2 -logFive(new RangeSeq(100, 1000)); -// → 100 -// → 101 -// → 102 -// → 103 -// → 104 +for (let value of Group.from(["a", "b", "c"])) { + console.log(value); +} +// → a +// → b +// → c +``` + +## 借鉴方法 + +在本章前面我提到,当你想忽略原型的属性时,对象的`hasOwnProperty`可以用作`in`运算符的更强大的替代方法。 但是如果你的映射需要包含`hasOwnProperty`这个词呢? 您将无法再调用该方法,因为对象的属性隐藏了方法值。 + +你能想到一种方法,对拥有自己的同名属性的对象,调用`hasOwnProperty`吗? + +``` +let map = {one: true, two: true, hasOwnProperty: true}; +// Fix this call +console.log(map.hasOwnProperty("one")); +// → true ```