五、TypeScript的类和接口

140 阅读7分钟

类的定义

ES6新加了class关键字。 TypeScript中的类和JavaScript中的类非常相似,只是多了一些类型的限制。

在TypeScript中,定义类的语法格式如下:

class Person {
  name: string;
  age: number;

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

  sayName() {
    console.log(this.name);
  }
}

const person = new Person("Tom", 18);
person.sayName(); // 输出:Tom

在上面的代码中,我们定义了一个类Person,该类有两个属性name和age,一个构造函数和一个方法sayName。

在构造函数中,我们使用this关键字来指向当前对象,然后将传入的参数赋值给对象的属性。

在方法sayName中,我们使用this关键字来指向当前对象,然后输出对象的name属性。

最后,我们使用new关键字来创建一个person对象,然后调用对象的sayName方法,输出对象的name属性。

类的继承

面向对象编程三大特性封装、继承、多态。

封装

封装是指将数据和行为包装在一起,形成一个不可分割的整体,对外部隐藏对象的内部细节,只暴露必要的接口。

封装的好处是可以提高代码的可维护性和可复用性,同时可以保护对象的内部状态,防止外部直接修改对象的属性。

在JavaScript中,封装可以通过闭包、模块化等方式实现。

在TypeScript中,封装可以通过访问修饰符来实现,包括public、private和protected。

多态

在JavaScript中,多态可以通过函数重载、对象的动态绑定等方式实现。

在TypeScript中,多态可以通过接口、抽象类等方式实现。

继承

继承是指子类可以继承父类的属性和方法,同时可以重写父类的方法,实现代码的复用和扩展。

继承的好处是可以减少代码的重复,提高代码的可维护性和可扩展性。

在JavaScript中,继承可以通过原型链、构造函数等方式实现。

在TypeScript中,继承可以通过extends关键字来实现。

继承类

在TypeScript中,继承类的语法格式如下:

class Animal {
  type: string;

  constructor(type: string) {
    this.type = type;
  }

  sayType() {
    console.log(this.type);
  }
}

class Cat extends Animal {
  constructor(type: string) {
    super(type);
  }

  sayType() {
    console.log(`I am a ${this.type}`);
  }
}

const cat = new Cat("cat");
cat.sayType(); // 输出:I am a cat

在上面的代码中,我们定义了一个类Animal,该类有一个属性type和一个方法sayType。

然后,我们定义了一个类Cat,该类继承自Animal类,并且重写了方法sayType。

在方法sayType中,我们使用super关键字来调用父类构造函数和方法,然后输出"I am a "和type属性。

最后,我们使用new关键字来创建一个cat对象,然后调用对象的sayType方法,输出"I am a cat"。

综上所述,继承类可以实现代码的复用和扩展,同时可以减少代码的重复,提高代码的可维护性和可扩展性。

多态

在面向对象编程中,多态是指同一种行为具有多种不同的表现形式,可以通过父类的引用指向子类的对象,实现代码的灵活性和扩展性。

在TypeScript中,多态可以通过接口、抽象类等方式实现。

接口多态

在TypeScript中,接口可以用来定义一组属性和方法的规范,然后可以通过实现接口来实现多态。

interface Animal {
  type: string;
  sayType(): void;
}

class Cat implements Animal {
  type: string;

  constructor(type: string) {
    this.type = type;
  }

  sayType() {
    console.log(`I am a ${this.type}`);
  }
}

class Dog implements Animal {
  type: string;

  constructor(type: string) {
    this.type = type;
  }

  sayType() {
    console.log(`I am a ${this.type}`);
  }
}

function say(animal: Animal) {
  animal.sayType();
}

const cat = new Cat("cat");
const dog = new Dog("dog");

say(cat); // 输出:I am a cat
say(dog); // 输出:I am a dog

在上面的代码中,我们定义了一个接口Animal,该接口有一个属性type和一个方法sayType。

然后,我们定义了一个类Cat,该类实现了Animal接口,并且重写了方法sayType。

接着,我们定义了一个类Dog,该类实现了Animal接口,并且重写了方法sayType。

最后,我们定义了一个函数say,该函数的参数animal是Animal类型,然后调用animal的sayType方法。

我们使用new关键字来创建一个cat对象和一个dog对象,然后分别调用函数say,输出"I am a cat"和"I am a dog"。

综上所述,接口多态可以实现代码的灵活性和扩展性,同时可以减少代码的重复,提高代码的可维护性和可扩展性。

普通类实现多态

class Animal {
  type: string;

