TypeScript进阶

TypeScript进阶

函数重载

函数重载(Function Overloading)是面向对象编程中的一个概念,它允许在同一个作用域内存在多个同名函数,但这些函数的参数列表(包括参数的数量、类型或顺序)必须不同。通过函数重载,可以根据不同的参数类型或数量调用到不同版本的函数,从而实现相同函数名下的不同功能。

然而,值得注意的是,TypeScript 和 JavaScript(ES6 之前的版本)原生并不直接支持函数重载,因为它们是基于原型的动态类型语言,并不具备静态类型语言(如 C++、Java)中那样的编译时类型检查机制。但是,TypeScript 通过类型注解和接口等特性,提供了一种模拟函数重载的语法糖。

在 TypeScript 中,你可以通过为同一个函数提供多个函数签名(Function Signatures)来模拟函数重载。这些函数签名定义了函数的参数类型和数量,但不包含函数体。然后,你提供一个函数实现,该实现与这些函数签名之一兼容(通常是与最通用的一个兼容)。

示例

// 函数重载签名  
function add(a: number, b: number): number;  
function add(a: string, b: string): string;  
  
// 函数实现  
function add(a: any, b: any): any {  
    if (typeof a === 'number' && typeof b === 'number') {  
        return a + b;  
    }  
    if (typeof a === 'string' && typeof b === 'string') {  
        return a + b;  
    }  
    throw new Error('Invalid arguments');  
}  
  
// 使用  
console.log(add(1, 2)); // 输出: 3  
console.log(add('hello', 'world')); // 输出: helloworld

在这个例子中,add 函数有两个重载签名:一个接受两个 number 类型的参数并返回一个 number,另一个接受两个 string 类型的参数并返回一个 string。然后,我们提供了一个兼容这两个签名的函数实现,该函数通过检查参数类型来决定执行哪种操作。

需要注意的是,虽然 TypeScript 提供了这种模拟函数重载的语法,但实际上在编译后的 JavaScript 代码中,这种重载并不存在。JavaScript 运行时并不区分这些重载签名,而是直接根据函数实现来处理调用。因此,这种重载更多的是在 TypeScript 编译时进行类型检查,以保证类型安全。

在TypeScript中,类可以包含多种特性,如封装、继承、多态、访问修饰符(如publicprivateprotected)、静态成员、抽象类以及readonly属性等。

首先,我们定义一个基础的Person类,然后创建一个继承自PersonEmployee类来展示继承和多态。同时,我们将在Person类中使用readonly属性来确保某个属性在创建对象后不可被修改。

// 定义一个抽象基类 Person,包含 readonly 属性  
abstract class Person {  
    protected readonly id: number; // readonly 属性,只能在构造函数中初始化  
    public name: string;  
  
    constructor(id: number, name: string) {  
        this.id = id; // 只能在构造函数中赋值  
        this.name = name;  
    }  
  
    // 抽象方法,子类必须实现  
    abstract greet(): void;  
  
    // 静态方法,属于类本身  
    static createPerson(id: number, name: string): Person {  
        // 这里假设有一个具体的子类实现,例如 Employee  
        return new Employee(id, name);  
    }  
}  
  
// Employee 类继承自 Person 类  
class Employee extends Person {  
    private department: string;  
  
    constructor(id: number, name: string, department: string) {  
        super(id, name); // 调用父类的构造函数  
        this.department = department;  
    }  
  
    // 实现父类的抽象方法  
    greet(): void {  
        console.log(`Hello, my name is ${this.name} and I work in ${this.department}.`);  
    }  
  
    // 添加一个新的方法  
    changeDepartment(newDepartment: string): void {  
        this.department = newDepartment;  
    }  
}  
  
// 使用类  
const emp1 = Employee.createPerson(1, 'Alice', 'IT'); // 注意:这里我们假设 createPerson 返回一个 Employee 实例  
// TypeScript 编译器会报错,因为 id 是 readonly 的,但这里只是示例说明  
// 实际上,createPerson 方法应该直接返回 new Employee(...)  
  
// 正确的使用方式  
const emp2 = new Employee(2, 'Bob', 'HR');  
emp2.greet(); // 输出: Hello, my name is Bob and I work in HR.  
  
// 尝试修改 readonly 属性(这会编译失败)  
// emp2.id = 3; // TypeScript 编译错误:Cannot assign to 'id' because it is a read-only property.  
  
// 修改其他属性  
emp2.name = 'Robert';  
emp2.greet(); // 输出: Hello, my name is Robert and I work in HR.  
  
