博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
③TypeScript 类(继承、静态属性和方法、抽象类)
阅读量:3965 次
发布时间:2019-05-24

本文共 13059 字,大约阅读时间需要 43 分钟。

TypeScript


写下博客主要用来分享知识内容,并便于自我复习和总结。

如有错误之处,请各位大佬指出。


类的定义

为了方便理解,我们先对比一下在es5中很像是类的用法:(构造函数)

// 构造函数function Person(){
this.name = "ls" // 属性 this.run = function(){
// 实例方法 console.log(this.name + "在跑步") }}Person.getInfo = function(){
console.log("我是静态方法")}// 原型链上的属性会被多个实例共享Person.prototype.sex = "男"Person.prototype.work = function(){
console.log(this.name + "在工作")}let p = new Person()console.log(p.name)p.run()console.log(p.sex)p.work()Person.getInfo()

再来看一下java的类:

public class ObjectAndClass {
public static void main(String args[]) {
pet cat = new pet("猫"); // 可以给构造函数传递参数 cat.say(); // 调用方法 System.out.println(cat.name); // 可以获取 pet dog = new pet(); // 也可以不传递参数 dog.say(); }}// 类,简单来说就是存储对象的属性和方法class pet {
// 对象标识符在java里分为private、public、default、protected,如果不标注默认为public // 被public标注的是公有变量,我们可以在类外使用 // 被private标注的是私有变量,只能在类里使用 // 具体内容在这里就不说了,在下面还会提到 // 并且,在java里,数据类型是一定要指明的。 String name = "狗"; private int a = 1; // java里的构造函数和类名是相同的 // 就算不手动创建,它其实也一定会创建这个无参的构造函数 // 那既然它会自动创建,那为什么要手动创建? // 这里需要注意: // 1、如果类里没有构造函数,那么不传参数也不会报错 // 因为它自动创建了无参的构造函数 // 2、但是如果类里存在有参的构造函数, // 那么这个无参构造函数,如果不手动创建,它是没效果的 // 也就是说,这时不传参数是会报错的 // 3、所以这也就是为什么,虽然我们创建了有参的构造函数 // 习惯上还要创建无参的构造函数 pet() {
} // 手动创建了有参构造函数,之后传递参数时一定需要严格对照 // 这也就是之前提到的函数的重载 // 在java里的重载,同名函数只要形参不同,那么调用同名函数时,就会根据实参的不同来调用函数 // 此时,我们创建实例时,如果传递参数,就会赋值给name // 如果不赋值,name就会为默认值 pet(String name) {
this.name = name; } public void say(){
System.out.println(this.name); }}

如果对上面的说明有了一些理解之后,我们来看一下ts中的类:

class Person{
name:string // 属性,前面省略了public关键词 // 注意:这里的构造函数和java一样, // 它其实也会创建一个无参的构造函数, // 但因为我们创建了有参的构造函数, // 所以无法使用无参构造函数,即一定要传参 // 为什么一定要注意这部分问题,看下面的继承就知道了 constructor(name:string) {
// 构造函数,实例化类的时候触发的方法 this.name = name } // 普通函数 run():void{
console.log(this.name + "在运动") } // set和get,获取和设置相应属性 getName():string{
return this.name } setName(name:string):void{
this.name = name }}let p = new Person("李四")p.run()

这里还有一些注意点。

刚才我们提到可以没有构造函数,它会默认创建一个无参构造函数,以下代码就可以证明:

class Person{
name:string = "李四" run():string{
return `${
this.name}在运动` }}let p = new Person() // 可以不传参,这时类里的参数就会使用默认值console.log(p.run())

但不能传参的用处肯定很少,我们不可能总用默认值的,所以我们需要创建有参构造函数。但这时就不能不传参数了。那它能不能像Java一样,创建很多构造函数呢?而且之前说到过ts存在重载机制的,如果手动创建一个无参构造函数不就解决问题了?

