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中,类可以包含多种特性,如封装、继承、多态、访问修饰符(如public
、private
、protected
)、静态成员、抽象类以及readonly
属性等。
首先,我们定义一个基础的Person
类,然后创建一个继承自Person
的Employee
类来展示继承和多态。同时,我们将在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
类中模拟了一个readonly
的name
属性,但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
是一个接口,它定义了name
、age
和greet
方法。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
属性的类型注解,以及get
和set
方法的返回类型和参数类型。在实例化Box
类时,我们可以指定T
的具体类型(如string
、number
或boolean
),这样Box
类就可以存储并返回相应类型的值了。
泛型类不仅限于只有一个类型参数,你还可以定义多个类型参数,比如class Box<T, U>
,然后在类中使用T
和U
作为不同类型注解的标识符。
泛型类的使用大大提高了代码的复用性和灵活性,使得你可以编写出更加通用和强大的TypeScript代码。