TypeScript入门到精通——TypeScript类型系统基础——类

  JavaScript 是一门面向对象的编程语言,它允许通过对象来建模和解决实际问题。同时,JavaScript 也支持基于原型链的对象继承机制。虽然大多数的面向对象编程语言都支持类,但是 JavaScript 语言在很长一段时间都没有支持它。在 JavaScript 程序中,需要使用函数来实现类的功能。

  在 ECMAScript 2015 规范中正式地定义了类。同时,TypeScript 语言也对类进行了全面的支持。

一、类的定义

  虽然 JavaScript 语言支持了类,但其本质上仍然是函数,类是一种语法糖。TypeScript 语言对 JavaScript 中的类进行了罗占,为其添加了类型支持,如实现接口、泛型类等。

  定义一个类需要使用 class 关键字。类型于函数定义,类的定义也有以下两种方式:

    • 类声明
    • 类表达式

1.1、类声明

  类声明能够创建一个类,类声明的语法如下所示:

class ClassName {
    // ...

} 

  在该语法中,class 是关键字;ClassName 表示类的名字。在类声明中的类名是必选的。按照惯例,类名的首字母应该大写。示例如下:

class Circle{
    radius: number;
}

const c = new Circle(); 

  与函数声明不同的是,类声明不会被提升,就是必须先声明后,再使用。示例如下:

const c0 = new Circle();   //错误


class Circle{
    radius: number;
}


const c1 = new Circle();   //正确

1.2、类表达式

  类表达式是另一种定义类的方式,它的语法如下所示:

const  Name = class ClassName {
    // ...
};  

  在该语法中,class 是关键字;Name 表示引用了该类的变量名;ClassName 表示类的名字。在类表达式中,类名 ClassName 是可选的。

  例如,下例中使用类表达式定义了一个匿名类,同时使用常量 Circle 引用了该匿名类:

const Circle = class {
    radius: number;
};  

  如果在类表达式中定义了类型,则该类型只能够在类内部使用,在类外不允许引用该类名。

const A = class B {

    name = B.name;
};

const b = new B(); // error

二、成员变量

  TypeScript 是一种基于 JavaScript 的强类型或静态类型语言。成员变量,也称为实例变量或属性,是定义在类中的变量,用于保存对象的状态。

  在 TypeScript 中,成员变量可以在类的构造函数中初始化,也可以在声明时直接赋值。这些变量属于类的实例,所以每个实例都会有一份自己的副本。

  下面是一个简单的例子:

class Animal {
    // 成员变量
    name: string;
    age: number;

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

let cat = new Animal('Tom', 5);
console.log(cat.name); // 输出 'Tom'
console.log(cat.age); // 输出 5

  TypeScript 的成员变量可以有访问修饰符,比如 public(默认)、private 和 protected。这些修饰符决定了成员变量的可见性,也就是它们在类的外部是否可以被访问或修改。

class Animal {
    private name: string; // 私有成员变量
    public age: number; // 公有成员变量

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

  在这个例子中,name 是私有的,所以在类的外部无法访问或修改它。而 age 是公有的,所以在类的外部可以访问和修改它。

三、成员函数

  成员函数也称作方法,声明成员函数与在对象字面量中声明方法是类似的。
  TypeScript 中的类成员函数是类中的一种方法,它们可以在类的实例上调用。类成员函数可以让你在类中定义一些行为或操作,这些行为或操作可以在类的实例上进行调用。
  下面是一个简单的 TypeScript 类和成员函数的例子:
class Person {
    name: string;
    age: number;

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

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

const person = new Person('Alice', 25);
person.sayHello(); // 输出:Hello, my name is Alice and I am 25 years old.

  在上面的例子中,Person 类有两个成员变量 name 和 age,以及一个成员函数 sayHello。sayHello 函数使用 console.log 输出一个问候语,其中包含当前实例的 name 和 age。 注意,在这个例子中使用了 this 关键字来引用当前类的实例。this 关键字可以用来访问类的成员变量和其他成员函数。 类成员函数还可以接收参数,如下所示:  

class Calculator {
    add(a: number, b: number): number {
        return a + b;
    }
}

const calculator = new Calculator();
console.log(calculator.add(2, 3)); // 输出:5

