当前位置: 首页 > news >正文

解读typescript中class类

TypeScript 中的类(Class)是面向对象编程(OOP)的核心,它基于 ES6 类的语法,并引入了类型系统和额外特性(如访问修饰符、抽象类等)。以下我为总结出了几点:

一、类的简单介绍

1. 定义类

class Person {
  // 属性(需初始化或通过构造函数赋值)
  name: string;
  age: number;

  // 构造函数
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  // 方法
  greet() {
    console.log(`Hello, I'm ${this.name}.`);
  }
}

// 使用
const alice = new Person("Alice", 25);
alice.greet(); // 输出:Hello, I'm Alice.

类的方法跟普通函数一样,可以使用参数默认值,以及函数重载(详情可阅读上一篇文章)。

class Point {
  x: number;
  y: number;
  constructor(x = 0, y = 0) {
    this.x = x;
    this.y = y;
  }
}

上面示例中,如果新建实例时,不提供属性xy的值,它们都等于默认值0

下面是函数重载的例子。

class Point {
  constructor(x:number, y:string);
  constructor(s:string);
  constructor(xs:number|string, y?:string) {
    // ...
  }
}

上面示例中,构造方法可以接受一个参数,也可以接受两个参数,采用函数重载进行类型声明。

2.存取器方法

存取器(accessor)是特殊的类方法,包括取值器(getter)和存值器(setter)两种方法。

它们用于读写某个属性,取值器用来读取属性,存值器用来写入属性。

class C {
  _name = '';
  get name() {
    return this._name;
  }
  set name(value) {
    this._name = value;
  }
}

上面示例中,get name()是取值器,其中get是关键词,name是属性名。外部读取name属性时,实例对象会自动调用这个方法,该方法的返回值就是name属性的值。

set name()是存值器,其中set是关键词,name是属性名。外部写入name属性时,实例对象会自动调用这个方法,并将所赋的值作为函数参数传入。

TypeScript 对存取器有以下规则。

(1)如果某个属性只有get方法,没有set方法,那么该属性自动成为只读属性。

class C {
  _name = 'foo';
  get name() {
    return this._name;
  }
}
const c = new C();
c.name = 'bar'; // 报错

上面示例中,name属性没有set方法,对该属性赋值就会报错。

(2)set方法的参数类型,必须兼容get方法的返回值类型,否则报错。

class C {
  _name = '';
  get name():string {
    return this._name;
  }
  set name(value:number) {
    this._name = value; // 报错
  }
}

上面示例中,get方法的返回值类型是字符串,与set方法参数类型不兼容,导致报错。

class C {
  _name = '';
  get name():string {
    return this._name;
  }
  set name(value:number|string) {
    this._name = String(value); // 正确
  }
}

上面示例中,set方法的参数类型(number|return)兼容get方法的返回值类型(string),这是允许的。但是,最终赋值的时候,还是必须保证与get方法的返回值类型一致。

另外,如果set方法的参数没有指定类型,那么会推断为与get方法返回值类型一致。

(3)get方法与set方法的可访问性必须一致,要么都为公开方法,要么都为私有方法。

3.属性索引

类允许定义属性索引。

class MyClass {
  [s:string]: boolean |
    ((s:string) => boolean);
  get(s:string) {
    return this[s] as boolean;
  }
}

上述代码表示:

  • 表示该类的实例允许任意字符串类型的属性名
  • 属性的值可以是两种类型:
    • boolean 类型
    • 函数类型:接收 string 参数并返回 boolean 的函数

这里给出几个示例方便大家理解:

const obj = new MyClass();

// 设置属性(符合索引签名要求)
obj.isActive = true;                  // ✅ 允许 boolean 值
obj.checkValid = (s: string) => s.length > 0; // ✅ 允许函数

// 使用 get 方法
console.log(obj.get('isActive'));      // ✅ 输出 true
console.log(obj.get('checkValid'));   // ❌ 运行时错误:函数无法被当作 boolean 使用

