一.简介

TypeScript 完全支持 ES2015 中引入的 class 关键字。

与其他 JavaScript 语言功能一样,TypeScript 添加了类型注释和其他语法,以允许你表达类和其他类型之间的关系。

1.字段

(1).在申明时同时给出类型
class Person {
  name: string;
  age: number;
}

(2).申明时不给出类型

如果不给出类型,那么typescript会自动推断为any类型的数据

class Person {
  name; // any类型
  age; // any类型
}

(3).声明时给出初值

typescript会自动推断出来数据的类型是什么,比如下面的就会推断出来数据的类型为string与number

class Person {
  name = "寻谬~流光"; // string
  age = 30; // number
}

(4).打开了TypeScript 的配置项strictPropertyInitialization

只要打开,就会检查属性是否设置了初值,如果没有就报错。

如果你打开了这个设置,但是某些情况下,不是在声明时赋值或在构造方法里面赋值,为了防止这个设置报错,可以使用非空断言。

class person {
  name!: string;
  age!: number;
}

属性名后面添加了感叹号,表示这两个属性肯定不会为空,所以 TypeScript 就不报错了

2.readonly 修饰符

属性名前面加上 readonly 修饰符,就表示该属性是只读的。实例对象不能修改这个属性。

  1. 属性前面有 readonly 修饰符,实例对象修改这个属性就会报错。
  2. 构造方法内部可以设置只读属性的初值
  3. 构造方法可以修改只读属性的值
class Person {
  readonly name = "寻谬~流光";
  readonly age!: number;

  // 哎构造函数内部可以自由修改readonly属性,不管是已经赋值了的,或者是没有赋值的数据类型
  constructor(age: number) {
    this.age = age;
    this.age = 20;
  }
}

const p = new Person(18);
console.log(p.name); // 寻谬~流光
console.log(p.age); // 20

p.age = 25; // 编译器报错, 不能对readonly属性重新赋值

3.构造器

constructor 方法是 class 的一个特殊方法,用于创建和初始化该类的对象实例。

类构造函数与函数非常相似。你可以添加带有类型注释、默认值和重载的参数

class Person {
  name = "寻谬~流光";
  age!: number;

  //通过给参数设置默认值,实现类似重载的效果。
  constructor(age: number = 18, name: string = "寻谬~流光") {
    this.age = age;
    this.name = name;
  }
}

let p1 = new Person();
let p2 = new Person(20, "寻觅~流光");
let p3 = new Person(undefined, "你好");

console.log(p1); // Person { name: '寻谬~流光', age: 18 }
console.log(p2); // Person { name: '寻觅~流光', age: 20 }
console.log(p3); // Person { name: '你好', age: 18 }

4.方法

类上的函数属性称为方法。

类的方法就是普通函数,类型声明方式与函数一致。

class Person {
  name = "寻谬~流光";
  age!: number;

  constructor(age: number = 18, name: string = "寻谬~流光") {
    this.age = age;
    this.name = name;
  }

  // 不添加函数返回值的类型,typescript会自动推导出来这个函数返回的数据类型((method) Person.add_age(num: number): number)
  add_age(num: number) {
    this.age += num;
    return this.age;
  }

  // 添加类型注释
  low_age(num: number = 1): number {
    this.age -= num;
    return this.age;
  }
}

5.获取器/设置器

