4

尝试重写

在此之前,通过《JavaScript => TypeScript 入门》已经掌握了类型声明的写法。原以为凭着那一条无往不利的规则,就可以开开心心的重写 JS 项目了。当我跃跃欲试去重写一个 JS 项目时,发现阻碍重重。

在投靠 TS 时,TypeScript 允许将 JS 渐进式的过渡到 TS:

  • 一些简单的基本类型声明的填充,比如:number, :string, :string[] 等等;

  • 将一些稍复杂的、暂时还未提炼好的结构声明为 any 类型;

  • 改后缀为 .ts, 执行 tsc 编译。

但一个明显的问题是,一个 JS 文件中往往有很多模块依赖,意味着要把所有这些模块都重新做一次类型声明,才能整体编译通过。

所以,要想畅通无阻的重写 JS,在上一篇入门的基础上,还得强忍着冲动,继续学习两部分内容:

1、TS 类的写法、特征
2、TS 模块、命名空间

好在,它们和 ES6 其实有着很多重合的地方——要记得,TS 是 ES6 的超集。所以如果掌握了 ES6,我们只要挑差异,找增量,进行迁移学习,就能快速掌握上述内容。

ES6 的类

ES6 的类的概念,和普遍编程语言的类概念一致,表示一种特有的数据结构。它既像一个函数(能被 new 调用),又像一个对象(包含了各种属性、方法)。

class MyClass {
  constructor() {
    this.attr = 'my attribute';
  }
  foo(name) {
    console.log('hello ' +  name);
  }
}
let myclass = new MyClass();

类中的方法属性被称为 “成员”,这些成员大概被分成 3 种类别:

  • 公有属性、公有方法

  • 私有属性、私有方法

  • 静态属性、静态方法

在写继承时,我们并不希望在所有时候,这些属性、方法都被继承到子类;在访问成员时,也不希望在任何时候,类实例的所有成员都无一例外可以被访问;有时候我们希望与此相反,这样能保持开放出去的信息简洁干净。

JS 很早就有这些概念,但到现在为止,ES6 并没有处理好这个事情。

私有属性、私有方法

ES6 没有直观的表示私有属性、私有方法的方案,只能通过变通的方式间接地表示。

const getKeys = Symbol('getKeys_');
const attr = Symbol('attr_');
class MyClass {
  constructor() {
    // 私有属性
    this[attr] = 'private attribute';
  }
  // 公有方法
  foo(config) {
    return this[fn](config);
  }
  // 私有方法
  [getKeys](config) {
    return Object.keys(config);
  }
};

这仅仅是一个间接取巧的方式避免直接被访问到,但很轻易就能绕过它:

let myclass = new MyClass();

let symbolsAttr = Object.getOwnPropertySymbols(myclass);
let attr_ = myclass[symbolsAttr[0]]; // 即访问到私有属性

let symbolsFn = Object.getOwnPropertySymbols(myclass.__proto__);
let getKeys_ = myclass[symbolsFn[0]]; // 即访问到私有方法

私有方法、私有属性的目的是不想开放过多的信息到外部。上面变通的方案虽然表面上达到了目的,但是不够直观,也不安全。

静态属性、静态方法

ES6 目前支持静态方法表示,类属性及静态属性目前作为提案还未正式成为标准。

class MyClass {
  name = 'jeremy';  // ES6 不被支持,仅作为提案
  static version = '1.0.0'; // ES6 不被支持,仅作为提案
  constructor() {
    console.log(MyClass.version); // '1.0.0'
  }
  static get Version() {
    return MyClass.version;
  }
}

区分这些成员身份,与动态类型、静态类型语言没有必然联系,可以预见,在不久的将来,ES6 这方面将得到完善。

TS 的类

鉴于 TS 是 ES6 的超集这一事实,TS 类当然也有私有属性、私有方法静态属性、静态方法等这些身份的成员。在标记成员身份上,TS 与 Java 有很多相似之处。比如与继承相关的访问修饰符,以及其他限定作用的非访问修饰符

访问修饰符

  • private

  • public

  • protected

