Typescript 凭什么可以和 JavaScript 并肩作战(1)—TypeScript 对类的支持

1,402 阅读6分钟

这是我参与8月更文挑战的第19天,活动详情查看:8月更文挑战

为了分享此篇文章,个人做了大量的工作,所以未经本人同意,请勿转载,在此表示感谢!

阅读提示:对 Typescript 中类继承(extends)和实现(implement)的区分给出解释和说明。

大家可能都是认为 Typescript 是 JavaScript 的超集(superset),可以编译为 Javascript。 Typescript 和 JavaScript 具有相同的能力,大家可能认为 Typescript 最终编译为 JavaScript,Typescript 作为 JavaScript 的超集,地位不断提升,算是比较很成功的 JavaScript 的超级,能够和其修饰的 JavaScript 位于同一排行榜,可见 TypeScript 有其独到之处。

在面向对象编程中,最核心的就是 class 概念,在 ES2015 中,javascript 已经引入的 class 关键词。不过今天我们来看一看 typescript 中如何对 JavaScript 的类的补充。

TypeScript 提供了对 ES2015 中引入的 class 关键词的完全支持。与其他 JavaScript 语言功能一样,TypeScript 增加了类型注释和其他语法,允许你表达类和其他类型之间的关系。

007.jpeg

定义类

类是将数据结构和一些可以操作和访问数据的行为组织在一起,具有一定封闭性。并且提供方法供用户访问和操作数据。

定义一个类

下面我们定义一个类 class 并不含任何内容。。虽然会涉及到用 TypeScript 创建类的一些基本方面,但其语法大多与用 JavaScript 创建类时相同。所以本将重点放在介绍 TypeScript 中的一些与 JavaScript 不同之处。

class Tut{}

类的属性

在类里面可以声明属性并指定类型,默认属性为 public 也就是可读写的属性,没有给出任何修饰符情况下,默认属性修饰符为 public

// class Tut{}
class Tut{
    title:string;
    lesson:number;
}

const machine_learning_tut = new Tut();
machine_learning_tut.title = "machine leaerning tut";
machine_learning_tut.lesson = 12;

console.log(machine_learning_tut) //Tut { title: 'machine leaerning tut', lesson: 12 }

属性声明时为属性指定类型是可选的,如果没有指定类型属性类型为 any

class Tut{
    title = "";
    lesson = 0;
}

const machine_learning_tut = new Tut();
machine_learning_tut.title = "machine leaerning tut";
//error TS2322: Type 'string' is not assignable to type 'number'.
machine_learning_tut.lesson = "12";

这里给属性进行进行了初始化,编译器会根据属性的初始值推断该属性类型,如果随后对该属性赋值不同于初始化值类型的值就会包编译错误,无法通过编译。

如果在编译时指定 strictPropertyInitializationtrue 这需要在构造函数初始化变量时给出初值。

{
    "compilerOptions": {
      ...
      "strictNullChecks":true,
      "strictPropertyInitialization":true,
    }
}
class Greeter{
    name:string
}

因为 "strictPropertyInitialization":true 而没有定义 Greeter 对于 name 属性给出初值编译时则会抛出下面错误信息。


Property 'name' has no initializer and is not definitely assigned in the constructor.

请注意, 因为"strictPropertyInitialization":true该字段需要在构造函数本身中初始化。TypeScript 不会分析你从构造函数中调用的方法来检测初始化,因为子类类可能会覆盖这些方法而无法初始化成员。

如果打算在构造函数以外为属性赋值,可以使用确定的赋值断言操作符(definite assignment assertion operator)!。

class Greeter{
    name!:string
}
class Employee{
    name:string
    constructor(name:string,public age:number){
        this.name = name;
    }
}

const mike = new Employee("mike",28)
console.log(mike.age)

私有属性

class VideoTut extends Tut{
    private title:string
    constructor(title:string){
        //'super' must be called before accessing 'this' in the constructor of a derived class.ts(17009)
        // console.log(this.category)
        super()
        this.title = title
    }
}

const videoTut = new VideoTut("machine learning");
// Property 'title' is private and only accessible within class 'VideoTut'
console.log(videoTut.title)

当将 titleprivate 修饰符修饰后该属性就成为了私有属性,外界无法访问。当试图访问时,就会抛出Property 'title' is private and only accessible within class 'VideoTut'错误。

只读属性(readonly)

如果类中属性前面有 readonly 修饰符,该属性值就只能在类的构造函数里进行修改。

class Employee{
    readonly position:string = "employee"
    name!:string;
    constructor(name:string,otherPosition?:string){
        if(otherPosition !== undefined){
            this.position = otherPosition;
            this.name = name;
        }
    }
    intro(){
        //Cannot assign to 'name' because it is a read-only property.ts(2540)
        this.position = "overide name field value"
    }
}

const mike = new Employee("mike");
//Cannot assign to 'position' because it is a read-only property.ts(2540)
mike.position = "overide name field valu"

构造函数(Constructors)

类构造函数与函数非常相似。可以添加带有类型注释的参数、默认值和重载。

class Tut{
    title:string;
    subTitle:string;

    constructor(title:string,subTitle:string){
        this.title = title;
        this.subTitle = subTitle
    }
}

构造函数重载,虽然构造函数也是函数,所以构造函数的重载也就是复合函数的重载。

class Tut{
    public title:string;
    public subTitle:string;

    constructor();
    constructor(title:string);
    constructor(title:any,subTitle?:any);
    constructor(title?:string,subTitle?:string){
        this.title = title;
        this.subTitle = subTitle;
    }
}

010.jpeg