class Person{
name:string = "李四" constructor() {
} constructor(name:string) {
this.name = name } run():string{
return `${
this.name}在运动` }}

通过代码报错,我们得知,这还真不行,constructor构造函数只能构建一个。

在这里插入图片描述


继承

也许你在学习js时,没有听过继承(因为我很感同身受,在学习js过程中,不知道js也有继承的方式。直到后来准备面试的时候,遇到继承问题)。因此,在说ts的继承前,如果各位不知道什么是继承,可以来学习一下:

说回正题,在ts中实现继承的方式,和java依旧很像,在这里就不展示java的写法了,直接看ts。

class Person{
name:string // 此时注意之前反复提到的构造函数问题 constructor(name:string) {
this.name = name } run():string{
return `${
this.name}在运动` }}// extends:继承// 继承后的子类,拥有父类的所有属性和方法class Web extends Person{
}// let w = new Web() // 不传参数会报错// 此时可以发现,虽然子类没有任何内容,// 但我们传递了参数,它依然输出内容// 这也就说明了,此时传递参数,它会去找父类的构造函数,// 而不是子类的无参构造函数let w = new Web("李四")console.log(w.run()) // 输出:李四在运动

在继承的时候,还有些问题需要注意:

假如子类不想用父类的构造函数,我们对其重写可以吗?

class Web extends Person{
name: string age: number constructor(name:string,age:number) {
this.name = name this.age = age }}

结果来看,它直接报错:

在这里插入图片描述
也就是说,继承后的子类,构造函数里一定要用super。也就是说,一定要用到父类的构造函数。基本用法是这样的:

class Person{
name:string constructor(name:string) {
this.name = name } run():string{
return `${
this.name}在运动` }}class Web extends Person{
constructor(name:string) {
super(name) }}let w = new Web("老六")console.log(w.run())

那么这里有个问题,如果子类中有和父类相同的同名函数和属性,它会使用父类的还是子类的呢?

class Person{
name:string = "李四" constructor(name:string) {
this.name = name } run():string{
return `${
this.name}在运动1` }}class Web extends Person{
name: string = "王五" constructor(name:string) {
super(name) } run():string{
return `${
this.name}在运动2` }}let w = new Web("老六")console.log(w.run())

在这里插入图片描述

先从结果看,如果子类和父类拥有同名函数,它会去使用子类的函数。并且,子类中的this.name用的是子类中的name,而不是父类的,并且传递的参数没起到作用。这是因为,我们传递参数的时候,它会去调用构造函数,构造函数中用到了super,也就是去用父类的构造函数,将传递的值赋给父类。因此,传递的参数不会影响到子类。而子类中出现了同名参数name,所以父类同名参数被覆盖了。同时,因为其设置了默认参数,所以就展示出了这样的内容。

那我们的本意应该是传递参数,可以影响输出结果,所以可以这么做:

(又或者在子类中不去定义同名参数)

class Person{
name:string = "李四" constructor(name:string) {
this.name = name } run():string{
return `${
this.name}在运动1` }}class Web extends Person{
name: string = "王五" constructor(name:string) {
super(name) this.name = name // 在子类中作出改变 } run():string{
return `${
this.name}在运动2` }}let w = new Web("老六")console.log(w.run())

在这里插入图片描述

假如把子类的run函数去掉,那就输出:老六在运动1

(虽然在该例中,用super传递参数给父类没有用,但那是因为我们用了同名属性,它会被子类覆盖。一般情况下,继承的目的就是在父类的基础上添加新属性和新方法,或者改进旧方法,因此同名属性使用不当可能会造成误解噢,因为本来就可以共用同一个属性。总之,在面对继承问题时,需要小心同名覆盖问题)

综上所述,如果子类中没有构造函数,它就会自动去使用父类的构造函数。而子类中如果写构造函数,那么它一定要用super去调用父类的构造函数。调用结束后,继续执行子类构造函数中内容。除了构造函数,也就是说,如果子类和父类有同名函数和属性,那么父类的内容就会被子类覆盖。


