0 Views 前端 with
本文字数:2,276 字 | 阅读时长 ≈ 8 min

0 Views 前端 with
本文字数:2,276 字 | 阅读时长 ≈ 8 min

类 / 继承描述了一种代码的组织结构形式——一种在软件中对真实世界中问题领域的建模方法。

类理论

类 / 继承描述了一种代码的组织结构形式——一种在软件中对真实世界中问题领域的建模方法。

面向对象编程强调的是数据和操作数据的行为本质上是互相关联的(当然, 不同的数据有不同的行为), 因此好的设计就是把数据以及和它相关的行为打包(或者说封装) 起来。这在正式的计算机科学中有时被称为数据结构。

举例来说, 用来表示一个单词或者短语的一串字符通常被称为字符串。 字符就是数据。 但是你关心的往往不是数据是什么, 而是可以对数据做什么, 所以可以应用在这种数据上的行为(计算长度、 添加数据、 搜索, 等等) 都被设计成 String 类的方法

所有字符串都是 String 类的一个实例, 也就是说它是一个包裹, 包含字符数据和我们可以应用在数据上的函数。

我们还可以使用类对数据结构进行分类, 可以把任意数据结构看作范围更广的定义的一种特例。

我们来看一个常见的例子,“汽车” 可以被看作“交通工具” 的一种特例, 后者是更广泛的类。

我们可以在软件中定义一个 Vehicle 类和一个 Car 类来对这种关系进行建模。 Vehicle 的定义可能包含推进器(比如引擎)、 载人能力等等, 这些都是 Vehicle 的行为。 我们在 Vehicle 中定义的是(几乎) 所有类型的交通工具(飞机、 火车和汽车) 都包含的东西。在我们的软件中, 对不同的交通工具重复定义“载人能力” 是没有意义的。 相反, 我们只在 Vehicle 中定义一次, 定义 Car 时, 只要声明它继承(或者扩展) 了 Vehicle 的这个基础定义就行。 Car 的定义就是对通用 Vehicle 定义的特殊化。

虽然 Vehicle 和 Car 会定义相同的方法, 但是实例中的数据可能是不同的, 比如每辆车独一无二的 VIN(Vehicle Identification Number, 车辆识别号码), 等等。这就是类、 继承和实例化。

类的另一个核心概念是多态, 这个概念是说父类的通用行为可以被子类用更特殊的行为重写。 实际上, 相对多态性允许我们从重写行为中引用基础行为

“类”设计模式

你可能从来没把类作为设计模式来看待, 讨论得最多的是面向对象设计模式, 比如迭代器 模式、 观察者模式、 工厂模式、 单例模式, 等等。 从这个角度来说, 我们似乎是在(低级)面向对象类的基础上实现了所有(高级) 设计模式, 似乎面向对象是优秀代码的基础。

当然, 如果你有函数式编程(比如 Monad) 的经验就会知道类也是非常常用的一种设计模式。 但是对于其他人来说, 这可能是第一次知道类并不是必须的编程基础, 而是一种可选的代码抽象

有些语言(比如 Java) 并不会给你选择的机会, 类并不是可选的——万物皆是类。 其他语言(比如 C/C++ 或者 PHP) 会提供过程化和面向类这两种语法, 开发者可以选择其中一种风格或者混用两种风格

JavaScript中的“类”

JavaScript 属于哪一类呢? 在相当长的一段时间里, JavaScript 只有一些近似类的语法元素(比如 new 和 instanceof)

这是不是意味着 JavaScript 中实际上有类呢? 简单来说: 不是。

由于类是一种设计模式, 所以你可以用一些方法近似实现类的功能。为了满足对于类设计模式的最普遍需求, JavaScript 提供了一些近似类的语法

虽然有近似类的语法, 但是 JavaScript 的机制似乎一直在阻止你使用类设计模式。 在近似类的表象之下, JavaScript 的机制其实和类完全不同。 语法糖和(广泛使用的) JavaScript“类” 库试图掩盖这个现实, 但是你迟早会面对它: 其他语言中的类和 JavaScript 中的“类” 并不一样。

