类
class 是 ECMAScript6 中新增的语法,用于定义一个 类,在 TypeScript 中也有,并且有更多特性
类的基础
与 ECMAScript 中的类语法结构基本类似
class关键字- 构造函数:
constructor - 成员属性定义
- 成员方法
- this关键字
重点要说明的是与 ECMAScript 中不同的点
什么是类
对象 : 对某种事物所拥有的特征和行为进行的一种结构化描述
interface User {
id: number;
username: string;
password: string;
postArticle(title: string, content: string): void;
}
// 通过 key / value 结构描述一个对象(一个用户)
let user1 = {
id: 1,
username: 'huanghuilian',
password: '123456',
postArticle(title: string, content: string) {
console.log(`${this.username} 发表了一篇文章: ${title}`)
}
}
let user2 = {
id: 2,
username: 'xiaoxiannv',
password: '654321',
postArticle(title: string, content: string) {
console.log(`${this.username} 发表了一篇文章: ${title}`)
}
}
类 : 对一类具有相同特性事物的抽象描述,通过 class 来描述一个类,组织类的结构
class User {
}
成员属性与方法定义
class User {
id: number;
username: string;
password: string;
postArticle(title: string, content: string): void {
// 在类的内部可以通过 `this` 来访问成员属性和方法
console.log(`${this.username} 发表了一篇文章: ${title}`)
}
}
在
ECMAScript7之前,类的成员属性是在构造函数中进行初始化的
构造函数
通过 new 运算符 + 类名,可以创建一个该类所描述的对象,我们称这个过程为:实例化
let user1 = new User;
let user2 = new User;
当我们 new User 的时候,会自动调用该类下的一个名为 constructor 的方法,如果没有显式定义该方法,则会自动创建一个无参的 constructor 的空方法
class User {
constructor() {}
}
注意:构造函数
constructor不允许有返回值类型标注
class User {
id: number;
username: string;
password: string;
constructor(id: number, username: string, password: string) {
this.id = id;
this.username = username;
this.password = password;
}
postArticle(title: string, content: string): void {
console.log(`${this.username} 发表了一篇文章: ${title}`)
}
}
let user1 = new User(1, 'huanghuilian', '123456');
let user2 = new User(2, 'xiaoxiannv', '654321');
构造函数参数属性
我们可以给构造函数参数添加修饰符来直接生成成员属性
class User {
constructor(
public id: number,
public username: string,
public password: string
) {
// 可以省略初始化赋值
}
postArticle(title: string, content: string): void {
console.log(`${this.username} 发表了一篇文章: ${title}`)
}
}
let user1 = new User(1, 'huanghuilian', '123456');
let user2 = new User(2, 'xiaoxiannv', '654321');
继承
我们可以通过 extends 关键字来实现类的继承
class VIP extends User {
}
super 关键字
在子类中,我们可以通过 super 来引用父类
- 如果子类有自己的构造函数,则需要在子类构造函数中显示的调用父类构造函数 :
super(//参数),否则会报错 - 在子类构造函数中只有在
super(//参数)之后才能访问this - 如果子类没有重写构造函数,则会在默认的
constructor中无参调用super() - 在子类中,可以通过
super来访问父类的成员属性和方法 - 通过
super访问父类的的同时,会自动绑定上下文对象为当前子类this
class VIP extends User {
constructor(
id: number,
username: string,
password: string,
public allowFileTypes = ['png','gif','jpg']
) {
super(id, username, password);
}
postAttachment(file: File): void {
console.log(`${this.username} 上传了一个附件: ${file.name}`)
}
}
let vip1 = new VIP(1, 'Leo', '123456');
let fileElement = <HTMLInputElement>document.querySelector('input[type="file"]');
let file = fileElement.files && fileElement.files[0];
file && vip1.postAttachment(file);
方法重载
class VIP extends User {
constructor(
id: number,
username: string,
password: string,
public allowFileTypes = ['png','gif','jpg']
) {
super(id, username, password);
}
// postArticle 方法重载
postArticle(title: string, content: string, file?: File): void {
// 通过 super 调用父类实例方法
super.postArticle(title, content);
file && this.postAttachment(file);
}
postAttachment(file: File): void {
console.log(`${this.username} 上传了一个附件: ${file.name}`)
}
}
// 具体使用场景
let vip1 = new VIP(1, 'Leo', '123456');
let fileElement = document.querySelector('input[type="file"]') as HTMLInputElement;
let buttonElement = document.querySelector('button') as HTMLButtonElement;
buttonElement.onclick = function() {
// vip1.postArticle('标题一', '内容一');
let file;
if (fileElement.files) {
file = fileElement.files[0];
// vip1.postAttachment(file);
}
vip1.postArticle('标题一', '内容一', file);
}
修饰符
有的时候,我们希望对类成员(属性、方法)进行一定的访问控制,来保证数据的安全,通过 类修饰符 可以做到这一点,目前 TypeScript 提供了四种修饰符:
- public:公有,默认
- protected:受保护
- private:私有
- readonly:只读
public 修饰符
这个是类成员的默认修饰符,它的访问级别为:
- 自身
- 子类
- 类外
protected 修饰符
它的访问级别为:
- 自身
- 子类
private 修饰符
它的访问级别为:
- 自身
readonly 修饰符
只读修饰符只能针对成员属性使用,且必须在声明时或构造函数里被初始化,它的访问级别为:
- 自身
- 子类
- 类外
class User {
constructor(
// 可以访问,但是一旦确定不能修改
readonly id: number,
// 可以访问,但是不能外部修改
protected username: string,
// 外部包括子类不能访问,也不可修改
private password: string
) {
// ...
}
// ...
}
let user1 = new User(1, 'huanghuilian', '123456');
寄存器
有的时候,我们需要对类成员 属性 进行更加细腻的控制,就可以使用 寄存器 来完成这个需求,通过 寄存器,我们可以对类成员属性的访问进行拦截并加以控制,更好的控制成员属性的设置和访问边界,寄存器分为两种:
- getter
- setter
getter
访问控制器,当访问指定成员属性时调用
setter
设置控制器,当设置指定成员属性时调用
class User {
private _id: number;
private _username: string;
private _password: string;
constructor(id: number, username: string, password: string) {
this.id = id;
this.username = username;
this.password = password;
}
public set id(id: number) {
this._id = id;
}
public get id() {
return this._id;
}
public set username(username: string) {
this._username = username;
}
public get username() {
return this._username;
}
public set password(password: string) {
if (password.length >= 6) {
this._password = password;
}
}
public get password() {
return '******';
}
// ...
}
静态成员
前面我们说到的是成员属性和方法都是实例对象的,但是有的时候,我们需要给类本身添加成员
type allow_file_type_list = 'png'|'gif'|'jpg'|'jpeg'|'webp';
class VIP extends User {
// static 必须再 readonly 之前
static readonly ALLOW_FILE_TYPE_LIST: Array<allow_file_type_list> = ['png','gif','jpg','jpeg','webp'];
private _allowFileTypes: Array<allow_file_type_list>;
constructor(
id: number,
username: string,
password: string,
allowFileTypes: Array<allow_file_type_list> = ['png','gif','jpg']
) {
super(id, username, password);
this._allowFileTypes = allowFileTypes;
}
public set allowFileTypes(types: Array<allow_file_type_list>) {
this._allowFileTypes = types;
}
public get allowFileTypes() {
return this._allowFileTypes;
}
public addType(type: allow_file_type_list) {
this._allowFileTypes.push(type);
}
}
let vip1 = new VIP(1, 'zMouse', '123456', ['jpg','jpeg']);
// vip1
console.log(vip1.allowFileTypes);
let vip2 = new VIP(2, 'MT', '654321');
// vip1
console.log(vip2.allowFileTypes);
// 所有 VIP 可以设置的附件类型
console.log(VIP.ALLOW_FILE_TYPE_LIST);
- 类的静态成员是属于类的,所以不能通过实例对象(包括 this)来进行访问,而是直接通过类名访问(不管是类内还是类外)
- 静态成员也可以通过访问修饰符进行修饰
- 静态成员属性一般约定(非规定)全大写
抽象类
有的时候,一个基类(父类)的一些方法无法确定具体的行为,而是由继承的子类去实现,看下面的例子,
现在要通过一个类来美化系统的 MessageBox,它包含了:alert、confirm 和 prompt,设计结构如下:
// MessageBox
class MessageBox {
constructor(){}
show(){}
close(){}
// 注意这里,对于alert、confirm 和 prompt,它们有自己不同的内容,所以MessageBox无法去确定setContent的具体行为
setContent(content: string){}
}
// alert
class Alert extends MessageBox {
constructor(){
super()
}
// 重写
setContent(content: string){
// 内容+一个确定按钮
}
}
// confirm
class Confirm extends MessageBox {
constructor(){
super()
}
// 重写
setContent(content: string){
// 内容+一个确定按钮+一个取消按钮
}
}
//prompt
class Prompt extends MessageBox {
constructor(){
super()
}
// 重写
setContent(content: string){
// 一个输入框+一个确定按钮+一个取消按钮
}
}
大家可以发现每个子类都重写了父类的 setContent 方法,父类的 setContent 方法并不需要去实现什么,这个时候我们可以抽象父类的 setContent 方法
abstract 关键字
如果一个方法没有具体的实现方法,则可以通过 abstract 关键字进行修饰
// MessageBox
abstract class MessageBox {
constructor(){}
show(){}
close(){}
// 注意这里,对于alert、confirm 和 prompt,它们有自己不同的内容,所以MessageBox无法去确定setContent的具体行为
abstract setContent(content: string): void
}
使用抽象类有一个好处:
约定了所有继承子类的所必须实现的方法,使类的涉及更加的规范
这里需要注意:
- abstract 修饰的方法不能有方法体
- 如果一个类有抽象方法,那么该类也必须为抽象的
- 如果一个类是抽象的,那么就不能使用 new 进行实例化(因为抽象类表名该类有未实现的方法,所以不允许实例化)
- 如果一个子类继承了一个抽象类,那么该子类就必须实现抽象类中的所有抽象方法,否则该类还得声明为抽象的
类与接口
通过接口,我们可以为对象定义一种结构和契约。我们还可以把接口与类进行结合,通过接口,让类去强制符合某种契约,从某个方面来说,当一个抽象类中只有抽象的时候,它就与接口没有太大区别了,但是 类会产生实体代码,接口不会
- 一个类使用
implements关键字来确定要实现的接口,当一个类implements了某个接口,那么该类必须实现接口中定义的结构
// 数据格式
interface SpreadSheetData {
name: string;
description: string;
}
// 定义一个SpreadSheet接口
interface SpreadSheetInfo {
getInfo(): SpreadSheetData;
}
// 用户
class User implements SpreadSheetInfo {
constructor(
private id: number,
private name: string,
private gender: string
) {
}
getInfo() {
return {
name: this.name,
description: `我叫 ${this.name},性别 ${this.gender}`
}
}
}
// 课程
class Course implements SpreadSheetInfo {
constructor(
private id: number,
private type: string,
private title: string,
) {
}
getInfo() {
return {
name: this.title,
description: `${this.type} 新课程 ${this.title}`
}
}
}
// 电子表格
class SpreadSheet {
private _datas: Array<SpreadSheetData>
public get datas() {
return this._datas;
}
add(origin: SpreadSheetInfo) {
this._datas.push( origin.getInfo() );
}
}
let spreadSheet = new SpreadSheet();
let user1 = new User(1, 'huanghuilian', '女');
let user2 = new User(1, 'xiaoxiannv', '女');
let course1 = new Course(1, 'js', 'vue');
let course2 = new Course(2, 'js', 'react');
spreadSheet.add( user1 );
spreadSheet.add( user2 );
spreadSheet.add( course1 );
spreadSheet.add( course2 );
TypeScript只支持单继承,不支持继承多个父类,而一个类可以实现多个接口,多个接口使用,分隔
interface SpreadSheetInfo {
getInfo(): SpreadSheetData;
}
interface IStorage extends ILogger {
save(data: string): void;
}
- 接口也可以继承
interface SpreadSheetInfo {
getInfo(): SpreadSheetData;
}
// IStorage
interface IStorage extends ILogger {
save(data: string): void;
}
类与对象类型
当我们在 TypeScript 定义一个类的时候,其实同时定义了两个不同的类型
- 类类型(构造函数类型)
- 对象类型
首先,对象类型好理解,就是我们的 new 出来的实例类型
那类类型是什么,我们知道 JavaScript 中的类,或者说是 TypeScript 中的类其实本质上还是一个函数,当然我们也称为构造函数,那么这个类或者构造函数本身也是有类型的,那么这个类型就是类的类型
class Person {
// 属于类的
static type = '人';
// 属于实例的
name: string;
age: number;
gender: string;
// 类的构造函数也是属于类的
constructor( name: string, age: number, gender: '男'|'女' = '男' ) {
this.name = name;
this.age = age;
this.gender = gender;
}
}
let p1 = new Person('huanghuilian', 18, '女');
let Person2: typeof Person = Person;
console.log(Person2.type);
封装一个工厂函数
function createInstance(constructor: Person): Person {
// 这是有错误的,因为 Person 表示的 new 出来的实例的类型,而不是构造函数(类)的类型
return new constructor('huanghuilian', 18, '女');
}
正确的做法
interface PersonConstructor {
new (name: string, age: number, gender: '男'|'女'): Person;
}
function createInstance(constructor: PersonConstructor): Person {
return new constructor('huanghuilian', 18, '女');
}
或者
type PersonConstructor = typeof Person;
function createInstance(constructor: PersonConstructor): Person {
return new constructor('huanghuilian', 18, '女');
}
注意上面的 typeof Person,我们就是通过 typeof 来获取这个类的类类型,这里的 typeof 与 JavaScript 中的 typeof 有一定的差异性,细节可见类型系统深入