类的构造函数签名和函数签名之间只有一些区别。

  • 构造函数不能有类型参数--这属于外层类的声明,有关这部分内容将在后面介绍。
  • 构造函数不能有返回类型注释,构造函数将返回类的实例类型。

Super 调用

class Tut {
    category:string ="programming"
}

class VideoTut extends Tut{
    constructor(){
        //'super' must be called before accessing 'this' in the constructor of a derived class.ts(17009)
        console.log(this.category)
        super()
    }
}

这里VideoTut 继承了

类的方法

在类内部定义函数被称为方法,方法可以使用与函数相同,并无明显区别。

class VideoTut extends Tut{
    private title:string;
    constructor(title:string){
        //'super' must be called before accessing 'this' in the constructor of a derived class.ts(17009)
        // console.log(this.category)
        super();
        this.title = title;
    }

    description():string{
        return `title: ${this.title}`
    }
}


const videoTut = new VideoTut("machine learning");
videoTut.description()//title: machine learning

008.jpeg

getters/setters

通过定义 set/get 方法我们可以设置一个属性访问设置,通过 set/get 外界可以访问或者修改一些类私有属性,但是无法修改修饰为 readonly 的属性。

class Employee{
    private _name:string
    constructor(name:string,public age:number){
        this._name = name;
    }
    get name(){
        return this._name;
    }
    set name(newName:string){
        this._name = newName;
    }
}

const mike = new Employee("mike",28)
console.log(mike.name)//mike

TypeScript 对访问器有一些特殊的推理规则。

  • 如果存在get,但没有set,则该属性自动是 readonly,如果在定义属性值已经指定了 readonly 则只有 set 方法
  • 如果没有指定setter参数的类型,将从getter的返回类型中推断出来
  • 获取器和设置器必须有相同的成员可见性

006.jpeg

继承和实现

继承(extends)

在大多数基于类的面向对象语言中,继承是一种机制,其中一个对象获得了父对象的所有属性和行为。继承允许程序员:创建建立在现有类之上的类

实现(implements)

可以用 implements 语句去实现一个类,编译过程会检查实现了 CanDoSomethingInterface 接口的类 Developer 是否满足实现接口要求。如果没有实现接口定义的内容,编译过程就会提示错误。

005.jpeg

interface CanDoSomethingInterface{
    canDoSomething():void;
}

class Employee{
    private _name:string
    constructor(name:string,public age:number){
        this._name = name;
    }
    get name(){
        return this._name;
    }
    set name(newName:string){
        this._name = newName;
    }
}

class Developer extends Employee implements CanDoSomethingInterface{

    canDoSomething():void{
        console.log(`${this.name} can write some code`)
    }
}

const mike = new Developer("mike",28)
// mike.name = "tony"
// console.log(mike.name)
mike.canDoSomething() //mike can write some code

通过继承,子类拥有其父类的所有属性和方法,可以覆写(实现)父类方法,也可以在父类基础扩展一些属性和方法。

可以实现多个接口

interface Sendable{
    send():void;
}

interface Receivable{
    receive():void;
}

class EMail implements Sendable,Receivable{
    send():void{

    }
    receive():void{
        
    }
}

对于一个类可以实现多个接口,上面 EMail 实现了 SendableReceivable 接口。看似接口实现和类继承很相似,但是这是两个不同的东西,首先接口更加抽象,很多语言将接口用 contract ,也是可以理解为类型,是一种基于行为或者数据的类型约束,或者契约,这个契约可以便于不同事物联系起来。

重要的是要明白, implements 子句只是检查类是否可以被当作接口类型,也就是检查该类是否实现了接口的方法。但不会改变类的类型或其方法。一个常见的错误来源是认为 implements 子句会改变类的类型。

在这个例子中,我们也许期望s的类型会受到 checkname: string参数的影响。其实不然--实现子句并没有改变类主体的检查方式或其类型的推断。

同样地,实现一个带有可选属性的接口并不能创建该属性。

覆写方法

子类也可以覆写基类的一个字段或属性。可以使用 super.语法来调用基类的方法。TypeScript要求子类始终是其基类的一个子类型。

class Employee{
  intro(){
    console.log("I am employee");
  }
}

class Developer extends Employee{
  intro(){
    super.intro();
    console.log("I'm a developer")
  }
}

const mike = new Developer()
mike.intro()

子类需要准寻其基类契约,所以子类可以作为基类类型来使用。

    class Employee{
        intro(){
            console.log("I am employee");
        }
    }

    class Developer extends Employee{
        intro(name?:string){
            if(name == undefined){
                super.intro();
            }else{
                console.log(`I'm a developer and my name is ${name}`)
            }
            
            
        }
    }

    const mike:Developer = new Developer()
    mike.intro("mike")

如果在子类实现 intro 方法时没有遵循父类对 intro方法定义就是抛出错误。

intro(name:string)
Property 'intro' in type 'Developer' is not assignable to the same property in base type 'Employee'.
  Type '(name: string) => void' is not assignable to type '() => void'.ts(2416)

011.jpeg

初始化顺序

    class Tut{
        level = "begin"
        constructor(){
            console.log(`level: ${this.level}`)
        }
    }

    class MachineLearningTut extends Tut{
        level = "advance" 
    }

    const tut = new MachineLearningTut()
    // level: begin

按照 JavaScript 的定义,类初始化的顺序是如何进行的。

  • 初始化基类的字段被
  • 执行基类构造函数
  • 初始化子类的字段
  • 执行子类构造函数 这意味着基类构造函数在自己的构造函数中看到了基类的 name 值而不是子类的 name,因为子类的字段初始化还没有运行。