类的机制

在许多面向类的语言中,“标准库” 会提供 Stack 类, 它是一种“栈” 数据结构(支持压入、 弹出, 等等)。 Stack 类内部会有一些变量来存储数据, 同时会提供一些公有的可访问行为(“方法”), 从而让你的代码可以和(隐藏的) 数据进行交互(比如添加、 删除数据)。但是在这些语言中, 你实际上并不是直接操作 Stack(除非创建一个静态类成员引用, 这超出了我们的讨论范围)。 Stack 类仅仅是一个抽象的表示, 它描述了所有“栈” 需要做的事, 但是它本身并不是一个“栈”。 你必须先实例化 Stack 类然后才能对它进行操作。

建造

“类” 和“实例” 的概念来源于房屋建造。

建筑师会规划出一个建筑的所有特性: 多宽、 多高、 多少个窗户以及窗户的位置, 甚至连建造墙和房顶需要的材料都要计划好。 在这个阶段他并不需要关心建筑会被建在哪, 也不需要关心会建造多少个这样的建筑

建筑师也不太关心建筑里的内容——家具、 壁纸、 吊扇等——他只关心需要用什么结构来容纳它们。

建筑蓝图只是建筑计划, 它们并不是真正的建筑, 我们还需要一个建筑工人来建造建筑。建筑工人会按照蓝图建造建筑。 实际上, 他会把规划好的特性从蓝图中复制到现实世界的建筑中

完成后, 建筑就成为了蓝图的物理实例, 本质上就是对蓝图的复制。 之后建筑工人就可以到下一个地方, 把所有工作都重复一遍, 再创建一份副本。

建筑和蓝图之间的关系是间接的。 你可以通过蓝图了解建筑的结构, 只观察建筑本身是无法获得这些信息的。 但是如果你想打开一扇门, 那就必须接触真实的建筑才行——蓝图只能表示门应该在哪, 但并不是真正的门。

一个类就是一张蓝图。 为了获得真正可以交互的对象, 我们必须按照类来建造(也可以说 实例化) 一个东西, 这个东西通常被称为实例, 有需要的话, 我们可以直接在实例上调用方法并访问其所有公有数据属性。

这个对象就是类中描述的所有特性的一份副本。

你走进一栋建筑时, 它的蓝图不太可能挂在墙上(尽管这个蓝图可能会保存在公共档案馆中)。 类似地, 你通常也不会使用一个实例对象来直接访问并操作它的类, 不过至少可以判断出这个实例对象来自哪个类。

把类和实例对象之间的关系看作是直接关系而不是间接关系通常更有助于理解。 类通过复制操作被实例化为对象形式:


如你所见, 箭头的方向是从左向右、 从上向下, 它表示概念和物理意义上发生的复制操作

构造函数

类实例是由一个特殊的类方法构造的, 这个方法名通常和类名相同, 被称为构造函数。 这个方法的任务就是初始化实例需要的所有信息(状态)。

举例来说, 思考下面这个关于类的伪代码(编造出来的语法) :

1
2
3
4
5
6
7
8
9
class CoolGuy {
specialTrick = nothing
CoolGuy( trick ) {
specialTrick = trick
}
showOff() {
output( "Here's my trick: ", specialTrick )
}
}

我们可以调用类构造函数来生成一个 CoolGuy 实例:

1
2
Joe = new CoolGuy( "jumping rope" )
Joe.showOff() // 这是我的绝技: 跳绳

注意, CoolGuy 类有一个 CoolGuy() 构造函数, 执行 new CoolGuy() 时实际上调用的就是它。 构造函数会返回一个对象(也就是类的一个实例), 之后我们可以在这个对象上调用 showOff()方法, 来输出指定 CoolGuy 的特长。

显然, 跳绳让乔成为了一个非常酷的家伙。 

类构造函数属于类, 而且通常和类同名。 此外, 构造函数大多需要用 new 来调, 这样语言引擎才知道你想要构造一个新的类实例

Sep 02, 2018