  在上面的例子中,Calculator 类有一个接收两个数字参数并返回它们之和的成员函数 add。

四、成员存取器

  TypeScript 中的类成员存取器(getter 和 setter)是一种特殊的类成员函数,用于访问和修改类的成员变量。它们提供了一种更安全和可控的方式来处理类的属性。

  getter 是一个只读函数,用于获取成员变量的值。它的名称与成员变量相同,但前面加上 get 关键字。getter 函数不接收任何参数,返回成员变量的值。

  setter 是一个写函数,用于设置成员变量的值。它的名称与成员变量相同,但前面加上 set 关键字。setter 函数接收一个参数,该参数是要设置的新值。

  下面是一个使用 getter 和 setter 的 TypeScript 类示例:

class Person {
    private _name: string;
    private _age: number;

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

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

    set name(value: string) {
        if (value.length > 0) {
            this._name = value;
        } else {
            console.log("Name cannot be empty.");
        }
    }

    get age(): number {
        return this._age;
    }

    set age(value: number) {
        if (value > 0) {
            this._age = value;
        } else {
            console.log("Age cannot be negative.");
        }
    }
}  

  在这个示例中,Person类有两个私有成员变量 _name 和 _age,以及对应的 getter 和 setter。

  getter 和 setter 允许我们更好地控制对成员变量的访问和修改。在这个例子中,setter 函数对输入值进行了检查,确保名字不为空,年龄不为负。如果不满足这些条件,setter 将不会修改成员变量,而是输出一条错误消息。这是一种数据验证的有效方式。 使用getter和setter的另一个好处是,它们可以让我们在未来更改类的内部实现时,保持对外部代码的兼容性。

  例如,如果我们以后决定不再直接存储_name和_age,而是将它们存储在数据库或远程服务器上,我们只需要修改getter和setter,而不需要修改所有使用这些变量的代码。getter和setter是TypeScript(和许多其他面向对象语言)中非常有用的特性,可以提高代码的可读性、安全性和可维护性。

五、索引成员

  类的索引成员会在类的类型中引入索引签名。

  索引签名包含两种:

    • 字符串索引: 使用字符串作为键来访问对象的属性。在类中,你可以使用字符串索引来访问类的属性。
      class MyClass {  
          private myProperty: string;  
          constructor() {  
              this.myProperty = "Hello, World!";  
          }  
      }  
        
      let myObject = new MyClass();  
      console.log(myObject["myProperty"]); // 输出 "Hello, World!"
      

        在上面的例子中,我们使用字符串索引 "myProperty" 来访问 MyClass 实例的属性。 

    • 数值索引: 使用数值作为键来访问对象的属性。在类中,你可以使用数值索引来访问类的属性或元素。
      class MyArray {  
          private myElements: number[];  
          constructor(elements: number[]) {  
              this.myElements = elements;  
          }  
      }  
        
      let myArray = new MyArray([1, 2, 3, 4, 5]);  
      console.log(myArray[2]); // 输出 3
      

        在上面的例子中,我们使用数值索引 来访问 MyArray 实例的元素

六、成员可访问性

  在 TypeScript 中,类的成员可以通过访问修饰符来控制其可访问性。访问修饰符有以下四种:

    • public:公共访问修饰符,表示类的成员可以从任何地方访问。
    • protected:受保护的访问修饰符,表示类的成员只能在类内部或派生类中访问。
    • private:私有访问修饰符,表示类的成员只能在类内部访问。
    • #private(私有字段):这是一种特殊的私有访问修饰符,用于声明私有字段。

6.1、public

     例如,假设有一个 Person 类,它有一个 name 属性和一个 greet 方法:

class Person {  
  public name: string;  
    
  public greet(): string {  
    return `Hello, my name is ${this.name}`;  
  }  
}

     可以直接创建 Person 的实例并访问其 name 属性和 greet 方法:

const person = new Person();  
person.name = "Alice";  
console.log(person.greet()); // 输出 "Hello, my name is Alice"

6.2、protected

      例如,假设有一个 Animal 类,它有一个 protected name 属性和一个 protected speak 方法:

class Animal {  
  protected name: string;  
    
