TypeScript类的定义与基本使用
TypeScript 类的定义基础
在 TypeScript 中,类是一种面向对象编程的基础结构,它用于封装数据和行为。类的定义由关键字 class
开始,后面跟着类名。例如:
class Person {
}
上述代码定义了一个名为 Person
的类。此时,这个类还没有任何属性和方法,但它已经是一个有效的类定义。
类的属性定义
类的属性是类所包含的数据成员。在 TypeScript 中,我们可以在类中直接声明属性,并指定其类型。例如:
class Person {
name: string;
age: number;
}
这里定义了 Person
类的两个属性 name
和 age
,分别为字符串类型和数字类型。属性在使用前必须声明,这有助于在编译时捕获类型错误。
我们还可以在声明属性时给它一个初始值:
class Person {
name: string = 'John';
age: number = 30;
}
这样,当创建 Person
类的实例时,name
会初始化为 John
,age
会初始化为 30
。
类的构造函数
构造函数是类中的一个特殊方法,它在创建类的实例时被调用。在 TypeScript 中,构造函数使用 constructor
关键字定义。例如:
class Person {
name: string;
age: number;
constructor(n: string, a: number) {
this.name = n;
this.age = a;
}
}
在上述代码中,Person
类的构造函数接受两个参数 n
和 a
,并将它们分别赋值给类的 name
和 age
属性。this
关键字在这里用于引用类的实例本身。
通过构造函数,我们可以在创建实例时传入不同的值来初始化对象。例如:
let person1 = new Person('Jane', 25);
console.log(person1.name); // 输出: Jane
console.log(person1.age); // 输出: 25
访问修饰符
TypeScript 提供了三种访问修饰符:public
、private
和 protected
,用于控制类的属性和方法的访问权限。
public
(默认):被public
修饰的属性和方法可以在类的内部和外部被访问。例如:
class Person {
public name: string;
public constructor(n: string) {
this.name = n;
}
public greet() {
return `Hello, I'm ${this.name}`;
}
}
let person = new Person('Bob');
console.log(person.name); // 输出: Bob
console.log(person.greet()); // 输出: Hello, I'm Bob
private
:被private
修饰的属性和方法只能在类的内部被访问。如果在类的外部尝试访问private
成员,会导致编译错误。例如:
class Person {
private name: string;
constructor(n: string) {
this.name = n;
}
private greet() {
return `Hello, I'm ${this.name}`;
}
public getGreeting() {
return this.greet();
}
}
let person = new Person('Alice');
// console.log(person.name); // 编译错误
// console.log(person.greet()); // 编译错误
console.log(person.getGreeting()); // 输出: Hello, I'm Alice
这里 name
属性和 greet
方法是 private
的,不能在类外部直接访问,但可以通过类内部的 public
方法 getGreeting
间接访问。
protected
:protected
修饰符与private
类似,不同之处在于protected
成员可以在类本身及其子类中被访问。例如:
class Animal {
protected name: string;
constructor(n: string) {
this.name = n;
}
protected speak() {
return `${this.name} makes a sound`;
}
}
class Dog extends Animal {
bark() {
return this.speak() + 'and barks';
}
}
let dog = new Dog('Buddy');
// console.log(dog.name); // 编译错误
// console.log(dog.speak()); // 编译错误
console.log(dog.bark()); // 输出: Buddy makes a sound and barks
在这个例子中,Dog
类继承自 Animal
类,它可以访问 Animal
类中 protected
的 name
属性和 speak
方法。
类的方法定义
类的方法是类中定义的函数,它可以操作类的属性,实现特定的行为。方法的定义与普通函数类似,只是它在类的内部。
实例方法
实例方法是与类的实例相关联的方法。它们可以访问和修改实例的属性。例如:
class Counter {
value: number;
constructor() {
this.value = 0;
}
increment() {
this.value++;
}
decrement() {
if (this.value > 0) {
this.value--;
}
}
getValue() {
return this.value;
}
}
let counter = new Counter();
counter.increment();
counter.increment();
console.log(counter.getValue()); // 输出: 2
counter.decrement();
console.log(counter.getValue()); // 输出: 1
在上述 Counter
类中,increment
、decrement
和 getValue
都是实例方法。它们可以访问和修改 Counter
实例的 value
属性。
静态方法
静态方法是与类本身相关联的方法,而不是与类的实例相关联。静态方法使用 static
关键字定义,它们不能直接访问实例属性,只能访问静态属性。例如:
class MathUtils {
static add(a: number, b: number): number {
return a + b;
}
static multiply(a: number, b: number): number {
return a * b;
}
}
let result1 = MathUtils.add(3, 5);
let result2 = MathUtils.multiply(4, 6);
console.log(result1); // 输出: 8
console.log(result2); // 输出: 24
在这个例子中,add
和 multiply
是 MathUtils
类的静态方法。我们通过类名直接调用这些方法,而不需要创建类的实例。
类的继承
继承是面向对象编程中的一个重要概念,它允许一个类从另一个类中获取属性和方法。在 TypeScript 中,使用 extends
关键字来实现继承。
基本继承
例如,我们有一个 Animal
类,然后创建一个 Dog
类继承自 Animal
类:
class Animal {
name: string;
constructor(n: string) {
this.name = n;
}
speak() {
return `${this.name} makes a sound`;
}
}
class Dog extends Animal {
breed: string;
constructor(n: string, b: string) {
super(n);
this.breed = b;
}
bark() {
return `${this.speak()} and barks`;
}
}
let dog = new Dog('Buddy', 'Golden Retriever');
console.log(dog.speak()); // 输出: Buddy makes a sound
console.log(dog.bark()); // 输出: Buddy makes a sound and barks
在上述代码中,Dog
类继承了 Animal
类的 name
属性和 speak
方法。Dog
类还定义了自己的 breed
属性和 bark
方法。在 Dog
类的构造函数中,使用 super
关键字调用了 Animal
类的构造函数,以初始化继承自 Animal
类的 name
属性。
重写方法
子类可以重写从父类继承的方法。当子类重写方法时,方法的签名(参数列表和返回类型)必须与父类中的方法签名相同(或兼容)。例如:
class Animal {
speak() {
return 'Animal makes a sound';
}
}
class Cat extends Animal {
speak() {
return 'Meow';
}
}
let animal = new Animal();
let cat = new Cat();
console.log(animal.speak()); // 输出: Animal makes a sound
console.log(cat.speak()); // 输出: Meow
这里 Cat
类重写了 Animal
类的 speak
方法,提供了自己的实现。
访问修饰符与继承
继承过程中,访问修饰符会影响子类对父类成员的访问。public
成员在子类中可以自由访问和重写;protected
成员可以在子类中访问和重写;private
成员在子类中不可访问。例如:
class Parent {
public publicMethod() {
return 'Public method';
}
protected protectedMethod() {
return 'Protected method';
}
private privateMethod() {
return 'Private method';
}
}
class Child extends Parent {
callProtected() {
return this.protectedMethod();
}
// callPrivate() { // 编译错误
// return this.privateMethod();
// }
}
let child = new Child();
console.log(child.publicMethod()); // 输出: Public method
console.log(child.callProtected()); // 输出: Protected method
// console.log(child.callPrivate()); // 编译错误
在这个例子中,Child
类可以访问和调用 Parent
类的 public
和 protected
方法,但不能访问 private
方法。
抽象类
抽象类是一种不能被实例化的类,它主要用于作为其他类的基类。抽象类可以包含抽象方法,抽象方法是没有实现体的方法,必须在子类中被实现。
定义抽象类和抽象方法
使用 abstract
关键字来定义抽象类和抽象方法。例如:
abstract class Shape {
abstract getArea(): number;
abstract getPerimeter(): number;
}
class Circle extends Shape {
radius: number;
constructor(r: number) {
super();
this.radius = r;
}
getArea(): number {
return Math.PI * this.radius * this.radius;
}
getPerimeter(): number {
return 2 * Math.PI * this.radius;
}
}
class Rectangle extends Shape {
width: number;
height: number;
constructor(w: number, h: number) {
super();
this.width = w;
this.height = h;
}
getArea(): number {
return this.width * this.height;
}
getPerimeter(): number {
return 2 * (this.width + this.height);
}
}
// let shape = new Shape(); // 编译错误,不能实例化抽象类
let circle = new Circle(5);
console.log(circle.getArea()); // 输出: 78.53981633974483
console.log(circle.getPerimeter()); // 输出: 31.41592653589793
let rectangle = new Rectangle(4, 6);
console.log(rectangle.getArea()); // 输出: 24
console.log(rectangle.getPerimeter()); // 输出: 20
在上述代码中,Shape
类是一个抽象类,它定义了两个抽象方法 getArea
和 getPerimeter
。Circle
和 Rectangle
类继承自 Shape
类,并实现了这些抽象方法。
抽象类的作用
抽象类为一组相关的类提供了一个通用的接口和部分实现。它强制子类遵循一定的契约,即必须实现抽象类中定义的抽象方法。这有助于提高代码的可维护性和可扩展性,特别是在大型项目中,不同的开发者可以基于抽象类来实现具体的功能,同时保持代码结构的一致性。
类与接口的关系
接口和类在 TypeScript 中都用于定义类型结构,但它们有着不同的用途和特点。
类实现接口
一个类可以实现一个或多个接口,以表明它提供了接口所定义的行为。使用 implements
关键字来实现接口。例如:
interface Printable {
print(): void;
}
class Book implements Printable {
title: string;
constructor(t: string) {
this.title = t;
}
print() {
console.log(`Book title: ${this.title}`);
}
}
let book = new Book('TypeScript in Action');
book.print(); // 输出: Book title: TypeScript in Action
在这个例子中,Book
类实现了 Printable
接口,这意味着 Book
类必须提供 print
方法的实现。
接口继承接口
接口之间也可以继承,通过继承可以扩展接口的功能。例如:
interface Shape {
getArea(): number;
}
interface RectangleShape extends Shape {
getPerimeter(): number;
}
class Rectangle implements RectangleShape {
width: number;
height: number;
constructor(w: number, h: number) {
this.width = w;
this.height = h;
}
getArea(): number {
return this.width * this.height;
}
getPerimeter(): number {
return 2 * (this.width + this.height);
}
}
let rectangle = new Rectangle(4, 6);
console.log(rectangle.getArea()); // 输出: 24
console.log(rectangle.getPerimeter()); // 输出: 20
这里 RectangleShape
接口继承自 Shape
接口,并添加了 getPerimeter
方法。Rectangle
类实现 RectangleShape
接口时,需要实现 Shape
接口的 getArea
方法和 RectangleShape
接口新增的 getPerimeter
方法。
类与接口的区别
- 实例化:类可以被实例化,创建具体的对象;而接口不能被实例化,它只是一种类型定义。
- 实现与继承:类通过
extends
关键字继承另一个类,通过implements
关键字实现接口;接口通过extends
关键字继承其他接口。 - 成员定义:类可以包含属性、方法、构造函数等各种成员,并且可以使用访问修饰符控制访问权限;接口主要定义方法签名和属性类型,不包含具体的实现代码,也没有访问修饰符。
类的高级特性
只读属性
在 TypeScript 中,可以使用 readonly
关键字将属性声明为只读。只读属性只能在声明时或构造函数中赋值。例如:
class Person {
readonly name: string;
constructor(n: string) {
this.name = n;
}
}
let person = new Person('Tom');
// person.name = 'Jerry'; // 编译错误,不能修改只读属性
这里 name
属性被声明为只读,在构造函数中赋值后,就不能再修改。
参数属性
参数属性是一种在构造函数参数中声明并初始化属性的便捷方式。例如:
class Person {
constructor(public name: string, public age: number) {
}
}
let person = new Person('Alice', 28);
console.log(person.name); // 输出: Alice
console.log(person.age); // 输出: 28
在上述代码中,name
和 age
属性通过构造函数参数直接声明并初始化,public
访问修饰符同时定义了属性的访问权限。
存取器(Getters 和 Setters)
存取器允许我们控制对类属性的访问。get
关键字用于定义读取属性值的方法,set
关键字用于定义设置属性值的方法。例如:
class Temperature {
private _value: number;
constructor(value: number) {
this._value = value;
}
get value() {
return this._value;
}
set value(newValue: number) {
if (newValue >= -273.15) {
this._value = newValue;
} else {
throw new Error('Temperature cannot be below absolute zero');
}
}
}
let temp = new Temperature(25);
console.log(temp.value); // 输出: 25
temp.value = 30;
console.log(temp.value); // 输出: 30
// temp.value = -274; // 抛出错误: Temperature cannot be below absolute zero
在这个例子中,value
属性通过 get
和 set
存取器进行访问控制。set
方法在设置值之前进行了有效性检查。
多态
多态是指同一个方法在不同的类中有不同的实现。通过继承和方法重写,TypeScript 实现了多态。例如:
class Animal {
speak() {
return 'Animal makes a sound';
}
}
class Dog extends Animal {
speak() {
return 'Woof';
}
}
class Cat extends Animal {
speak() {
return 'Meow';
}
}
function makeSound(animal: Animal) {
console.log(animal.speak());
}
let dog = new Dog();
let cat = new Cat();
makeSound(dog); // 输出: Woof
makeSound(cat); // 输出: Meow
在上述代码中,makeSound
函数接受一个 Animal
类型的参数,当传入不同子类(Dog
或 Cat
)的实例时,会调用相应子类重写的 speak
方法,这就是多态的体现。
使用类进行模块化开发
在实际项目中,通常会将类组织成模块,以提高代码的可维护性和复用性。
模块的定义
在 TypeScript 中,可以使用 export
关键字将类导出为模块。例如,创建一个 person.ts
文件:
export class Person {
name: string;
age: number;
constructor(n: string, a: number) {
this.name = n;
this.age = a;
}
greet() {
return `Hello, I'm ${this.name} and I'm ${this.age} years old`;
}
}
在另一个文件 main.ts
中,可以导入并使用这个类:
import { Person } from './person';
let person = new Person('Bob', 35);
console.log(person.greet()); // 输出: Hello, I'm Bob and I'm 35 years old
这里使用 import
语句从 person.ts
文件中导入了 Person
类。
模块的组织
可以在一个模块中导出多个类,也可以将相关的类组织在不同的文件中,通过模块进行管理。例如,创建一个 animal.ts
文件:
export class Animal {
name: string;
constructor(n: string) {
this.name = n;
}
speak() {
return `${this.name} makes a sound`;
}
}
export class Dog extends Animal {
bark() {
return `${this.speak()} and barks`;
}
}
在 main.ts
文件中导入并使用这些类:
import { Animal, Dog } from './animal';
let animal = new Animal('Generic Animal');
console.log(animal.speak()); // 输出: Generic Animal makes a sound
let dog = new Dog('Buddy');
console.log(dog.speak()); // 输出: Buddy makes a sound
console.log(dog.bark()); // 输出: Buddy makes a sound and barks
通过合理地组织模块和类,可以使项目结构更加清晰,代码更易于维护和扩展。
总结类在前端开发中的应用
在前端开发中,TypeScript 的类提供了强大的面向对象编程能力。通过类,我们可以更好地组织和管理代码,封装数据和行为,实现代码的复用和扩展。
在构建复杂的用户界面时,类可以用于表示视图组件,将组件的状态和行为封装在类中,通过继承和多态实现组件的复用和定制。例如,我们可以定义一个基础的 UIComponent
类,然后让 Button
、Input
等具体的 UI 组件类继承自它,实现统一的样式和交互逻辑。
同时,类与接口的结合使用,可以更好地定义组件之间的契约和交互方式,提高代码的可维护性和可测试性。在处理业务逻辑时,类可以用于表示业务实体和业务规则,通过合理地设计类的属性和方法,实现业务逻辑的清晰表达和高效执行。
总之,熟练掌握 TypeScript 类的定义与使用,对于提升前端开发的质量和效率具有重要意义。无论是小型项目还是大型企业级应用,类都能在代码的组织和架构中发挥关键作用。