Skip to content

类、修饰符与多态

涵盖类、修饰符与多态等面向对象相关语法。


第一部分:类

TypeScript 是面向对象的 JavaScript。

类描述了所创建的对象共同的属性和方法

需要注意的是子类只能 extends 一个父类(单继承);可通过 继承链(如 Child → Mid → Base)逐层扩展,但不是 C++ 那种「多父类多重继承」。

类继承后,子类可以对父类的方法重新定义,这个过程称之为方法的重写。

其中 super 关键字是对父类的直接引用,该关键字可以引用父类的属性和方法。

类的方法&变量

构造方法、实例变量、实例方法,静态变量、静态方法

  1. 创建类类名规范:使用大驼峰方式命名
js
class DemoClass {}
  1. 类的构造方法、实例变量、实例方法的创建和使用
js
class DemoClass {
  // 1. 实例变量
  var1_: string;
  var2_: string = "2";

  // 2. 实例方法
  methods1(p1: string) {
    console.log("实例方法1" + "---" + p1);
  }
  methods2(p2: string) {
    console.log("实例方法2" + "---" + p2);
  }

  // 3. 构造方法
  constructor(var1, var2) {
    this.var1_ = var1;
    this.var2_ = var2;
  }
}

// 4. 创建实例、调用构造方法、改变实例变量、获取实例化对象
const demoClass = new DemoClass("实例变量1", "实例变量2");
console.log(demoClass);

// 5. 获取实例变量、改变实例变量
console.log(demoClass.var1_);
demoClass.var1_ = "改变实例变量1";
console.log(demoClass.var1_);

// 6. 调用实例方法
demoClass.methods1("参数1");
  1. 类的静态变量、静态方法的创建和使用 static
js
class DemoClass {
  // 1. 静态变量
  static var1_: string;
  static var2_: string = "2";

  // 2. 静态方法
  static methods1(p1: string) {
    console.log("静态方法1" + "---" + p1);
    DemoClass.var1_ = "在类的内部改变静态变量";
  }
  static methods2(p2: string) {
    console.log("静态方法2" + "---" + p2);
  }
}

// 3. 获取静态变量、改变静态变量
console.log(DemoClass.var1_);
DemoClass.var1_ = "在类的外部改变静态变量1";
console.log(DemoClass.var1_);

// 4. 调用静态方法、改变静态变量
DemoClass.methods1("参数1");
console.log(DemoClass.var1_);
  1. 改变实例变量的三种方式
js
// 1. 通过实例化对象赋值改变
class DemoClass1 {
  var1_: string = "实例变量初始值";
}

const demoClass1 = new DemoClass1();
console.log(demoClass1.var1_);
demoClass1.var1_ = "通过实例化对象赋值改变";
console.log(demoClass1.var1_);
console.log("----------------------------------------");

// 2. 通过构造函数改变
class DemoClass2 {
  var1_: string = "实例变量初始值";
  constructor(var1: string) {
    this.var1_ = var1; // 在构造函数中赋值改变
  }
}

const demoClass2 = new DemoClass2("通过构造函数改变");
console.log(demoClass2.var1_);
console.log("----------------------------------------");

// 3. 通过 getter 和 setter 方法改变

// 注意:
// 1. 此法通常配合 private 修饰符使用:不允许外部通过实例变量名修改变实例变量,必须通过get和set方法名访问和设置实例变量
// 2. 通常对实例变量进行逻辑处理时使用此法
// 3. 在变量外需要用方法名称来设置和获取实例变量

class DemoClass3 {
  private var1_: string = "实例变量初始值";
  get var1(): string {
    return this.var1_;
  }
  set var1(v: string) {
    if (1 === 1) {
      this.var1_ = v + '----- setter 格式化';
    } else {
      this.var1_ = undefined;
    }
  }
}

const demoClass3 = new DemoClass3();
console.log(demoClass3);

demoClass3.var1 = "通过 getter 和 setter 方法改变";
console.log(demoClass3.var1);
  1. 注意
shell
1. 静态变量和静态方法在类加载的时候就会分配内存,直接通过类名访问,而成员变量只有在类实例化后才会分配内存,通过实例化对象访问
因此,静态变量和静态方法存在时,实例变量和实例方法还未存在,所以:
静态变量和静态方法 无法 使用实例变量和实例方法,
实例变量和实例方法 可以 使用静态变量和静态方法

