TypeScript学习笔记(五) 类

大家好,我是半虹,这篇文章来讲 TypeScript 中的类


类是面向对象编程中的重要概念,可以提供一种组织和规划代码的方式

同时也是对象的蓝图,定义对象的状态和行为,也即对象的属性和方法

下面具体介绍  TypeScript  中类的用法,包括类的定义、使用、拓展等


1、定义

使用 class  关键字可以定义类,在类中可以声明属性和方法

(1)声明属性

可以在类的顶层声明属性,同时还能显式声明属性类型

若无显式声明,那么就会被隐式推导为 any

class Person {
  // 声明属性
  name: string; // 属性名为 name,类型为 string
  age : number; // 属性名为 age ,类型为 number
}

另外还能设置属性默认值,此时属性类型声明可以省略

编译器会根据属性默认值来自动推导类型

class Person {
  // 声明属性
  name = 'Jack'; // 属性名为 name,默认值为 Jack,此时类型会被推导为 string
  age  = 18;     // 属性名为 age ,默认值为 18  ,此时类型会被推导为 number
}

(2)声明方法

同样在类的顶层声明方法,此时可以在函数字面量声明参数和返回值类型

函数内可以使用 this 访问类中其它的属性和方法

class Person {
  name: string;
  age : number;

  // 声明方法
  // 接收零个参数,返回一个字符串
  sayHi():string {
    return `My name is ${this.name}. I am ${this.age} years old.`
  }
}

(3)构造函数

构造函数在实例化对象时自动执行并返回一个对象,构造函数名称必须为 constructor

通常会在该函数内执行一些必要的初始化工作,例如初始化属性值

class Person {
  name: string;
  age : number;

  // 构造函数
  // 接收一个字符串和一个数字,分别用于初始化 name 和 age 属性,默认返回 Person 类型对象
  constructor(name:string, age:number) {
    this.name = name;
    this.age  = age ;
  }

  sayHi():string {
    return `My name is ${this.name}. I am ${this.age} years old.`
  }
}

(4)只读属性

可以使用 readonly 关键字声明某个属性只读,表示该属性无法被修改

只读属性 只能在声明时赋予初始值,或是在构造函数中赋予或修改其值

class Person {
  readonly name: string = 'Jack'; // 声明只读属性,同时赋予初始值
  readonly age : number;          // 声明只读属性,没有赋予初始值

  // 构造函数
  constructor() {
    this.name = 'Jacky'; // 修改只读属性,编译正常
    this.age  = 18;      // 赋值只读属性,编译正常
  }

  // 普通方法
  changeProps() {
    this.name = 'Jacky'; // 修改只读属性,编译错误
    this.age  = 18;      // 修改只读属性,编译错误
  }
}

(5)存取器方法

存取器是特殊的类方法,用于控制对成员的访问,具体来说包括有两类:

  • 取值器方法:使用  get  关键字定义,在属性被读取时调用,用于控制属性读取行为
  • 存值器方法:使用  set  关键字定义,在属性被写入时调用,用于控制属性写入行为
class Person {
  _name = 'Jack';

  // 取值器方法,其中 get 是关键字,name 是属性名,在读取 name 属性时,该方法会自动调用
  // 该方法不接收任何参数,且返回值类型就是属性类型
  get name() {
    return this._name;
  }

  // 存值器方法,其中 set 是关键字,name 是属性名,在写入 name 属性时,该方法会自动调用
  // 该方法只接收一个参数,且参数的类型默认是取值器返回类型
  set name(newName) {
    this._name = newName;
  }
}

注意,一个属性的存取器并不要求同时都有声明,此时会有以下的表现:

  • 如果一个属性只声明取值器,不声明存值器,那么写入该属性时会失败
  • 如果一个属性只声明存值器,不声明取值器,那么读取该属性时会返回 undefined
class Person {
  _name = 'Jack';
  _age  = 18;

  get name() {              // 只有取值器
    return this._name;
  }

  set age(newAge) {         // 只有存值器
    this._age = newAge;
  }

