TypeScript 类
类
在 TypeScript 中,类是一种用于创建对象和组织代码的结构。
创建一个类:
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
sayHi() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
}
let person1 = new Person('Alice', 25);
person1.sayHi();
声明一个 Person
类。这个类有3个成员:
name
和age
是类的属性,用于存储对象的状态信息。constructor
是类的构造函数,用于在创建对象时进行初始化操作,为属性赋值。sayHi
是类的方法,用于定义对象的行为。
在引用任何一个类成员的时候都用了 this
。 它表示我们访问的是类的成员。
继承
在 TypeScript 中,类继承是一种面向对象编程的特性,允许一个类(子类)从另一个类(父类)继承属性和方法。
示例:
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Dog extends Animal {
breed: string;
constructor(name: string) {
super(name);
this.breed = breed;
}
bark() {
console.log('Woof! Woof!');
}
}
let dog = new Dog('Buddy', 'Labrador');
dog.move(10);
dog.bark();
上述代码:
- 定义父类
Animal
,它具有属性name
、构造函数constructor
和方法move
。 - 定义子类
Dog
,通过extends
关键字继承自Animal
。-
Dog
必须调用super()
,它会执行基类的构造函数。
在构造函数里访问this
的属性之前,我们 一定要调用super()
。 这个是TypeScript强制执行的一条重要规则。 -
Dog
类有自己的属性breed
。 -
Dog
类还具有自己的方法bark
。
-
这个例子展示了最基本的继承:类从基类中继承了属性和方法。
Dog
是一个 派生类,它派生自 Animal
基类,通过 extends
关键字。 派生类通常被称作 子类,基类通常被称作 超类。
子类可以重写父类的方法,以提供自己的实现。
示例:
class Bird extends Animal {
constructor(name: string) {
super(name);
}
// 重写move方法
move(distanceInMeters: number) {
console.log(`${this.name} flew ${distanceInMeters}m.`);
}
}
let bird = new Bird('Sparrow');
bird.move(5);
let bird1: Animal = new Bird("magpie");
bird1.move(10);
示例中,Bird
类重写了父类 Animal
的 move
方法,以提供适合鸟类移动的描述。
注意:bird1
被声明为 Animal
类型,但因为它的值是 Bird
,调用 bird1.move(10)
时,它会调用 Bird
里重写的move
方法。
在不同的子类中重写父类的方法,使 方法 根据不同的类而具有不同的功能。
类继承有助于代码的复用和组织,通过继承,可以在子类中扩展和修改父类的功能,从而构建更复杂和有层次的对象模型。
类的访问修饰符
类支持访问修饰符:
public
(公共):默认,可在任何地方访问。private
(私有):只能在类内部访问,子类和类的外部不能访问。protected
(受保护): 在类内部和子类中访问。
公共( public
)
public
是默认的修饰符,如果没有明确指定,类成员就是公共的。
也可以明确的将一个成员标记成 public
。
public
成员可以在类的内部、子类以及类的实例的任何地方被访问和修改。
重写上面的 Animal
类:
class Animal {
public name: string;
public constructor(theName: string) { this.name = theName; }
public move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
私有(private
)
当成员被标记成 private
时,它就不能在声明它的类的外部访问。
private
成员只能在其所属的类内部被访问和修改。在类的外部(包括子类)访问私有成员会导致编译错误。
private
成员在类的实例中不能访问。
示例:
class Animal {
// name 是 Animal 的私有成员
private name: string;
constructor(theName: string) { this.name = theName; }
}
class Dog extends Animal {
constructor(name: string) {
super(name);
}
bark() {
// Error: Property 'name' is private and only accessible within class 'Animal'.
// 此句报错:属性“name”为私有属性,只能在类“Animal”中访问。
console.log(`The dog's name is ${this.name}`);
}
}
new Animal("Cat").name; // 错误: 属性“name”为私有属性,只能在类“Animal”中访问。
TypeScript使用的是结构性类型系统。 当我们比较两种不同的类型时,并不在乎它们从何处而来,如果所有成员的类型都是兼容的,我们就认为它们的类型是兼容的。
当比较带有 private
或 protected
成员的类型的时候:如果其中一个类型里包含一个 private
成员,那么只有当另外一个类型中也存在这样一个 private
成员, 并且它们都是来自同一处声明时,我们才认为这两个类型是兼容的。 对于 protected
成员也使用这个规则。
示例:
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
class Rhino extends Animal {
constructor() { super("Rhino"); }
}
class Employee {
private name: string;
constructor(theName: string) { this.name = theName; }
}
let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee("Bob");
// 合法,因为 Rhino 是 Animal 的子类,子类的对象可以赋值给父类类型的变量,这是多态的一种体现。
animal = rhino;
// 错误。不能将类型“Employee”分配给类型“Animal”。类型具有私有属性“name”的单独声明。
// Employee 和 Animal 没有继承关系,并且它们的私有属性 name 是相互独立和不兼容的。
animal = employee;
受保护(protected
)
protected
修饰符与 private
修饰符的行为很相似,但有一点不同: protected
成员在派生类中仍然可以访问。
protected
成员在类的实例中不能访问。
示例:
class Animal {
protected name: string;
constructor(theName: string) { this.name = theName; }
}
class Dog extends Animal {
protected color: string;
constructor(name: string, color: string) {
super(name);
this.color = color
}
bark() {
console.log(`The dog's name is ${this.name}, and color is ${this.color}`);
}
}
let dog = new Dog("Carl", "white");
dog.bark(); // "The dog's name is Carl, and color is white"
new Animal("Cat").name; // 错误: 属性“name”受保护,只能在类“Animal”及其子类中访问。
注意:不能在 Animal
类外使用 name
,可以通过 Dog
类的实例方法访问,因为 Dog
是由 Animal
派生而来的。
构造函数也可以被标记成 protected
。 这意味着这个类不能在包含它的类外被实例化,但是能被继承。
示例:
class Animal {
protected name: string;
// 把构造函数标记为protected
protected constructor(theName: string) { this.name = theName; }
}
// Dog 能继承 Animal
// Dog 调用 `super()`,执行基类 Animal 受保护的 构造函数。
class Dog extends Animal {
color: string;
constructor(name: string, color: string) {
super(name);
this.color = color
}
bark() {
console.log(`The dog's name is ${this.name}, and color is ${this.color}`);
}
}
let dog = new Dog("Carl", "white");
dog.bark(); // "The dog's name is Carl, and color is white"
new Animal("Cat").name; // 错误: 类“Animal”的构造函数是受保护的,仅可在类声明中访问。
readonly修饰符
readonly
成员只能读取,不能被重新赋值。
readonly
成员必须在声明时或构造函数里被初始化。
class Point {
readonly x: number;
readonly y: number = 10;
constructor(x: number, y?: number) {
this.x = x;
this.y = y ?? this.y;
}
showPoint() {
console.log(`x: ${this.x}, y: ${this.y}`)
}
}
let p = new Point(10, 20);
p.showPoint();
let p1 = new Point(50);
p1.showPoint();
p1.x = 30; // Error: 无法为“x”赋值,因为它是只读属性。
参数属性
在 TypeScript 中,类的参数属性是一种简洁的方式来同时声明和初始化类的属性。
示例:
class Point {
constructor(readonly x: number, readonly y?: number) {}
}
在构造函数里仅使用 readonly x: number, readonly y?: number
参数来创建和初始化 x
、y
。
把声明和赋值合并在一个地方。
参数属性通过给构造函数参数前面添加一个访问限定符来声明。比如:
class Animal {
constructor(protected name: string;) {}
}
class Animal {
constructor(private name: string;) {}
}
getters/setters
TypeScript支持通过 getters
(获取器)和 setters
(设置器)来截取对对象成员的访问。
示例:没有使用getters/setters
class User {
name: string;
}
let user = new User();
user.name = "Rigo";
console.log(user.name); // "Rigo"
user.name = "张三";
console.log(user.name); // "张三"
可以随意的设置 name
。
把 Animal
类改写成使用 get
和 set
: set
方法中,验证用户输入的密码,密码正确才可以修改name
。使用get
方法获取 name
。
class User {
private _name: string = "";
get name(): string {
return this._name;
}
set name(newName: string) {
let pwd = prompt("请输入密码", "password");
if(pwd === "password") {
this._name = newName;
}else {
console.log("密码错误!");
}
}
}
let user = new User();
user.name = "张三";
console.log(user.name); // 张三
在 User
类中,private _name: string
声明变量会报错:Property ‘_name’ has no initializer and is not definitely assigned in the constructor.
这个错误是因为 TypeScript 在严格的类型检查模式下,要求非 readonly
且类型不可为 null
或 undefined
的属性必须在构造函数中被初始化,或者具有明确的初始值。
private _name: string = "";
为 _name
属性添加初始值 ""
时,满足了 TypeScript 对于属性初始化的要求。TypeScript 就能够确定在对象创建时该属性有一个明确的初始状态,从而避免了可能出现未初始化就被使用的情况。
注意:只有 get
且没有 set
的存取器自动被推断为 readonly
。 因为没有 set
提供修改值的方法。
静态属性
在 TypeScript 中,静态属性是属于类本身而不是类的实例的属性。
类的实例成员:仅当类被实例化的时候才会被初始化的属性。
类的静态成员:属于类本身而不是类的实例的属性。
每个实例通过 类名.静态属性
访问静态属性。比如示例中的MyClass.staticProperty
。
示例:
class MyClass {
// 静态属性
static staticProperty: number = 10;
// 实例方法
instanceMethod() {
console.log(`实例化后访问静态属性值:${MyClass.staticProperty}`);
}
}
// 直接通过类名访问静态属性
console.log(MyClass.staticProperty);
// 实例化后,才能通过实例 instance 访问 instanceMethod()
let instance = new MyClass();
instance.instanceMethod();
let instance1 = new MyClass();
instance1.instanceMethod();
在这个示例中,无论创建多少个 instance
的实例,staticProperty
的值都是共享且不变的。
静态属性通常用于存储与类相关的全局数据或共享状态,并且在类的所有实例之间是共享的。
抽象类
抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。 抽象类可以包含成员的实现细节。
abstract
关键字是用于定义抽象类和在抽象类内部定义抽象方法。
// 定义抽象类 Animal
abstract class Animal {
// 抽象方法 makeSound
abstract makeSound(): void;
// 非抽象方法 move
move() {
console.log('The animal is moving.');
}
}
抽象类中的抽象方法不包含具体实现并且必须在派生类中实现。
抽象方法只定义方法签名。
抽象方法必须包含 abstract
关键字并且可以包含访问修饰符。
abstract class Animal {
abstract makeSound(): void; // 必须在派生类中实现
move() {
console.log("The animal is moving.");
}
}
class Dog extends Animal {
makeSound() {
console.log("Woof!");
}
jump() {
console.log("dog jump")
}
}
let dog = new Dog();
dog.makeSound();
dog.move();
dog.jump();
let cat = new Animal(); // Error: 无法创建抽象类的实例。
由于 Animal
是抽象类,不能直接创建 Animal
的实例。
允许创建一个对抽象类型的引用:
abstract class Animal {
abstract makeSound(): void; // 必须在派生类中实现
move() {
console.log("The animal is moving.");
}
}
class Dog extends Animal {
makeSound() {
console.log("Woof!");
}
}
class Cat extends Animal {
makeSound() {
console.log("喵~");
}
}
// operateAnimal 函数接受一个类型为 Animal 的参数。
function operateAnimal(animal: Animal) {
animal.makeSound();
animal.move();
}
let dog = new Dog();
let cat = new Cat();
operateAnimal(dog);
operateAnimal(cat);
operateAnimal
函数接受一个类型为 Animal
的参数。
当我们调用 operateAnimal(dog)
和 operateAnimal(cat)
时,参数传递的实际对象分别是 dog
和 cat
的实例。在函数内部,我们可以通过 animal
这个抽象类型的引用统一地调用它们共有的方法。
构造函数
在 TypeScript 中,构造函数是类中的一个特殊方法,用于在创建类的实例时进行初始化操作。
当在TypeScript里声明了一个类的时候,实际上同时声明了很多东西。 首先就是类的 实例的类型。
声明一个类:
class Person {
name: string;
age: number;
// 这是构造函数
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
let person1: Person = new Person('Alice', 25);
在上述代码中,constructor
就是 Person
类的构造函数。
let person1: Person
,声明了一个变量 person1
,它的类型为 Person
类的实例类型。意思是 Person
类的实例的类型是 Person
。
person1
预期存储的是通过 new Person(...)
方式创建出来的符合 Person
类结构的对象实例。
把这段代码编译成js:
var Person = /** @class */ (function () {
// 这是构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
return Person;
}());
var person1 = new Person('Alice', 25);
这段代码使用了立即执行函数表达式(IIFE)来定义一个名为 Person
的类。
在这个立即执行函数内部:
- 定义了一个名为
Person
的构造函数function Person(name, age) {...}
。 - 这个构造函数接受两个参数:
name
和age
。 - 在函数内部,通过
this.name = name;
和this.age = age;
将传入的参数分别赋值给实例的name
和age
属性,从而完成对新创建实例的初始化。 - 最后,整个立即执行函数返回了这个构造函数
Person
。
var Person
将被赋值为构造函数。 当我们调用 new
并执行了这个函数后,便会得到一个类的实例。
把类当做接口使用
类定义会创建两个东西:类的实例类型和一个构造函数。 因为类可以创建出类型,所以能在允许使用接口的地方使用类。
class Point {
x: number;
y: number;
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};