本文介绍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面向对象编程方面的知识。
下一篇介绍泛型。