非访问修饰符

  • static

  • readonly

  • abstract

class User {
    readonly name: string;
    public age: number;
    private sex: string;
    protected marriage: string;
    constructor(name: string, sex: string) { 
        this.name = name;
        this.sex = sex;
    }
    showAge() {
        console.log(`${this.name}, age ${this.age}`);
    }
}

访问修饰符

1、当成员被标记成 private 时,它就不能在声明它的类的外部访问。
2、protected 修饰符与 private 修饰符的行为很相似,但有一点不同,protected 成员在派生类中仍然可以访问
3、被声明为 public 的类、方法、构造方法和接口能够被任何其他类访问。

class User {
    readonly name: string;
    public age: number;
    private sex: string;
    protected marriage: string;
    constructor(name: string, sex: string) { 
        this.name = name;
        this.sex = sex;
    }
    showAge() {
        console.log(this.age);
    }
}

let a = new User('jerry', 'male');
console.log(a.sex); // 编译报错:私有属性不许在本类之外被访问
console.log(a.marriage); // 编译报错:私有属性不许在类之外被访问
a.name = 'jeremy'; // 编译报错:只读属性不许再次被写入

访问修饰符主要用在继承的可访问性上,继承好比遗传,用基因类比理解它们就很有意思:

  • private 私有基因——本体有效,不会被继承,即子类中无法访问到

  • protected 被保护基因——保护血统纯正,只允许在继承体系中访问

  • public 公共基因——子类、其他类都能访问到

有了访问修饰符,每个成员都有扮演着与身份对应的角色,真正做到名副其实。

访问修饰符和非访问修饰符加起来有6个甚至更多,它们如何与类、接口一起搭配工作,考虑到组合情况非常多,此处不去细致的探究。可以参考 Java 的规则:

default (即缺省,什么也不写): 在同一包(等同于JS中的模块)内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。

private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)

public : 对所有类可见。使用对象:类、接口、变量、方法

protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。

当然更直观的方式是上代码 + 编译。TS 的编译工具有着非常友好、明确编译错误提示。

class Demo {
    static private showSelf() {}
}
// 编译错误提示
error TS1029: 'private' modifier must precede 'static' modifier.

当然,去查阅官方文档也是一个好办法。

非访问修饰符

非访问修饰符不关心可访问性,被它单独标记的成员,在任何时候都能访问到。目前至少有这些:

  • static

  • get

  • set

  • readonly

  • abstract

从字面上看,不难理解它们的用途。这里和 ES6 差别较大的是 abstract

abstract 修饰符

abstract 修饰符用来定义抽象类和在抽象类内部定义抽象方法。

在 Java 中,抽象类不能用来实例化对象,主要做为其它派生类的基类使用。 不同于接口,抽象类可以包含成员的实现细节。

TS 中也是这样规定的:抽象类不允许直接被实例化。

还有一点很重要,抽象类中的抽象方法可以不包含具体实现,但必须在派生类中实现。

// 抽象类
abstract class User {
    readonly name: string;
    public age: number;
    private sex: string;
    protected marriage: string;
    constructor(name: string, sex: string) { 
        this.name = name;
        this.sex = sex;
    }
    showAge() {
        console.log(this.age);
    }
    // 抽象方法
    abstract showName(): void;
}

// let a = new User('jerry', 'male'); // 编译报错: 抽象类不允许直接实例化

容易忽略的一个问题是,一个类中一旦出现抽象方法,那这个类整个应该被标记为 abstract

class UserA extends User {
    constructor(name: string, sex: string) {
        super(name, sex);
        // console.log(this.sex); // 编译报错:私有变量不能被访问
    }
    // 抽象方法必须要在子类中实现
    // 若无此方法,编译报错
    showName() {
       console.log(this.name); 
    }
}

基本上,对 TS 类只需要掌握这些,深入的部分自然在实践中继续领悟。

对了,还有模块、命名空间没有梳理。因为码文字的麻烦,只得留到后续再补。但是直接搬用 ES6 的模块import, export 就完全够用了。

So,这回真的去重写了。


Jeremy_young
128 声望2 粉丝