尝试重写
在此之前,通过《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,这回真的去重写了。