  getProps() {
    console.log(this.name); // 正常
    console.log(this.age);  // 编译时正常,运行时返回 undefined
  }

  setProps() {
    this.name = 'Jacky';    // 编译时报错,运行时不会执行任何逻辑
    this.age = 21;          // 正常
  }
}

2、使用

(1)实例化

使用  new  关键字可以实例化对象,每个对象都有类中定义的属性和方法

需要注意的是,不同的实例化对象中属性和方法是独立的,它们互不影响

// 定义类

class Person {
  // 声明属性
  name = 'Jack';

  // 声明方法
  sayHi() {
    console.log('Hi');
  }
}

// 实例化对象

let person1 = new Person(); // person1 变量的类型是 Person,可以显式指定为 person1:Person
let person2 = new Person(); // person2 变量的类型是 Person,可以显式指定为 person2:Person

person1.name = 'Jacky';
console.log(person1.name); // Jacky
console.log(person2.name); // Jack

person1.sayHi = function() {
  console.log('Hello');
}
person1.sayHi(); // Hello
person2.sayHi(); // Hi

(2)类型

TypeScript 中代码分为两种,分别是值代码和类型代码,二者位于不同的命名空间

一般情况下,代码要么属于值代码,要么属于类型代码,而在编译时只保留值代码

// 值代码
// 编译时会被保留
let a = 'string';
let b = function() { console.log('function'); }

// 类型代码
// 编译时会被删除
type c = number|string;
interface d {
  e: number;
  f: string;
}

但类声明比较特别,就和枚举一样,其既是类型也是值

编译器会根据上下文自动推断这时候用的是类型还是值,这种特性用起来十分方便

// 枚举
enum G {
  h,
  i,
  j,
}
let g:G = G.h;     // 其中,第一个 G 表示的是类型,第二个 G 表示的是值

// 类
class K {}
let k:K = new K(); // 其中,第一个 K 表示的是类型,第二个 K 表示的是值

总结一下,类本身就是类型,不过此时表示的是该类实例对象的类型

与此同时,类本身也是值,而这时可以使用 typeof 获取该类作为值时的类型

class Point {
  x:number;
  y:number;
  // 构造函数
  constructor(x:number, y:number) {
    this.x = x;
    this.y = y;
  }
}

function createPoint(
  PointClass: typeof Point, // 接收的是 Point 类的本身,不是 Point 类的实例对象,此时可以使用 typeof 获取类的类型
  x:number,
  y:number,
):Point {
  return new PointClass(x, y);
}

let point1:Point = createPoint(Point, 1, 2);
let point2:Point = createPoint(Point, 3, 4);

本质上,类不过是构造函数的语法糖,因此也可以用构造函数的类型签名描述类的类型

构造函数的类型签名相比于普通函数,主要区别在于前面多了  new  关键字来进行修饰

// 普通函数的类型签名有两种写法,因此构造函数的类型签名也是有两种写法,一种是简写版,一种是完整版

type PointType1 = new (x:number, y:number) => Point; // 简写版
type PointType2 = {                                  // 完整版
  new (x:number, y:number): Point;
}

function createPoint(
  PointClass: PointType1, // 或者是 PointType2
  x:number,
  y:number,
):Point {
  return new PointClass(x, y);
}

(3)静态成员

之前介绍的类中定义的属性和方法,类本身是无法使用的,只能由实例化对象使用

但是有些场景下,一些属性和方法更适合由类本身使用嘞,例如:

  • 类的所有实例需要共享某些属性或方法
  • 某些属性或方法是与类的实例对象无关

这时候就需要定义静态成员,静态成员只由类本身使用,并不能由实例化对象使用

可以使用 static 关键字定义静态成员,静态成员有二,包括静态属性和静态方法

class Test {
  // 实例属性
  x:number = 0;
  // 静态属性
  static y:number = 0;
  // 实例方法
  printX() {
    console.log(this.x);
  }
  // 静态方法
  static printY() {
    console.log(this.y);
  }
}

