在 TypeScript 中声明和实例化类

10,072 阅读9分钟

这是我参与11月更文挑战的第14天,活动详情查看:11月更文挑战

TypeScript 中的类简介

利用类,你可以采用标准方式表达常见的面向对象的模式,从而使继承等功能更具可读性和互操作性。 在 TypeScript 中,除了使用接口和函数描述对象类型外,类还有另外一种方法来定义对象的形状。

如果你还没有使用过类,则查看几个基本概念可能会有所帮助。

可以将类视为用于生成对象的蓝图,如汽车。 Car 类描述汽车的属性,例如品牌、颜色或车门数量。 它还介绍了汽车可以执行的行为,如加速、制动或转向。

但 Car 类只是一个制造汽车的计划。 必须从 Car 类生成一个实例 Car,然后才能成为向其分配属性值的对象(例如将颜色设置为蓝色)或调用其行为(如踩下制动。)

Car 类可重复使用,以创建任意数量的新 Car 对象,每个对象都有其自身的特征。 还可以扩展此 Car 类。 例如,ElectricCar 类可以扩展 Car。 它将具有与 Car 相同的属性和行为,但也可以有其自己的唯一属性和行为,如档位和充电操作。

类封装该对象的数据。 数据和行为包括在类中,但两者的详细信息可以对代码中使用对象的人员隐藏。 例如,如果调用 turn 对象的方法 Car,则无需确切了解方向盘的工作方式,只需要知道汽车会在你指示左转时左转。 该类用作一个黑盒,其中所有特性和行为仅通过属性和方法公开,从而限制了编码人员使用它的能力。

类组件

  • 属性(也称为字段)是对象的数据(或特性)。 这些是可以在代码中设置或返回的对象的定义特征。
  • constructor 是一个特殊函数,用于基于类创建和初始化对象。 创建类的新实例时,构造函数使用类形状创建一个新对象,并使用传递给它的值对其进行初始化。
  • 访问器是一种用于 get 或 set 属性值的函数类型。 属性可以是只读的,只需省略类中的 set 访问器,或者通过省略 get 访问器使其不可访问(如果尝试访问它,该属性将返回 undefined,即使在初始化期间为其赋值也是如此。)
  • 方法是定义对象可以执行的行为或操作的函数。 可以调用这些方法来调用对象的行为。 还可以定义只能从类本身访问的方法,并且通常由类中的其他方法调用以执行任务。

设计说明

可以创建类来为数据建模、封装功能、提供模板以及许多其他用途。 因此,上面列出的组件在你创建的每个类中都不是必需的。 可能只需要实用工具对象的方法和构造函数,或者只需要属性来管理数据。

创建类

若要创建类,请定义其成员:属性、constructor、访问器和方法。

  1. 使用 class 关键字后跟类名 Car 创建一个新的 class。 按照约定,类名为 PascalCase。 另外,还可以添加一些注释,以便更轻松地将类成员添加到正确位置。我看着跟JAVA没什么两样.

    class Car {
        // Properties
    
        // Constructor
    
        // Accessors
    
        // Methods
    
    }
    

声明类属性

可以将类属性视为在初始化时传递给对象的原始数据。

  1. 声明 Car 类的三个属性:_model: string、_color: string 和 _doors: number。比如这样 此时你的编辑器会报错 因为你还没有构造方法

    // Properties
    _make: string;
    _color: string;
    _doors: number;
    

定义类构造函数

TypeScript 中的类创建两个不同的类型:实例类型(定义类实例的成员)和 constructor 函数类型(定义类 constructor 函数的成员)。 constructor 函数类型也称为“静态端”类型,因为它包括类的静态成员。

使用 constructor 可以简化类,并在使用多个类类时使它们更易于管理。

一个类最多只能包含一个 constructor 声明。 如果类不包含 constructor 声明,则提供自动构造函数。

比如你的代码可以类似这样

TypeScript复制

// Constructor
constructor(make: string, color: string, doors = 4) {
    this._make = make;
    this._color = color;
    this._doors = doors;
}

到这里,如果你是java的使用者可能会好奇,_的作用是什么 我有强迫症,这不好看.其实去掉也无所谓,属性名称前的下划线 (_) 在属性声明中不是必需的,但它提供了一种方法来区分属性声明和可通过构造函数访问的参数,同时仍以直观方式将两者结合在一起。

