今天看啥  ›  专栏  ›  钱塘风华

Day40:类理论

钱塘风华  · 简书  ·  · 2019-05-03 22:20

【书名】:你不知道的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())?

相比之下,JavaScript 要简单得多:它本身并不提供“多重继承”功能。




原文地址:访问原文地址
快照地址: 访问文章快照