// 调用静态方法(虽然在这个例子中我们没有直接用到它)  
// let person: Person = Person.createPerson(3, 'Charlie');  
// 但注意,因为 Person 是抽象类,所以不能直接实例化,除非 createPerson 被具体实现为返回某个非抽象子类的实例。

注意:在上面的代码中,我使用了abstract关键字来定义一个抽象基类Person,这意味着你不能直接实例化Person类。同时,我添加了一个readonly属性id,它只能在构造函数中被初始化,之后不能被修改。这展示了readonly属性的特点。

另外,我注意到Employee.createPerson的假设用法可能会导致混淆,因为通常我们不会让静态方法返回抽象的基类实例。在实际情况中,你可能会有一个具体的工厂函数或方法来根据需要返回Employee或其他Person子类的实例。在上面的代码中,我保留了static createPerson方法以展示静态成员的用法,但请注意其实现可能需要调整以符合实际情况。

存取器

在TypeScript中,存取器(Accessors)允许你拦截对对象属性的访问和修改,从而执行一些自定义操作。存取器包括getter和setter。Getter用于返回属性值,而setter用于在属性值被修改时执行代码。

下面是一个包含存取器的TypeScript类示例,该类同时展示了类的其他特点,如封装、继承和多态(尽管在这个简单的例子中多态性可能不太明显)。此外,我们还将包含一个readonly属性,但请注意readonly属性通常不会与setter一起使用,因为readonly意味着该属性不能被重新赋值。不过,我们可以通过getter来模拟一个只读的属性,同时在内部维护一个私有变量。

class Person {  
    private _name: string; // 私有变量,用于存储name的值  
  
    // readonly属性通常不会有setter,但我们可以使用getter来模拟它  
    get name(): string {  
        return this._name;  
    }  
  
    // 构造函数,初始化_name  
    constructor(name: string) {  
        this._name = name;  
    }  
  
    // setter,允许修改_name的值,但在这个例子中我们不会为name设置setter  
    // 如果需要,可以取消注释以下代码来允许修改name(但这将违反readonly的意图)  
    /*  
    set name(value: string) {  
        this._name = value;  
    }  
    */  
  
    // 一个普通的方法  
    greet(): void {  
        console.log(`Hello, my name is ${this.name}.`);  
    }  
  
    // 静态方法  
    static sayHello(name: string): void {  
        console.log(`Hello, ${name}!`);  
    }  
}  
  
// 继承自Person的类  
class Employee extends Person {  
    private department: string;  
  
    constructor(name: string, department: string) {  
        super(name); // 调用父类的构造函数  
        this.department = department;  
    }  
  
    // 重写greet方法以包含部门信息  
    greet(): void {  
        console.log(`Hello, my name is ${this.name} and I work in ${this.department}.`);  
    }  
  
    // Employee特有的方法  
    changeDepartment(newDepartment: string): void {  
        this.department = newDepartment;  
    }  
}  
  
// 使用类  
const emp = new Employee('Alice', 'IT');  
emp.greet(); // 输出: Hello, my name is Alice and I work in IT.  
  
// 尝试修改name属性(这将失败,因为没有setter)  
// emp.name = 'Bob'; // TypeScript 编译错误:Property 'name' is missing in type 'Employee' but required in type '{ name: string; }'.  
// 注意:上面的错误实际上是因为TypeScript的严格模式,但即使没有严格模式,如果我们没有为name提供setter,尝试赋值也会导致运行时错误。  
  
// 调用静态方法  
Person.sayHello('World'); // 输出: Hello, World!

在这个例子中,Person类有一个私有属性_name和一个公开的getter来访问它,但没有setter(被注释掉了),从而模拟了一个readonly属性的行为。Employee类继承自Person类,并重写了greet方法来包含部门信息。我们还展示了如何调用静态方法。

请注意,虽然我们在Person类中模拟了一个readonlyname属性,但TypeScript的readonly关键字实际上是在类型层面强制属性不可变的。在这个例子中,我们没有直接使用readonly关键字,而是通过不提供setter来模拟这种行为。如果你确实想要一个真正的readonly属性,你应该在属性声明中使用readonly关键字,并且不要为它提供setter。但是,由于readonly属性不能在构造函数之外被修改,因此你通常不需要为它们提供setter。

接口类

在TypeScript中,并没有直接称为“接口类”的概念。不过,你可能是在谈论接口(Interfaces)以及它们如何与类(Classes)一起工作。接口在TypeScript中是一个强大的特性,它允许你为对象的形状(即对象具有哪些属性以及这些属性的类型)定义一个契约。类可以实现这些接口,从而确保它们遵循接口定义的契约。