  protected speak(): string {  
    return `My name is ${this.name}`;  
  }  
}

你可以在派生类中访问 Animal 的受保护成员:

class Dog extends Animal {  
  bark(): void {  
    console.log(this.name); // 可以访问受保护的 name 属性  
    console.log(this.speak()); // 可以访问受保护的 speak 方法  
  }  
}

但在类的外部无法直接访问受保护的成员:

const dog = new Dog(); // 错误!Dog 类不能直接实例化,只能通过继承 Animal 类的方式创建派生类对象。  
console.log(dog.name); // 错误!name 属性是受保护的,不能从外部直接访问。  
console.log(dog.speak()); // 错误!speak 方法是受保护的,不能从外部直接访问。 

6.3、private

     例如,假设有一个 BankAccount 类,它有一个 private balance 属性和一个 deposit 方法:

class BankAccount {  
  private balance: number; // private 属性只能在 BankAccount 类内部直接访问,不能从外部或派生类中直接访问。

6.4、私有字段

      例如,让我们以 BankAccount 类为例,添加一个私有的 #balance 字段:

class BankAccount {  
  #balance: number; // 私有字段  
  
  constructor(initialBalance: number) {  
    this.#balance = initialBalance; // 在类内部访问和赋值私有字段  
  }  
  
  deposit(amount: number) {  
    this.#balance += amount; // 在类内部访问和操作私有字段  
  }  
  
  get balance() {  
    return this.#balance; // 通过 getter 访问私有字段(在外部不可直接访问)  
  }  
}

      建一个 BankAccount 的实例,并使用其 deposit 方法以及通过其 balance getter 来查看余额:

const account = new BankAccount(1000); // 创建一个 BankAccount 实例  
console.log(account.balance); // 输出 1000  
account.deposit(500); // 存入 500  
console.log(account.balance); // 输出 1500

 但是,如果你尝试直接访问 #balance 字段(无论是从类的外部还是从派生类中),TypeScript 都会给出错误,因为它是私有的。例如:

console.log(account.#balance); // 错误!无法直接从外部访问 #balance 字段

七、构造函数 

  在 TypeScript 中,类是一种用户自定义的数据类型,它允许您封装数据和相关操作。构造函数是类的一个特殊方法,用于初始化新创建的对象实例的状态。

  下面是一个简单的示例,展示了如何在 TypeScript 中定义一个带有构造函数的类:

class Person {  
  constructor(public name: string, public age: number) {}  
  
  greet() {  
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);  
  }  
}  
  
// 创建一个 Person 对象实例  
const person1 = new Person("Alice", 25);  
person1.greet(); // 输出: Hello, my name is Alice and I am 25 years old.

  在上面的示例中,Person 类有一个构造函数。当您使用 new 关键字创建 Person 类的对象实例时,构造函数会被调用,并传入 name 和 age 参数。这些参数被用来初始化新创建的对象实例的 name 和 age 属性。

  注意,在 TypeScript 中,构造函数使用 constructor 关键字进行定义,并且它们总是在类定义的顶部。在构造函数中,您可以定义并初始化类的属性或执行其他必要的初始化操作。

  构造函数可以有参数,并且参数可以带有类型注解。在上面的示例中,构造函数的参数具有类型注解 public name: string 和 public age: number,这表示构造函数期望传入一个字符串和一个数字作为参数。通过使用类型注解,TypeScript 可以帮助您在编译时捕获类型错误。 

八、参数成员

  在 TypeScript 中,类的参数成员是指在类构造函数中定义的参数。这些参数被用来初始化类的属性或执行其他必要的初始化操作。

  在类构造函数中,可以定义多个参数,每个参数都具有一个类型注解。这些类型注解用于指定参数的数据类型,以便在编译时进行类型检查。

  下面是一个示例,展示了如何在 TypeScript 类中定义参数成员:

class Person {  
  constructor(public name: string, public age: number) {  
    // 在构造函数内部可以执行其他初始化操作  
  }  
  
  greet() {  
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);  
  }  
}