// 类型错误示例
obj.count = 10; // ❌ 类型错误:必须是 boolean 或函数类型

二、类与接口

1.implements 关键字

interface 接口或 type 别名,可以用对象的形式,为 class 指定一组检查条件。然后,类使用 implements 关键字,表示当前类满足这些外部类型条件的限制。

interface Country {
  name:string;
  capital:string;
}
// 或者
type Country = {
  name:string;
  capital:string;
}
class MyCountry implements Country {
  name = '';
  capital = '';
}

上面示例中,interfacetype都可以定义一个对象类型。类MyCountry使用implements关键字,表示该类的实例对象满足这个外部类型。

类可以定义接口没有声明的方法和属性。

interface Point {
  x: number;
  y: number;
}
class MyPoint implements Point {
  x = 1;
  y = 1;
  z:number = 1;
}

implements关键字后面,不仅可以是接口,也可以是另一个类。这时,后面的类将被当作接口。

class Car {
  id:number = 1;
  move():void {};
}
class MyCar implements Car {
  id = 2; // 不可省略
  move():void {};   // 不可省略
}

上面示例中,implements后面是类Car,这时 TypeScript 就把Car视为一个接口,要求MyCar实现Car里面的每一个属性和方法,否则就会报错。所以,这时不能因为Car类已经实现过一次,而在MyCar类省略属性或方法。

2.多接口的实现

类的继承

class Car implements MotorVehicle {
}
class SecretCar extends Car implements Flyable, Swimmable {
}

上面示例中,Car类实现了MotorVehicle,而SecretCar类继承了Car类,然后再实现FlyableSwimmable两个接口,相当于SecretCar类同时实现了三个接口。

接口的继承

interface MotorVehicle {
  // ...
}
interface Flyable {
  // ...
}
interface Swimmable {
  // ...
}
interface SuperCar extends MotoVehicle,Flyable, Swimmable {
  // ...
}
class SecretCar implements SuperCar {
  // ...
}

上面示例中,接口SuperCar通过SuperCar接口,就间接实现了多个接口。

3.类与接口的合并

TypeScript 不允许两个同名的类,但是如果一个类和一个接口同名,那么接口会被合并进类。

class A {
  x:number = 1;
}
interface A {
  y:number;
}
let a = new A();
a.y = 10;
a.x // 1
a.y // 10

上面示例中,类A与接口A同名,后者会被合并进前者的类型定义。

三、Class 类型

1.实例类型

TypeScript 的类本身就是一种类型,但是它代表该类的实例类型,而不是 class 的自身类型。

//  定义 Color 类
class Color {
  name: string; // 声明类的字符串类型属性
  
  // 构造函数:接收字符串参数,用于初始化 name 属性
  constructor(name: string) {
    this.name = name;
  }
}

//  创建实例
const green: Color = new Color('green'); 
// 等效于:
// const green = new Color('green'); // TypeScript 可以自动推断类型

上述代码的实例化过程:

new Color('green') 的执行过程:
1. 创建空对象 {}
2. 调用构造函数 constructor('green')
3. 设置 this.name = 'green'
4. 返回初始化后的对象

类型检查示例:

green.name = 'red'; // ✅ 允许修改字符串值
green.name = 123;   // ❌ 类型错误:不能将 number 赋值给 string

const wrong: Color = { name: 'blue' }; 
// ❌ 错误:虽然对象结构相同,但缺少构造函数初始化过程

2.结构类型原则

Class 也遵循“结构类型原则”。一个对象只要满足 Class 的实例结构,就跟该 Class 属于同一个类型。

class Foo {
  id!:number;
}
function fn(arg:Foo) {
  // ...
}
const bar = {
  id: 10,
  amount: 100,
};
fn(bar); // 正确

上面示例中,对象bar满足类Foo的实例结构,只是多了一个属性amount。所以,它可以当作参数,传入函数fn()

如果两个类的实例结构相同,那么这两个类就是兼容的,可以用在对方的使用场合。即使其中的一个类添加了一个属性,,TypeScript 也会认为是同一个类型。

