43、TypeScript 编程:类、接口与泛型的深入解析

TypeScript 编程:类、接口与泛型的深入解析

1. 类的基础概念

在编程领域,如果你有 Java 或 C# 的开发经验,那么对类和继承的经典概念应该并不陌生。在这些语言中,类的定义会作为一个独立的实体(类似于蓝图)加载到内存中,并由该类的所有实例共享。若一个类继承自另一个类,对象会使用两个类组合后的蓝图进行实例化。

TypeScript 作为 JavaScript 的超集,而 JavaScript 仅支持原型式继承,即通过将一个对象附加到另一个对象的原型属性上来创建继承层次结构,这种继承是动态创建的。在 TypeScript 里, class 关键字是一种语法糖,用于简化编码。最终,类会被转译为具有原型式继承的 JavaScript 对象。

下面是一个简单的 Person 类示例,它包含四个属性,用于存储名字、姓氏、年龄和社会安全号码:

class Person {
    public firstName: string;
    public lastName: string;
    public age: number;
    private _ssn: string;
    constructor(firstName: string, lastName: string, age: number, ssn: string) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
        this._ssn = ssn;
    }
}
const p = new Person("John", "Smith", 29, "123-90-4567");

TypeScript 还支持类构造函数,可在实例化对象时初始化对象变量,构造函数在对象创建时仅被调用一次。

2. 访问修饰符

JavaScript 没有直接声明变量或方法为私有(对外部代码隐藏)的方式。若要隐藏对象的属性或方法,需要创建一个闭包,既不将该属性附加到 this 变量,也不在闭包的返回语句中返回它。

TypeScript 提供了 public protected private 关键字,用于在开发阶段控制对对象成员的访问:
- public :默认情况下,所有类成员都具有公共访问权限,可从类外部访问。
- protected :声明为 protected 的成员在类及其子类中可见。
- private :声明为 private 的成员仅在类内部可见。

以下是使用 private 关键字隐藏 _ssn 属性的示例:

class Person {
    public firstName: string;
    public lastName: string;
    public age: number;
    private _ssn: string;
    constructor(firstName: string, lastName: string, age: number, ssn: string) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
        this._ssn = ssn;
    }
}
const p = new Person("John", "Smith", 29, "123-90-4567");
// 尝试从外部访问私有属性会导致编译错误
// console.log("Last name: " + p.lastName + " SSN: " + p._ssn); 

需要注意的是, private 关键字仅在 TypeScript 代码中起作用,生成的 JavaScript 代码会将类的所有属性和方法视为公共的。

TypeScript 还允许在构造函数参数中提供访问修饰符,如下所示:

class Person {
    constructor(public firstName: string, public lastName: string, public age: number, private _ssn: string) {}
}
const p = new Person("John", "Smith", 29, "123-90-4567");

这种方式下,TypeScript 编译器会根据构造函数参数创建并保留类属性,无需显式声明和初始化。

3. 方法

在类中声明的函数称为方法。在 JavaScript 中,需要在对象的原型上声明方法;而在 TypeScript 中,可像在其他面向对象语言中一样,通过指定名称后跟括号和花括号来声明方法。

以下是一个带有 doSomething 方法的 MyClass 类示例:

class MyClass {
    doSomething(howManyTimes: number): void {
        // 在这里执行操作
    }
}
const mc = new MyClass();
mc.doSomething(5);

类成员可分为静态成员和实例成员:
- 实例成员:需要先创建类的实例,然后通过引用变量访问。
- 静态成员:使用 static 关键字声明,其值在类的所有实例之间共享,无需创建实例即可访问。

class MyClass {
    static doSomething(howManyTimes: number): void {
        // 在这里执行操作
    }
}
MyClass.doSomething(5);

若在类的一个方法中调用另一个类方法,应使用类名而非 this 关键字。

4. 继承

JavaScript 支持基于对象的原型式继承,即在运行时将一个对象指定为另一个对象的原型。TypeScript 提供了 extends 关键字用于类的继承,类似于 ES6 和其他面向对象语言。在转译为 JavaScript 时,生成的代码会使用原型式继承的语法。

以下是一个 Employee 类继承自 Person 类的示例:

class Employee extends Person {
    department: string;
    constructor(firstName: string, lastName: string, age: number, _ssn: string, department: string) {
        super(firstName, lastName, age, _ssn);
        this.department = department;
    }
}

在子类中调用父类的方法时,可直接使用方法名;若要显式调用父类的方法,则需使用 super 关键字。 super 关键字有两种用法:
- 在派生类的构造函数中作为方法调用。
- 用于显式调用父类的方法,常用于方法重写。

