typeScript与ECMAScript面向对象编程差异

479 阅读9分钟

classECMAScript6 中新增的语法,用于定义一个 类,在 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 来获取这个类的类类型,这里的 typeofJavaScript 中的 typeof 有一定的差异性,细节可见类型系统深入