  在上面的示例中,Person类有一个构造函数,它接受两个参数:nameage。这些参数被用来初始化类的属性nameage。注意,在构造函数参数的右侧使用类型注解public name: stringpublic age: number,这表示这些参数应该传入一个字符串和一个数字。

  在构造函数内部,您可以执行其他初始化操作,例如为属性赋予默认值或进行其他必要的设置。在上面的示例中,构造函数没有进行其他操作,但您可以根据需要添加其他逻辑。

当您创建一个类的实例时,需要传递相应的参数给构造函数。例如:

const person1 = new Person("Alice", 25);  
person1.greet(); // 输出: Hello, my name is Alice and I am 25 years old.

  在上面的代码中,我们使用new关键字创建了一个Person类的对象实例,并传递了两个参数给构造函数:"Alice"25。这些参数被用来初始化person1对象的nameage属性。然后,我们调用greet方法来输出问候语。

九、继承

9.1、重写基类成员

  在 TypeScript 中,派生类可以重写基类的成员。例如,如果我们有一个基类Animal和一个派生类DogDog可以重写Animalspeak方法:

class Animal {  
    speak(volume: number) {  
        console.log(`The animal speaks with volume ${volume}`);  
    }  
}  
  
class Dog extends Animal {  
    speak(volume: number) {  
        // 重写基类的 speak 方法  
        console.log(`The dog barks with volume ${volume}`);  
    }  
}  
  
let dog = new Dog();  
dog.speak(5);  // 输出: "The dog barks with volume 5"

9.2、派生类实例化

  当我们创建一个派生类的实例时,这个实例会自动继承基类的属性和方法。在上述的例子中,我们可以创建一个Dog类的实例并调用speak方法:

let dog = new Dog();  
dog.speak(5);  // 输出: "The dog barks with volume 5"

9.3、单继承

  在 TypeScript 中,一个类只能继承另一个类。这就是单继承的含义。

  例如,我们可以创建一个Mammal类,它是Animal的子类:

class Mammal extends Animal {  
    feed() {  
        console.log("The mammal is being fed");  
    }  
}

Mammal继承了Animal的所有属性和方法,包括speak。  

9.4、接口继承类

  在 TypeScript 中,接口可以继承类或另一个接口的成员。这允许我们创建更具体的接口。例如,我们可以创建一个DogService接口,它继承自AnimalService接口:

interface AnimalService {  
    getAnimal(): Animal;  
}  
  
interface DogService extends AnimalService {  
    getDog(): Dog;  // 扩展了 AnimalService 接口的 getAnimal 方法  
}

  DogService接口继承了AnimalService接口的所有成员(包括getAnimal方法),并添加了一个新的方法getDog

十、实现接口

  在TypeScript中,类可以实现接口。实现接口可以让一个类拥有接口所定义的方法和属性。这有助于实现代码的解耦和增强可读性。

  要实现一个接口,类需要包含与接口中定义的方法和属性相同的签名。下面是一个示例:

//定义一个接口 Person
interface Person {  
  name: string;  //一个字符串类型的属性。
  age: number;   // 一个数字类型的属性。
  greet: (message: string) => void;  //一个函数,接收一个字符串参数 message,没有返回值(返回类型为 void)。
}  

//定义一个类 Emplyee
//这个类实现了上述定义的 Person 接口。这意味着它必须包含与 Persion 接口中定义的所有属性和方法相同的签名。
//在 TypeScript 中,implements 是一个关键字,用于实现接口。它表示一个类将实现一个接口的所有方法和属性。在这个例子中,Employee 类实现了 Person 接口,这意味着 Employee 类必须包含与 Person 接口中定义的所有属性和方法相同的签名。
class Employee implements Person {  
  name: string;  //一个字符串类型的属性。
  age: number;   //一个数字类型的属性。
  
  //类的构造函数,接收两个参数,一个字符串 name 和一个数字 age。这些参数用于初始化上述的两个属性。
  constructor(name: string, age: number) {  
    this.name = name;  
    this.age = age;  
  }  
  
  //实现接口的 greet 方法。这个方法接收一个字符串参数 message 打印。
  greet(message: string) {  
    console.log(`${message}, ${this.name}!`);  
  }  
} 

