Day40(类理论)

【书名】:你不知道的JavaScript(上卷)
【作者】:Kyle Simpson
【本书总页码】:213
【已读页码】:150
面向对象与类
面向对象编程强调的是数据和操作数据的行为本质上是互相关联的,因此好的设计就是把数据以及和它相关的行为封装起来。这在正式的计算机科学中有时被称为数据结构。
类具有封装(有封装就有实例化)、继承和多态度三大特征。
类不是一种必须的编程基础,而是一种可选的代码抽象。他是一种设计模式。有些语言(比如 Java)并不会给你选择的机会,类并不是可选的——万物皆是类。
JavaScript中的“类”
JavaScript 中实际上有类呢?简单来说:不是。
由于类是一种设计模式,所以可以用一些方法近似实现类的功能。为了满足对于类设计模式的最普遍需求,JavaScript 提供了一些近似类的语法。
虽然有近似类的语法,但是 JavaScript 的机制似乎一直在阻止你使用类设计模式。在近似类的表象之下,JavaScript 的机制其实和类完全不同。其他语言中的类和 JavaScript中的“类”并不一样。
类的机制
在许多面向类的语言中,“标准库”会提供 Stack 类,它是一种“栈”数据结构(支持压入、弹出,等等)。Stack 类内部会有一些变量来存储数据,同时会提供一些公有的可访问行为(“方法”),从而让代码可以和(隐藏的)数据进行交互(比如添加、删除数据)。
但是在这些语言中,实际上并不是直接操作 Stack(除非创建一个静态类成员引用)。Stack 类仅仅是一个抽象的表示,它描述了所有“栈”需要做的事,但是它本身并不是一个“栈”。必须先实例化 Stack 类然后才能对它进行操作。
1. 实例化
为了获得真正可以交互的对象,必须按照类来实例化一个东西,这个东西通常被称为实例,有需要的话,我们可以直接在实例上调用方法并访问其所有公有数据属性。
这个对象就是类中描述的所有特性的一份副本。
你通常也不会使用一个实例对象来直接访问并操作它的类,不过至少可以判断出这个实例对象来自哪个类。
2. 构造函数
类实例是由一个特殊的类方法构造的,这个方法名通常和类名相同,被称为构造函数。这个方法的任务就是初始化实例需要的所有信息(状态)。
类构造函数属于类,而且通常和类同名。此外,构造函数大多需要用 new 来调,这样语言引擎才知道你想要构造一个新的类实例。
伪代码如下:
class Person {
words = ''
Person (word) {
words = word
}
say () {
output("I said: ", words)
}
}
// 调用
han = new Person("My name is han.")
han.say() // I said: My name is han.
类的继承
在面向类的语言中,你可以先定义一个类,然后定义一个继承前者的类。后者通常被称为“子类”,前者通常被称为“父类”。在编程语言中,我们假设只有一个父类。定义好一个子类之后,相对于父类来说它就是一个独立并且完全不同的类。子类会包含父类行为的原始副本,但是也可以重写所有继承的行为甚至定义新行为。非常重要的一点是,我们讨论的父类和子类并不是实例。
思考下面关于类继承的伪代码:(伪代码省略了这些类的构造函数)
class Vehicle {
engines = 1
ignition() {
output( "Turning on my engine." );
}
drive() {
ignition();
output( "Steering and moving forward!" )
}
}
class Car inherits Vehicle {
wheels = 4
drive() {
inherited:drive()
output( "Rolling on all ", wheels, " wheels!" )
}
}
class SpeedBoat inherits Vehicle {
engines = 2
ignition() {
output( "Turning on my ", engines, " engines." )
}
pilot() {
inherited:drive()
output( "Speeding through the water with ease!" )
}
}
我们通过定义 Vehicle 类来假设一种发动机,一种点火方式,一种驾驶方法。
接下来我们定义了两类具体的交通工具:Car 和 SpeedBoat。它们都从 Vehicle 继承了通用的特性并根据自身类别修改了某些特性。汽车需要四个轮子,快艇需要两个发动机,因此它必须启动两个发动机的点火装置。
1. 多态
Car 重写了继承自父类的 drive() 方法,但是之后 Car 调用了 inherited:drive() 方法,这表明 Car 可以引用继承来的原始 drive() 方法。快艇的 pilot() 方法同样引用了原始drive() 方法。
这个技术被称为多态或者虚拟多态。任何方法都可以引用继承层次中高层的方法(无论高层的方法名和当前方法名是否相同)。
在许多语言中可以使用 super 来代替inherited:,它的含义是“超类”(superclass),表示当前类的父类 / 祖先类。
多态的另一个方面是,在继承链的不同层次中一个方法名可以被多次定义,当调用方法时会自动选择合适的定义。
在传统的面向类的语言中 super 还有一个功能,就是从子类的构造函数中通过super 可以直接调用父类的构造函数。通常来说这没什么问题,因为对于真正的类来说,构造函数是属于类的。然而,在 JavaScript 中恰好相反——实际上“类”是属于构造函数的(类似 Foo.prototype... 这样的类型引用)。由于JavaScript 中父类和子类的关系只存在于两者构造函数对应的 .prototype 对象中,因此它们的构造函数之间并不存在直接联系,从而无法简单地实现两者的相对引用(在 ES6 的类中可以通过 super 来“解决”这个问题)。
在 pilot() 中通过多态引用了(继承来的)Vehicle 中的 drive()。但是那个 drive() 方法直接通过名字(而不是相对引用)引用了 ignotion() 方法。那么语言引擎会使用哪个 ignition() 呢,Vehicle 的还是 SpeedBoat 的?实际上它会使用SpeedBoat 的 ignition()。如果你直接实例化了 Vehicle 类然后调用它的 drive(),那语言引擎就会使用 Vehicle 中的 ignition() 方法。
换言之,ignition() 方法定义的多态性取决于你是在哪个类的实例中引用它。
在子类(而不是它们创建的实例对象!)中也可以相对引用它继承的父类,这种相对引用通常被称为 super。
需要注意,子类得到的仅仅是继承自父类行为的一份副本。子类对继承到的一个方法进行“重写”,不会影响父类中的方法,这两个方法互不影响,因此才能使用多态引用访问父类中的方法(如果重写会影响父类的方法,那重写之后父类中的原始方法就不存在了,自然也无法引用)。
多态并不表示子类和父类有关联,子类得到的只是父类的一份副本。类的继承其实就是复制。
2. 多重继承
有些面向类的语言允许你继承多个“父类”。多重继承意味着所有父类的定义都会被复制到子类中。
从表面上来,对于类来说这似乎是一个非常有用的功能,可以把许多功能组合在一起。然而,这个机制同时也会带来很多复杂的问题。如果两个父类中都定义了 drive() 方法的话,子类引用的是哪个呢?难道每次都需要手动指定具体父类的 drive() 方法吗?
除此之外,还有一种被称为钻石问题的变种。在钻石问题中,子类 D 继承自两个父类(B和 C),这两个父类都继承自 A。如果 A 中有 drive() 方法并且 B 和 C 都重写了这个方法(多态),那当 D 引用 drive() 时应当选择哪个版本呢(B:drive() 还是 C:drive())?
【Day40(类理论)】相比之下,JavaScript 要简单得多:它本身并不提供“多重继承”功能。

    推荐阅读