Appearance
类
类是面对象语言的基础之一,TypeScript 也存在类,即 Class。TypeScript 允许在类成员、构造函数参数和方法签名上添加类型注解,以明确指定成员的类型。这使得编译器可以在编译时进行类型检查,捕获潜在的类型错误,并提供更好的编码辅助。JavaScript 没有这种静态类型检查功能。TypeScript 的类提供了更多的静态类型检查和其他特性,使得类的使用更加严谨和可维护。 传统的 JavaScript 程序使用函数和基于原型的继承来创建可重用的组件,但对于熟悉使用面向对象方式的程序员来讲就有些棘手,因为他们用的是基于类的继承并且对象是由类构建出来的。 从 ECMAScript 2015,也就是 ECMAScript 6 开始,JavaScript 程序员将能够使用基于类的面向对象的方式。 使用 TypeScript,我们允许开发者现在就使用这些特性,并且编译后的 JavaScript 可以在所有主流浏览器和平台上运行,而不需要等到下个 JavaScript 版本。
TypeScript 的类更像 C#或者 Dart,Java。下面我们来看第一个类:
typescript
class Person {
private name: string; // 使用 private 关键字定义私有属性
age: number; // 默认为公共属性
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
sayHello(): void {
console.log(`Hello, my name is ${this.name}.`); // 访问私有属性
}
}
const person = new Person("Alice", 25); // 创建类的实例
person.sayHello(); // 调用类的方法
console.log(person.age); // 访问类的属性
在上面的示例中,我们定义了一个名为 Person 的类。它有两个属性:name 是私有属性,age 是公共属性。我们使用 private 关键字将 name 属性声明为私有,只能在类的内部访问。
构造函数使用 constructor 关键字,在实例化类时被调用,用于初始化类的属性。
sayHello 是一个类的方法,它使用 void 来表示没有返回值。在方法中,我们使用模板字符串和 this 关键字来访问类的属性。
然后,我们创建了一个类的实例 person,并调用了 sayHello 方法和访问了 age 属性。
这个示例突出了以下语法特征:
类的成员和属性的可访问性修饰符(private 和默认的 public)。
构造函数的使用和初始化类的属性。
类的方法的定义和访问。
在类的内部使用 this 关键字来引用当前类的实例。
公共,私有与受保护的修饰符
在上面的示例中, 我们已经看到 private 关键字,它定义了私有属性。
TypeScript 还提供了 public 和 protected 修饰符,分别用于定义公有属性和受保护的属性。
public 修饰符:默认情况下,所有成员都是 public 的,可以任意访问。
protected 修饰符:protected 修饰符与 private 修饰符类似,但允许在派生类中访问。 在 JavaScript 中,我们通常使用 getter 和 setter 方法来处理私有属性。TypeScript 提供了更简单的方式来定义 getter 和 setter 方法,使用 get 和 set 关键字。 在 TypeScript 中,我们可以使用 get 和 set 关键字来定义 getter 和 setter 方法,如下所示:
typescript
class Person {
private _name: string;
constructor(name: string) {
this._name = name;
}
get name(): string {
return this._name;
}
set name(value: string) {
this._name = value;
}
sayHello(): void {
console.log(`Hello, my name is ${this.name}.`);
this.name = "Bob";
console.log(`My new name is ${this.name}.`);
}
}
protected修饰符与private修饰符的行为很相似,但有一点不同,protected成员在派生类中仍然可以访问。例如:
ts
class Person {
protected name: string;
constructor(name: string) { this.name = name; }
}
class Employee extends Person {
private department: string;
constructor(name: string, department: string) {
super(name)
this.department = department;
}
public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}
let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // 错误
TypeScript使用的是结构性类型系统。 当我们比较两种不同的类型时,并不在乎它们从何处而来,如果所有成员的类型都是兼容的,我们就认为它们的类型是兼容的。 然而,当我们比较带有private或protected成员的类型的时候,情况就不同了。 如果其中一个类型里包含一个private成员,那么只有当另外一个类型中也存在这样一个private成员, 并且它们都是来自同一处声明时,我们才认为这两个类型是兼容的。 对于protected成员也使用这个规则。
下面来看一个例子,更好地说明了这一点:
ts
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");
animal = rhino;
animal = employee; // 错误: Animal 与 Employee 不兼容.
注意,我们不能在Person类外使用name,但是我们仍然可以通过Employee类的实例方法访问,因为Employee是由Person派生而来的。 构造函数也可以被标记成protected。 这意味着这个类不能在包含它的类外被实例化,但是能被继承。
readonly修饰符
readonly修饰符与public修饰符一起使用,你可以使用readonly关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。 表示只读属性:
ts
class Octopus {
readonly name: string;
readonly numberOfLegs: number = 8;
constructor (theName: string) {
this.name = theName;
}
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // 错误! name 是只读的.
存取器
上面的代码中,我们提到过get/set TypeScript支持通过getters/setters来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。
先,存取器要求你将编译器设置为输出ECMAScript 5或更高。 不支持降级到ECMAScript 3。 其次,只带有get不带有set的存取器自动被推断为readonly。 这在从代码生成.d.ts文件时是有帮助的,因为利用这个属性的用户会看到不允许够改变它的值。
在属性被更新时,作为回调,这样的场景存取器会显得十分有用。
把类当做接口使用
如上一节里所讲的,类定义会创建两个东西:类的实例类型和一个构造函数。 因为类可以创建出类型,所以你能够在允许使用接口的地方使用类。
在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}.`);
}
}
class Student implements Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
sayHello(): void {
console.log(`Hello, I'm a student. My name is ${this.name}.`);
}
study(): void {
console.log(`${this.name} is studying.`);
}
}
const student = new Student("Alice", 20);
student.sayHello(); // 输出:Hello, I'm a student. My name is Alice.
student.study(); // 输出:Alice is studying.
在上面的示例中,我们定义了一个Person
类,它有name
和age
属性,以及sayHello
方法。然后,我们定义了一个Student
类,该类实现了Person
接口(使用关键字implements
)。Student
类必须实现接口中定义的所有属性和方法。
在Student
类中,我们声明了name
和age
属性,并实现了sayHello
方法和新增了study
方法。
接下来,我们创建了一个Student
类的实例,并调用了sayHello
和study
方法。通过将Student
类实现为Person
接口,我们可以确保Student
类具有接口中定义的属性和方法。
在这种用法下,类被视为接口,接口被视为一种约束,从而实现了类之间的约定和一致性。