TypeScript教程(四)– 面向对象编程

本文介绍TypeScript面向对象编程方面的知识。我们将快速介绍什么是面向对象编程,类,构造函数,属性和方法,访问控制关键字,Getter和Setter,静态成员,索引签名,继承,多态性,抽象类,接口等。


什么是面向对象编程

面向对象编程是一种编程范式或者叫风格,它是众多编程范式中的一种。

JavaScript、TypeScript仅支持面向对象编程和函数式编程两种。

对象由属性和方法组成,对象中的变量叫做属性,而对象中的函数则被称为方法,每一个对象仅负责单一的职责。程序由众多对象组成,对象之间互相协作完成程序的功能。

面向对象编程经常被拿来与函数式编程进行比较,有些人痴迷于函数式编程,而有的人更喜欢面向对象编程。

这类似两种工具,但没有最好的工具!

不要只爱上一种工具并尝试使用该工具解决每一个问题


创建类

类是创建对象的蓝图。我们创建一个银行的Account类,类名用Pascal命名法。该类有id, owner, balance属性,以及 deposit方法。

我们用构造函数 constructor来初始化属性。构造函数不能有返回值,因为它总是返回一个Account类的实例。实例的属性用 this开头访问。

编译成 JavaScript代码,我们看到,在JavaScript里,Class是没有属性代码的,其他都一致。


创建对象

使用 new 关键字创建对象,类似调用函数,我们提供初始属性值。

我们编译并运行查看,发现如果用typeof对象,都是返回 object,这不太符合我们的要求,我们需要知道实际的类是什么,一般要用instanceof来判断。

顺便说一下,在VSCode中,对象提示的蓝色图标表示是对象的成员properties,紫色图标是对象的methods。


只读属性和可选属性

看Account对象,我们肯定不希望 id 被修改,可以加上修饰语 readonly,该属性只允许在构造函数里设置,不允许在其他地方修改,否则就会提示编译错误。

另外,假设有一个属性nickname,不是每个Account对象都必须设置,可以加上 ? 号表示可选。


访问控制关键字

Account类的deposit方法中,除了账户余额增加,我们还需要添加一条存款记录,这样可以知道谁在什么时间存款了多少。但有个问题,Account的实例account可以直接设置balance,比如:

account.balance = -1;

这就需要用到访问控制关键字了。

访问控制关键字或者叫修饰符有三种:public, private, protected。

这一节我们先说public 和 private。

默认情况下,属性和方法都是public,外部均可以访问。

我们可以给balance加上 private修饰符,这样就只能在类内部访问了。private属性主要用于构建健壮的代码,有些初学者用private 属性来存储敏感数据是不恰当的。

根据约定,private 属性一般加下划线前缀 _ 。

同样的,private 修饰符也可以加在方法method前面用于控制外部不能访问。


参数属性

我们定义类的属性,然后又在构造函数里初始化属性,代码有点重复。TypeScript可以使用参数属性,帮助我们简化代码。


Get 和 Set 

早些时候我们用方法 getBalance()在外部获取私有属性_balance,实际上有更好的方法:get。

与get相反,set 用于设置,用法类似。


索引签名(Index Signatures)

我们知道,TypeScript严格按照定义的类来生成对象,但有些时候,我们希望能像JavaScript一样,动态生成属性,这就要用到Index Signatures了。

假设有一个分配剧场座位的类(class): SeatAssignment,根据售票的情况,将座位号与购买人对应起来。很明显,我们不可能给每个座位都分配一个属性,需要动态生成属性。


静态成员

假设有一个共享汽车的应用程序,类Ride有passenger, pickupLocation, dropOffLocation等属性,还有一个重要的属性就是目前的乘车人数量。按照之前的知识,可能如下这样。

这里使用了2个Ride对象,每个对象分别维护activeRides,所以结果都是1,这不是我们想要的结果。

我们需要将activeRides放在一个地方,而不是每个对象中,这就产生了静态成员,在activeRides属性前面加关键字static。静态成员属于类而不是具体的某个对象。