接口定义了一组属性的类型,但不实现它们。类可以实现接口,这意味着类必须提供接口中声明的所有属性和方法的具体实现。如果类没有实现接口中声明的所有内容,TypeScript编译器将会报错。

这里有一个关于如何在TypeScript中使用接口和类的简单示例:

// 定义一个接口IPerson  
interface IPerson {  
    name: string;  
    age: number;  
    greet(): void; // 注意这里我们声明了一个方法签名,但没有实现它  
}  
  
// 一个实现了IPerson接口的类Person  
class Person implements IPerson {  
    name: string;  
    age: number;  
  
    constructor(name: string, age: number) {  
        this.name = name;  
        this.age = age;  
    }  
  
    // 实现greet方法  
    greet(): void {  
        console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);  
    }  
}  
  
// 使用Person类  
const person = new Person('Alice', 30);  
person.greet(); // 输出: Hello, my name is Alice and I am 30 years old.  
  
// 尝试创建一个不满足IPerson接口的对象(这将失败,但TypeScript的类型检查会在编译时捕获这个问题)  
// const invalidPerson: IPerson = { name: 'Bob' }; // TypeScript 编译错误:Type '{ name: string; }' is missing the following properties from type 'IPerson': age, greet  
  
// 但是,如果你只想要一个简单的对象形状,而不需要类的继承或其他面向对象的特性,你可以直接使用接口来类型注解对象字面量  
const anotherPerson: IPerson = {  
    name: 'Charlie',  
    age: 25,  
    greet: function() {  
        console.log(`Hi, I'm ${this.name} and I'm ${this.age} years old.`);  
    }  
};  
anotherPerson.greet(); // 输出: Hi, I'm Charlie and I'm 25 years old.

在这个例子中,IPerson是一个接口,它定义了nameagegreet方法。Person类实现了IPerson接口,这意味着它必须有一个名为name的字符串属性、一个名为age的数字属性,以及一个名为greet的方法,该方法没有参数且没有返回值。然后,我们创建了一个Person类的实例并调用了它的greet方法。最后,我们还展示了如何使用接口来类型注解一个对象字面量,即使这个对象字面量不是通过类创建的。

请注意,接口本身并不包含实现(即它们不包含方法体或属性值的初始化),它们只是定义了对象的形状。类或其他对象字面量必须提供接口中声明的所有属性和方法的实现。

泛型类

在TypeScript中,泛型类(Generic Classes)允许你在创建类的时候不指定具体的类型,而是在使用类的时候(即实例化对象的时候)指定类型。这样做的好处是你可以编写灵活、可复用的代码,这些代码可以处理多种数据类型。

泛型类通过在类名后面添加<T>(其中T是一个类型参数,你可以根据需要使用不同的名称)来定义。在类的内部,你可以使用T作为类型注解,以表示在实例化类时指定的具体类型。

下面是一个泛型类的简单示例:

// 定义一个泛型类 Box  
class Box<T> {  
    private value: T;  
  
    // 构造函数,接收一个类型为T的参数  
    constructor(value: T) {  
        this.value = value;  
    }  
  
    // 一个方法,返回Box中存储的值  
    get(): T {  
        return this.value;  
    }  
  
    // 一个方法,设置Box中存储的值  
    set(newValue: T): void {  
        this.value = newValue;  
    }  
}  
  
// 使用Box类存储字符串  
let stringBox = new Box<string>("Hello, TypeScript!");  
console.log(stringBox.get()); // 输出: Hello, TypeScript!  
  
// 使用Box类存储数字  
let numberBox = new Box<number>(42);  
console.log(numberBox.get()); // 输出: 42  
  
// 如果在实例化时没有指定类型参数,TypeScript 会尝试根据提供的值来推断类型  
let booleanBox = new Box(true); // TypeScript 推断出 T 为 boolean  
console.log(booleanBox.get()); // 输出: true

在这个例子中,Box类是一个泛型类,它有一个类型参数T。这个T被用作value属性的类型注解,以及getset方法的返回类型和参数类型。在实例化Box类时,我们可以指定T的具体类型(如stringnumberboolean),这样Box类就可以存储并返回相应类型的值了。

泛型类不仅限于只有一个类型参数,你还可以定义多个类型参数,比如class Box<T, U>,然后在类中使用TU作为不同类型注解的标识符。

泛型类的使用大大提高了代码的复用性和灵活性,使得你可以编写出更加通用和强大的TypeScript代码。

  • 12
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值