定义访问器

虽然你可以直接访问类属性(它们在默认情况下都是 public),但 TypeScript 支持使用 getter/setter 作为拦截对属性的访问的方法。 这使你可以更精细地控制如何在每个对象上访问成员。

若要 set 或从代码中返回对象成员的值,则必须在类中定义 get 和 set 访问器。

如果你是java的使用者,会惊呼 这不是自动生成get/set方法吗

  1. 为返回 _make 属性值的 make 参数定义一个 get 块。

    // Accessors
    get make() {
        return this._make;
    }
    
  2. 为 make 参数定义一个 set 块,将 _make 属性的值设置为 make 参数的值。

    set make(make) {
        this._make = make;
    }
    
  3. 还可以使用 get 和 set 块来验证数据、施加约束或在将数据返回到程序之前对数据执行其他操作。 为 color 参数定义 get 和 set 块,但这次返回连接到 _color 属性值的字符串。

    get color() {
        return 'The color of the car is ' + this._color;
    }
    set color(color) {
        this._color = color;
    }
    
  4. 定义 doors 参数的 get 和 set 块。 返回 _doors 属性的值之前,请确认参数 doors 的值是偶数。 否则,将引发错误。

    get doors() {
        return this._doors;
    }
    set doors(doors) {
        if ((doors % 2) === 0) {
            this._doors = doors;
        } else {
            throw new Error('Doors must be an even number');
        }
    }
    

定义类方法

可以在类中定义任何 TypeScript 函数,并将其作为对象的方法调用,或者从类中的其他函数调用。 这些类成员描述类可执行的行为,并可执行类所需的任何其他任务

  1. 为 Car 类定义这四个方法:accelerate、brake、turn 和 worker。 你会注意到没有 function 关键字。 在类中定义函数时,不需要也不允许这样做,因此它有助于保持语法简洁。

    // Methods
    accelerate(speed: number): string {
        return `${this.worker()} is accelerating to ${speed} MPH.`
    }
    brake(): string {
        return `${this.worker()} is braking with the standard braking system.`
    }
    turn(direction: 'left' | 'right'): string {
        return `${this.worker()} is turning ${direction}`;
    }
    // This function performs work for the other method functions
    worker(): string {
        return this._make;
    }
    

实例化类

此时,你有一个名为 Car 的类,该类有三个属性,你可以获取和设置这些属性的值。 它还具有四种方法。 现在,你可以使用 Car 关键字实例化 new 类并向其传递参数,从而创建新的 Car 对象。

  1. 在类声明下方,声明一个名为 myCar1 的变量,并为其分配一个新的 Car 对象,为 make、color 和 doors 参数传递值(确保为 doors 参数分配一个偶数。)

    let myCar1 = new Car('Cool Car Company', 'blue', 2);  // Instantiates the Car object with all parameters
    
  2. 现在可以访问新 myCar1 对象的属性。 输入 myCar1.,应会看到类中定义的成员列表,包括 color 和 _color。 选择“运行”可将这两个属性的值返回到控制台。

    此时如果你使用vscode 在终端 执行tsc .\文件名时报错 Accessors are only available when targeting ECMAScript 5 and higher.

    请使用以下命令

    tsc -t es5 .\文件名.ts
    
    console.log(myCar1.color);
    console.log(myCar1._color);
    

    然后执行编译好的js 文件 node .\文件名.js