  constructor(type: string) {
    this.type = type;
  }

  sayType() {
    console.log(`I am a ${this.type}`);
  }
}

class Cat extends Animal {
  constructor(type: string) {
    super(type);
  }
}

class Dog extends Animal {
  constructor(type: string) {
    super(type);
  }
}

function say(animal: Animal) {
  animal.sayType();
}

const cat = new Cat("cat");
const dog = new Dog("dog");

say(cat); // 输出:I am a cat
say(dog); // 输出:I am a dog

抽象类实现多态

抽象类是一种特殊的类,它不能被实例化,只能被继承。抽象类可以包含抽象方法和非抽象方法。

抽象方法是一种没有实现的方法,它只有方法签名,没有方法体。抽象方法必须在子类中实现。

非抽象方法是一种有实现的方法,它可以被子类继承和重写,也可以被子类直接调用。

在TypeScript中,抽象类可以通过abstract关键字来定义,抽象方法可以通过abstract关键字来定义。

例如:

abstract class Animal {
  type: string;

  constructor(type: string) {
    this.type = type;
  }

  abstract sayType(): void;
}

class Cat extends Animal {
  constructor(type: string) {
    super(type);
  }

  sayType() {
    console.log(`I am a ${this.type}`);
  }
}

class Dog extends Animal {
  constructor(type: string) {
    super(type);
  }

  sayType() {
    console.log(`I am a ${this.type}`);
  }
}

function say(animal: Animal) {
  animal.sayType();
}

const cat = new Cat("cat");
const dog = new Dog("dog");

say(cat); // 输出:I am a cat
say(dog); // 输出:I am a dog

综上所述,普通类也可以实现多态,但是抽象类可以更好地实现多态,因为抽象类可以限制子类必须实现某些方法,从而提高代码的可维护性和可扩展性

类的成员修饰符

在TypeScript中,类的修饰符有public、private和protected。

1. public修饰符

public修饰符表示该属性或方法是公共的,可以在类的内部和外部访问。

class Animal {
  public type: string;

  constructor(type: string) {
    this.type = type;
  }

  public sayType() {
    console.log(`I am a ${this.type}`);
  }
}

const animal = new Animal("animal");

console.log(animal.type); // 输出:animal
animal.sayType(); // 输出:I am a animal

在上面的代码中,我们定义了一个类Animal,该类有一个公共属性type和一个公共方法sayType。

然后,我们使用new关键字来创建一个animal对象,并且访问animal的type属性和sayType方法,输出"animal"和"I am a animal"。

2. private修饰符

private修饰符表示该属性或方法是私有的,只能在类的内部访问,不能在类的外部访问。

class Animal {
  private type: string;

  constructor(type: string) {
    this.type = type;
  }

  private sayType() {
    console.log(`I am a ${this.type}`);
  }
}

const animal = new Animal("animal");

console.log(animal.type); // 报错:Property 'type' is private and only accessible within class 'Animal'.
animal.sayType(); // 报错:Property 'sayType' is private and only accessible within class 'Animal'.

在上面的代码中,我们定义了一个类Animal,该类有一个私有属性type和一个私有方法sayType。

然后,我们使用new关键字来创建一个animal对象,并且访问animal的type属性和sayType方法,都会报错,因为它们都是私有的,只能在类的内部访问。

3.protected修饰符

protected修饰符表示该属性或方法是受保护的,只能在类的内部和子类中访问,不能在类的外部访问。

class Animal {
  protected type: string;

  constructor(type: string) {
    this.type = type;
  }

  protected sayType() {
    console.log(`I am a ${this.type}`);
  }
}

class Cat extends Animal {
  constructor(type: string) {
    super(type);
  }

  public sayType() {
    console.log(`I am a cat`);
  }
}

const cat = new Cat("cat");

console.log(cat.type); // 报错:Property 'type' is protected and only accessible within class 'Animal' and its subclasses.
cat.sayType(); // 输出:I am a cat

在上面的代码中,我们定义了一个类Animal,该类有一个受保护的属性type和一个受保护的方法sayType。

然后,我们定义了一个类Cat,该类继承自Animal类,并且重写了sayType方法。

最后,我们使用new关键字来创建一个cat对象,并且访问cat的type属性和sayType方法,访问type属性会报错,因为它是受保护的,只能在Animal类和它的子类中访问,访问sayType方法会输出"I am a cat",因为它已经被重写了。

综上所述,类的修饰符可以控制类的属性和方法的访问权限,public表示公共的,private表示私有的,protected表示受保护的。

