③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的继承前,如果各位不知道什么是继承,可以来学习一下:JS继承的几种方式

说回正题,在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设计出来让我们来定义标准的,它就会有很多束缚了,一旦出现问题就会报错,相比多态里出现问题可能就无法及时发现问题所在。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

只爭朝夕不負韶華

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值