TypeScript 类

在 TypeScript 中,类是一种用于创建对象和组织代码的结构。

创建一个类:

class Person {
    name: string;
    age: number;

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

    sayHi() {
        console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
    }
}

let person1 = new Person('Alice', 25);
person1.sayHi();

声明一个 Person 类。这个类有3个成员:

  • nameage 是类的属性,用于存储对象的状态信息。
  • constructor 是类的构造函数,用于在创建对象时进行初始化操作,为属性赋值。
  • sayHi 是类的方法,用于定义对象的行为。

在引用任何一个类成员的时候都用了 this。 它表示我们访问的是类的成员。

继承

在 TypeScript 中,类继承是一种面向对象编程的特性,允许一个类(子类)从另一个类(父类)继承属性和方法。

示例:

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

class Dog extends Animal {
    breed: string;
    constructor(name: string) {
        super(name);
        this.breed = breed;
    }
    bark() {
        console.log('Woof! Woof!');
    }
}

let dog = new Dog('Buddy', 'Labrador');
dog.move(10); 
dog.bark(); 

上述代码:

  • 定义父类 Animal ,它具有属性 name 、构造函数constructor和方法 move
  • 定义子类 Dog ,通过 extends 关键字继承自 Animal
    • Dog 必须调用 super(),它会执行基类的构造函数。
      在构造函数里访问 this 的属性之前,我们 一定要调用 super()。 这个是TypeScript强制执行的一条重要规则。

    • Dog 类有自己的属性 breed

    • Dog 类还具有自己的方法 bark

这个例子展示了最基本的继承:类从基类中继承了属性和方法。
Dog是一个 派生类,它派生自 Animal 基类,通过 extends关键字。 派生类通常被称作 子类,基类通常被称作 超类。

子类可以重写父类的方法,以提供自己的实现。
示例:

class Bird extends Animal {
    constructor(name: string) {
        super(name);
    }
    // 重写move方法
    move(distanceInMeters: number) {
        console.log(`${this.name} flew ${distanceInMeters}m.`);
    }
}

let bird = new Bird('Sparrow');
bird.move(5); 

let bird1: Animal = new Bird("magpie");
bird1.move(10);

示例中,Bird 类重写了父类 Animalmove 方法,以提供适合鸟类移动的描述。

注意:bird1 被声明为 Animal类型,但因为它的值是 Bird,调用 bird1.move(10)时,它会调用 Bird 里重写的move方法。

在不同的子类中重写父类的方法,使 方法 根据不同的类而具有不同的功能。

类继承有助于代码的复用和组织,通过继承,可以在子类中扩展和修改父类的功能,从而构建更复杂和有层次的对象模型。

类的访问修饰符

类支持访问修饰符:

  • public(公共):默认,可在任何地方访问。
  • private(私有):只能在类内部访问,子类和类的外部不能访问。
  • protected(受保护): 在类内部和子类中访问。