Test.x;        // 编译报错
Test.y;        // 编译正常
Test.printX(); // 编译报错
Test.printY(); // 编译正常

let test = new Test();

test.x;        // 编译正常
test.y;        // 编译报错
test.printX(); // 编译正常
test.printY(); // 编译报错

3、继承 (extends)

(1)继承

继承是面向对象编程中的重要概念,允许一个类(子类)基于另外一个类(父类)定义

子类能继承父类的属性和方法,并且能在此基础上添加自己的属性和方法

可以用  extends 关键字实现,在该关键字之后可指定当前类继承的父类

class Father {
  greeting:string = 'Hi.';
  sayHi() {
    console.log(this.greeting);
  }
}

class Son extends Father { // 可以用 extends 关键字实现继承
  identity:string = 'I am son.';
  identify() {
    console.log(this.greeting, this.identity);
  }
}

let f = new Father();
let s = new Son();

s.sayHi();    // 子类可以继承父类的属性和方法,输出为:Hi.
s.identify(); // 子类也能添加自己的属性和方法,输出为:Hi. I am son.

// 根据结构类型原则
// 任何可以使用父类的地方都可以使用子类代替 ,但反之不行
let a:Father = f;
a = s; // 编译正常
let b:Son = s;
b = f; // 编译报错

子类可定义与父类同名的属性和方法,此时定义的属性和方法会覆盖父类的属性和方法

但是要注意,这些同名的属性和方法要兼容原来的类型声明,否则编译器会报错不通过

class Father {
  greeting:string = 'Hi.';
  sayHi() {
    console.log(this.greeting);
  }
}

class Son extends Father {
  greeting:string = 'Hello.'; // 同名属性
  identity:string = 'I am son.';
  sayHi() { // 同名方法
    console.log(this.greeting, 'I am new.');
  }
  identify() {
    console.log(this.greeting, this.identity);
  }
}

let f = new Father();
let s = new Son();

f.sayHi();    // Hi.
s.sayHi();    // Hello. I am new.
s.identify(); // Hello. I am son.

子类方法中还能调用父类方法,此时只要使用 super 关键字即可

  • 构造函数中,调用方式为 super(),此时会调用 父类的构造函数

    而且要注意,子类如果有 构造函数 ,构造函数中 一定要有 super()

  • 普通方法中,调用方式为 super.父类方法()

class Father {
  greeting:string;
  constructor(greeting) {
    this.greeting = greeting;
  }
  sayHi() {
    console.log(this.greeting);
  }
}

class Son extends Father {
  greeting:string;
  identity:string;
  constructor(greeting, identity) {
    super(greeting); // 调用父类的构造函数,如果缺少,就会报错
    this.identity = identity;
  }
  sayHi() {
    super.sayHi();   // 调用父类的普通方法,需要注意,虽然此时执行的是父类方法的逻辑,但是其中的 this 指向子类
    console.log(this.greeting, 'I am new.');
  }
  identify() {
    super.sayHi();   // 调用父类的普通方法,需要注意,虽然此时执行的是父类方法的逻辑,但是其中的 this 指向子类
    console.log(this.greeting, this.identity);
  }
}

let f = new Father('Hi.');
let s = new Son('Hello.', 'I am son.');

f.sayHi();    // Hi.
s.sayHi();    // Hello.           (注意这里是 Hello,而不是 Hi)
              // Hello. I am new.
s.identify(); // Hello.           (注意这里是 Hello,而不是 Hi)
              // Hello. I am son.

(2)访问修饰符

为了控制类中成员对外部代码的可见性,TypeScript 提供有三种访问修饰符

这些修饰符对类本身以及类继承后成员的访问都有着一定的约束

  • public       表示公有成员,该成员既能在类内部访问,也能通过类实例访问

    public       成员可被继承,且继承后的可访问性不变

  • protected 表示保护成员,该成员可以在类内部访问,不能通过类实例访问

    protected 成员可被继承,且继承后的可访问性不变,但也可以将其设置为 public

  • private     表示私有成员,该成员可以在类内部访问,不能通过类实例访问

    private     成员不可继承,且子类定义父类私有成员的同名成员会导致报错