image.png

  1. 成员 _color 表示类中定义的属性,而 color 是传递给构造函数的参数。 引用 _color 时,你将访问属性的原始数据,这将返回 'blue'。 引用 color 时,你将通过 get 或 set 访问器访问属性,这将返回 'The color of the car is blue'。 理解两者之间的区别非常重要,因为在获取或设置数据之前,你通常不希望在未对数据进行验证或执行其他操作的情况下直接访问属性。

  2. 请注意,doors 参数的 set 块会测试该值,以确定它是偶数还是奇数。 测试方法是通过声明一个名为 myCar2 的变量并为其分配新的 Car 对象,为 make、color 和 doors 参数传入值。 此时,将 doors 参数的值设置为奇数。 现在,选择“运行”。 发生什么情况? 为什么?

    let myCar2 = new Car('Galaxy Motors', 'red', 3);
    

    哪有三个门的车,这一步赋值,按照我们的逻辑会报错,但是程序不会

  3. 尽管向 doors 传递了一个奇数,但它在编译和运行时没有出现错误,因为在 constructor 中没有发生数据验证。 尝试将 doors 的值设置为另一个奇数(例如 myCar2.doors = 5)并对其进行测试。 按照我们期望这应会调用 set 块并引发错误。所以 如果要在初始化 Car 对象时执行此验证步骤,则应向 constructor 添加验证检查。

    constructor(make: string, color: string, doors = 4) {
        this._make = make;
        this._color = color;
        if ((doors % 2) === 0) {
            this._doors = doors;
        } else {
            throw new Error('Doors must be an even number');
        }
    }
    
  4. 通过从对象初始化中省略可选参数 doors,对其进行测试。

    let myCar3 = new Car('Galaxy Motors', 'gray');
    console.log(myCar3.doors);  // returns 4, the default value
    

    因为我们在构造方法已经是设置了默认值,所以省略可选参数doors 也没问题,默认就是4

  5. 通过将返回值发送到控制台来测试方法。

    console.log(myCar1.accelerate(35));
    console.log(myCar1.brake());
    console.log(myCar1.turn('right'));
    

访问修饰符

默认情况下,所有类成员均为 public。 这意味着可以从包含类的外部访问它们。 在前面返回 Car 类的两个成员 _color(在类中定义的属性)和 color(在 constructor 中定义的参数)时,可以看到这样一个示例。有时我们希望同时提供对这两个成员的访问,但通常希望通过只允许通过 get 或 set 访问器访问来控制对属性中包含的原始数据的访问。

还可以控制对方法函数的访问。 例如,Car 类包含一个名为 worker 的函数,该函数只通过类中的其他方法函数调用。 直接从类的外部调用此函数可能会导致意外结果。

在 TypeScript 中,可以通过在成员名称前添加 public、private 或 protected 关键字来控制类成员的可见性。

好家伙 不能跟说跟java一模一样,也可以说是十分相似了

在 TypeScript 中,可以通过在成员名称前添加 public、private 或 protected 关键字来控制类成员的可见性。

访问修饰符说明
public如果不指定访问修饰符,则默认为 public。 还可以使用 public 关键字显式地将成员设置为 public。
private如果使用 private 关键字修改成员,则不能从其包含类的外部访问该成员。
protectedprotected 修饰符的作用与 private 修饰符非常类似,但也可以在派生类中访问声明 protected 的成员。

TypeScript 是一种结构化类型系统。 比较两个不同类型时,无论它们来自何处,如果所有成员的类型都是兼容的,则可以认为类型本身是兼容的。 但是,在比较具有 private 和 protected 成员的类型时,这些类型的处理方式不同。 对于两个被视为兼容的类型,如果其中一个类型具有 private 成员,则另一个必须具有源自同一声明的 private 成员。 这同样适用于 protected 成员。

此外,还可以通过使用 readonly 修饰符将属性设置为 readonly。 readonly 属性只能在其声明时或在 constructor 中初始化时设置。

将访问修饰符应用于类

没有设置属性私有以前:

image.png

定义静态属性

目前定义的类的属性和方法是实例属性,这意味着它们将在类对象的每个实例中实例化和调用。 还有另一种类型的属性称为静态属性。 静态属性和方法由类的所有实例共享。

若要将属性设置为静态,请在属性或方法名称前使用 static 关键字。

可以将一个新的 static 属性添加到名为 numberOfCars 的 Car 类,该类存储 Car 类实例化的次数,并将其初始值设置为 0。 然后,在构造函数中,将计数递增 1。

你的代码类似于这样:

TypeScript复制

class Car {
    // Properties
    private static numberOfCars: number = 0;  // New static property
    private _make: string;
    private _color: string;
    private _doors: number;

    // Constructor
    constructor(make: string, color: string, doors = 4) {
        this._make = make;
        this._color = color;
        this._doors = doors;
        Car.numberOfCars++; // Increments the value of the static property
    }
    // ...
}

请注意,在访问静态属性时,请使用语法 className.propertyName 而不是 this.。

还可以定义静态方法。 可以调用 getNumberOfCars 方法返回 numberOfCars 的值。

public static getNumberOfCars(): number {
    return Car.numberOfCars;
}