存取器(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属性时,实例对象会自动调用这个方法,并将所赋的值作为函数参数传入。

(1).如果某个属性只有get方法,没有set方法,那么该属性自动成为只读属性。
class Person {
  _name: string;
  _age: number;

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

  // name为只读属性
  get name(): String {
    return this._name + "___ABC";
  }
}
const p = new Person("Tom", 20);
console.log(p.name); // Tom___ABC

p.name = "Jerry"; // 报错,不能修改只读属性

(2).set方法的参数类型,必须兼容get方法的返回值类型,否则报错。
class Person {
  _name: string;
  _age: number;

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

  get name(): string {
    return this._name + "___ABC";
  }
  // 正确写法
  // set name(value: string) {
  //   this._name = value;
  // }

  // 报错
  set name(value: number) {
    this._name = value;
  }
}

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

正确写法:

class Person {
  _name: string;
  _age: number;

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

  private get name(): string {
    return this._name + "___ABC";
  }
  private set name(value: string) {
    this._name = value;
  }
}

错误写法:

class Person {
  _name: string;
  _age: number;

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

  private get name(): string {
    return this._name + "___ABC";
  }

  // Get 访问器必须至少具有与 Setter 相同的可访问性
  public set name(value: string) {
    this._name = value;
  }
}

6.索引签名

类可以声明索引签名;这些工作与 其他对象类型的索引签名 相同:

类允许定义属性索引。

class MyClass {
  [s: string]: boolean | ((s: string) => boolean);

  get(s: string) {
    return this[s] as boolean;
  }
}

上面示例中,[s:string]表示所有属性名类型为字符串的属性,它们的属性值要么是布尔值,要么是返回布尔值的函数。

注意,由于类的方法是一种特殊属性(属性值为函数的属性),所以属性索引的类型定义也涵盖了方法。如果一个对象同时定义了属性索引和方法,那么前者必须包含后者的类型。

class MyClass {
  [s: string]: boolean;
  f() {
    // 报错
    return true;
  }
}

上面示例中,属性索引的类型里面不包括方法,导致后面的方法f()定义直接报错。正确的写法是下面这样。

class MyClass {
  [s: string]: boolean | (() => boolean);
  f() {
    return true;
  }
}

属性存取器等同于方法,也必须包括在属性索引里面。

class MyClass {
  [s: string]: boolean;

  get(s: string) {
    // 报错
    return this[s] as boolean;
  }
}

上面示例中,属性索引没有给出方法的类型,导致get()方法报错。

二.类的 interface 接口

1.implements 关键字

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

interface Person {
  name: string;
  age: number;
}

// 或者这种写法
// type Person = {
//   name: string;
//   age: number;
// };

class Student implements Person {
  name: string;
  age: number;

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

 你可以使用 implements 子句来检查一个类是否满足特定的 interface。如果一个类未能正确实现它,则会触发错误:

interface Pingable {
  ping(): void;
}

class Sonar implements Pingable {
  ping() {
    console.log("ping!");
  }
}

class Ball implements Pingable {
  pong() {
    console.log("pong!");
  }
}

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

class Person {
  name: string;
  age: number;

  add: (value: number) => number;
}

// 实现接口
class Student implements Person {
  name: string;
  age: number;

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

  add(value: number) {
    this.age += value;
    return this.age;
  }
}

注意,interface 描述的是类的对外接口,也就是实例的公开属性和公开方法,不能定义私有的属性和方法。这是因为 TypeScript 设计者认为,私有属性是类的内部实现,接口作为模板,不应该涉及类的内部代码写法。

2.实现多个接口

类可以实现多个接口(其实是接受多重限制),每个接口之间使用逗号分隔。

interface student {
  //...
}
interface teacher {
  // ...
}

// 继承多个接口
class class_people implements student, teacher {
  // ...
}

3.类与接口的合并

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

interface person {
  name: string;
}
class person {
  age: number;
  adress: string;
  id: number;
}

const p: person = {
  name: "Tom",
  age: 25,
  adress: "China",
  id: 123456,
};

console.log(p.name); // Tom

就比如上

定义了person的接口与person的类,接口定义的会被合并到class的类型定义中去

三.类继承

1.extends 从句

类可能来自基类。派生类具有其基类的所有属性和方法,还可以定义额外的成员。

class father {
  name: string;
  age: number;
}
class son extends father {
  name: string;
  age: number;
  adress: string;
}
let s = new son();
s.name = "Tom";
s.age = 20;
s.adress = "China";
console.log(s);
  • super(name, age):在子类构造函数中,必须先调用父类的构造函数,才能使用 this
  • 如果子类重写了父类方法,也可以在方法中用 super.方法名() 调用父类的方法。

2.super语句

super 语句用于在子类中调用父类的构造函数或父类的方法。

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

class Son extends Father {
  adress: string;
  constructor(name: string, age: number, adress: string) {
    super(name, age); // 调用父类构造函数
    this.adress = adress;
  }
}

const s = new Son("Tom", 20, "China");

3.override语句

在 TypeScript 中,override 关键字用于明确表示子类正在重写父类的成员方法或属性。这样可以让编译器检查你是否真的在重写父类已有的方法,防止拼写错误或父类没有该方法时出错。

class Father {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  sayHello() {
    console.log(`Hello, I am ${this.name}`);
  }
}

class Son extends Father {
  adress: string;
  constructor(name: string, age: number, adress: string) {
    super(name, age); // 调用父类构造函数
    this.adress = adress;
  }

  // 重写父类方法
  override sayHello() {
    super.sayHello(); // 可选:调用父类方法
    console.log(`I live in ${this.adress}`);
  }
}

const s = new Son("Tom", 20, "China");
s.sayHello();
// 输出:
// Hello, I am Tom
// I live in China
  • override 只能用于重写父类已有的方法或属性。
  • 如果父类没有 sayHello 方法,写 override sayHello() 会报错,防止误写。
  • 推荐在所有重写父类成员时加上 override,代码更规范、安全。

四.成员可见性

类的内部成员的外部可访问性,由三个可访问性修饰符(access modifiers)控制:publicprivateprotected

这三个修饰符的位置,都写在属性或方法的最前面。

1.public

(1). 基本用法

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

正常情况下,除非为了醒目和代码可读性,public都是省略不写的。

class Person {
  public name: string;     // 显式声明为public
  age: number;             // 隐式public(默认)
  
  public constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  
  public greet() {        // 方法默认也是public
    return `Hello, I'm ${this.name}`;
  }
}

const person = new Person("Alice", 30);
console.log(person.name);  // 合法:外部可访问public成员
person.greet();            // 合法

2.protected

(1). 基本用法

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

class Animal {
  protected name: string;
  
  constructor(name: string) {
    this.name = name;
  }
  
  protected move(distance: number) {
    console.log(`${this.name} moved ${distance}m.`);
  }
}

class Dog extends Animal {
  constructor(name: string) {
    super(name);
  }
  
  bark() {
    console.log(`${this.name} barks!`); // 合法:子类可访问protected成员
    this.move(5);                      // 合法:子类可调用protected方法
  }
}

const dog = new Dog("Buddy");
// console.log(dog.name); // 错误:实例无法访问protected成员
// dog.move(10);          // 错误
dog.bark();              // 合法

(2).子类不仅可以拿到父类的保护成员,还可以定义同名成员。
class Vehicle {
  protected speed: number = 0;
  
  protected accelerate(value: number) {
    this.speed += value;
  }
}

class Car extends Vehicle {
  protected speed: number = 10; // 子类可以定义同名protected成员
  
  override accelerate(value: number) {
    this.speed += value * 2;   // 子类可以重写protected方法
  }
}

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

3.private

(1). 基本用法

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

class BankAccount {
  private balance: number = 0;
  
  constructor(initialDeposit: number) {
    this.balance = initialDeposit;
  }
  
  public deposit(amount: number) {
    this.balance += amount; // 合法:类内部可访问private成员
  }
  
  public getBalance() {
    return this.balance;    // 合法
  }
}

const account = new BankAccount(1000);
// console.log(account.balance); // 错误:无法访问private成员
account.deposit(500);
console.log(account.getBalance()); // 合法:通过public方法访问

(2).子类不能定义父类私有成员的同名成员。
class Base {
  private code: string = "123";
}

class Derived extends Base {
  // private code: number = 456; // 错误:不能重定义父类私有成员
}

(3).如果在类的内部,当前类的实例可以获取私有成员。

4.实例属性的简写形式

在构造函数参数前加上 publicprotected 或 private,TypeScript 会自动为你声明并初始化同名实例属性。

这种写法更简洁,常用于数据类或 DTO(数据传输对象)。

这样可以让代码更简洁、易读。

class Config {
  constructor(
    public apiKey: string,
    public timeout: number = 5000,
    private debug?: boolean
  ) {}
}

const config = new Config("123-456");
console.log(config.timeout); // 输出: 5000 (默认值)

五.静态成员

类的内部可以使用static关键字,定义静态成员。

静态成员的特点:

  1. 包括静态属性和静态方法,只能通过类名直接访问,无法通过实例调用。
  2. 类加载时创建,仅一份
  3. 子类可以继承父类的静态成员,也可以重写静态方法
class Person {
  static value = 1;
  constructor(public name: string) {}

  log_value() {
    console.log(Person.value);
  }

  add_value(num: number) {
    Person.value += num;
  }
}

// 只可以通过类名直接访问到静态属性和方法
// 这里静态方法同样如此,就不写了
console.log(Person.value);

let p: Person = new Person("Tom");
let p2: Person = new Person("Jerry");

p.log_value(); // 1
p2.log_value(); // 1

p.add_value(2);

p.log_value(); // 3
p2.log_value(); // 3

p2.add_value(3);

p.log_value(); // 6
p2.log_value(); // 6

六.抽象类,抽象成员

TypeScript 允许在类的定义前面,加上关键字abstract,表示该类不能被实例化,只能当作其他类的模板。这种类就叫做“抽象类”(abastract class)。

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

不能实例化抽象类

abstract class Animal {
  constructor(public name: string, public age: number) {}
}

// const Animal_1 = new Animal("Animal", 1); // 错误! 不能实例化抽象类

class Dog extends Animal {
  constructor(name: string, age: number, public breed: string) {
    super(name, age); // 实现构造函数
  }

  bark() {
    console.log(`${this.name} is barking.`);
  }
}

2.抽象类的子类也可以是抽象类,也就是说,抽象类可以继承其他抽象类。

abstract class Animal {
  constructor(public name: string, public age: number) {}
}

abstract class Dog extends Animal {
  constructor(name: string, age: number, public breed: string) {
    super(name, age);
  }

  sound(): void {
    console.log("W-T-F !");
  }
}

class Labrador extends Dog {
  constructor(name: string, age: number) {
    super(name, age, "Labrador");
  }

  bark(): void {
    console.log("Woof, woof!");
  }
}

const myDog = new Labrador("Buddy", 3);
myDog.sound(); // 输出: W-T-F !

3.抽象成员

如果抽象类的属性前面加上abstract,就表明子类必须给出该方法的实现。

abstract class Animal {
  constructor(public name: string, public age: number) {}
  abstract makeSound(): void; // 抽象成员函数
}

class Dog extends Animal {
  // 实现抽象成员函数
  makeSound() {
    console.log("w t f!");
  }
}

(1)抽象成员只能存在于抽象类,不能存在于普通类。

(2)抽象成员不能有具体实现的代码。也就是说,已经实现好的成员前面不能加abstract关键字。

(3)抽象成员前也不能有private修饰符,否则无法在子类中实现该成员。

(4)一个子类最多只能继承一个抽象类。

总之,抽象类的作用是,确保各种相关的子类都拥有跟基类相同的接口,可以看作是模板。其中的抽象成员都是必须由子类实现的成员,非抽象成员则表示基类已经实现的、由所有子类共享的成员。

七.this 问题

在类中,this 始终指向当前实例对象,但这个指向会受到调用方式的影响。

  • 当通过 实例.方法() 调用时,this 正常指向实例;
  • 当方法被单独提取(如赋值给变量、作为回调)时,this 会丢失指向(变成 undefined 或全局对象)。

这是因为类的方法默认定义在原型上,调用时需要依赖调用者来确定 this 的指向(即 “谁调用,this 指向谁”)。

1.常见的this场景问题

(1).方法作为回调函数时,this 丢失

原因callback 只是一个函数引用,调用时没有 “调用者”,this 无法指向 user 实例。

class User {
  name: string = "Alice";

  sayHi() {
    console.log(`Hi, I'm ${this.name}`); 
    // 这里的 this 会在回调中丢失!
  }
}

const user = new User();
// 直接调用:this 指向 user 实例(正常)
user.sayHi(); // 输出:Hi, I'm Alice

// 将方法提取为回调函数
const callback = user.sayHi; 
// 单独调用时,this 指向 undefined(严格模式下)
callback(); // 报错:Cannot read property 'name' of undefined

(2).事件监听中的 this 丢失

原因:DOM 事件回调中,this 默认指向触发事件的 DOM 元素,覆盖了类实例的 this

class Button {
  label: string = "Click me";

  onClick() {
    console.log(`Clicked ${this.label}`); 
    // 事件触发时,this 指向触发事件的 DOM 元素,而非实例
  }
}

const btn = new Button();
// 给按钮绑定点击事件(假设页面有个 id 为 "btn" 的按钮)
document.getElementById("btn")?.addEventListener("click", btn.onClick);

// 当点击按钮时:
// 输出:Clicked undefined(因为 this 指向按钮 DOM 元素,而非 Button 实例)

(3).箭头函数与普通方法的 this 差异

原因:箭头函数没有自己的 this,它的 this 是定义时捕获的上下文(此处即类实例)。

class Test {
  value: number = 10;

  // 普通方法:this 依赖调用者
  normalMethod() {
    console.log(this.value);
  }

  // 箭头函数方法:this 固定指向实例
  arrowMethod = () => {
    console.log(this.value);
  };
}

const test = new Test();
const normal = test.normalMethod;
const arrow = test.arrowMethod;

normal(); // 报错:this 丢失
arrow();  // 正常输出:10(this 始终指向 test 实例)

2.解决 this 指向问题的 3 种方案

(1).在构造函数中绑定 this(最常用)

通过 this.方法 = this.方法.bind(this) 强制绑定 this 到实例:

class User {
  name: string = "Alice";

  constructor() {
    // 绑定 this 到当前实例
    this.sayHi = this.sayHi.bind(this);
  }

  sayHi() {
    console.log(`Hi, I'm ${this.name}`);
  }
}

const user = new User();
const callback = user.sayHi;
callback(); // 正常输出:Hi, I'm Alice(this 已绑定)

(2).使用箭头函数定义方法(简洁)

直接将方法定义为箭头函数,利用箭头函数的 this 特性固定指向:

class Button {
  label: string = "Click me";

  // 箭头函数方法:this 永远指向实例
  onClick = () => {
    console.log(`Clicked ${this.label}`);
  };
}

const btn = new Button();
document.getElementById("btn")?.addEventListener("click", btn.onClick);
// 点击时输出:Clicked Click me(this 正确指向 Button 实例)

(3).调用时通过 call/apply 指定 this

在调用方法时,用 call 或 apply 手动指定 this

class User {
  name: string = "Bob";
  sayHi() {
    console.log(`Hi, I'm ${this.name}`);
  }
}

const user = new User();
const callback = user.sayHi;

// 调用时用 call 绑定 this
callback.call(user); // 输出:Hi, I'm Bob

八.泛型类

类也可以写成泛型,使用类型参数。关于泛型的详细介绍

class person<T> {
  name: T;
  age: number;
  constructor(name: T, age: number) {
    this.name = name;
    this.age = age;
  }
}

let p1: person<string> = new person("Tom", 25);
console.log(p1.name); // output: Tom

注意,静态成员不能使用泛型的类型参数。

下一篇:TypeScript---泛型

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