另外,我们不希望外部可以设置activeRides,加上private修饰符,并添加getter。


继承

各个类有共同的部分,编码就会有重复,消除重复有很多方法,继承是其中一种。将共同的部分提取出来形成一个父类,子类继承该父类,然后仅编写不同的部分即可。

如图,Student和Teacher都有fisrtName、lastName、fullName属性,都有walk()、talk()方法,我们将共同的部提取出来形成Person类。

Student继承于Person类,同时也有己的属性studentId和自己的方法takeTest,student对象能使用Person类的成员和自己的成员。

这里我们将所有类放在同一个文件,但作为最佳实践,最好将类放在不同的文件里,后面我们在讲模块时再细谈。


方法重写

有时候我们希望修改父类的一些实现,比如Teacher类的fullName前面需要添加”Professor”前缀,可以使用 override关键字重写 fullName。

如果我们去掉override关键字,结果也是一样的,但会有一些问题,后面会说到,所以我们在tsconfig.json里,开启:

“noImplicitOverride”: true,  

在子类重写父类方法时未提供override关键字时给出警告。


多态性

多态性意思是根据实际呈现多种状态。我们来看个例子。

我们用一个函数printNames打印出学生和老师的全名。

多态性是很强大,明天我们要是再创建一个类Principal,可以不修改原来的类的情况下实现展示不同的全名。

面向对象编程有一个开放闭合原则:扩展是开放的,修改是闭合的。当然100%做到开放闭合是不太可能的,但多态性可以让我们更好地遵循。

前面我们说到过,在子类重写父类的方法时,最好写上override关键字,这在多态性里是很重要的,否则子类重写与父类同名的方法,但不按父类方法的行为重写,就会破坏多态性。


Private 和 Protected

之前我们说类成员的访问控制修饰符除了private和public, 还有protected。 

外部不能访问protected成员和private成员。但protected成员是可以继承的,也就是子类中能看到父类的protected成员,而private不可以,这是protected与private的区别。

Protected不常使用,因为它增加了我们应用的耦合。

除非你知道自己在做什么,否则不要使用protected。

使用public和private 就可以了。


抽象类和抽象方法

假设我们要在画布上画一些图形,各个形状有共同属性color和共同方法render,可以创建类 Shape。

实际上,父类Shape是不知道如何画的,只有继承它的子类才知道如何画,所以生成shape实例并调用render是无意义的,需要阻止这个行为。

这就产生了抽象类和抽象方法。用关键字 abstract 表示。

抽象类会阻止生成实例,其它类必须继承它实现具体的功能。

抽象方法也只能存在于抽象类中。


接口

面向对象编程有一个构建块叫做接口 Interface,用于定义对象的结构。

比如,我们要构造一个日历,这个世界上有很多日历,比如Google日历,Outlook日历,Apple日历等,我们将他们的共同点提取出来,形成一个Calendar基类。

我们看到,当我们将Calendar类编译成JavaScript时,抽象方法是不存在的,抽象类抽象方法在JavaScript是没有的,仅由TypeScript编译器感知。

前面使用抽象类抽象方法构造日历很好,没什么问题。

接下来我们用接口Interface做同样的构建。

我们看到,用interface构造代码更简洁。生成的index.js代码根本就是空的,说明JavaScript里根本不存在接口概念,纯粹用于TypeScript编译器。

接口也是可以继承的,比如CloudCalendar继承Calendar并新增sync()方法。

定义了接口,我们需要实现它,比如GoogleCalendar要实现CloudCalendar接口,使用的关键字是 implements 。

小提示:利用vscode,可以快速填充实现接口的代码,上图的addEvent, removeEvent, sync 方法都是vscode自动生成的,但属性name还不能自动生成,需要手动编写构造函数。

有人可能会问:到底该用abstract抽象类还是interface接口?

这个要看具体情况。

如果基类没有任何逻辑,仅定义了结构,应该使用接口,代码更简洁;基类有一些逻辑需要共享给子类的话,就该使用抽象类。


小结

本文介绍TypeScript面向对象编程方面的知识。

下一篇介绍泛型。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注