class Person {
  name: string;
}
class Customer {
  name: string;
}
class Man {
  name: string;
  age: number;
}

// 正确
const cust:Customer = new Person();
// 正确
const cust:Customer = new Man();

但是注意,如果Man类比Customer类少一个属性age,它就不满足Customer类型的实例结构,就报错了。因为在使用Customer类型的情况下,可能会用到它的age属性,而Person类就没有这个属性。

class Man{
  name: string;
}
class Customer {
  name: string;
  age: number;
}
// 报错
const cust:Customer = new Man();

不仅是类,如果某个对象跟某个 class 的实例结构相同,TypeScript 也认为两者的类型相同。

注意,确定两个类的兼容关系时,只检查实例成员,不考虑静态成员和构造方法。

class Point {
  x: number;
  y: number;
  static t: number;
  constructor(x:number) {}
}
class Position {
  x: number;
  y: number;
  z: number;
  constructor(x:string) {}
}
const point:Point = new Position('');

上面示例中,PointPosition的静态属性和构造方法都不一样,但因为Point的实例成员与Position相同,所以Position兼容Point

四、类的继承

类(这里又称“子类”)可以使用 extends 关键字继承另一个类(这里又称“基类”)的所有属性和方法。

class A {
  greet() {
    console.log('Hello, world!');
  }
}
class B extends A {
}
const b = new B();
b.greet() // "Hello, world!"

子类可以覆盖基类的同名方法。

class B extends A {
  greet(name?: string) {
    if (name === undefined) {
      super.greet();
    } else {
      console.log(`Hello, ${name}`);
    }
  }
}

但是,子类的同名方法不能与基类的类型定义相冲突。

class A {
  greet() {
    console.log('Hello, world!');
  }
}
class B extends A {
  // 报错
  greet(name:string) {
    console.log(`Hello, ${name}`);
  }
}

上面示例中,子类Bgreet()有一个name参数,跟基类Agreet()定义不兼容,因此就报错了。

五、访问修饰符

TypeScript 提供三种访问控制修饰符:

  • public(默认):任何地方可访问。
  • private:仅类内部可访问。
  • protected:类内部和子类可访问。

public

public修饰符表示这是公开成员,外部可以自由访问。

class Greeter {
  public greet() {
    console.log("hi!");
  }
}
const g = new Greeter();
g.greet();

private

private修饰符表示私有成员,只能用在当前类的内部,类的实例和子类都不能使用该成员。

class A {
  private x:number = 0;
}
const a = new A();
a.x // 报错
class B extends A {
  showX() {
    console.log(this.x); // 报错
  }
}

严格地说,private定义的私有成员,并不是真正意义的私有成员。一方面,编译成 JavaScript 后,private关键字就被剥离了,这时外部访问该成员就不会报错。另一方面,由于前一个原因,TypeScript 对于访问private成员没有严格禁止,使用方括号写法([])或者in运算符,实例对象就能访问该成员。

class A {
  private x = 1;
}
const a = new A();
a['x'] // 1
if ('x' in a) { // 正确
  // ...
}

由于private存在这些问题,加上它是 ES6 标准发布前出台的,而 ES6 引入了自己的私有成员写法#propName因此建议不使用private改用 ES6 的写法,获得真正意义的私有成员。

class A {
  #x = 1;
}
const a = new A();
a['x'] // 报错

protected

protected修饰符表示该成员是保护成员,只能在类的内部使用该成员,实例无法使用该成员,但是子类内部可以使用。

class A {
  protected x = 1;
}
class B extends A {
  getX() {
    return this.x;
  }
}
const a = new A();
const b = new B();
a.x // 报错
b.getX() // 1

子类不仅可以拿到父类的保护成员,还可以定义同名成员。

class A {
  protected x = 1;
}
class B extends A {
  x = 2;
}

上面示例中,子类B定义了父类A的同名成员x,并且父类的x是保护成员,子类将其改成了公开成员。B类的x属性前面没有修饰符,等同于修饰符是public,外界可以读取这个属性。