对于super还有一点补充:super不是只能用于构造函数,其它函数也可以用:

class Person{
name:string = "李四" getName():string{
return this.name }}class Web extends Person{
run():string{
return super.getName()+'在跑步' } eat():string{
return super.getName()+'在吃饭' }}let w = new Web()console.log(w.run())console.log(w.eat())

对于构造函数还有一点补充:我们在使用构造函数的时候,还有一种简化代码方式:

(这样做之后,我们就不需要额外声明属性,只需要加一个public关键字就可以了)

class Person{
constructor(public name:string) {
} getName():string{
return this.name }}class Boy extends Person{
constructor(public age:number) {
super('李四') } getAge():number{
return this.age }}const p = new Boy(15)console.log(p.getName())console.log(p.getAge())

类里面的修饰符

虽然之前在定义属性的时候没有写修饰符,但它们在ts中是存在的。

ts提供了三种修饰符:public、protected、private。

public:公有类型。在当前类里面、子类、当前类外面都可以访问。

(如果省略修饰符,那么属性的修饰符默认为public)

class Person{
public name:string // 公有属性 constructor(name:string) {
this.name = name } run():string{
return `${
this.name}在运动` // 公有属性在类里可以访问 }}class Web extends Person{
constructor(name:string) {
super(name) } work():string{
return `${
this.name}在工作` // 公有属性在子类可以访问 }}let p = new Person("李四")console.log(p.name) // 公有属性在类外可以访问

protected:保护类型。在当前类里面、子类里面可以访问,在当前类外面无法访问。

class Person{
protected name:string // 保护属性 constructor(name:string) {
this.name = name } run():string{
return `${
this.name}在运动` // 保护属性在类里可以访问 }}class Web extends Person{
constructor(name:string) {
super(name) } work():string{
return `${
this.name}在工作` // 保护属性在子类可以访问 }}let p = new Person("李四")// 报错:console.log(p.name) // 保护属性在类外无法访问

在这里插入图片描述


private:私有类型。在当前类里面可以访问,子类、当前类外面都无法访问。

class Person{
private name:string // 私有属性 constructor(name:string) {
this.name = name } run():string{
return `${
this.name}在运动` // 私有属性在类里可以访问 }}class Web extends Person{
constructor(name:string) {
super(name) } work():string{
// 报错: return `${
this.name}在工作` // 私有属性在子类无法访问 }}let p = new Person("李四")// 报错:console.log(p.name) // 私有属性在类外无法访问

在这里插入图片描述


知道了上述三种修饰符的特性后,再补充一些内容:

刚才的继承中,我们知道子类会覆盖父类中同名属性和方法,那修饰符可以覆盖吗?

class Person{
private age:number; constructor(age:number) {
this.age = age; }}class Web extends Person{
public age:number constructor(age:number) {
super(age) this.age = age; }}let p = new Person(15)

结果表明,我们无法修改同名属性的修饰符:

在这里插入图片描述
那会不会是private的问题呢,因为private只能在当前类里访问?那我们交换上面代码的修饰符:

class Person{
public age:number; constructor(age:number) {
this.age = age; }}class Web extends Person{
private age:number constructor(age:number) {
super(age) this.age = age; }}let p = new Person(15)

结果表明,我们依然无法修改。

在这里插入图片描述
其它的同理,只要涉及到修饰符的变化,都会报错。

道理其实很简单,我既然在父类中为属性或方法设置了相应修饰符,也就代表着我希望它们的作用范围被控制,这样得到的结果一定会是我们想要的。既然父类都设置好了属性或方法的修饰符,那就不希望它在相应的作用范围外被调用。现在在子类中却想修改它,只可能是误操作,所以ts报错(除非是设计父类时出现问题)。


那我想定义一个同名且修饰符相同的属性,总可以了吧?

class Person{
private age:number; constructor(age:number) {
this.age = age; }}class Web extends Person{
private age:number constructor(age:number) {
super(age) this.age = age; }}let p = new Person(15)

这回就真的是因为private的问题了,我们无法声明一个同名private属性或方法。(退一步讲,如果能声明,那private可就没用了)

(public和protected,因为子类可以用,所以不会出现报错)
在这里插入图片描述


get和set方法

看一个例子:

class Person{
constructor(public _age:number) {
} get age(){
return this._age; } set age(age:number){
this._age = age; }}const p = new Person(18)console.log(p.age)p.age = 21;

可以发现,这么做看起来多此一举,因为最后还是要获取或修改age,我为什么不直接去修改或获取?

其实使用get和set,是可以满足重用性的。比如,每次我都对数据进行处理再返回,我使用get只需要p.age就能获取,而不是用函数p.age()。如果我想对传进去的参数进行判断,每个参数都进行判断肯定不行,那我在类外再写个函数判断 不如直接用set判断。

class Person{
constructor(public _age:number) {
} get age(){
// 和普通函数一样,可以对返回值进行设计 // 但它必须返回get到的内容 return this._age + 5; } set age(age:number){
// 满足重用性,只要传入值就可以作某些操作 if(age > 18){
this._age = age; }else{
console.log('年龄不大于18') } }}const p = new Person(18);// 我们直接p.age,不用p.age()console.log(p.age);// 满足重用性,我们不需要在这里进行age判断// 在类中就帮我们封装好了方法p.age = 17;p.age = 21;

这里补充一点,get可以设置返回值类型,只要能匹配上类型就可以:

get age():number{
return this._age; }

但是,set是绝对无法设置返回值类型的。

在这里插入图片描述


最后要说的是,get和set也可以对private和protected属性生效,即在它们的使用范围外,也可以修改或调用它们的值了:

class Person{
constructor(private _age:number) {
} get age(){
return this._age; } set age(age:number){
this._age = age; }}const p = new Person(18);console.log(p.age);p.age = 19;

这么做可并没有违背private和protected的初衷噢,虽然它们的本意确实是限制属性的使用范围,但说到底我们还是要使用它们的,使用get和set就可以更好的对它们进行一些封装修饰,然后展示出来或者是存储进去。所以,在类外想要访问它们,就需要使用get和set的类名,得到我们想要的结果了。


静态属性 和 静态方法

先来回顾一下es5中的静态属性和方法:

function Person(){
this.run1 = function(){
// 实例方法 console.log("run1") }}Person.sex = "ls" // 静态属性Person.run2 = function(){
// 静态方法 console.log("run2")}let p = new Person()p.run1() // 实例方法的调用console.log(Person.sex) // 静态属性的调用Person.run2() // 静态方法的调用

在ts中使用静态属性和方法,需要有static关键字,且在静态方法里只能用静态属性:

class Person{
public name:string = "ls" public sex:string = "男" static age:number = 20 // 静态属性 constructor(name:string) {
this.name = name } run():void{
// 实例方法 console.log(`${
this.name}在运动`) } static print():void{
// 静态方法 console.log("print方法") // 静态方法里没办法直接调用类里面的属性 // 报错: console.log("print:"+this.name) // 输出 print:Person console.log("print:"+this.sex) // 输出 print:undefined // 如果想使用类里面的属性,只需要让类里面的属性变为静态属性 console.log("print:"+this.age) // 输出 print:20 }}// 静态属性和方法的好处,就是我们不需要去实例化对象,就能直接去使用console.log(Person.age)Person.print()

但是从输出结果就可以发现,name这个属性特殊,如果没有静态属性叫name,它会输出类名,同时也是会报错的。那如果我们创建静态属性name,可不可以解决这个问题呢?那我们就可以发现,我们不能取一个静态属性名为name的属性:

在这里插入图片描述
静态属性name和其中设置的函数name冲突了,所以这也就是为什么会输出类名。所以之后需要注意,name这个属性名很特殊,它不能设置为静态属性。


只读属性

在类里还有一种修饰属性的符号,它是readonly,只读属性。和它的命名一样,这种属性在我们通过构造函数传递进来后,就无法再修改了:

class Person{
public readonly name:string; constructor(name:string) {
this.name = name; }}const p = new Person('李四');p.name = '王五'; // 报错console.log(p.name);

在这里插入图片描述

用set也无法修改噢。
在这里插入图片描述


多态

多态:父类定义一个方法但不去实现,让继承它的子类去实现,这样一来每一个子类都可以有不同的表现。所以多态在这里也属于继承。

在最开始学习的时候,可能会觉得奇怪:为什么要创建这么一个看似毫无作用的父类?首先,创建了这样的父类,它本身的代码量很少,方便其它人在看到这个父类的时候,就知道各个功能要实现什么功能,就相当于是个参考文档。并且,让子类去继承这样的父类,也就有了一个参考,避免之后子类误操作创建了同名方法去覆盖父类的方法。我们只需要对其进行扩展即可,而且不同的子类可以为其设置不同的表现效果。

class Animal{
name:string constructor(name:string) {
this.name = name } eat(){
// 之后子类会对其进行改变 console.log("吃的方法") }}class Dog extends Animal{
constructor(name:string) {
// 子类可以写构造函数,也可以不写 // 如果写,那就必须要super super(name) } eat(){
return this.name + "吃肉" }}class Cat extends Animal{
// 子类可以写构造函数,也可以不写 // 如果写,那就必须要super eat(){
return this.name + "吃鱼" }}let dog = new Dog("旺财")let cat = new Cat("阿愈")console.log(dog.eat()) // 输出:旺财吃肉console.log(cat.eat()) // 输出:阿愈吃鱼

抽象类

ts中的抽象类:它用来提供其他类继承的基类,不能直接被实例化。如果想使用抽象类,用abstract关键字来定义抽象类和抽象方法,抽象类中的抽象方法不包含具体实现并且必须在派生类中实现。需要注意,abstract抽象方法只能放在抽象类里面。

abstract class Animal{
public name:string constructor(name:string) {
this.name = name; } abstract eat():any run(){
console.log("其它方法可以不实现") }}class Dog extends Animal{
constructor(name:string) {
super(name) } // 抽象类的子类必须实现抽象方法,否则报错 eat(){
return this.name + "吃肉" }}let cat = new Animal('阿愈'); // 报错:无法直接实例化抽象类let dog = new Dog("旺财")console.log(dog.eat()) // 输出:旺财吃肉

那现在回头聊多态,就可以发现,其实多态的用法没那么多要求,因为它更像是自己给自己写的标准,父类其实就是普通的类,子类不需要一定实现父类的函数。而抽象类是ts设计出来让我们来定义标准的,它就会有很多束缚了,一旦出现问题就会报错,相比多态里出现问题可能就无法及时发现问题所在。

转载地址:http://bmyki.baihongyu.com/

你可能感兴趣的文章
DB2 脚本
查看>>
DB2 锁升级失败将引起死锁
查看>>
遇到问题该如何解决
查看>>
[第21课] 二项分布3
查看>>
[第22课] 二项分布4
查看>>
Pandas 筛选数据
查看>>
Pandas 复合索引
查看>>
[第23课] 期望值E(X)
查看>>
[第24课] 二项分布的期望值
查看>>
Pandas 处理 NaN
查看>>
Pandas 分组统计
查看>>
Pandas 多 DataFrame联接
查看>>
Sybase 系列文章目录
查看>>
SQLServer
查看>>
Hibernate 通过 Hibernate 访问数据库
查看>>
java面试题
查看>>
消息队列相关(MQ)
查看>>
生成短连接
查看>>
java多线程
查看>>
mybatis高级结果映射
查看>>