JavaScript 是一门面向对象的编程语言,它允许通过对象来建模和解决实际问题。同时,JavaScript 也支持基于原型链的对象继承机制。虽然大多数的面向对象编程语言都支持类,但是 JavaScript 语言在很长一段时间都没有支持它。在 JavaScript 程序中,需要使用函数来实现类的功能。
在 ECMAScript 2015 规范中正式地定义了类。同时,TypeScript 语言也对类进行了全面的支持。
一、类的定义
虽然 JavaScript 语言支持了类,但其本质上仍然是函数,类是一种语法糖。TypeScript 语言对 JavaScript 中的类进行了罗占,为其添加了类型支持,如实现接口、泛型类等。
定义一个类需要使用 class 关键字。类型于函数定义,类的定义也有以下两种方式:
1.1、类声明
类声明能够创建一个类,类声明的语法如下所示:
class ClassName {
// ...
在该语法中,class 是关键字;ClassName 表示类的名字。在类声明中的类名是必选的。按照惯例,类名的首字母应该大写。示例如下:
class Circle{
radius: number;
const c = new Circle();
与函数声明不同的是,类声明不会被提升,就是必须先声明后,再使用。示例如下:
const c0 = new Circle(); //错误
class Circle{
radius: number;
const c1 = new Circle(); //正确
1.2、类表达式
类表达式是另一种定义类的方式,它的语法如下所示:
const Name = class ClassName {
// ...
在该语法中,class 是关键字;Name 表示引用了该类的变量名;ClassName 表示类的名字。在类表达式中,类名 ClassName 是可选的。
例如,下例中使用类表达式定义了一个匿名类,同时使用常量 Circle 引用了该匿名类:
const Circle = class {
radius: number;
如果在类表达式中定义了类型,则该类型只能够在类内部使用,在类外不允许引用该类名。
const A = class B {
name = B.name;
const b = new B(); // error
二、成员变量
TypeScript 是一种基于 JavaScript 的强类型或静态类型语言。成员变量,也称为实例变量或属性,是定义在类中的变量,用于保存对象的状态。
在 TypeScript 中,成员变量可以在类的构造函数中初始化,也可以在声明时直接赋值。这些变量属于类的实例,所以每个实例都会有一份自己的副本。
下面是一个简单的例子:
class Animal {
// 成员变量
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
let cat = new Animal('Tom', 5);
console.log(cat.name); // 输出 'Tom'
console.log(cat.age); // 输出 5
TypeScript 的成员变量可以有访问修饰符,比如 public(默认)、private 和 protected。这些修饰符决定了成员变量的可见性,也就是它们在类的外部是否可以被访问或修改。
class Animal {
private name: string; // 私有成员变量
public age: number; // 公有成员变量
constructor(name: string, age: number) {
this.name = name;
this.age = age;
在这个例子中,name 是私有的,所以在类的外部无法访问或修改它。而 age 是公有的,所以在类的外部可以访问和修改它。
三、成员函数
成员函数也称作方法,声明成员函数与在对象字面量中声明方法是类似的。
TypeScript 中的类成员函数是类中的一种方法,它们可以在类的实例上调用。类成员函数可以让你在类中定义一些行为或操作,这些行为或操作可以在类的实例上进行调用。
下面是一个简单的 TypeScript 类和成员函数的例子:
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
sayHello(): void {
console.log(Hello, my name is ${this.name} and I am ${this.age} years old.);
const person = new Person('Alice', 25);
person.sayHello(); // 输出:Hello, my name is Alice and I am 25 years old.
在上面的例子中,Person 类有两个成员变量 name 和 age,以及一个成员函数 sayHello。sayHello 函数使用 console.log 输出一个问候语,其中包含当前实例的 name 和 age。 注意,在这个例子中使用了 this 关键字来引用当前类的实例。this 关键字可以用来访问类的成员变量和其他成员函数。 类成员函数还可以接收参数,如下所示:
class Calculator {
add(a: number, b: number): number {
return a + b;
const calculator = new Calculator();
console.log(calculator.add(2, 3)); // 输出:5
在上面的例子中,Calculator 类有一个接收两个数字参数并返回它们之和的成员函数 add。
四、成员存取器
TypeScript 中的类成员存取器(getter 和 setter)是一种特殊的类成员函数,用于访问和修改类的成员变量。它们提供了一种更安全和可控的方式来处理类的属性。
getter 是一个只读函数,用于获取成员变量的值。它的名称与成员变量相同,但前面加上 get 关键字。getter 函数不接收任何参数,返回成员变量的值。
setter 是一个写函数,用于设置成员变量的值。它的名称与成员变量相同,但前面加上 set 关键字。setter 函数接收一个参数,该参数是要设置的新值。
下面是一个使用 getter 和 setter 的 TypeScript 类示例:
class Person {
private _name: string;
private _age: number;
constructor(name: string, age: number) {
this._name = name;
this._age = age;
get name(): string {
return this._name;
set name(value: string) {
if (value.length > 0) {
this._name = value;
} else {
console.log("Name cannot be empty.");
get age(): number {
return this._age;
set age(value: number) {
if (value > 0) {
this._age = value;
} else {
console.log("Age cannot be negative.");
在这个示例中,Person类有两个私有成员变量 _name 和 _age,以及对应的 getter 和 setter。
getter 和 setter 允许我们更好地控制对成员变量的访问和修改。在这个例子中,setter 函数对输入值进行了检查,确保名字不为空,年龄不为负。如果不满足这些条件,setter 将不会修改成员变量,而是输出一条错误消息。这是一种数据验证的有效方式。 使用getter和setter的另一个好处是,它们可以让我们在未来更改类的内部实现时,保持对外部代码的兼容性。
例如,如果我们以后决定不再直接存储_name和_age,而是将它们存储在数据库或远程服务器上,我们只需要修改getter和setter,而不需要修改所有使用这些变量的代码。getter和setter是TypeScript(和许多其他面向对象语言)中非常有用的特性,可以提高代码的可读性、安全性和可维护性。
五、索引成员
类的索引成员会在类的类型中引入索引签名。
索引签名包含两种:
字符串索引: 使用字符串作为键来访问对象的属性。在类中,你可以使用字符串索引来访问类的属性。
class MyClass {
private myProperty: string;
constructor() {
this.myProperty = "Hello, World!";
let myObject = new MyClass();
console.log(myObject["myProperty"]); // 输出 "Hello, World!"
在上面的例子中,我们使用字符串索引 "myProperty"
来访问 MyClass
实例的属性。
数值索引: 使用数值作为键来访问对象的属性。在类中,你可以使用数值索引来访问类的属性或元素。
class MyArray {
private myElements: number[];
constructor(elements: number[]) {
this.myElements = elements;
let myArray = new MyArray([1, 2, 3, 4, 5]);
console.log(myArray[2]); // 输出 3
在上面的例子中,我们使用数值索引 2
来访问 MyArray
实例的元素
public:公共访问修饰符,表示类的成员可以从任何地方访问。
protected:受保护的访问修饰符,表示类的成员只能在类内部或派生类中访问。
private:私有访问修饰符,表示类的成员只能在类内部访问。
#private(私有字段):这是一种特殊的私有访问修饰符,用于声明私有字段。
6.1、public
例如,假设有一个 Person
类,它有一个 name
属性和一个 greet
方法:
class Person {
public name: string;
public greet(): string {
return `Hello, my name is ${this.name}`;
可以直接创建 Person
的实例并访问其 name
属性和 greet
方法:
const person = new Person();
person.name = "Alice";
console.log(person.greet()); // 输出 "Hello, my name is Alice"
6.2、protected
例如,假设有一个 Animal 类,它有一个 protected name 属性和一个 protected speak 方法:
class Animal {
protected name: string;
protected speak(): string {
return `My name is ${this.name}`;
你可以在派生类中访问 Animal
的受保护成员:
class Dog extends Animal {
bark(): void {
console.log(this.name); // 可以访问受保护的 name 属性
console.log(this.speak()); // 可以访问受保护的 speak 方法
但在类的外部无法直接访问受保护的成员:
const dog = new Dog(); // 错误!Dog 类不能直接实例化,只能通过继承 Animal 类的方式创建派生类对象。
console.log(dog.name); // 错误!name 属性是受保护的,不能从外部直接访问。
console.log(dog.speak()); // 错误!speak 方法是受保护的,不能从外部直接访问。
6.3、private
例如,假设有一个 BankAccount
类,它有一个 private balance
属性和一个 deposit
方法:
class BankAccount {
private balance: number; // private 属性只能在 BankAccount 类内部直接访问,不能从外部或派生类中直接访问。
6.4、私有字段
例如,让我们以 BankAccount
类为例,添加一个私有的 #balance
字段:
class BankAccount {
#balance: number; // 私有字段
constructor(initialBalance: number) {
this.#balance = initialBalance; // 在类内部访问和赋值私有字段
deposit(amount: number) {
this.#balance += amount; // 在类内部访问和操作私有字段
get balance() {
return this.#balance; // 通过 getter 访问私有字段(在外部不可直接访问)
建一个 BankAccount
的实例,并使用其 deposit
方法以及通过其 balance
getter 来查看余额:
const account = new BankAccount(1000); // 创建一个 BankAccount 实例
console.log(account.balance); // 输出 1000
account.deposit(500); // 存入 500
console.log(account.balance); // 输出 1500
但是,如果你尝试直接访问 #balance
字段(无论是从类的外部还是从派生类中),TypeScript 都会给出错误,因为它是私有的。例如:
console.log(account.#balance); // 错误!无法直接从外部访问 #balance 字段
七、构造函数
在 TypeScript 中,类是一种用户自定义的数据类型,它允许您封装数据和相关操作。构造函数是类的一个特殊方法,用于初始化新创建的对象实例的状态。
下面是一个简单的示例,展示了如何在 TypeScript 中定义一个带有构造函数的类:
class Person {
constructor(public name: string, public age: number) {}
greet() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
// 创建一个 Person 对象实例
const person1 = new Person("Alice", 25);
person1.greet(); // 输出: Hello, my name is Alice and I am 25 years old.
在上面的示例中,Person
类有一个构造函数。当您使用 new
关键字创建 Person
类的对象实例时,构造函数会被调用,并传入 name
和 age
参数。这些参数被用来初始化新创建的对象实例的 name
和 age
属性。
注意,在 TypeScript 中,构造函数使用 constructor
关键字进行定义,并且它们总是在类定义的顶部。在构造函数中,您可以定义并初始化类的属性或执行其他必要的初始化操作。
构造函数可以有参数,并且参数可以带有类型注解。在上面的示例中,构造函数的参数具有类型注解 public name: string
和 public age: number
,这表示构造函数期望传入一个字符串和一个数字作为参数。通过使用类型注解,TypeScript 可以帮助您在编译时捕获类型错误。
八、参数成员
在 TypeScript 中,类的参数成员是指在类构造函数中定义的参数。这些参数被用来初始化类的属性或执行其他必要的初始化操作。
在类构造函数中,可以定义多个参数,每个参数都具有一个类型注解。这些类型注解用于指定参数的数据类型,以便在编译时进行类型检查。
下面是一个示例,展示了如何在 TypeScript 类中定义参数成员:
class Person {
constructor(public name: string, public age: number) {
// 在构造函数内部可以执行其他初始化操作
greet() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
在上面的示例中,Person
类有一个构造函数,它接受两个参数:name
和age
。这些参数被用来初始化类的属性name
和age
。注意,在构造函数参数的右侧使用类型注解public name: string
和public age: number
,这表示这些参数应该传入一个字符串和一个数字。
在构造函数内部,您可以执行其他初始化操作,例如为属性赋予默认值或进行其他必要的设置。在上面的示例中,构造函数没有进行其他操作,但您可以根据需要添加其他逻辑。
当您创建一个类的实例时,需要传递相应的参数给构造函数。例如:
const person1 = new Person("Alice", 25);
person1.greet(); // 输出: Hello, my name is Alice and I am 25 years old.
在上面的代码中,我们使用new
关键字创建了一个Person
类的对象实例,并传递了两个参数给构造函数:"Alice"
和25
。这些参数被用来初始化person1
对象的name
和age
属性。然后,我们调用greet方
法来输出问候语。
9.1、重写基类成员
在 TypeScript 中,派生类可以重写基类的成员。例如,如果我们有一个基类Animal
和一个派生类Dog
,Dog
可以重写Animal
的speak
方法:
class Animal {
speak(volume: number) {
console.log(`The animal speaks with volume ${volume}`);
class Dog extends Animal {
speak(volume: number) {
// 重写基类的 speak 方法
console.log(`The dog barks with volume ${volume}`);
let dog = new Dog();
dog.speak(5); // 输出: "The dog barks with volume 5"
9.2、派生类实例化
当我们创建一个派生类的实例时,这个实例会自动继承基类的属性和方法。在上述的例子中,我们可以创建一个Dog
类的实例并调用speak
方法:
let dog = new Dog();
dog.speak(5); // 输出: "The dog barks with volume 5"
9.3、单继承
在 TypeScript 中,一个类只能继承另一个类。这就是单继承的含义。
例如,我们可以创建一个Mammal
类,它是Animal
的子类:
class Mammal extends Animal {
feed() {
console.log("The mammal is being fed");
Mammal
继承了Animal
的所有属性和方法,包括speak
。
9.4、接口继承类
在 TypeScript 中,接口可以继承类或另一个接口的成员。这允许我们创建更具体的接口。例如,我们可以创建一个DogService
接口,它继承自AnimalService
接口:
interface AnimalService {
getAnimal(): Animal;
interface DogService extends AnimalService {
getDog(): Dog; // 扩展了 AnimalService 接口的 getAnimal 方法
DogService
接口继承了AnimalService
接口的所有成员(包括getAnimal
方法),并添加了一个新的方法getDog
。
十、实现接口
在TypeScript中,类可以实现接口。实现接口可以让一个类拥有接口所定义的方法和属性。这有助于实现代码的解耦和增强可读性。
要实现一个接口,类需要包含与接口中定义的方法和属性相同的签名。下面是一个示例:
//定义一个接口 Person
interface Person {
name: string; //一个字符串类型的属性。
age: number; // 一个数字类型的属性。
greet: (message: string) => void; //一个函数,接收一个字符串参数 message,没有返回值(返回类型为 void)。
//定义一个类 Emplyee
//这个类实现了上述定义的 Person 接口。这意味着它必须包含与 Persion 接口中定义的所有属性和方法相同的签名。
//在 TypeScript 中,implements 是一个关键字,用于实现接口。它表示一个类将实现一个接口的所有方法和属性。在这个例子中,Employee 类实现了 Person 接口,这意味着 Employee 类必须包含与 Person 接口中定义的所有属性和方法相同的签名。
class Employee implements Person {
name: string; //一个字符串类型的属性。
age: number; //一个数字类型的属性。
//类的构造函数,接收两个参数,一个字符串 name 和一个数字 age。这些参数用于初始化上述的两个属性。
constructor(name: string, age: number) {
this.name = name;
this.age = age;
//实现接口的 greet 方法。这个方法接收一个字符串参数 message 打印。
greet(message: string) {
console.log(`${message}, ${this.name}!`);
现在,我们可以创建一个Employee
对象并调用其方法:
const employee = new Employee("John", 30);
employee.greet("How are you?"); // 输出: "How are you, John!"
通过实现接口,我们可以确保类具有所需的规范,并且可以使用接口中定义的方法和属性来扩展类的功能。这使得代码更加灵活和可维护。在 TypeScript 中,接口提供了一种方法来定义一个类必须具有的结构和行为,以及可以在多个类之间共享的通用模板。
十一、静态成员
11.1、静态成员可访问性
在 TypeScript 中,类可以包含静态成员。静态成员是属于类的成员,而不是类的实例的成员。这意味着你可以在不创建类的实例的情况下访问静态成员。静态成员在类中定义,并且在类的任何实例上都是可访问的。
下面是一个简单的示例,展示了如何在 TypeScript 中定义和使用静态成员:
class MyClass {
static staticProperty: string = 'Hello, World!';
static staticMethod() {
console.log('This is a static method.');
// 访问静态属性
console.log(MyClass.staticProperty); // 输出:Hello, World!
// 调用静态方法
MyClass.staticMethod(); // 输出:This is a static method.
关于静态成员的访问性,TypeScript支持使用public
、protected
和private
修饰符来指定静态成员的可见性。这些修饰符的使用方式与实例成员相同。例如,你可以将静态属性或方法声明为public
,使其在类的任何地方都可以访问;或者使用private
修饰符将其限制在类的内部。
11.2、继承静态成员
继承是面向对象编程的一个重要概念,TypeScript 也支持类的继承。然而,与实例成员不同,静态成员不能被继承。这意味着子类不能继承父类的静态成员。每个类都有自己独立的静态成员。
虽然静态成员不能被继承,但子类可以通过原型链访问父类的静态成员。当你在子类中访问一个静态成员时,如果该成员在父类中不存在,那么将会在子类的原型链上查找该成员。这使得你可以在子类中重写父类的静态成员,就像你可以重写实例方法一样。
下面是一个示例,展示了如何在 TypeScript 中使用静态成员的继承:
class ParentClass {
static staticProperty: string = 'Hello from Parent';
class ChildClass extends ParentClass {
static staticProperty: string = 'Hello from Child';
// 访问父类的静态属性
console.log(ParentClass.staticProperty); // 输出:Hello from Parent
// 访问子类的静态属性(会覆盖父类的静态属性)
console.log(ChildClass.staticProperty); // 输出:Hello from Child
在上面的示例中,ParentClass
和ChildClass
都定义了一个名为staticProperty
的静态属性。由于ChildClass
继承自ParentClass
,所以ChildClass
可以访问ParentClass
的静态属性。同时,ChildClass
也定义了自己的静态属性,并且通过使用static
关键字,确保了它不会被继承。
十二、抽象类和抽象成员
在 TypeScript 中,抽象类(Abstract Class)和抽象成员(Abstract Members)是用于实现面向对象编程的重要特性。它们提供了一种方式来定义不能直接实例化的类,而是用作其他类的基类。
12.1、抽象类
抽象类是一种不能直接实例化的类,它用于定义抽象成员。抽象类只能被继承,并且派生类必须实现所有的抽象成员。
在 TypeScript 中,使用abstract
关键字来声明抽象类。下面是一个简单的示例:
abstract class AbstractClass {
abstract member1(): void;
abstract member2(): string;
在上面的示例中,AbstractClass
是一个抽象类,它定义了两个抽象成员member1
和member2
。这两个成员都被声明为抽象成员,因此派生类必须提供它们的具体实现。
12.2、抽象成员
抽象成员是定义在抽象类中的方法或属性,它没有具体的实现。派生类必须提供抽象成员的具体实现。
在 TypeScript 中,使用abstract
关键字来声明抽象成员。下面是一个简单的示例:
abstract class AbstractClass {
abstract member1(): void; // 抽象方法
abstract member2(): string; // 抽象属性
在上面的示例中,member1
和 member2
都被声明为抽象成员。这意味着任何继承自AbstractClass
的类都必须提供它们的具体实现。
派生类可以通过实现抽象成员来继承抽象类的行为。下面是一个使用抽象类和抽象成员的示例:
class DerivedClass extends AbstractClass {
member1(): void {
// 实现抽象方法 member1 的具体逻辑
member2(): string {
// 实现抽象属性 member2 的具体逻辑并返回一个字符串
return "Hello, world!";
在上面的示例中,DerivedClass
继承自AbstractClass
并实现了所有的抽象成员。现在,我们可以创建DerivedClass
的实例并调用它的方法:
const obj = new DerivedClass(); // 创建派生类的实例
obj.member1(); // 调用派生类实现的方法
console.log(obj.member2()); // 访问派生类实现的属性并打印结果:"Hello, world!"
十三、this 类型
在 TypeScript 中,类的this
类型是指在该类中this
关键字的类型。在 TypeScript 中,this
关键字用于访问当前对象的属性和方法。
在类中,this
关键字的类型是该类的实例类型。这意味着,当你在类的方法中使用this
关键字时,它引用的类型是该类的实例类型。
下面是一个简单的示例,展示了如何在 TypeScript 类中使用this
类型:
class MyClass {
constructor(private name: string) {}
sayHello() {
console.log(`Hello, my name is ${this.name}`); // this 指向 MyClass 的实例
let myObject = new MyClass("Alice");
myObject.sayHello(); // 输出 "Hello, my name is Alice"
在上面的示例中,MyClass
是一个简单的类,有一个私有属性name
和一个公有方法sayHello
。在sayHello
方法中,this
关键字用于引用MyClass
的实例对象。在实例化MyClass
时,我们传递了一个字符串参数 "Alice" 给构造函数,从而设置了实例的name
属性。然后,我们调用sayHello
方法,它使用this.name
来访问和打印实例的name
属性。
在 TypeScript 中,你可以使用类型断言来明确指定this
的类型。例如,如果你想在某个方法中明确指定this
的类型是MyClass
的实例类型,你可以使用类型断言:
class MyClass {
constructor(private name: string) {}
sayHello() {
console.log(`Hello, my name is ${this.name}`); // this 指向 MyClass 的实例
getThisType() {
// 使用类型断言明确指定 this 的类型为 MyClass 的实例类型
let thisType: MyClass = this;
return thisType;
十四、类类型
在 TypeScript 中,一个类的类型是由其所有实例共享的。这意味着,如果你有一个类的实例,你可以使用该类的任何方法或访问其任何属性,而无需再次实例化该类。类类型是一种用于表示类的结构和行为的类型。
在 TypeScript 中,类类型是通过使用class
关键字来声明的。下面是一个简单的示例:
class MyClass {
constructor(private name: string) {}
sayHello() {
console.log(`Hello, my name is ${this.name}`);
// MyClass 的类类型
let myClassType: MyClass;
// 创建一个 MyClass 的实例
let myObject = new MyClass("Alice");
// 可以使用类的实例方法
myObject.sayHello(); // 输出 "Hello, my name is Alice"
// 可以使用类的实例属性(在这个例子中没有实例属性,但可以有)
console.log(myObject.name); // 输出 "Alice"
在上面的示例中,我们声明了一个名为MyClass
的类,它有一个私有属性name
和一个公有方法sayHello
。然后,我们声明了一个名为myClassType
的变量,该变量的类型是MyClass
。这意味着我们可以将MyClass
的实例赋值给myClassType
变量。然后,我们创建了一个MyClass
的实例,并将其赋值给myObject
变量。我们可以使用myObject
变量来调用sayHello
方法,并访问name
属性。
需要注意的是,在 TypeScript 中,类的构造函数必须使用new
关键字来调用。这是因为 TypeScript 使用构造函数来创建类的实例,并且使用new
关键字可以确保类的实例被正确地初始化。