那么什么时候定义实例的,什么时候定义静态的呢?
需要将状态存储起来的时候就定义实例变量,需要维护状态的时候就定义实例方法
与实例属性无关的操作还想与类建立联系就定义静态变量和静态方法

实例属性和静态属性最大的区别是:类的实例属性在类实例化后保存在实例对象中,且类每次实例化创建的新对象都是相互独立的,即:存储空间不同且不共享
而类的静态属性挂在类本身上,多实例共享同一份存储;若希望多个入口共用一份与实例无关的配置或工具方法,可放在 **static** 上。
实例变量:类内使用 this.变量名,类外使用 实例化对象.变量名
静态变量:类内、类外都使用 类名.变量名

3. 实例变量会成为实例化对象的属性,而静态变量不会成为实例化对象的属性

类的继承

方法重写:子类和父类的方法重名时,子类自己会重写父类方法

子类可继承父类的所有属性,包括静态变量和方法

  1. 单继承
js
// 1. 创建父类(实例成员+静态成员+构造函数)
class Parent {
  pvar1_: string = "父类实例变量1";
  pmethods1() {
    console.log("父类实例方法1");
  }
  static spvar1_: string = "父类静态变量1";
  constructor(param: string) {
    this.pvar1_ = param;
  }
}

// 2. 子类继承父类(实例成员+静态成员+构造函数)
class Child extends Parent {
  cvar1_: string = "子类实例变量";
  constructor(param1: string, param2: string) {
    // super 为父类的构造方法
    super(param1);
    this.cvar1_ = param2;
  }
}

// 3. 子类继承父类的 实例成员+静态成员+构造函数
const child = new Child("改变父类实例变量", "改变子类实例变量");
console.log(child.cvar1_); // 子类自己的实例变量
console.log(child.pvar1_); // 继承父类的实例变量
child.pmethods1(); // 继承父类的实例方法
console.log(Child.spvar1_); // 继承父类的静态变量
  1. 多继承:TypeScript 不支持一个子类 extends 多个父类;下例仅说明「多 extends 写法非法」。
js
class P1 {}
class P2 {}
// class Child extends P1, P2 {} // 不允许:只能继承一个类
class Child extends P1 {}
// class Child extends p2 {} // 不允许:child变量重复会冲突

多层继承(继承链)

js
class P1 {}
class P2 extends P1 {}
class Child extends P2 {}

抽象类

  • 抽象类不可以被实例化,只能被继承
  • 抽象类中定义的抽象方法必须被继承它的子类重写
  • 抽象类中的抽象方法无方法体,子类必须实现;非抽象成员可有默认实现。接口描述对象形状,不含实现;类 implements 接口时需实现接口中的成员(可选属性可不实现)。
  • 在抽象类中的抽象方法不写方法体
  • 一个类只能继承一个抽象类
js
//  定义抽象类
abstract class Person {
  name: string;
  age?: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  // 非抽象方法
  eat(food: string): void {
    console.log(this.name + "" + food);
  }

  // 抽象方法,无方法体
  abstract play(ball: string): void;
}

// 定义子类继承抽象类
class Student extends Person {
  sex: string;
  constructor(name: string, age: number, sex: string) {
    super(name, age);
    this.name = name;
    this.sex = sex;
  }

  // 在子类中必须重写抽象类的抽象方法
  play(ball: string): void {
    console.log(this.name + "" + ball);
  }
}

let alias = new Student("alias", 18, "");
console.log(alias);
alias.eat("苹果");
alias.play("篮球");

TypeScript 接口

可以理解为一种约束,它可以用来约定对象的结构,我们去使用一个接口就要去遵循这个接口的所有约定。

typescript 的接口只是为我们有结构的数据做类型约束,在编译后消失

接口的语法

注意:接口的属性以分号 | 逗号 | 什么也不加 结尾(都行)

语法:interface 接口名

命名规范:常用 I 开头大驼峰命名法

ts
// 定义 人 接口
interface IPerson {
  name: string;
}

// 定义 用户信息 接口
interface IUserInfo {
  username: string;
  password: string;
}

接口的属性类型和权限

  1. 只读属性:在被定义后不可被修改
ts
interface IPerson {
  name: string;
  age: number;
  readonly isLive: boolean; // 只读属性,不可被修改
}

const xiaoming: IPerson = {
  name: '小明',
  age: 7,
  isLive: true,
};