doSomething() {
    super.doSomething();
    // 在这里添加更多功能
}
5. 接口

JavaScript 不支持接口,而在其他面向对象语言中,接口用于引入 API 必须遵守的代码契约。TypeScript 包含 interface implements 关键字来支持接口,但接口不会被转译为 JavaScript 代码,仅用于在开发过程中避免使用错误的类型。

在 TypeScript 中,使用接口有两个主要原因:
- 声明一个接口,定义包含多个属性的自定义类型,然后声明一个接受该类型对象作为参数的方法,编译器会检查传入的对象是否包含接口中声明的所有属性。

interface IPerson {
    firstName: string;
    lastName: string;
    age: number;
    ssn?: string;
}
class Person {
    constructor(public config: IPerson) {}
}
let aPerson: IPerson = {
    firstName: "John",
    lastName: "Smith",
    age: 29
}
let p = new Person(aPerson);
console.log("Last name: " + p.config.lastName );
  • 声明一个包含抽象(未实现)方法的接口,当类声明实现该接口时,必须为所有抽象方法提供实现。
interface IPayable {
    increasePay(percent: number): boolean
}
class Employee implements IPayable {
    increasePay(percent: number): boolean {
        console.log(`Increasing salary by ${percent}`);
        return true;
    }
}
class Contractor implements IPayable {
    increaseCap: number = 20;
    increasePay(percent: number): boolean {
        if (percent < this.increaseCap) {
            console.log(`Increasing hourly rate by ${percent}`);
            return true;
        } else {
            console.log(`Sorry, the increase cap for contractors is ${this.increaseCap}`);
            return false;
        }
    }
}
6. 泛型

TypeScript 支持参数化类型,即泛型,可在多种场景中使用。例如,可创建一个能接受任何类型值的函数,但在调用时可显式指定具体类型;也可指定数组中允许的对象类型,若尝试添加不同类型的对象,TypeScript 编译器会报错。

以下是使用泛型数组的示例:

class Person {
    name: string;
}
class Employee extends Person {
    department: number;
}
class Animal {
    breed: string;
}
let workers: Array<Person> = [];
workers[0] = new Person();
workers[1] = new Employee();
// workers[2] = new Animal(); // 编译时错误

TypeScript 使用结构类型系统,即如果两个不同类型包含相同的成员,则认为它们是兼容的。与 Java 和 C# 的名义类型系统不同,名义类型系统根据类型名称检查类型,而结构类型系统根据类型结构检查。

你还可以创建支持泛型的自定义类和函数,以下是一个使用泛型的 Comparator 接口示例:

interface Comparator<T> {
    compareTo(value: T): number;
}
class Rectangle implements Comparator<Rectangle> {
    constructor(private width: number, private height: number) {};
    compareTo(value: Rectangle): number {
        if (this.width * this.height >= value.width * value.height) {
            return 1;
        } else {
            return -1;
        }
    }
}
let rect1: Rectangle = new Rectangle(2, 5);
let rect2: Rectangle = new Rectangle(2, 3);
rect1.compareTo(rect2) === 1? console.log("rect1 is bigger") : console.log("rect1 is smaller");

综上所述,TypeScript 的类、接口和泛型为开发者提供了强大的工具,能够提高代码的可维护性、可扩展性和类型安全性。通过合理运用这些特性,可以编写出更加健壮和高效的代码。

下面是一个简单的流程图,展示了 TypeScript 类的创建和使用过程:

graph TD;
    A[定义类] --> B[实例化对象];
    B --> C[访问成员];
    C --> D{成员类型};
    D -- 实例成员 --> E[通过实例访问];
    D -- 静态成员 --> F[通过类名访问];

同时,我们可以用表格总结一下访问修饰符的特点:
| 访问修饰符 | 含义 | 可见范围 |
| ---- | ---- | ---- |
| public | 公共访问权限 | 类内外均可访问 |
| protected | 受保护的访问权限 | 类及其子类中可见 |
| private | 私有访问权限 | 仅在类内部可见 |

TypeScript 编程:类、接口与泛型的深入解析

7. 泛型的更多应用场景与注意事项

泛型在 TypeScript 中有着广泛的应用场景,除了前面提到的数组和自定义接口,在函数、类等方面也能发挥重要作用。

7.1 泛型函数

泛型函数允许函数处理多种类型的数据,而不需要为每种类型都编写一个特定的函数。以下是一个简单的泛型函数示例:

function identity<T>(arg: T): T {
    return arg;
}
let output1 = identity<string>("myString");
let output2 = identity<number>(100);

在这个例子中, identity 函数使用泛型类型 T 作为参数和返回值的类型。调用时,可以显式指定类型参数,也可以让 TypeScript 自动推断。

7.2 泛型类

泛型类可以在类的层面上使用泛型类型,使得类可以处理不同类型的数据。以下是一个泛型类的示例:

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) { return x + y; };

在这个例子中, GenericNumber 类使用泛型类型 T 来定义 zeroValue 属性和 add 方法的参数及返回值类型。

7.3 泛型约束

有时候,我们希望泛型类型满足某些特定的条件,这就需要使用泛型约束。以下是一个使用泛型约束的示例:

interface Lengthwise {
    length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);
    return arg;
}

在这个例子中,泛型类型 T 被约束为必须实现 Lengthwise 接口,即必须包含 length 属性。

8. 接口与类的结合使用

接口和类在 TypeScript 中可以很好地结合使用,通过接口定义类的行为和属性,使得代码更加规范和易于维护。

8.1 接口继承

接口可以继承其他接口,从而扩展接口的功能。以下是一个接口继承的示例:

interface Shape {
    color: string;
}
interface Square extends Shape {
    sideLength: number;
}
let square: Square = { color: "blue", sideLength: 10 };

在这个例子中, Square 接口继承自 Shape 接口,并添加了 sideLength 属性。

8.2 类实现多个接口

一个类可以实现多个接口,从而具备多个接口定义的行为。以下是一个类实现多个接口的示例:

interface Alarm {
    alert(): void;
}
interface Light {
    lightOn(): void;
    lightOff(): void;
}
class Car implements Alarm, Light {
    alert() {
        console.log("Car is alarming");
    }
    lightOn() {
        console.log("Car light is on");
    }
    lightOff() {
        console.log("Car light is off");
    }
}

在这个例子中, Car 类实现了 Alarm Light 两个接口,具备了报警和控制灯光的功能。

9. 类的高级特性

除了前面介绍的类的基本概念和继承,TypeScript 中的类还有一些高级特性,如抽象类和存取器。

9.1 抽象类

抽象类是一种不能被实例化的类,它可以包含抽象方法和具体方法。抽象方法是一种没有具体实现的方法,必须在子类中实现。以下是一个抽象类的示例:

abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log("Moving...");
    }
}
class Dog extends Animal {
    makeSound() {
        console.log("Woof!");
    }
}
let dog = new Dog();
dog.makeSound();
dog.move();

在这个例子中, Animal 是一个抽象类,包含抽象方法 makeSound 和具体方法 move Dog 类继承自 Animal 类,并实现了 makeSound 方法。

9.2 存取器

存取器允许我们对类的属性进行更精细的控制,通过 get set 方法来访问和修改属性。以下是一个存取器的示例:

class Employee {
    private _fullName: string;
    get fullName(): string {
        return this._fullName;
    }
    set fullName(newName: string) {
        if (newName && newName.length > 5) {
            this._fullName = newName;
        } else {
            console.log("Error: fullName must be at least 5 characters long");
        }
    }
}
let employee = new Employee();
employee.fullName = "John Doe";
console.log(employee.fullName);

在这个例子中, Employee 类使用存取器来控制 _fullName 属性的访问和修改。

10. 总结

通过对 TypeScript 中类、接口和泛型的深入学习,我们可以看到它们为开发者提供了强大的工具,能够提高代码的可维护性、可扩展性和类型安全性。

以下是一个总结表格,对比了 JavaScript 和 TypeScript 在类、接口和泛型方面的差异:
| 特性 | JavaScript | TypeScript |
| ---- | ---- | ---- |
| 类 | 仅支持原型式继承 | 支持经典的类和继承,有访问修饰符等特性 |
| 接口 | 不支持 | 支持接口,用于定义代码契约 |
| 泛型 | 不支持 | 支持泛型,提高代码的复用性和类型安全性 |

同时,我们可以用一个 mermaid 流程图来展示 TypeScript 代码的开发流程:

graph TD;
    A[定义接口和类] --> B[使用泛型];
    B --> C[实现继承和多态];
    C --> D[编译为 JavaScript];
    D --> E[运行代码];

在实际开发中,合理运用 TypeScript 的类、接口和泛型,可以编写出更加健壮、高效和易于维护的代码。开发者可以根据具体的需求,灵活选择合适的特性来构建应用程序。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值