类
类的概念
在介绍 TypeScript
中类的用法之前,我们有必要先在这里对类相关的概念做一个简单的介绍。
- 类(
Class
):定义了一件事物的抽象特点,包含它的属性和方法; - 对象(
Object
):类的实例,通过new
生成; - 面向对象(
OOP
)的三大特性:封装、继承、多态; - 封装(
Encapsulation
):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据; - 继承(
Inheritance
):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性; - 多态(
Polymorphism
):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如Cat
和Dog
都继承自Animal
,但是分别实现了自己的eat
方法。此时针对某一个实例,我们无需了解它是Cat
还是Dog
,就可以直接调用eat
方法,程序会自动判断出来应该如何执行eat
; - 存取器(
getter & setter
):用以改变属性的读取和赋值行为; - 修饰符(
Modifiers
):修饰符是一些关键字,用于限定成员或类型的性质。比如public
表示公有属性或方法; - 抽象类(
Abstract Class
):抽象类是供其他类继承的基类,抽象类不允许被实例化。抽象类中的抽象方法必须在子类中被实现; - 接口(
Interfaces
):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(Implements
)。一个类只能继承自另一个类,但是可以实现多个接口
类的基本定义与使用
class Greeter {
// 声明属性
greeting: string
// 构造方法
constructor(message: string) {
this.greeting = message
}
// 一般方法
greet() {
return 'Hello, ' + this.greeting
}
}
// 创建类的实例
let greeter = new Greeter('world')
// 调用实例的方法
greeter.greet()
类的继承
class Animal {
run(distance: number) {
console.log(`Animal run ${distance}m`)
}
}
class Dog extends Animal {
cry() {
console.log('wang! wang!')
}
}
const dog = new Dog()
dog.cry()
dog.run(100) // 可以调用
使用 extends
关键字实现继承,子类中使用 super
关键字来调用父类的构造函数和方法。从基类中继承了属性和方法。 这里,Dog
是一个派生类,它派生自 Animal
基类,通过 extends
关键字。 派生类通常被称作子类,基类通常被称作超类。
因为 Dog
继承了 Animal
的功能,因此我们可以创建一个 Dog
的实例,它拥有 cry()
和 run()
实例方法。
class Animal {
name: string
constructor(name: string) {
this.name = name
}
run(distance: number = 0) {
console.log(`${this.name} run ${distance}m`)
}
}
class Snake extends Animal {
constructor(name: string) {
// 调用父类型构造方法
super(name)
}
// 重写父类型的方法
run(distance: number = 5) {
console.log('sliding...')
super.run(distance)
}
}
class Horse extends Animal {
constructor(name: string) {
// 调用父类型构造方法
super(name)
}
// 重写父类型的方法
run(distance: number = 50) {
console.log('dashing...')
// 调用父类型的一般方法
super.run(distance)
}
xxx() {
console.log('xxx()')
}
}
const snake = new Snake('sn')
snake.run()
const horse = new Horse('ho')
horse.run()
// 父类型引用指向子类型的实例 ==> 多态
const tom: Animal = new Horse('ho22')
tom.run()
/* 如果子类型没有扩展的方法, 可以让子类型引用指向父类型的实例 */
const tom3: Snake = new Animal('tom3')
tom3.run()
/* 如果子类型有扩展的方法, 不能让子类型引用指向父类型的实例 */
// const tom2: Horse = new Animal('tom2')
// tom2.run()
这个例子展示了一些上面没有提到的特性。 这一次,我们使用 extends
关键字创建了 Animal
的两个子类:Horse
和 Snake
。
与前一个例子的不同之处是,派生类拥有自己的构造函数,并且在自己的构造函数中必须调用 super()
来执行基类的构造函数。 而且,在构造函数里访问 this
的属性之前,我们一定要调用 super()
。
这个例子演示了如何在子类里可以重写父类的方法。Snake
类和 Horse
类都创建了 run
方法,它们重写了从 Animal
继承来的 run
方法,使得 run
方法根据不同的类而具有不同的功能。注意,即使 tom
被声明为 Animal
类型,但因为它的值仍然是 Horse
,所以调用 tom.run(34)
时,它还是会调用 Horse
里重写的方法。
存取器
TypeScript
支持通过 getter/setter
来改变属性的赋值和读取行为。 它能帮助你有效的控制对对象成员的访问。
class Person {
firstName: string = 'A'
lastName: string = 'B'
get fullName() {
return this.firstName + '-' + this.lastName
}
set fullName(value) {
const names = value.split('-')
this.firstName = names[0]
this.lastName = names[1]
}
}
const p = new Person()
console.log(p.fullName)
p.firstName = 'C'
p.lastName = 'D'
console.log(p.fullName)
p.fullName = 'E-F'
console.log(p.firstName, p.lastName)
类的静态成员
到目前为止,我们只讨论了类的实例成员,那些仅当类被实例化的时候才会被初始化的属性。 我们也可以创建类的静态成员,这些属性存在于类本身上面而不是类的实例上。
静态属性, 是类对象的属性,非静态属性, 是类的实例对象的属性。
静态成员分为静态属性和静态方法,在 ES7
提案中,我们可以在变量或方法的前面加上 static
关键字来定义静态属性或静态方法。
静态属性
typescriptclass Animal { static num = 42 constructor() { // ... } } console.log(Animal.num) // 42
静态方法
typescriptclass Animal { static isAnimal(a) { return a instanceof Animal } } let a = new Animal('Jack') Animal.isAnimal(a) // true a.isAnimal(a) // TypeError: a.isAnimal is not a function
访问修饰符
TypeScript
可以使用三种访问修饰符(Access Modifiers
),分别是 public
、private
和 protected
。
public
修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是public
的;private
修饰的属性或方法是私有的,不能在声明它的类的外部访问;protected
修饰的属性或方法是受保护的,它和private
类似,区别是它在子类中也是允许被访问的;
public
在上面的例子里,我们可以自由的访问程序里定义的成员。 如果你对其它语言中的类比较了解,就会注意到我们在之前的代码里并没有使用 public
来做修饰;例如,C# 要求必须明确地使用 public
指定成员是可见的。 在 TypeScript
里,成员都默认为 public
。
class Animal {
public name
public constructor(name) {
this.name = name
}
}
let a = new Animal('Jack')
console.log(a.name) // Jack
a.name = 'Tom'
console.log(a.name) // Tom
上面的例子中,name
被设置为了 public
,所以直接访问实例的 name
属性是允许的。
private
很多时候,我们希望有的属性是无法直接存取的,这时候就可以用 private
了,当成员被标记成 private
时,它就不能在声明它的类的外部访问。
class Animal {
private name
public constructor(name) {
this.name = name
}
}
let a = new Animal('Jack')
console.log(a.name) // Jack
a.name = 'Tom'
// index.ts(9,13): error TS2341: Property 'name' is private and only accessible within class 'Animal'.
// index.ts(10,1): error TS2341: Property 'name' is private and only accessible within class 'Animal'.
需要注意的是,TypeScript
编译之后的代码中,并没有限制 private
属性在外部的可访问性。
上面的例子编译后的代码是:
var Animal = (function () {
function Animal(name) {
this.name = name
}
return Animal
})()
var a = new Animal('Jack')
console.log(a.name)
a.name = 'Tom'
使用 private
修饰的属性或方法,在子类中也是不允许访问的:
class Animal {
private name
public constructor(name) {
this.name = name
}
}
class Cat extends Animal {
constructor(name) {
super(name)
console.log(this.name)
}
}
// index.ts(11,17): error TS2341: Property 'name' is private and only accessible within class 'Animal'.
而如果是用 protected
修饰,则允许在子类中访问。
protected
protected
修饰符与 private
修饰符的行为很相似,但有一点不同,protected
成员在派生类中仍然可以访问。例如:
class Animal {
public name: string
public constructor(name: string) {
this.name = name
}
public run(distance: number = 0) {
console.log(`${this.name} run ${distance}m`)
}
}
class Person extends Animal {
private age: number = 18
protected sex: string = '男'
run(distance: number = 5) {
console.log('Person jumping...')
super.run(distance)
}
}
class Student extends Person {
run(distance: number = 6) {
console.log('Student jumping...')
console.log(this.sex) // 子类能看到父类中受保护的成员
// console.log(this.age) // 子类看不到父类中私有的成员
super.run(distance)
}
}
console.log(new Person('abc').name) // 公开的可见
// console.log(new Person('abc').sex) // 受保护的不可见
// console.log(new Person('abc').age) // 私有的不可见
readonly 修饰符
你可以使用 readonly
关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。
class Person {
readonly name: string = 'abc'
constructor(name: string) {
this.name = name
}
}
let john = new Person('John')
// john.name = 'peter' // error
参数属性
在上面的例子中,我们必须在 Person
类里定义一个只读成员 name
和一个参数为 name
的构造函数,并且立刻将 name
的值赋给 this.name
,这种情况经常会遇到。 参数属性可以方便地让我们在一个地方定义并初始化一个成员。 下面的例子是对之前 Person
类的修改版,使用了参数属性:
class Person2 {
constructor(readonly name: string) {}
}
const p = new Person2('jack')
console.log(p.name)
注意看我们是如何舍弃参数 name
,仅在构造函数里使用 readonly name: string
参数来创建和初始化 name
成员。 我们把声明和赋值合并至一处。
参数属性通过给构造函数参数前面添加一个访问限定符来声明。使用 private
限定一个参数属性会声明并初始化一个私有成员;对于 public
和 protected
来说也是一样
抽象类
抽象类做为其它派生类的基类使用。abstract
关键字用于定义抽象类和在抽象类内部定义抽象方法。
什么是抽象类?
首先,抽象类是不允许被实例化的:
abstract class Animal {
public name
public constructor(name) {
this.name = name
}
public abstract sayHi()
}
let a = new Animal('Jack')
// index.ts(9,11): error TS2511: Cannot create an instance of the abstract class 'Animal'.
上面的例子中,我们定义了一个抽象类 Animal
,并且定义了一个抽象方法 sayHi
。在实例化抽象类的时候报错了。
其次,抽象类中的抽象方法必须被子类实现:
abstract class Animal {
public name
public constructor(name) {
this.name = name
}
public abstract sayHi()
}
class Cat extends Animal {
public eat() {
console.log(`${this.name} is eating.`)
}
}
let cat = new Cat('Tom')
// index.ts(9,7): error TS2515: Non-abstract class 'Cat' does not implement inherited abstract member 'sayHi' from class 'Animal'.
上面的例子中,我们定义了一个类 Cat
继承了抽象类 Animal
,但是没有实现抽象方法 sayHi
,所以编译报错了。
下面是一个正确使用抽象类的例子:
abstract class Animal {
public name
public constructor(name) {
this.name = name
}
public abstract sayHi()
}
class Cat extends Animal {
public sayHi() {
console.log(`Meow, My name is ${this.name}`)
}
}
let cat = new Cat('Tom')
上面的例子中,我们实现了抽象方法 sayHi
,编译通过了。