  现在,我们可以创建一个Employee对象并调用其方法:

const employee = new Employee("John", 30);  
employee.greet("How are you?"); // 输出: "How are you, John!"

  通过实现接口,我们可以确保类具有所需的规范,并且可以使用接口中定义的方法和属性来扩展类的功能。这使得代码更加灵活和可维护。在 TypeScript 中,接口提供了一种方法来定义一个类必须具有的结构和行为,以及可以在多个类之间共享的通用模板。  

十一、静态成员

11.1、静态成员可访问性

  在 TypeScript 中,类可以包含静态成员。静态成员是属于类的成员,而不是类的实例的成员。这意味着你可以在不创建类的实例的情况下访问静态成员。静态成员在类中定义,并且在类的任何实例上都是可访问的。

 下面是一个简单的示例,展示了如何在 TypeScript 中定义和使用静态成员:
class MyClass {  
  static staticProperty: string = 'Hello, World!';  
  
  static staticMethod() {  
    console.log('This is a static method.');  
  }  
}  
  
// 访问静态属性  
console.log(MyClass.staticProperty); // 输出:Hello, World!  
  
// 调用静态方法  
MyClass.staticMethod(); // 输出:This is a static method.

  关于静态成员的访问性,TypeScript支持使用publicprotectedprivate修饰符来指定静态成员的可见性。这些修饰符的使用方式与实例成员相同。例如,你可以将静态属性或方法声明为public,使其在类的任何地方都可以访问;或者使用private修饰符将其限制在类的内部。

11.2、继承静态成员

  继承是面向对象编程的一个重要概念,TypeScript 也支持类的继承。然而,与实例成员不同,静态成员不能被继承。这意味着子类不能继承父类的静态成员。每个类都有自己独立的静态成员。

  虽然静态成员不能被继承,但子类可以通过原型链访问父类的静态成员。当你在子类中访问一个静态成员时,如果该成员在父类中不存在,那么将会在子类的原型链上查找该成员。这使得你可以在子类中重写父类的静态成员,就像你可以重写实例方法一样。

  下面是一个示例,展示了如何在 TypeScript 中使用静态成员的继承:

class ParentClass {  
  static staticProperty: string = 'Hello from Parent';  
}  
  
class ChildClass extends ParentClass {  
  static staticProperty: string = 'Hello from Child';  
}  
  
// 访问父类的静态属性  
console.log(ParentClass.staticProperty); // 输出:Hello from Parent  
  
// 访问子类的静态属性(会覆盖父类的静态属性)  
console.log(ChildClass.staticProperty); // 输出:Hello from Child

  在上面的示例中,ParentClassChildClass都定义了一个名为staticProperty的静态属性。由于ChildClass继承自ParentClass,所以ChildClass可以访问ParentClass的静态属性。同时,ChildClass也定义了自己的静态属性,并且通过使用static关键字,确保了它不会被继承。

十二、抽象类和抽象成员

  在 TypeScript 中,抽象类(Abstract Class)和抽象成员(Abstract Members)是用于实现面向对象编程的重要特性。它们提供了一种方式来定义不能直接实例化的类,而是用作其他类的基类。

12.1、抽象类

  抽象类是一种不能直接实例化的类,它用于定义抽象成员。抽象类只能被继承,并且派生类必须实现所有的抽象成员。

  在 TypeScript 中,使用abstract关键字来声明抽象类。下面是一个简单的示例:

abstract class AbstractClass {  
    abstract member1(): void;  
    abstract member2(): string;  
}

  在上面的示例中,AbstractClass是一个抽象类,它定义了两个抽象成员member1member2。这两个成员都被声明为抽象成员,因此派生类必须提供它们的具体实现。

12.2、抽象成员

  抽象成员是定义在抽象类中的方法或属性,它没有具体的实现。派生类必须提供抽象成员的具体实现。

  在 TypeScript 中,使用abstract关键字来声明抽象成员。下面是一个简单的示例:

abstract class AbstractClass {  
    abstract member1(): void; // 抽象方法  
    abstract member2(): string; // 抽象属性  
}

  在上面的示例中,member1和 member2都被声明为抽象成员。这意味着任何继承自AbstractClass的类都必须提供它们的具体实现。