xiaoming.age = 19;
// xiaoming.isLive = false; // 只读属性,不可被修改
  1. 可选属性:接口属性是可选属性的情况下可以不必重写
ts
interface IPerson {
  name: string;
  age: number;
  live?: boolean; // 可选属性,可以不被重写
}

const xiaoming: IPerson = {
  name: '小明',
  age: 7,
};
  1. 索引类型规范:只是定义接口属性的键名和键值的数据类型,不限制键值对的数量
ts
// 定义对象的索引类型属性:属性名为字符串,属性值为字符串或者数字
interface IPerson {
  [key: string]: string | number;
}
const xiaoming: IPerson = {
  name: '小明',
  age: 7,
};

// 定义数组的索引类型属性,key值必须是数字类型
interface IPersonNameList {
  [key: number]: string;
}
const personNameList: IPersonNameList = ['王一', '王二', '王三', '王四', '王五'];

接口的使用方式

接口是抽象的说明,常用于约束对象、数组、类的属性权限、数据类型等,并根据接口定义的属性给与相应代码提示

  1. 约束对象
js
interface IPerson {
  name: string;
  age: number;
}

const xiaoming: IPerson = {
  name: "小明",
  //age: 7, // age 属性在接口中已定义,所以必须有此属性
  age: 7,
};
xiaoming.age = 19;
  1. 约束数组:一般以索引类型的方式约束数组的值的数据类型,key 必须是 number 类型
js
// 定义数组的索引类型属性,key值必须是数字类型
interface IPersonNameList {
  [key: number]: string;
}
const personNameList: IPersonNameList = ["王一", "王二", "王三", "王四", "王五"];
  1. 约束类:定义类 实现 接口,那么此类就会接受接口约束
js
// 定义接口规范
interface IPerson {
  name: string;
  age: number;
  play(ball: string): void;
  say(): string;
}

// Person 类实现 IPerson 接口,接受其约束
class Person implements IPerson {
  name: string;
  age: number;
  play(ball: string): void {
    console.log(this.name + "正在玩" + ball);
  }
  say(): string {
    return this.name + "说: 我今年" + this.age + '';
  }

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

const xiaoming = new Person("小明",23);
xiaoming.play("篮球");
console.log(xiaoming.say());
  1. 约束函数
js
interface IPerson {
  (name: string, age: number): void; // void 代表无返回值,把它换成数据类型,可限制此函数的返回值数据类型
}
const logPersonInfo: IPerson = (name: string, age: number) => {
  console.log(name + age);
};
logPersonInfo("小明", 18);

接口继承

接口继承另一个接口、也可以实现多继承

js
// 定义接口
interface IP1 {
  name: string;
}
interface IP2 {
  age: number;
  eat(food: string): void;
}

// 多继承,方法可重写
interface IStudent extends IP1, IP2 {
  grade: string;
  eat(food?: string): string; // 方法重写
  sleep(time?: string): string;
}

// 类实现接口,接受接口约束
class Student implements IStudent {
  name: string;
  age: number;
  grade: string;
  eat(food?: string): string {
    return this.name + "" + food;
  }
  sleep(time?: string): string {
    return this.name + "睡了" + time;
  }
  constructor(name: string, age: number, grade: string) {
    this.name = name;
    this.age = age;
    this.grade = grade;
  }
}

const xiaoming = new Student("小明", 18, "一年级");
console.log(xiaoming.eat(""));
console.log(xiaoming.sleep("12小时"));

第二部分:多态

不同类型的对象可以通过相同的接口(或父类)进行交互,且具体行为会根据对象的实际类型而变化

多态主要通过继承和接口实现两种方式体现

基于继承的多态

当子类继承父类并重写父类的方法时,通过父类类型的变量引用子类对象,调用方法时会执行子类的实现,这就是继承带来的多态

ts
// 在这个例子中,letAnimalSound 函数接收 Animal 类型的参数,但实际传入的是 Dog 或 Cat 的实例。调用 makeSound 时,会根据对象的实际类型执行对应的子类实现,体现了多态。

class Animal {
  // 父类方法
  makeSound(): void {
    console.log("动物发出声音");
  }
}

class Dog extends Animal {
  // 重写父类方法
  makeSound(): void {
    console.log("汪汪汪");
  }
}

class Cat extends Animal {
  // 重写父类方法
  makeSound(): void {
    console.log("喵喵喵");
  }
}

// 多态体现:通过父类类型接收不同子类对象
function letAnimalSound(animal: Animal): void {
  animal.makeSound(); // 实际执行的是子类的方法
}

// 调用时传入不同子类实例
letAnimalSound(new Dog()); // 输出:汪汪汪
letAnimalSound(new Cat()); // 输出:喵喵喵

基于接口的多态

接口定义了一组规范,不同类可以实现同一个接口,并重写接口中的方法。通过接口类型的变量引用不同实现类的对象,调用方法时会执行各自的实现,这是接口带来的多态。

ts
// printArea 函数接收 Shape 接口类型的参数,但实际传入的是 Circle 或 Rectangle 的实例。调用 getArea 时,会根据对象的实际类型执行对应的实现,同样体现了多态。

interface Shape {
  getArea(): number; // 接口定义的方法
}

class Circle implements Shape {
  radius: number;
  constructor(radius: number) {
    this.radius = radius;
  }
  // 实现接口方法
  getArea(): number {
    return Math.PI * this.radius **2;
  }
}

class Rectangle implements Shape {
  width: number;
  height: number;
  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }
  // 实现接口方法
  getArea(): number {
    return this.width * this.height;
  }
}