在类的外部,实例对象不能读取保护成员,但是在类的内部可以。

class A {
  protected x = 1;
  f(obj:A) {
    console.log(obj.x);
  }
}
const a = new A();
a.x // 报错
a.f(a) // 1

六、静态成员

静态成员属于类本身而非实例,通过 static 定义:

class MathUtils {
  static PI = 3.1415926;

  static sum(a: number, b: number) {
    return a + b;
  }
}

console.log(MathUtils.PI); // 3.1415926
console.log(MathUtils.sum(1, 2)); // 3

静态成员是只能通过类本身使用的成员,不能通过实例对象使用。

class Demo {
  static staticVal = 10; // 静态属性
  instanceVal = 20;     // 实例属性
  static x = 0;

  static printX() {
    console.log(Demo.x); // 通过类名访问静态属性
  }

  static staticMethod() {
    console.log(this.staticVal); // ✅ 正确访问静态属性
    console.log(this.instanceVal); // ❌ 错误:无法访问实例属性
  }
}

// 错误访问方式
// 直接通过类名访问静态属性
Demo.x;        // → 0 

// 直接通过类名调用静态方法
Demo.printX(); // → 输出 0


// 正确访问方式
console.log(Demo.staticVal);  // ✅ 10
const obj = new Demo();
console.log(obj.instanceVal); // ✅ 20

七、抽象类

abstract 定义抽象类和抽象方法,抽象类不可实例化,抽象方法必须由子类实现:

abstract class A {
  id = 1;
}
const a = new A(); // 报错

抽象类只能当作基类使用,用来在它的基础上定义子类。

abstract class A {
  id = 1;
}
class B extends A {
  amount = 100;
}
const b = new B();
b.id // 1
b.amount // 100

例子实现;

abstract class Shape {
  abstract area(): number;

  printArea() {
    console.log(`Area: ${this.area()}`);
  }
}

class Circle extends Shape {
  radius: number;
  constructor(radius: number) {
    super();
    this.radius = radius;
  }

  area() {
    return Math.PI * this.radius ** 2;
  }
}

const circle = new Circle(5);
circle.printArea(); // 输出:Area: 78.5398...

相关文章:

  • Springboot JPA ShardingSphere 根据年分表java详细代码Demo
  • Java Stream API:现代化集合处理的艺术
  • AI比人脑更强,因为被植入思维模型【49】冰山理论思维模型
  • 鱼骨图分析法实战:5步定位系统故障
  • Linux系统学习Day2——在Linux系统中开发OpenCV
  • 【微机及接口技术】- 第九章 串行通信与串行接口(上)
  • 路由表的最终地址 root 路由跟踪,最终到哪里去
  • RK-realtime Linux
  • python(49)-串口接收与发送
  • Android audio(6)-audiopolicyservice介绍
  • C++Cherno 学习笔记day17 [66]-[70] 类型双关、联合体、虚析构函数、类型转换、条件与操作断点
  • 华为OD全流程解析+备考攻略+经验分享
  • VS Code连接服务器编写Python文件
  • 【Docker】Dockerfile 编写实践
  • MYSQL数据库语法补充
  • 区间 DP 详解
  • XMLHttpRequest vs Fetch API:一场跨越时代的“浏览器宫斗剧“
  • 什么是软件测试(目的、意义、流程)
  • STM32在裸机(无RTOS)环境下,需要手动实现队列机制来替代FreeRTOS的CAN发送接收函数
  • 第四篇:系统分析师——12-16章
  • 万科再获深铁集团借款,今年已累计获股东借款近120亿元
  • 汕头违建豪宅“英之园”将强拆,当地:将根据公告期内具体情况采取下一步措施
  • 第1现场 | 美国称将取消制裁,对叙利亚意味着什么
  • 经济日报整版聚焦:上海构建法治化营商环境,交出高分答卷
  • 落实中美经贸高层会谈重要共识,中方调整对美加征关税措施
  • 海运港口股掀涨停潮!回应关税下调利好,有货代称美线舱位爆了