只读属性

在TypeScript中,可以使用readonly关键字来定义只读属性。

只读属性是指一旦被赋值后,就不能再被修改的属性。(准确上来说不能通过对象的属性来修改,可以使用类型断言或者类型转换来实现。)

class Animal {
  readonly type: string;

  constructor(type: string) {
    this.type = type;
  }
}

const animal = new Animal("animal");

console.log(animal.type); // 输出:animal

animal.type = "cat"; // 报错:Cannot assign to 'type' because it is a read-only property.

(animal as any).type = "cat";

console.log(animal.type); // 输出:cat

get的set

在TypeScript中,getter和setter是一种属性访问器,它们允许您读取和写入类中的私有成员。getter和setter是一对函数,getter函数用于获取属性值,setter函数用于设置属性值。以下是一个示例:

class Example {
  private _name: string;

  get name(): string {
    return this._name;
  }

  set name(newName: string) {
    this._name = newName;
  }
}

const example = new Example();
example.name = "CursorBot";
console.log(example.name); // Output: "CursorBot"

上面的示例中,我们定义了一个名为Example的类,它具有一个名为_name的私有成员。

我们还定义了一个名为name的getter和setter,它们允许我们读取和写入_name成员。

在类的实例化过程中,我们可以使用setter函数设置name属性的值,然后使用getter函数获取该值。

类的静态成员

在 TypeScript 中,你可以使用 static 关键字来定义类的静态成员。静态成员是属于类本身的,而不是属于类的实例的。这意味着你可以在不创建类的实例的情况下访问静态成员。

下面是一个使用静态成员的示例:

class MyClass {
  static myStaticProperty = 42;

  static myStaticMethod() {
    console.log('Hello, world!');
  }
}

console.log(MyClass.myStaticProperty); // 输出 42
MyClass.myStaticMethod(); // 输出 "Hello, world!"

在这个例子中,我们定义了一个名为 MyClass的类,并在其中定义了一个静态属性 myStaticProperty 和一个静态方法 myStaticMethod。我们可以在不创建 MyClass的实例的情况下访问这些静态成员。

接口

在 TypeScript 中,接口是一种用于描述对象的形状的方式。接口可以描述对象的属性、方法和索引签名。你可以使用接口来定义函数的参数类型、返回值类型和类的类型。

下面是一个使用接口描述对象的示例:

interface MyObject {
  myProperty: string;
  myMethod(): void;
}

const myInstance: MyObject = {
  myProperty: 'Hello, world!',
  myMethod() {
    console.log(this.myProperty);
  }
};

myInstance.myMethod(); // 输出 "Hello, world!"

在这个例子中,我们定义了一个名为 MyObject的接口,它描述了一个具有 myProperty属性和 myMethod方法的对象的形状。

然后,我们创建了一个符合 MyObject接口的对象,并将其赋值给一个类型为 MyObject的变量 myInstance

我们可以通过 myInstance访问对象的属性和方法。

索引类型

在 TypeScript 中,你可以使用索引类型来描述具有动态属性名称的对象的类型。索引类型可以用于描述对象的属性类型和方法参数类型。

下面是一个使用索引类型描述对象类型的示例:

interface MyObject {
  [key: string]: number;
}

const myObject: MyObject = {
  foo: 42,
  bar: 1337      //在 JavaScript 中,对象的属性名称可以是字符串或符号。当你使用数字作为属性名称
};              //时,它会被自动转换为字符串

console.log(myObject.foo); // 输出 42
console.log(myObject.bar); // 输出 1337

使用接口来定义函数类型

在 TypeScript 中,你可以使用接口来定义函数的参数类型、返回值类型和类的类型。

下面是一个使用接口定义函数类型的示例:

interface MyFunction {
  (arg1: string, arg2: number): boolean;
}

const myFunction: MyFunction = (arg1, arg2) => {
  console.log(arg1, arg2);
  return true;
};

myFunction('Hello, world!', 42); // 输出 "Hello, world!" 和 42

在这个例子中,我们定义了一个名为 MyFunction 的接口,它描述了一个具有两个参数(一个字符串和一个数字)和一个布尔返回值的函数的类型。然后,我们创建了一个符合 MyFunction 接口的函数,并将其赋值给一个类型为 MyFunction 的变量 myFunction。我们可以通过 myFunction 调用这个函数,并传递两个参数。

使用接口来定义类的类型

在 TypeScript 中,你可以使用接口来定义类的类型。接口可以描述类的实例部分和静态部分的类型。