// 多态体现:通过接口类型接收不同实现类对象
function printArea(shape: Shape): void {
  console.log(`面积:${shape.getArea()}`); // 实际执行的是实现类的方法
}

// 调用时传入不同实现类实例
printArea(new Circle(5)); // 输出:面积:78.5398...
printArea(new Rectangle(4, 6)); // 输出:面积:24

总结

TS 完全支持多态,其本质是通过父类或接口统一调用方式,而具体行为由子类或实现类决定,这使得代码更加灵活、可扩展,符合面向对象编程的设计原则(如 “里氏替换原则”)。


第三部分:修饰符

在 TypeScript 中,修饰符用于控制类的属性和方法的访问权限

public(默认)

  • 含义:公开的,默认修饰符(不写时默认为 public)。
  • 访问范围:类内部、子类、类外部都可访问。
ts
class Person {
  public name: string; // 等价于 name: string
  public constructor(name: string) {
    this.name = name;
  }
  public sayHello() {
    console.log(`Hello, ${this.name}`);
  }
}

const person = new Person("Alice");
console.log(person.name); // 可访问
person.sayHello(); // 可调用

protected(保护)

  • 含义:受保护的。
  • 访问范围:当前类内部和子类中可访问,类外部不可访问。
ts
class Person {
    protected gender: string;
    constructor(gender: string) {
        this.gender = gender; // 类内部可访问
    }
}

class Student extends Person {
    getGender() {
        return this.gender; // 子类可访问
    }
}

const student = new Student("female");
console.log(student.gender); // 报错:属性“gender”受保护,只能在类“Person”及其子类中访问
console.log(student.getGender()); // 正确:通过子类方法访问

private(私有)

  • 含义:私有的。
  • 访问范围:仅在当前类内部可访问,子类和外部均不可访问。
ts
class Person {
    private age: number;
    constructor(age: number) {
        this.age = age; // 类内部可访问
    }
    private getAge() {
        return this.age; // 类内部可调用
    }
}

const person = new Person(18);
console.log(person.age); // 报错:属性“age”为私有属性,只能在类“Person”中访问
person.getAge(); // 报错:方法“getAge”为私有方法,只能在类“Person”中访问

readonly(只读)

  • 含义:只读的(非访问权限控制,而是限制修改)。
  • 特点:只能在声明时或构造函数中赋值,之后不可修改。可与 public/private/protected 结合使用
ts
class Person {
    public readonly id: number;
    constructor(id: number) {
        this.id = id; // 构造函数中可赋值
    }
}

const person = new Person(1001);
person.id = 1002; // 报错:无法分配到“id”,因为它是只读属性

static(静态)

  • 含义:静态的,属于类本身而非实例。
  • 访问方式:通过 类名.属性/方法 访问,实例无法访问。
  • 可与其他修饰符结合(如 static public、static private 等)。
ts
class MathUtil {
    static PI: number = 3.14;
    static calculateArea(radius: number) {
        return this.PI * radius **2;
    }
}

console.log(MathUtil.PI); // 3.14
console.log(MathUtil.calculateArea(2)); // 12.56

总结

访问权限修饰符:public(默认)、private、protected 控制访问范围。

readonly 限制属性只读,static 定义类级别的成员。