公共( public

public是默认的修饰符,如果没有明确指定,类成员就是公共的。
也可以明确的将一个成员标记成 public

public成员可以在类的内部、子类以及类的实例的任何地方被访问和修改。

重写上面的 Animal类:

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

私有(private

当成员被标记成 private时,它就不能在声明它的类的外部访问。

private成员只能在其所属的类内部被访问和修改。在类的外部(包括子类)访问私有成员会导致编译错误。
private成员在类的实例中不能访问。

示例:

class Animal {
	// name 是 Animal 的私有成员
    private name: string;
    constructor(theName: string) { this.name = theName; }
}
class Dog extends Animal {
    constructor(name: string) {
        super(name);
    }
    bark() {
    	// Error:  Property 'name' is private and only accessible within class 'Animal'.
    	// 此句报错:属性“name”为私有属性,只能在类“Animal”中访问。
        console.log(`The dog's name is ${this.name}`);
    }
}

new Animal("Cat").name; // 错误: 属性“name”为私有属性,只能在类“Animal”中访问。

TypeScript使用的是结构性类型系统。 当我们比较两种不同的类型时,并不在乎它们从何处而来,如果所有成员的类型都是兼容的,我们就认为它们的类型是兼容的。

当比较带有 privateprotected成员的类型的时候:如果其中一个类型里包含一个 private成员,那么只有当另外一个类型中也存在这样一个 private成员, 并且它们都是来自同一处声明时,我们才认为这两个类型是兼容的。 对于 protected成员也使用这个规则。

示例:

class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

class Rhino extends Animal {
    constructor() { super("Rhino"); }
}

class Employee {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee("Bob");

// 合法,因为 Rhino 是 Animal 的子类,子类的对象可以赋值给父类类型的变量,这是多态的一种体现。
animal = rhino; 

// 错误。不能将类型“Employee”分配给类型“Animal”。类型具有私有属性“name”的单独声明。
// Employee 和 Animal 没有继承关系,并且它们的私有属性 name 是相互独立和不兼容的。
animal = employee; 

受保护(protected

protected修饰符与 private修饰符的行为很相似,但有一点不同: protected成员在派生类中仍然可以访问。
protected成员在类的实例中不能访问。

示例:

class Animal {
    protected name: string;
    constructor(theName: string) { this.name = theName; }
}
class Dog extends Animal {
	protected color: string;
    constructor(name: string, color: string) {
        super(name);
        this.color = color
    }
    bark() {
        console.log(`The dog's name is ${this.name}, and color is ${this.color}`);
    }
}

let dog = new Dog("Carl", "white");
dog.bark(); // "The dog's name is Carl, and color is white" 

new Animal("Cat").name; // 错误: 属性“name”受保护,只能在类“Animal”及其子类中访问。

注意:不能在 Animal 类外使用 name,可以通过 Dog 类的实例方法访问,因为 Dog 是由 Animal 派生而来的。

构造函数也可以被标记成 protected。 这意味着这个类不能在包含它的类外被实例化,但是能被继承。
示例:

class Animal {
    protected name: string;
    // 把构造函数标记为protected
    protected constructor(theName: string) { this.name = theName; }
}
// Dog 能继承 Animal
// Dog 调用 `super()`,执行基类 Animal 受保护的 构造函数。
class Dog extends Animal {
	color: string;
    constructor(name: string, color: string) {
        super(name);
        this.color = color
    }
    bark() {
        console.log(`The dog's name is ${this.name}, and color is ${this.color}`);
    }
}

let dog = new Dog("Carl", "white");
dog.bark(); // "The dog's name is Carl, and color is white" 

new Animal("Cat").name; // 错误: 类“Animal”的构造函数是受保护的,仅可在类声明中访问。

readonly修饰符

readonly成员只能读取,不能被重新赋值。
readonly成员必须在声明时或构造函数里被初始化。

class Point {
  readonly x: number;
  readonly y: number = 10;
  constructor(x: number, y?: number) {
    this.x = x;
    this.y = y ?? this.y;
  }
  showPoint() {
  	console.log(`x: ${this.x}, y: ${this.y}`)
  }
}
let p = new Point(10, 20);
p.showPoint();

let p1 = new Point(50);
p1.showPoint();
p1.x = 30; // Error: 无法为“x”赋值,因为它是只读属性。

参数属性

在 TypeScript 中,类的参数属性是一种简洁的方式来同时声明和初始化类的属性。
示例:

class Point {
  constructor(readonly x: number, readonly y?: number) {}
}

在构造函数里仅使用 readonly x: number, readonly y?: number 参数来创建和初始化 xy
把声明和赋值合并在一个地方。

参数属性通过给构造函数参数前面添加一个访问限定符来声明。比如:

class Animal {
    constructor(protected name: string;) {}
}
class Animal {
    constructor(private name: string;) {}
}

getters/setters

TypeScript支持通过 getters(获取器)和 setters(设置器)来截取对对象成员的访问。

示例:没有使用getters/setters

class User {
    name: string;
}
let user = new User();
user.name = "Rigo";
console.log(user.name);  // "Rigo"
user.name = "张三";
console.log(user.name);  // "张三"

可以随意的设置 name

Animal 类改写成使用 getsetset方法中,验证用户输入的密码,密码正确才可以修改name。使用get 方法获取 name

class User {
  private _name: string = "";
  get name(): string {
    return this._name;
  }
  set name(newName: string) {
    let pwd = prompt("请输入密码", "password");
    if(pwd === "password") {
      this._name = newName;
    }else {
      console.log("密码错误!");
    }
  }
}

let user = new User();
user.name = "张三";
console.log(user.name); // 张三

User 类中,private _name: string声明变量会报错:Property ‘_name’ has no initializer and is not definitely assigned in the constructor.
这个错误是因为 TypeScript 在严格的类型检查模式下,要求非 readonly 且类型不可为 nullundefined 的属性必须在构造函数中被初始化,或者具有明确的初始值。

private _name: string = "";_name 属性添加初始值 "" 时,满足了 TypeScript 对于属性初始化的要求。TypeScript 就能够确定在对象创建时该属性有一个明确的初始状态,从而避免了可能出现未初始化就被使用的情况。

注意:只有 get 且没有 set 的存取器自动被推断为 readonly。 因为没有 set 提供修改值的方法。

静态属性

在 TypeScript 中,静态属性是属于类本身而不是类的实例的属性。

类的实例成员:仅当类被实例化的时候才会被初始化的属性。
类的静态成员:属于类本身而不是类的实例的属性。
每个实例通过 类名.静态属性 访问静态属性。比如示例中的MyClass.staticProperty
示例:

class MyClass {
    // 静态属性
    static staticProperty: number = 10;
    
    // 实例方法
    instanceMethod() {
        console.log(`实例化后访问静态属性值:${MyClass.staticProperty}`);
    }
}

// 直接通过类名访问静态属性
console.log(MyClass.staticProperty); 

// 实例化后,才能通过实例 instance 访问 instanceMethod()
let instance = new MyClass();
instance.instanceMethod();

let instance1 = new MyClass();
instance1.instanceMethod();

在这个示例中,无论创建多少个 instance 的实例,staticProperty 的值都是共享且不变的。

静态属性通常用于存储与类相关的全局数据或共享状态,并且在类的所有实例之间是共享的。

抽象类

抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。 抽象类可以包含成员的实现细节。
abstract关键字是用于定义抽象类和在抽象类内部定义抽象方法。

// 定义抽象类 Animal
abstract class Animal {
	// 抽象方法 makeSound
    abstract makeSound(): void;
    // 非抽象方法 move
    move() {
        console.log('The animal is moving.');
    }
}

抽象类中的抽象方法不包含具体实现并且必须在派生类中实现。
抽象方法只定义方法签名。
抽象方法必须包含 abstract关键字并且可以包含访问修饰符。

abstract class Animal {
  abstract makeSound(): void; // 必须在派生类中实现
  move() {
      console.log("The animal is moving.");
  }
}

class Dog extends Animal {
  makeSound() {
      console.log("Woof!");
  }
  jump() {
    console.log("dog jump")
  }
}

let dog = new Dog();
dog.makeSound();
dog.move();
dog.jump();

let cat = new Animal(); // Error: 无法创建抽象类的实例。

由于 Animal 是抽象类,不能直接创建 Animal 的实例。

允许创建一个对抽象类型的引用:

abstract class Animal {
  abstract makeSound(): void; // 必须在派生类中实现
  move() {
      console.log("The animal is moving.");
  }
}

class Dog extends Animal {
  makeSound() {
      console.log("Woof!");
  }
}

class Cat extends Animal {
  makeSound() {
      console.log("喵~");
  }
}

// operateAnimal 函数接受一个类型为 Animal 的参数。
function operateAnimal(animal: Animal) {
    animal.makeSound();
    animal.move();
}

let dog = new Dog();
let cat = new Cat();
operateAnimal(dog);
operateAnimal(cat);

operateAnimal 函数接受一个类型为 Animal 的参数。
当我们调用 operateAnimal(dog)operateAnimal(cat) 时,参数传递的实际对象分别是 dogcat 的实例。在函数内部,我们可以通过 animal 这个抽象类型的引用统一地调用它们共有的方法。

构造函数

在 TypeScript 中,构造函数是类中的一个特殊方法,用于在创建类的实例时进行初始化操作。

当在TypeScript里声明了一个类的时候,实际上同时声明了很多东西。 首先就是类的 实例的类型。

声明一个类:

class Person {
    name: string;
    age: number;

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

let person1: Person = new Person('Alice', 25);

在上述代码中,constructor 就是 Person 类的构造函数。
let person1: Person,声明了一个变量 person1 ,它的类型为 Person 类的实例类型。意思是 Person 类的实例的类型是 Person
person1 预期存储的是通过 new Person(...) 方式创建出来的符合 Person 类结构的对象实例。

把这段代码编译成js:

var Person = /** @class */ (function () {
    // 这是构造函数
    function Person(name, age) {
        this.name = name;
        this.age = age;
    }
    return Person;
}());
var person1 = new Person('Alice', 25);

这段代码使用了立即执行函数表达式(IIFE)来定义一个名为 Person 的类。
在这个立即执行函数内部:

  • 定义了一个名为 Person 的构造函数 function Person(name, age) {...}
  • 这个构造函数接受两个参数:nameage
  • 在函数内部,通过 this.name = name;this.age = age; 将传入的参数分别赋值给实例的 nameage 属性,从而完成对新创建实例的初始化。
  • 最后,整个立即执行函数返回了这个构造函数 Person

var Person将被赋值为构造函数。 当我们调用 new并执行了这个函数后,便会得到一个类的实例。

把类当做接口使用

类定义会创建两个东西:类的实例类型和一个构造函数。 因为类可以创建出类型,所以能在允许使用接口的地方使用类。

class Point {
    x: number;
    y: number;
}

interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3};