下面是一个使用接口定义类类型的示例:

interface MyInterface {
  myProperty: string;
  myMethod(): void;
}

class MyClass implements MyInterface {
  myProperty = 'Hello, world!';

  myMethod() {
    console.log(this.myProperty);
  }
}

const myInstance: MyInterface = new MyClass();
myInstance.myMethod(); // 输出 "Hello, world!"

在这个例子中,我们定义了一个名为 MyInterface的接口,它描述了一个具有 myProperty属性和  myMethod方法的类的类型。

然后,我们定义了一个名为 MyClass的类,并实现了 MyInterface接口。

最后,我们创建了一个 MyClass的实例,并将其赋值给一个类型为 MyInterface的变量 myInstance

我们可以通过 myInstance访问 MyClass的实例部分和静态部分的成员。

接口的继承

在 TypeScript 中,接口可以继承其他接口。这使得你可以将多个接口组合成一个更大的接口,从而使代码更加模块化和可重用。

下面是一个使用接口继承的示例:

interface Animal {
  name: string;
  eat(food: string): void;
}

interface Dog extends Animal {
  breed: string;
  bark(): void;
}

class Labrador implements Dog {
  name: string;
  breed: string;

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

  eat(food: string) {
    console.log(`${this.name} is eating ${food}`);
  }

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

const myDog = new Labrador("Buddy", "Labrador Retriever");
myDog.eat("dog food"); // 输出 "Buddy is eating dog food"
myDog.bark(); // 输出 "Buddy is barking"

在这个例子中,我们定义了两个接口 Animal和 Dog

Dog接口继承了 Animal接口,并添加了一个 breed属性和一个 bark方法。

然后,我们定义了一个名为 Labrador的类,它实现了 Dog接口。

最后,我们创建了一个 Labrador类的实例,并调用了它的 eat 和 bark方法。

交叉类型

在 TypeScript 中,交叉类型可以用于将多个类型组合成一个类型。交叉类型表示同时具有多个类型的值的类型。

下面是一个使用交叉类型的示例:

interface A {
  a: string;
}

interface B {
  b: number;
}

type C = A & B;  //交叉  与
type D = A | B   //联合  或
const c: C = {
  a: "hello",
  b: 42
};

const d: D = {
  b:43
} 

console.log(c.a); // 输出 "hello"
console.log(c.b); // 输出 42

接口的继承交叉类型可以将多个接口组合成一个更大的接口,从而使代码更加模块化和可重用。

类的实现

在 TypeScript 中,接口可以用于描述对象的形状,包括对象的属性和方法。接口本身并不实现任何功能,它只是定义了一个类型要使用接口,你需要创建一个符合接口定义的对象或类


interface Person {
  name: string;
  age: number;
  sayHello(): void;
}

class Student implements Person {   //定义了一个名为 Student的类,它实现了 Person接口。
  name: string;
  age: number;

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

  sayHello() {
    console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
  }
}
function sayHello(sayable: Person) { 
  sayable.sayHello()
}

sayHello(new Student("jk",18))  //Student实现了PerSon类,所以Student实例可以当函数入参。


const student = new Student("Alice", 20);
student.sayHello(); // 输出 "Hello, my name is Alice and I'm 20 years old."

就比如一个接口里面有一个函数

sayHello(): void;

会被经常调用。你就可以在多个类的定义上实现这个接口

class Student implements Person,然后定义这个函数,并在外部也定义这个函数, 函数的入参设置为那个接口类型。

function sayHello(sayable: Person) { sayable.sayHello() }

最后如果调用那个函数只需要传入实现接口的类的实例即可

sayHello(new Student("jk",18))。面向接口编程。

type和interface区别

在 TypeScript 中,interface和 type都可以用于定义类型。它们的主要区别在于:

  1. interface可以被合并,而 type不行。这意味着你可以定义多个同名的 interface,它们会自动合并成一个接口。但是,如果你定义多个同名的type,它们会报错。

  2. interface可以被 extends和 implements,而type不行。这意味着你可以使用 interface来扩展其他接口或类,或者让类实现某个接口。但是,type不能这样做。

  3. type可以使用联合类型、交叉类型和元组类型,而 interface不行。这意味着你可以使用 type 来定义更复杂的类型,例如联合类型、交叉类型和元组类型。但是,interface不能这样做。

总的来说,interface更适合用于描述对象的形状,而 type更适合用于定义复杂的类型。但是,它们的使用场景有很大的重叠,具体使用哪个取决于你的个人喜好和项目需求。