需要注意的是 ,如果没有设置任何访问修饰符,默认就是 public

class Father {
  public x = 'x';    // 公有成员
  protected y = 'y'; // 保护成员
  private z = 'z';   // 私有成员

  // 类内部
  testF() {
    console.log(this.x); // 编译正常,公有成员可以在类内部访问
    console.log(this.y); // 编译正常,保护成员可以在类内部访问
    console.log(this.z); // 编译正常,私有成员可以在类内部访问
  }
}
// 类实例
let f = new Father();
f.x; // 编译正常,公有成员可以用类实例访问
f.y; // 编译报错,保护成员不能用类实例访问
f.z; // 编译报错,私有成员不能用类实例访问

class Son extends Father {
  // 继承后:
  // 公有成员 x 可被继承,且其可访问性仍为 public
  // 保护成员 y 可被继承,且其可访问性仍为 protected
  // 私有成员 z 不可继承

  // 等价于:
  // public x = 'x';     // 公有成员
  // protected y = 'y';  // 保护成员,此时可以将其改为公有成员:public y = 'y';

  // 类内部
  testS() {
    console.log(this.x); // 编译正常,公有成员可以在类内部访问
    console.log(this.y); // 编译正常,保护成员可以在类内部访问
    console.log(this.z); // 编译错误,相当于无定义
  }
}
// 类实例
let s = new Son();
s.x; // 编译正常,公有成员可以用类实例访问
s.y; // 编译报错,保护成员不能用类实例访问
s.z; // 编译报错,相当于无定义

4、实现 (implements)

我们知道,typeinterface 都能用于定义一个类型声明

那么其实,typeinterface 也能用对象形式的类型声明为类指定一组检查条件

type typeC = {
  p1: number;
  p2: string;
  f1: (x:number, y:number) => number;
}

interface interfaceC {
  p1: number;
  p2: string;
  f1: (x:number, y:number) => number;
}

在定义类时,可以用 implements 指定检查条件,如有多个,则用逗号分隔

在实现类时,需要满足这些条件, 具体来说:

  • 类需要实现检查条件中的所有成员,且成员的类型也要满足条件
  • 类可以定义检查条件中没有的成员
class C implements typeC {
  p1: number;
  p2: string;
  f1(x:number, y:number):number { return x + y; }
  f2(x:string, y:string):string { return x + y; }
}

5、抽象 (abstract)

在定义类时,如果在类的前面加上 abstract 关键字,则表示该类是抽象类

抽象类无法被实例化,故只能作为父类使用, 普通类可以继承该类并实例化

// 抽象类
abstract class Person {
  name: string;
  age : number;
}
let p = new Person(); // 编译错误,抽象类无法实例化

// 抽象类
// 抽象类可以继承抽象类
abstract class Doctor extends Person {
  
}
let d = new Doctor(); // 编译错误,抽象类无法实例化

// 普通类
// 普通类可以继承抽象类
class Lawyer extends Person {
  
}
let l = new Lawyer(); // 编译正常,普通类可以实例化

在抽象类中,如果在成员前面加上 abstract 关键字,则表示成员是抽象成员

抽象成员不能有具体实现,而普通子类则必须 实现所有的抽象成员,否则报错

abstract class Person {
  // 抽象成员只能存在于抽象类
  abstract name: string;
  abstract age : number;
  abstract addF(x:number, y:number):number; // 抽象成员不能有具体实现
}

// 抽象子类,可以不实现抽象成员
abstract class Doctor extends Person {
  
}

// 普通子类,必须要实现抽象成员
class Lawyer extends Person {
  name: string;
  age : number;
  addF (x:number, y:number):number { return x + y; }
}


好啦,本文到此结束,感谢您的阅读!

如果你觉得这篇文章有需要修改完善的地方,欢迎在评论区留下你宝贵的意见或者建议

如果你觉得这篇文章还不错的话,欢迎点赞、收藏、关注,你的支持是对我最大的鼓励 (/ω\)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值