类的基本示例
class Greeter {
greeting: string
constructor(message: string) {
this.greeting = message
}
greet() {
return 'Hello, ' + this.greeting
}
}
let greeter = new Greeter('world')
我们声明一个 Greeter
类。这个类有 3 个成员:一个叫做 greeting
的属性,一个constructor
构造函数和一个 greet
方法。
你会注意到,我们在引用任何一个类成员的时候都用了 this
。 它表示我们访问的是类的成员。
最后一行,我们使用 new
构造了Greeter
类的一个实例。它会调用之前定义的构造函数,创建一个 Greeter
类型的新对象,并执行构造函数初始化它。
继承
在 TypeScript 里,我们可以使用常用的面向对象模式。 基于类的程序设计中一种最基本的模式是允许使用继承来扩展现有的类。
class Animal {
move(distance: number = 0) {
console.log(`Animal moved ${distance}m.`)
}
}
class Dog extends Animal {
bark() {
console.log('Woof! Woof!')
}
}
const dog = new Dog()
dog.bark()
dog.move(10)
这个例子展示了最基本的继承:类从基类中继承了属性和方法。 这里,Dog
是一个 派生类,它派生自 Animal
基类,通过 extends
关键字。 派生类通常被称作子类,基类通常被称作超类。
因为 Dog
继承了 Animal
的功能,因此我们可以创建一个 Dog
的实例,它能够 bark()
和 move()
。
第二个例子:
class Animal {
name: string
constructor(name: string) {
this.name = name
}
move(distance: number = 0) {
console.log(`${this.name} moved ${distance}m.`)
}
}
class Snake extends Animal {
constructor(name: string) {
super(name)
}
move(distance: number = 5) {
console.log('Slithering...')
super.move(distance)
}
}
class Horse extends Animal {
constructor(name: string) {
super(name)
}
move(distance: number = 45) {
console.log('Galloping...')
super.move(distance)
}
}
let sam = new Snake('Sammy')
let tom: Animal = new Horse('Tommy')
sam.move()
tom.move(34)
这个例子展示了一些上面没有提到的特性。 这一次,我们使用 extends
关键字创建了 Animal
的两个子类:Horse
和 Snake
。
与前一个例子的不同点是,派生类包含了一个构造函数,它 必须调用 super()
,它会执行基类的构造函数。 而且,在构造函数里访问 this 的属性之前,我们 一定要调用 super()
。 这个是 TypeScript 强制执行的一条重要规则。
这个例子演示了如何在子类里可以重写父类的方法。Snake类和 Horse 类都创建了 move 方法,它们重写了从 Animal 继承来的 move 方法,使得 move 方法根据不同的类而具有不同的功能。注意,即使 tom 被声明为 Animal 类型,但因为它的值是 Horse,调用 tom.move(34) 时,它会调用 Horse 里重写的方法。
公共,私有与受保护的修饰符
默认为public
在上面的例子里,我们可以自由的访问程序里定义的成员。 如果你对其它语言中的类比较了解,就会注意到我们在之前的代码里并没有使用 public 来做修饰;
class Animal {
public name: string
public constructor(name: string) {
this.name = name
}
public move(distance: number) {
console.log(`${this.name} moved ${distance}m.`)
}
}
private
当成员被标记成 private 时,它就不能在声明它的类的外部访问。比如:
class Animal {
private name: string
constructor(name: string) {
this.name = name
}
}
new Animal('Cat').name // 错误: 'name' 是私有的.
如果其中一个类型里包含一个 private 成员,那么只有当另外一个类型中也存在这样一个 private 成员,并且它们都是来自同一处声明时,我们才认为这两个类型是兼容的。
class Animal {
private name: string
constructor(name: string) {
this.name = name
}
}
class Rhino extends Animal {
constructor() {
super('Rhino')
}
}
class Employee {
private name: string
constructor(name: string) {
this.name = name
}
}
let animal = new Animal('Goat')
let rhino = new Rhino()
let employee = new Employee('Bob')
animal = rhino
animal = employee // 错误: Animal 与 Employee 不兼容.
protected:受保护修饰符
class Person {
// protected: 受保护成员,只能在子类中使用
protected name: string
//protected: 不可被 new Person()
protected constructor(name: string) {
this.name = name
}
}
class Employee extends Person {
private department: string
constructor(name: string, department: string) {
super(name)
this.department = department
}
getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}`
}
}
let howard = new Employee('Howard', 'Sales')
console.log(howard.getElevatorPitch())
构造函数也可以被标记成 protected。 这意味着这个类不能在包含它的类外被实例化,但是能被继承。
readonly修饰符
使用 readonly 关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。
class Person {
readonly name: string
constructor(name: string) {
this.name = name
}
}
let john = new Person('john')
john.name = '' //error:不可以对只读属性赋值
// 给参数加上readonly修饰符,同样不可修改
class Person {
constructor(readonly name: string) {
}
}
let bob = new Person('Bob')
console.log(bob.name)
bob.name = '' //报错
存取器
TypeScript 支持通过 getters/setters 来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。
下面例子是验证密码,然后允许修改name信息
let passcode = 'secret passcode'
class Employee {
private _fullName: string
get fullName(): string {
return this._fullName
}
set fullName(newName: string) {
if (passcode && passcode == 'secret passcode') {
this._fullName = newName
}
else {
console.log('Error: Unauthorized update of employee!')
}
}
}
let employee = new Employee()
employee.fullName = 'Bob Smith'
if (employee.fullName) {
console.log(employee.fullName)
}
我们可以修改一下密码,来验证一下存取器是否是工作的。当密码不对时,会提示我们没有权限去修改员工。
对于存取器有下面几点需要注意的:
首先,存取器要求你将编译器设置为输出 ECMAScript 5 或更高。 不支持降级到 ECMAScript 3。其次,只带有 get 不带有 set 的存取器自动被推断为 readonly。这在从代码生成 .d.ts 文件时是有帮助的,因为利用这个属性的用户会看到不允许够改变它的值
静态属性
创建类的静态成员,这些属性存在于类本身上面而不是类的实例上。 在这个例子里,我们使用 static
定义 origin
,因为它是所有网格都会用到的属性。 每个实例想要访问这个属性的时候,都要在 origin 前面加上类名。 如同在实例属性上使用 this.xxx
来访问属性一样,这里我们使用 Grid.xxx
来访问静态属性。
// 创建网格类
class Grid {
static origin = { x: 0, y: 0 }
scale: number
constructor(scale: number) {
this.scale = scale
}
// 计算点距离
calculateDistanceFromOrigin(point: { x: number, y: number }) {
// 计算坐标差
let xDist = point.x - Grid.origin.x
let yDist = point.y - Grid.origin.y
return Math.sqrt(xDist * xDist + yDist * yDist) * this.scale
}
}
let grid1 = new Grid(1.0) // 传入缩放比例
let grid2 = new Grid(5.0)
console.log(grid1.calculateDistanceFromOrigin({ x: 3, y: 4 })) // 5
console.log(grid2.calculateDistanceFromOrigin({ x: 3, y: 4 })) // 25
抽象类
抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。不同于接口,抽象类可以包含成员的实现细节。 abstract
关键字是用于定义抽象类和在抽象类内部定义抽象方法。
// abstract 定义抽象类
abstract class Animal {
// abstract 抽象方法
abstract makeSound(): void
move(): void {
console.log('roaming the earth...')
}
}
抽象类中的抽象方法不包含具体实现并且必须在派生类中实现。 抽象方法的语法与接口方法相似。两者都是定义方法签名但不包含方法体。 然而,抽象方法必须包含 abstract 关键字并且可以包含访问修饰符。
abstract class Department {
name: string
constructor(name: string) {
this.name = name
}
printName(): void {
console.log(`Department name ${this.name}`)
}
// 定义函数签名,在派生类中具体实现
abstract printMeeting(): void
}
class AccountingDepartment extends Department {
constructor() {
super('Accounting and Auditing')// 在派生类的构造函数中必须调用 super()
}
// 实现抽象方法
printMeeting(): void {
console.log('The Accounting Department meets each Monday at 10am.')
}
// 定义自己的成员方法
generateReports(): void {
console.log('Generating accounting reports...')
}
}
let department: Department // 允许创建一个对抽象类型的引用
department = new Department() // 错误: 不能创建一个抽象类的实例
department = new AccountingDepartment() // 允许对一个抽象子类进行实例化和赋值
department.printName()
department.printMeeting()
department.generateReports() // 错误: 方法在声明的抽象类中不存在
高级技巧
构造函数
当你在 TypeScript 里声明了一个类的时候,实际上同时声明了很多东西。首先就是类的实例的类型
class Greeter {
static standardGreeting = 'Hello, there'
greeting: string
constructor(message: string) {
this.greeting = message
}
greet() {
return 'Hello, ' + this.greeting
}
}
// 我们写了 let greeter: Greeter,意思是 Greeter 类的实例的类型是 Greeter。
let greeter: Greeter
greeter = new Greeter('world')
console.log(greeter.greet())
稍微改写一下这个例子
class Greeter {
static standardGreeting = 'Hello, there'
greeting: string
constructor(message?: string) {
this.greeting = message
}
greet() {
if (this.greeting) {
return `Hello, ${this.greeting}`
} else {
return Greeter.standardGreeting
}
}
}
let greeter: Greeter
greeter = new Greeter()
console.log(greeter.greet())//Hello, there
// 使用 typeof 声明 greeterMaker 是 Greeter的类的静态类型
let greeterMaker: typeof Greeter = Greeter
// 接下来可以修改静态属性
greeterMaker.standardGreeting = 'Hey there'
let greeter2 = new greeterMaker()
console.log(greeter.greet())//Hey there
我们直接使用类。 我们创建了一个叫做 greeterMaker
的变量。这个变量保存了这个类或者说保存了类构造函数。 然后我们使用 typeof Greeter
,意思是取 Greeter
类的类型,而不是实例的类型。或者更确切的说,"告诉我 Greeter
标识符的类型",也就是构造函数的类型。 这个类型包含了类的所有静态成员和构造函数。 之后,就和前面一样,我们在 greeterMaker
上使用 new
,创建 Greeter
的实例。
把类当做接口使用
类定义会创建两个东西:类的实例类型和一个构造函数。 因为类可以创建出类型,所以你能够在允许使用接口的地方使用类。
class Point {
x: number
y: number
}
// extends使Point3d可以共享 Point下的属性
interface Point3d extends Point {
z: number
}
let point3d: Point3d = {x: 1, y: 2, z: 3}