照常实例化 Car 类,然后使用语法 Car.getCars() 返回实例数。

// Instantiate the Car object with all parameters
let myCar1 = new Car('Cool Car Company', 'blue', 2);
// Instantiates the Car object with all parameters
let myCar2 = new Car('Galaxy Motors', 'blue', 2);
// Returns 2
console.log(Car.getNumberOfCars());

使用继承扩展类

通过继承,可以建立关系并生成对象组合中类的层次结构

使用继承的一些原因包括:

  • 代码可重用性。 可以一次性开发,并在许多地方重用。 此外,这有助于避免在代码中出现冗余。
  • 可以使用一个基类派生层次结构中的任意数量的子类。 例如,Car 层次结构中的子类还可以包括 SUV 类或 Convertible 类。
  • 不必在许多具有类似功能的不同类中更改代码,只需在基类中更改一次即可。

例如,你可以 extend Car 类以创建一个名为 ElectricCar 的新类。 ElectricCar 类将继承 Car 类的属性和方法,但也可以有其自己的唯一属性和行为,例如 range 和 charge。 因此,通过扩展 Car 类,你可以创建新重用 Car 类中的代码的新类,然后在其上进行构建。Car 类包括属性品牌、颜色和车门以及方法加速、制动和转向。 当 ElectricCar 类扩展 Car 时,它包括汽车的所有属性和方法,以及一个名为“档位”的新属性和一个名为“充电”的新方法。 ElectricCar 是一个子类,该子类使用 extends 关键字从 Car 基类派生。 (基类也称为超类或父类。)因为 ElectricCar 扩展了 Car 的功能,所以你可以创建一个 ElectricCar 实例,该实例可以支持 accelerate、brake 和 turn。 如果需要对基类中的代码进行更改,只需在 Car 类中更改它,然后所有 Car 的子类就会继承这些更改。

重写方法

当派生类对于基类的某个成员函数具有不同定义时,将被视为要重写基函数。 当你在一个子类中创建一个与基类中的函数同名的函数,但它有不同的功能时,就会发生重写。

例如,假设电动汽车使用的制动系统不同于传统汽车(称为再生制动)。 因此,你可能希望使用专用于 ElectricCar 子类的方法来重写 Car 基类中的 brake 方法。

扩展类

  1. 在 Car 下,创建一个 extends Car 的名为 ElectricCar 的新类。

    跟java差不多

    class ElectricCar extends Car {
        // Properties unique to ElectricCar
    ​
        // Constructor
    ​
        // Accessors
    ​
        // Methods
    ​
    }
    
  2. 将 ElectricCar 类的唯一的属性 _range 声明为 number 类型的 private 属性。

    // Properties
    private _range: number;
    
  3. 子类 constructor 与基类 constructor 在某些方面有所不同。

    • 参数列表可以包含基类和子类的任何属性。 (和 TypeScript 中的所有参数列表一样,请记住,必需参数必须出现在可选参数之前。)
    • 在 constructor 正文中,你必须添加 super() 关键字以包括来自基类的参数。 super 关键字在运行时执行基类的 constructor。
    • 在引用子类中的属性时,super 关键字必须出现在对 this. 的任何引用之前。
  4. 为 ElectricCar 定义 constructor 类,包括基类的 _make、_color 和 _doors 属性以及子类的 _range 属性。 在 constructor 中,将参数 doors 的默认值设置为 2。

    // Constructor
    constructor(make: string, color: string, range: number, doors = 2) {
        super(make, color, doors);
        this._range = range;
    }
    
  5. 为 range 参数定义 get 和 set 访问器。

    // Accessors
    get range() {
        return this._range;
    }
    set range(range) {
        this._range = range;
    }
    
  6. 输入以下 charge 方法,将消息返回到控制台。 此方法包括对在 Car 类中定义的 worker 函数的调用。 但它引发了错误“属性“worker”是私有的,只能在类“Car”中访问”。 你知道如何修正这个问题吗?

    // Methods
    charge() {
        console.log(this.worker() + " is charging.")
    }
    
  7. 在 Car 类中,将 worker 函数的访问修饰符从 private 更改为 protected。 这允许 Car 类的子类使用该函数,同时对可访问从类中实例化的对象的成员保持隐藏。 charge 方法中的错误现在应已解决。

  8. 测试新的 ElectricCar 类,验证它是否按预期工作。

    let spark = new ElectricCar('Spark Motors','silver', 124, 2);
    let eCar = new ElectricCar('Electric Car Co.', 'black', 263);
    console.log(eCar.doors);         // returns the default, 2
    spark.charge();                  // returns "Spark Motors is charging"
    
  9. 在 ElectricCar 类中定义新的 brake 方法,该方法具有不同的实现细节。 请注意,brake 方法的参数签名和返回类型必须与 Car 类中的 brake 方法相同。

    // Overrides the brake method of the Car class
    brake(): string {
        return `${this.worker()}  is braking with the regenerative braking system.`
    }
    
  10. 测试新方法并验证它是否按预期工作。

    console.log(spark.brake());  // returns "Spark Motors is braking with the regenerative braking system"
    

