TypeScript
中的类是基于ES6中的类语法进行扩展的,类是一种面向对象的编程范式,它允许将属性和行为封装在一个单独的实体中,并通过类的实例来访问和操作这些属性和行为。
在TypeScript中,类由属性、方法和构造函数组成,可以使用箭头函数或lambda表达式来定义类方法。类可以继承自另一个类并实现接口。除此之外,TypeScript中的类支持访问修饰符(public、private和protected)和静态属性和方法。
下面详细介绍一下TypeScript中类的特性:
1. 类的定义
定义类的语法如下:
class ClassName {
// 类的属性
propertyName: type;
// 构造函数
constructor() {
// 构造函数的实现
}
// 类的方法
methodName() {
// 方法的实现
}
}
2. 类的实例化
使用new关键字来创建类的实例。例如:
let obj = new ClassName();
obj.methodName();
3. 类的属性
在TypeScript中,类的属性可以被定义为类的成员变量,并且可以赋予类型、访问修饰符和默认值等元素。
(1). 类的成员变量的类型
类的成员变量可以被定义为一个类型,这个类型可以是基本类型、自定义类型或其他复杂类型。
class Person {
name: string;
age: number;
gender: 'male' | 'female';
}
上述代码中,Person类的属性分别是字符串类型的name、数字类型的age和一个只能取’male’或’female’两个值的gender属性。
(2). 默认值
在TypeScript中,类的属性可以设置默认值,如果没有手动赋值则会使用默认值。
class Person {
name: string = 'Unknown';
age: number = 0;
gender: 'male' | 'female' = 'male';
}
上述代码中,如果没有手动为name、age和gender属性赋值,则使用默认值’Unknown’、0和’male’。
(3). 只读属性
在TypeScript中,类包含属性和方法,属性可以分为可读写和只读两种。只读属性指的是对象创建后不允许修改的属性,类中只读属性声明时需要使用关键字"readonly"。
以下是一个简单的只读属性的示例:
class Person{
readonly name: string;
constructor(name: string){
this.name = name;
}
}
const person = new Person("张三");
person.name = "李四"; // 编译时会报错,因为name是只读属性
上述示例中,类Person包含一个只读属性name, 构造函数中为该属性赋值,对象创建后该属性值不允许修改。
下面举几个例子说明一下只读属性的作用:
1.防止意外的重写
假设需要定义一个Point类,包含x和y两个坐标属性,它们在对象创建后不应该被修改,这时候使用只读属性可以防止程序员意外地重写这些属性:
class Point{
readonly x: number;
readonly y: number;
constructor(x: number, y: number){
this.x = x;
this.y = y;
}
}
const point = new Point(0, 0);
point.x = 1; // 编译时会报错,因为x是只读属性
上述示例中,只读属性x和y将在构造函数中初始化,对象创建后它们将不可修改,可以有效地避免由于意外错误导致的程序异常。
2.强制约束属性的只读性
通过只读属性,可以明确指定部分属性在对象创建后不可修改,从而便于程序员更好地理解和使用该类:
class User{
readonly id: number;
name: string;
constructor(id: number, name: string){
this.id = id;
this.name = name;
}
}
const user = new User(1, "张三");
user.id = 2; // 编译时会报错,因为id是只读属性
上述示例中,id属性被明确指定为只读属性,而name属性则可读可写。通过只读属性,可以更好地约束属性的使用方式,避免程序员将其设计为可修改的属性。
3.类似const声明变量的功能
使用只读属性类似于在变量声明时使用const关键字声明变量,只读属性的值在对象创建后固定不变,与const变量的值一样也是在声明时就需要确定。
class Circle{
readonly pi: number = 3.14;
readonly radius: number;
constructor(radius: number){
this.radius = radius;
}
get circumference(): number{
return 2 * this.pi * this.radius;
}
}
const circle = new Circle(10);
console.log(circle.circumference); // 输出31.4
circle.pi = 3; // 编译时会报错,因为pi是只读属性
上述示例中,通过只读属性pi保存圆周率,在Circumference方法中使用pi和radius计算圆的周长。只读属性pi和const变量具有类似的功能,它们的值在声明时需要确定,对象创建后不可修改。
总之,只读属性是TypeScript类语法的重要特性之一,它可以有效地约束属性的使用方式,避免程序员意外地修改、重写属性,以及更好地表达属性的只读性。
4. 构造器
TypeScript是JavaScript的一个超集,它提供了类和接口等面向对象的特性。类是TypeScript中封装数据(属性)和行为(方法)的一种方式,类的构造器负责初始化对象的状态,创建类的实例。下面将从多个角度介绍TypeScript中类的构造器。
(1). 构造器的基本语法
在TypeScript中,使用关键字class
定义类,使用constructor
定义构造器,如下所示:
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
在上面的代码中,Person
类包含name
和age
属性,而constructor
方法接受name
和age
两个参数,然后用它们来设置类中对应的属性。
(2). 构造器的重载
TypeScript支持方法和构造器的重载,在构造器重载中可以通过不同的参数类型和数量来区分不同的构造器,如下所示:
class Person {
name: string;
age?: number;
constructor(name: string);
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
在上面的代码中,构造器被重载了,如果传入一个参数,则会使用第一个构造器,而如果传入两个参数,则会使用第二个构造器。
(3). 构造器中的访问修饰符
在构造参数中加入访问修饰符可以在参数前增加修饰符public
、private
和protected
,用于指定属性的可访问性,如下所示:
class Person {
constructor(public name: string, private age: number) { }
}
在上面的代码中,constructor
中的name
被声明为public
,而age
被声明为private
,这意味着name
可以被类的其他代码使用和修改,而age
只能在类内部访问和修改。
???
什么是访问修饰符?
不着急,接着往下看,下面有详细介绍。嘿嘿
(4). 父类的构造器
在子类中,如果需要调用父类的构造器,可以使用super()
方法,如下所示:
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
}
class Dog extends Animal {
constructor(name: string, public breed: string) {
super(name); // 调用父类的构造器
}
}
在上面的代码中,Dog
类继承了Animal
类,并在子类的构造器中使用super(name)
调用了父类的构造器,因为子类中需要初始化与父类相同的属性。
(5). 抽象类的构造器
抽象类是一种特殊的类,它不能被实例化,只能被继承,抽象类的构造器也不能被实例化,但是可以被子类调用。如下所示:
abstract class Animal {
constructor(public name: string) { }
abstract eat(food: string): void;
}
class Dog extends Animal {
constructor(name: string, public breed: string) {
super(name);
}
eat(food: string): void {
console.log(`${this.name} is eating ${food}`);
}
}
在上面的代码中,Animal
类被声明为抽象类,其中构造器中的name
属性被声明为public
,而eat
方法则被声明为抽象方法,子类中必须实现它。在子类的构造器中使用super()
方法调用了父类的构造器。
抽象类也一样,在下面会有详细介绍,这里只是用来举个例子
5. 类的方法
TypeScript中的类可以看作是一种特殊的数据类型,由属性和方法组成。类的方法是指在类中定义的函数,可以实现该类的特定操作。
定义类方法时,需要注意参数列表和返回值类型声明
- 参数列表:方法可以接收任意数量的参数;
- 返回值类型声明:可以用void表示不返回任何值,也可以返回指定类型的值。
接下来我们通过几个例子来分析类方法:
(1). 实现类的行为
类方法可以用于实现类的行为,比如一个汽车类可以定义一个“加速”方法,一个手机类可以定义一个“拍照”方法,这些方法可以包含参数和返回值,以便满足实际应用。
例如定义一个Person类,包含一个greet方法用于打招呼:
class Person {
greet(name: string) {
console.log(`Hello ${name}!`);
}
}
const person = new Person();
person.greet('Tom'); // 输出 "Hello Tom!"
(2). 使用方法链式调用
因为方法可以返回this指针,所以可以使用方法链式调用的写法。
例如定义一个Animal类,包含一个eat方法和一个run方法,这两个方法返回自身的实例,可以通过链式调用来实现连续运动的效果:
class Animal {
eat() {
console.log('I am eating.');
return this;
}
run() {
console.log('I am running.');
return this;
}
}
const animal = new Animal();
animal
.eat()
.run()
.eat(); // 输出 "I am eating." 和 "I am running." 和 "I am eating."
(3). 通过继承扩展方法
类可以通过继承来扩展方法,派生类可以继承父类的方法,并添加自己的方法或重写父类的方法。
例如定义一个Bird类继承Animal类,添加一个fly方法:
class Bird extends Animal {
fly() {
console.log('I am flying.');
return this;
}
}
const bird = new Bird();
bird
.eat()
.run()
.fly(); // 输出 "I am eating." 和 "I am running." 和 "I am flying."
(4). 使用静态方法
静态方法是指在类上定义的方法,不需要通过实例才能调用。静态方法可以用于工具类等场景,不需要实例化该类就可以使用。
例如定义一个MathUtil工具类,包含一个静态的add方法用于两个数相加:
class MathUtil {
static add(a: number, b: number) {
return a + b;
}
}
console.log(MathUtil.add(1, 2)); // 输出 3
(5). 使用getter和setter
getter和setter是一种特殊的方法,用于获取和设置类的属性值。getter用于获取属性值,setter用于设置属性值。它们的语法与普通的方法有所不同,可以使用get和set关键字来定义。
例如定义一个Person类,包含一个age属性,用getter和setter来访问和修改该属性:
class Person {
private _age: number = 0;
get age() {
return this._age;
}
set age(value: number) {
if (value < 0) {
throw new Error('Invalid age.');
}
this._age = value;
}
}
const person = new Person();
person.age = 18; // 调用setter方法修改属性值
console.log(person.age); // 调用getter方法获取属性值,输出 18
总之,在定义类方法时需要注意可见性、参数和返回值类型声明等问题;在实例化后使用类方法时需要调用相关的方法,获取返回值或修改对象的属性值。同时,类方法还可以通过继承、静态方法、getter和setter等技巧来实现各种不同的需求。
6. 访问修饰符
TypeScript中的访问修饰符有public、private和protected,分别用于控制成员变量和成员函数在类的内部和外部的可见性。
- public修饰符:公共成员,可在类内部和外部访问。
- private修饰符:私有成员,只能在类内部访问。
- protected修饰符:受保护成员,只能在类内部和派生类中访问。
(1). public
public是默认的访问修改符,表示成员对所有代码可见。例如:
class Person {
public name: string;
public sayHello() {
console.log("Hello, " + this.name);
}
}
const person = new Person();
person.name = "Tom";
person.sayHello(); // Hello, Tom
在上面的示例中,name和sayHello()方法都是public的,可以被外部代码直接访问。
(2). private
TypeScript中的private关键字用于指定类中的成员或方法只能在该类内部进行访问和修改,即在类的外部范围内是不可见的。
- private修饰属性
在类中定义一个私有属性时,只有在该类内部的方法才能访问和修改该属性。
class Person {
private name: string;
constructor(name: string) {
this.name = name;
}
public sayHello(): void {
console.log(`Hello, my name is ${this.name}`);
}
}
const p = new Person("Tom");
p.name = "Jerry"; // error
p.sayHello(); // "Hello, my name is Tom"
在上面的例子中,我们在类的内部定义了一个私有属性name
,在构造函数中进行初始化。在其公共方法sayHello()
中,可以通过this.name
来访问和使用该属性。但是在类的外部,我们无法直接访问和修改该属性,如上例中的p.name = "Jerry"
就会报错。
- private修饰构造函数
在构造函数中将参数定义为私有属性,可以省略手动给属性赋值的过程。
class Person {
constructor(private name: string) {}
public sayHello(): void {
console.log(`Hello, my name is ${this.name}`);
}
}
const p = new Person("Tom");
p.name = "Jerry"; // error
p.sayHello(); // "Hello, my name is Tom"
在以上代码中,我们在构造函数的参数上加上了private
关键字,这意味着该参数将自动转换为类的私有属性。在类中,我们可以直接使用this.name
来访问和修改该属性。但在类的外部,依然不可见和不可操作。
- private修饰方法
如果我们不想将整个属性设置为私有的,也可以仅将其访问方法设置为私有,这意味着任何外部的调用者都无法直接访问该方法。
class Person {
private age: number;
constructor(age: number) {
this.age = age;
}
private getAge(): number {
return this.age;
}
public sayAge(): void {
console.log(`My age is ${this.getAge()}`);
}
}
const p = new Person(18);
p.getAge(); // error
p.sayAge(); // "My age is 18"
在上例中,我们在类中定义了一个私有方法getAge()
来返回属性age
的值,这意味着该方法只能在该类内部被调用。为了让外部访问类内部私有属性的方式更优雅,我们又定义了一个公共方法sayAge()
,它通过this.getAge()
来获取私有属性,并输出到控制台上。在类的外部,我们无法直接调用getAge()
方法,但是可以通过sayAge()
方法来获取属性的值。
- private修饰继承
在继承关系中,子类无法访问和修改父类的私有属性或方法。
class Animal {
private name: string;
constructor(name: string) {
this.name = name;
}
public sayHello(): void {
console.log(`Hello, I'm ${this.name}`);
}
}
class Dog extends Animal {
constructor(name: string) {
super(name);
}
public rename(newName: string): void {
this.name = newName; // error
}
}
const d = new Dog("Xiaohei");
d.sayHello(); // "Hello, I'm Xiaohei"
d.rename("Wangcai"); // error
在上例中,我们定义了一个父类Animal
,其中的属性和方法都被设置为私有。子类Dog
继承自Animal
,但是在Dog
内部,我们无法使用this.name
来获取或修改父类中的私有属性name
。因此,在调用d.rename("Wangcai")
时,就会被认为是非法操作,产生编译时错误。
private
关键字用于指定类中的成员或方法只能在该类的内部进行访问和修改。从属性、构造函数、方法和继承等多个角度来分别举例说明了使用private关键字的不同场景和用法。在实际开发中,我们通常会大量地使用private来保护类的内部数据,降低了代码的耦合性,提高了代码的健壮性和安全性。
(3). protected
protected与private类似,也表示只有在类中和子类中可见的成员。例如:
class Person {
protected name: string;
}
class Employee extends Person {
public sayHello() {
console.log("Hello, " + this.name);
}
}
const employee = new Employee();
employee.name = "Tom"; // Error: 'name' is protected
employee.sayHello(); // Hello, Tom
在上面的示例中,name是protected的,只能在Person及其子类内部使用。Employee类是Person的子类,所以它可以访问name。
通过访问修改符,我们可以控制类中成员的可见性和可操作性,从而提高代码的可维护性。
7. 类的继承
TypeScript是基于面向对象编程(OO)的基础上开发的,因此它支持类的继承,接口的实现,并且它也有类的成员重写的特性。下面我们将对这些内容进行详细介绍。
(1)、继承
在 TypeScript 中,可以通过关键字 extends
来实现继承,这使得一个类可以继承另一个类的成员属性和方法。继承有以下几个特点:
-
子类继承父类的属性和方法,并且可以拥有自己的属性和方法。
-
子类可以在继承父类的基础上进行扩展或重写父类的属性和方法。
-
子类可以访问父类的属性和方法,但父类不能访问子类的属性和方法。
举例说明:
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Dog extends Animal {
bark() {
console.log('Woof! Woof!');
}
move(distanceInMeters = 5) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
const dog = new Dog('Buddy');
dog.bark();
dog.move();
在这个例子中,Animal 类是基类,它有一个属性 name 和一个方法 move。 Dog 类继承了 Animal 类,它有一个新增的方法 bark,并且重写了基类的方法 move。我们也可以看到,Dog 类在创建实例时,需要传递一个 name 参数,并且调用基类的构造函数来初始化这个属性值。
(2)、implements
接口是 TypeScript 重要的特性之一,接口定义了一个类或对象需要遵循的规范。在 TypeScript 中,可以使用 implements 关键字来实现接口的实现。实现接口的类必须实现接口中定义的所有成员。
举例说明:
interface Runner {
move(distanceInMeters: number): void;
}
class Person implements Runner {
move(distanceInMeters: number) {
console.log(`Person moved ${distanceInMeters}m.`);
}
}
const person = new Person();
person.move(10);
在这个例子中,我们定义了一个 Runner 接口,里面有一个 move 方法。Person 类实现了 Runner 接口,因此要实现接口中定义的 move 方法。这个类创建了一个实例并且调用了 move 方法。
(3)、重写方法
在 TypeScript 中,子类可以重写父类的方法,这样可以在子类中实现特定的行为。通过使用 super 关键字,可以访问到父类的属性和方法。
举例说明:
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Snake extends Animal {
move(distanceInMeters = 5) {
console.log('Slithering...');
super.move(distanceInMeters);
}
}
const snake = new Snake('Python');
snake.move();
在这个例子中,Snake 类重写了父类 Animal 的 move 方法,它在子类中新增了一个 Slithering… 的输出,并且调用了 super.move(distanceInMeters) 来访问父类的 move 方法。
(4). 继承的执行顺序
在 TypeScript 中,类的继承遵循以下顺序:
- 构造函数是在实例化时调用的。
- 子类的构造函数必须先调用
super()
,这会调用父类的构造函数,并初始化父类的成员。 - 在调用
super()
后,子类才能访问父类的成员。 - 如果子类没有定义构造函数,那么它会继承父类的构造函数。
当一个子类继承一个父类,且子类中没有写构造函数时,在实例化子类时,会按照以下顺序执行:
- 调用子类的构造函数。
- 子类构造函数调用父类的构造函数。
- 在父类的构造函数中,如果用到了与子类同名的属性,那么该属性会按照父类的方式初始化。
- 父类构造函数返回后,子类继续执行自己的构造函数。
例如,假设有如下代码:
class Parent {
name: string;
constructor(name: string) {
console.log('Parent constructor');
this.name = name;
}
}
class Child extends Parent {
name: string;
}
const child = new Child('Alice');
console.log(child.name); // Output: 'Alice'
在实例化子类 Child
时,由于 Child
没有定义构造函数,因此会继承 Parent
的构造函数。执行顺序如下:
- 调用
Child
的构造函数。 - 由于
Child
没有定义构造函数,因此会继承Parent
的构造函数。 - 在
Parent
的构造函数中,会初始化this.name
属性。 Parent
构造函数返回后,Child
继续执行自己的构造函数。- 因为
Child
中没有定义构造函数,所以构造函数直接返回,此处并没有什么可执行的代码。
最后,输出 child.name
,它的值是 'Alice'
。这是因为虽然 Child
中也有一个名为 name
的属性,但在执行父类构造函数时,父类初始化的 name
属性覆盖了子类的属性。
(4)、注意事项
在 TypeScript 中,类的继承、接口的实现和方法的重写都有相应的注意事项:
-
类的继承中,只能继承单个类,不能继承多个类。
-
通过使用 implements 关键字来实现接口的实现,这个类必须实现接口中定义的所有成员,否则会报错。
-
在父类和子类中,如果有同名的方法或属性,子类中的成员会覆盖父类中的成员。为了避免这种情况,在重写方法时,必须调用 super 关键字来访问父类中的成员。
TypeScript 提供了类的继承、接口的实现和方法的重写等 OO 特性,这些特性使得代码更加清晰、结构更加规范、维护更加便捷。需要注意的是,这些特性都有自己的使用场景和使用限制,开发者需要根据实际需求谨慎使用。
8. 抽象类(Abstract)
抽象类是一个不能被实例化的类,它的作用在于为其他类提供一种基础模板或蓝图,从而强制对其子类实现某些特定方法或属性。在TypeScript中,可以通过在类名前面添加 abstract
关键字来声明一个抽象类。
以下是抽象类的一些特点和使用场景:
1. 抽象类无法被实例化
抽象类不同于普通的类,它只能被用作其他类的父类,无法直接实例化。这是因为抽象类中可能存在没有实现的抽象方法或属性,无法满足实例的要求。
举例:
abstract class Animal {
name: string;
abstract makeSound(): void; // 抽象方法
}
const a = new Animal(); // 报错:无法创建抽象类的实例
2. 抽象类中可以有抽象方法
抽象方法没有具体的实现代码,只有方法的声明,需要子类实现。使用抽象方法可以强制子类必须实现某些功能,从而遵循了面向对象的开闭原则。
举例:
abstract class Animal {
name: string;
abstract makeSound(): void; // 抽象方法
}
class Dog extends Animal {
makeSound() {
console.log('汪汪汪');
}
}
const dog = new Dog();
dog.makeSound(); // 输出: 汪汪汪
3. 抽象类中可以有普通方法
除了抽象方法之外,抽象类中也可以有普通的实例方法。这些方法通常都是子类共用的方法,可以在抽象类中提前定义,避免了重复代码。
举例:
abstract class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
public eat(food: string) { // 普通方法
console.log(`${this.name}正在吃${food}`);
}
abstract makeSound(): void; // 抽象方法
}
class Dog extends Animal {
constructor(name: string) {
super(name);
}
makeSound() {
console.log('汪汪汪');
}
}
const dog = new Dog('小狗');
dog.eat('狗粮'); // 输出: 小狗正在吃狗粮
4. 抽象类可以作为类型
抽象类也可以被用来作为类型来声明变量或参数类型。
举例:
abstract class Animal {
name: string;
abstract makeSound(): void; // 抽象方法
}
function makeAnimalSound(animal: Animal) {
animal.makeSound();
}
class Dog extends Animal {
makeSound() {
console.log('汪汪汪');
}
}
const dog = new Dog();
makeAnimalSound(dog);
抽象类在TypeScript中是一种非常强大的抽象概念,可以用来规范子类的结构和行为,提高代码的可读性和可维护性。在实际项目中,抽象类由于其高效而且实用的特性,在许多场景中都得到了广泛的应用。
9. 静态成员static
1. 静态属性和方法
静态属性和方法是类的属性和方法,与对象无关,可以通过类名直接访问。使用static关键字来定义静态属性和方法。例如:
class Calculator {
public static PI: number = 3.14;
public static add(x: number, y: number): number {
return x + y;
}
}
console.log(Calculator.PI); //输出:3.14
console.log(Calculator.add(1, 2)); //输出:3
2. 静态区块
TypeScript中的静态区块也称为静态初始化块,是一个代码块,用于为类或对象的静态成员(静态属性或静态方法)初始化。
静态区块使用关键字static定义,并且在类或对象的定义中出现在属性或方法定义之前。静态区块中的代码在类或对象实例化之前执行,用于初始化静态成员。静态区块只会执行一次。
下面是一个使用静态区块的示例:
class MyClass {
static myStaticProp: number;
static {
// 静态块中初始化静态属性
MyClass.myStaticProp = 42;
}
}
在上面的代码中,通过static关键字定义了一个静态属性myStaticProp,并在静态区块中初始化它的值为42。
静态区块可以包含任何合法的代码,包括赋值、条件语句、循环以及函数调用等。但是,它不能访问任何非静态的成员,因为静态区块在类或对象实例化之前执行,不会有实例对象存在。
3. 使用静态成员注意点
使用静态成员时需要注意
以下几点:
1. 静态成员只能在类内部通过类名进行访问,而不能通过类的实例进行访问。
例如,我们可以定义一个类:
class Foo {
static bar = 'Hello, world!';
}
这个类中有一个静态成员 bar,它的值是字符串 ‘Hello, world!’。
现在我们可以在类的内部访问这个静态成员:
class Foo {
static bar = 'Hello, world!';
static getBar() {
return Foo.bar;
}
}
console.log(Foo.getBar()); // 输出 "Hello, world!"
在类的内部,我们可以通过类名 Foo 来访问静态成员 bar。
但是,在类的实例中,我们不能直接通过实例来访问静态成员:
class Foo {
static bar = 'Hello, world!';
}
const foo = new Foo();
console.log(foo.bar); // 输出 "undefined"
在这个例子中,我们创建了一个 Foo 的实例 foo,并试图通过 foo.bar 来访问静态成员 bar。但是,在 TypeScript 中,我们不能通过实例来访问静态成员,因此这里输出了 undefined。
因此,需要在类的内部通过类名来访问静态成员,在类的实例中无法直接访问。
2. 静态成员可以在类外部进行访问和修改,但是在 TypeScript 中,建议不要这样做。
在 TypeScript 中,静态成员可以使用类名进行访问和修改,如下例所示:
class Person {
static age: number = 18;
}
console.log(Person.age); // 输出 18
Person.age = 20;
console.log(Person.age); // 输出 20
但是,尽管可以这样做,建议不要在类外部直接访问和修改静态成员,这是因为:
-
静态成员通常表示类级别的信息,类外部访问和修改可能会导致不必要的破坏和错误。
-
通过类的方法来访问和修改静态成员可以更好地封装类的实现细节,使代码更加安全和可维护。
因此,建议在 TypeScript 中尽量遵循面向对象编程的原则,通过类的方法来访问和修改静态成员。
3. 静态成员可以和非静态成员共存,但不能访问非静态成员,因为非静态成员是依赖于类的实例创建的。
假设有一个类 Person,其中有一个非静态成员 name 和一个静态成员 age:
class Person {
name: string;
static age: number = 0;
constructor(name: string) {
this.name = name;
Person.age++;
}
static sayAge() {
console.log(`My age is ${Person.age}`);
// console.log(`My name is ${this.name}`); // Cannot access 'name' because it is a non-static property
}
}
let person1 = new Person('Alice');
let person2 = new Person('Bob');
Person.sayAge(); // My age is 2
在上面的例子中,静态成员 age 被定义为类级别的,不依赖于类的实例,因此可以被访问和修改。而非静态成员 name 是实例级别的,依赖于类的实例创建,因此无法在静态方法 sayAge() 中访问它。
当静态成员和非静态成员共存时,静态成员保持类级别的状态,而非静态成员则是实例级别的状态。他们可以互相独立存在,但在访问时需要注意它们对应的作用域。
4. 静态方法也是可以继承的,而且可以在子类中通过 super
关键字进行调用。
class Animal {
static sayHello() {
console.log('Hello');
}
}
class Dog extends Animal {
static bark() {
console.log('Woof');
super.sayHello(); // 调用父类的静态方法
}
}
Dog.bark(); // 输出 "Woof" 和 "Hello"
在这个例子中,我们定义了一个 Animal
类和一个 Dog
类。Animal
类中有一个静态方法 sayHello()
,它输出一个字符串 “Hello”。Dog
类继承了 Animal
类,并添加了一个静态方法 bark()
,它在输出一个字符串 “Woof” 后使用 super.sayHello()
调用了父类的静态方法。
最后,我们调用 Dog.bark()
方法,它输出 “Woof” 和 “Hello”。这证明了静态方法可以被继承,并且可以在子类中通过 super
关键字进行调用。
5. 静态成员是在类的声明中定义的,而不是在实例化时定义的。
class MyClass {
static myStaticProperty = "Hello, I am static!";
myNormalProperty = "Hello, I am normal!";
static myStaticMethod() {
console.log("Hello, I am static method!");
}
myNormalMethod() {
console.log("Hello, I am normal method!");
}
}
console.log(MyClass.myStaticProperty); // 输出:Hello, I am static!
const obj = new MyClass();
console.log(obj.myNormalProperty); // 输出:Hello, I am normal!
MyClass.myStaticMethod(); // 输出:Hello, I am static method!
obj.myNormalMethod(); // 输出:Hello, I am normal method!
从以上代码可以看出,静态成员(包括静态属性和静态方法)是通过 static
关键字在类的声明中定义的,可以通过 类名.成员名
的方式来访问,不需要实例化类。而普通的成员(包括普通属性和普通方法)则是在类的实例化时定义的,只能通过类的实例化对象来访问。
10. 泛型类
泛型类是一种在TypeScript中定义类时可以参数化类型的机制。泛型类可以在定义时指定类中的某些属性或方法使用某种类型的占位符,然后在实例化对象时,具体指定这个占位符的类型。这种机制可以在编写泛型算法和数据结构时非常有用。
泛型类的定义方式类似于泛型函数,只需要在类名后使用< >,在尖括号内定义泛型占位符,如下所示:
class GenericClass<T> {
private value: T;
constructor(val: T) {
this.value = val;
}
getValue(): T {
return this.value;
}
}
在上述代码中,我们定义了一个泛型类GenericClass,其中有一个用来保存值的属性value和一个用来获取这个值的方法getValue。类中的泛型占位符T可以代表任意类型,例如数字、字符串、对象等。在类的构造函数中,我们将实际值val传入,并保存到类的属性value中,这个值可能是任意类型。
当我们要使用这个泛型类时,需要指定具体的类型,例如:
let numObj = new GenericClass<number>(100);
console.log(numObj.getValue()); // Output: 100
let strObj = new GenericClass<string>("Hello");
console.log(strObj.getValue()); // Output: "Hello"
在上述代码中,我们先实例化一个泛型类GenericClass来保存数字,然后实例化一个泛型类GenericClass来保存字符串。在实例化时,我们需要传入具体的类型参数,这里分别为number和string。
在使用泛型类时需要注意以下几点:
- 在定义类时,类名后的尖括号内的泛型占位符可以有任意名称,但通常使用单个大写字母T来表示类型。
- 在实例化时,需要传入具体的类型参数。如果不传入类型参数,TypeScript会自动推断出适合的类型。
- 当使用泛型类时,可以多次重复调用同一个泛型类并传入不同的类型参数。
- 泛型类可以继承其他类,也可以被其他类继承。如果一个泛型类继承了其他类,那么在实例化时需要传入父类所需要的类型参数。
下面举几个例子来说明泛型类的使用:
// 例子1:泛型类中使用接口
interface Printable {
print(): void;
}
class GenericClass<T extends Printable> {
private value: T;
constructor(val: T) {
this.value = val;
}
getValue(): T {
return this.value;
}
printValue(): void {
this.value.print();
}
}
在上述例子中,我们定义了一个接口Printable,它规定了具有print方法的对象。然后我们定义一个泛型类GenericClass,它的类型参数必须是实现了Printable接口的对象。在类的方法printValue中,我们调用了传入对象的print方法。
// 例子2:泛型类中使用多个类型参数
class Pair<T1, T2> {
private first: T1;
private second: T2;
constructor(first: T1, second: T2) {
this.first = first;
this.second = second;
}
getFirst(): T1 {
return this.first;
}
getSecond(): T2 {
return this.second;
}
}
在上述例子中,我们定义了一个泛型类Pair,它有两个类型参数T1和T2,分别代表对象的两个值。在类的构造函数中,我们传入了两个值,并存储到类的属性first和second中,这两个值可能是不同的类型。
// 例子3:泛型类的继承
class BaseClass<T> {
private value: T;
constructor(val: T) {
this.value = val;
}
getValue(): T {
return this.value;
}
}
class SubClass<T> extends BaseClass<T> {
// ...
}
在上述例子中,我们定义了一个基类BaseClass,它是一个泛型类,有一个类型参数T,它有一个属性value和一个方法getValue。然后我们定义了一个子类SubClass,它也是一个泛型类,并继承了BaseClass。当实例化SubClass时,需要传入T类型参数,并传入给BaseClass的构造函数中。
泛型类是一种强大的类型抽象机制,可以使得代码更加通用、可复用和可扩展。在使用时需要注意泛型类的参数化和类型推断机制,以及泛型占位符在类定义中的使用方法。
11. 类中的this指向问题
在 TypeScript 中,类在运行时使用 this 的情况与 JavaScript 类似。在 TypeScript 中,类可以被视为蓝图或模板来创建对象。在类的实例化时,使用 new 关键字创建新的对象时,运行时 this 的指向会发生改变。下面分别从实例化时、方法中和回调函数中三个角度举例说明。
- 实例化时:
当使用 new 关键字创建类的实例时,this 指向该实例。例如:
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
getName() {
return this.name;
}
}
const person = new Person('Tom');
console.log(person.getName()); // 输出 "Tom"
在这个例子中,this 始终指向实例 person。
- 方法中:
在类的方法中,this 的指向与方法的调用方式有关。如果方法是通过类的实例来调用的,则 this 指向该实例;如果方法是作为回调函数传递给其他函数的,则 this 的指向将发生改变。例如:
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
getName() {
return this.name;
}
sayHello() {
console.log(`Hello, ${this.getName()}!`);
}
}
const person = new Person('Tom');
person.sayHello(); // 输出 "Hello, Tom!"
const getNameFn = person.getName;
console.log(getNameFn()); // 输出 undefined
const sayHelloFn = person.sayHello;
sayHelloFn(); // 输出 "Hello, undefined!"
在这个例子中,person.getName() 的输出为 “Tom”,而 getNameFn() 的结果为 undefined。因为第一个调用是通过 person 对象来调用 getName() 方法的,this 的指向是 person,而第二个调用是将 getName() 方法赋值给一个新的变量 getNameFn,然后单独调用该函数,此时函数内部的 this 已经指向了全局对象,因此返回 undefined。
同样的,sayHelloFn() 的输出为 “Hello, undefined!”,因为它是作为回调函数传递给其他函数(即没有绑定到 person 对象上)而被调用的,因此 this 的指向是全局对象,getName() 方法无法被正确调用。
- 回调函数中:
当类的方法作为回调函数传递给其他函数时,this 的指向也会发生改变,需要使用 bind()、call() 或 apply() 方法将 this 绑定到对象上。例如:
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
getName() {
return this.name;
}
sayHello() {
console.log(`Hello, ${this.getName()}!`);
}
}
function setTimeoutCallback(callback: () => void, time: number) {
setTimeout(callback.bind(this), time);
}
const person = new Person('Tom');
setTimeoutCallback(person.sayHello, 1000); // 输出 "Hello, Tom!"(1秒后)
在这个例子中,setTimeoutCallback() 将 person.sayHello 方法作为回调函数传递给了 setTimeout(),但是该方法内部的 this 指向已经发生了变化,需要使用 bind() 方法将 this 绑定到 person 对象上。
类在 TypeScript 中在运行时 this 的指向问题需要根据具体的代码结构和使用方式进行分析和处理。为了保证正确性,需要注意绑定 this 的方式。