  派生类可以通过实现抽象成员来继承抽象类的行为。下面是一个使用抽象类和抽象成员的示例:

class DerivedClass extends AbstractClass {  
    member1(): void {  
        // 实现抽象方法 member1 的具体逻辑  
    }  
    member2(): string {  
        // 实现抽象属性 member2 的具体逻辑并返回一个字符串  
        return "Hello, world!";  
    }  
}

  在上面的示例中,DerivedClass继承自AbstractClass并实现了所有的抽象成员。现在,我们可以创建DerivedClass的实例并调用它的方法:

const obj = new DerivedClass(); // 创建派生类的实例  
obj.member1(); // 调用派生类实现的方法  
console.log(obj.member2()); // 访问派生类实现的属性并打印结果:"Hello, world!" 

十三、this 类型

  在 TypeScript 中,类的this类型是指在该类中this关键字的类型。在 TypeScript 中,this关键字用于访问当前对象的属性和方法。

  在类中,this关键字的类型是该类的实例类型。这意味着,当你在类的方法中使用this关键字时,它引用的类型是该类的实例类型。

  下面是一个简单的示例,展示了如何在 TypeScript 类中使用this类型:

class MyClass {  
  constructor(private name: string) {}  
  
  sayHello() {  
    console.log(`Hello, my name is ${this.name}`); // this 指向 MyClass 的实例  
  }  
}  
  
let myObject = new MyClass("Alice");  
myObject.sayHello(); // 输出 "Hello, my name is Alice"

  在上面的示例中,MyClass是一个简单的类,有一个私有属性name和一个公有方法sayHello。在sayHello方法中,this关键字用于引用MyClass的实例对象。在实例化MyClass时,我们传递了一个字符串参数 "Alice" 给构造函数,从而设置了实例的name属性。然后,我们调用sayHello方法,它使用this.name来访问和打印实例的name属性。

  在 TypeScript 中,你可以使用类型断言来明确指定this的类型。例如,如果你想在某个方法中明确指定this的类型是MyClass的实例类型,你可以使用类型断言:

class MyClass {  
  constructor(private name: string) {}  
  
  sayHello() {  
    console.log(`Hello, my name is ${this.name}`); // this 指向 MyClass 的实例  
  }  
  
  getThisType() {  
    // 使用类型断言明确指定 this 的类型为 MyClass 的实例类型  
    let thisType: MyClass = this;  
    return thisType;  
  }  
} 

十四、类类型    

  在 TypeScript 中,一个类的类型是由其所有实例共享的。这意味着,如果你有一个类的实例,你可以使用该类的任何方法或访问其任何属性,而无需再次实例化该类。类类型是一种用于表示类的结构和行为的类型。

  在 TypeScript 中,类类型是通过使用class关键字来声明的。下面是一个简单的示例:

class MyClass {  
  constructor(private name: string) {}  
  
  sayHello() {  
    console.log(`Hello, my name is ${this.name}`);  
  }  
}  
  
// MyClass 的类类型  
let myClassType: MyClass;  
  
// 创建一个 MyClass 的实例  
let myObject = new MyClass("Alice");  
  
// 可以使用类的实例方法  
myObject.sayHello(); // 输出 "Hello, my name is Alice"  
  
// 可以使用类的实例属性(在这个例子中没有实例属性,但可以有)  
console.log(myObject.name); // 输出 "Alice"

  在上面的示例中,我们声明了一个名为MyClass的类,它有一个私有属性name和一个公有方法sayHello。然后,我们声明了一个名为myClassType的变量,该变量的类型是MyClass。这意味着我们可以将MyClass的实例赋值给myClassType变量。然后,我们创建了一个MyClass的实例,并将其赋值给myObject变量。我们可以使用myObject变量来调用sayHello方法,并访问name属性。

  需要注意的是,在 TypeScript 中,类的构造函数必须使用new关键字来调用。这是因为 TypeScript 使用构造函数来创建类的实例,并且使用new关键字可以确保类的实例被正确地初始化。

posted @ 2023-10-30 13:47  左扬  阅读(142)  评论(0编辑  收藏  举报
levels of contents