声明一个接口以确保类按规范

接口就是约定的规范.在 Typescript 中,可以使用接口来建立描述对象的必需属性及其类型的“代码协定”。 因此,你可以使用接口来确保类实例形状。 类声明可以引用其 implements 子句中的一个或多个接口来验证它们是否提供接口的实现。

  1. 声明一个 Vehicle 接口,该接口描述 Car 类的属性和方法。

    interface Vehicle {
        make: string;
        color: string;
        doors: number;
        accelerate(speed: number): string;
        brake(): string;
        turn(direction: 'left' | 'right'): string;
    }
    
  2. 请注意,该接口包含构造函数的参数,而不是属性。 请尝试包括其中一个私有属性(例如 _make: string)。 TypeScript 将引发错误,因为接口只能描述类的公共面,不能包括私有成员。 这会阻止你使用它们来检查类实例的私有端是否也具有正确类型。

  3. 现在可以在 Car 类中实现 Vehicle 接口。 当你生成类的详细信息时,TypeScript 将确保类遵循接口中所述的代码协定。

    class Car implements Vehicle {
        // ...
    }
    

TypeScript 提供几个关键方法来定义对象结构 - 类和接口

何时使用接口

接口是一种 TypeScript 设计时构造。 由于 JavaScript 没有接口概念,因此当 TypeScript 转译为 JavaScript 时,它们就会被删除。 这意味着它们完全是无足轻重的,没有在生成的文件中占用空间,对将要执行的代码不会产生负面影响。

不同于其他编程语言(其中接口只能与类一起使用),TypeScript 允许使用接口来定义数据结构,而无需类。 可以使用接口来定义函数的参数对象,定义各种框架属性的结构,并定义对象在远程服务或 API 中的外观。

例如,如果相关数据是用来存储关于狗的信息,那么可以创建如下所示的接口:

interface Dog {
    id?: number;
    name: string;
    age: number;
    description: string;
}

此接口可以在客户端和服务器代码的共享模块中使用,确保两端的数据结构是相同的。 在客户端上,你可能会有从定义的服务器 API 检索狗的代码,如下所示:

async loadDog(id: number): Dog {
    return await (await fetch('demoUrl')).json() as Dog;
}

通过使用接口,loadDog 可让 TypeScript 知道对象的结构。 不需要创建类来确保这一点。

何时使用类

任何编程语言中的接口和类之间的主要区别在于,类允许你定义实现的详细信息。 接口仅定义数据的结构。 类允许你定义方法、字段和属性。 类还提供了模板化对象的方法,进而定义默认值。

返回到以上示例,在服务器上,你可能想要添加代码以将狗加载或保存到数据库中。 在数据库中管理数据的一种常用方法是使用称为“活动记录模式”的方法,这意味着对象本身具有 save、load 和类似的方法。 我们可以使用上面定义的 Dog 接口,以确保具有相同的属性和结构,同时添加执行操作所需的代码。

class DogRecord implements Dog {
    id: number;
    name: string;
    age: number;
    description: string;
​
    constructor({name, age, description, id = 0}: Dog) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.description = description;
    }
​
    static load(id: number): DogRecord {
        // code to load dog from database
        return dog;
    }
​
    save() {
        // code to save dog to database
    }
}

总结

当你继续使用 TypeScript 时,会发现许多新的实例,尤其是接口,会让你的开发工作更加轻松。 关于接口,TypeScript 的一个关键特性是不需要类。 这允许你在需要定义数据结构的时候使用它们,而无需创建完整的类实现。