这段时间有点忙,今天开始又空闲了,那就继续我们的TypeScript之旅!
总览:TypeScript图形渲染实战(2D架构设计和实现)详介
TypeScript图形渲染实战2D架构设计与实现:第2章 使用TypeScript实现Doom3词法解析器(1)
TypeScript图形渲染实战2D架构设计与实现:第2章 使用TypeScript实现Doom3词法解析器(2:Token与Tokenizer)
在上一节中,我们主要介绍了在TypeScript中如何:
- import引入模块中定义的类、函数、变量以及类型等,export导出模块中的自定义的接口;
- 声明接口方法;
- 声明接口的(只读)属性;
- ES6中的模版字符串;
今天的文章中,我们将会通过实现IDoom3Token接口来了解TypeScript如下几个要点:
- 使用implements关键字实现接口;
- 使用class关键字声明类;
- 类的三个访问级别:private / protected / public,默认为public;
- TypeScript中类型数组的初始化;
- 类的成员变量的初始化;
- 成员变量的显示断言赋值声明;
- constructor关键字实现构造函数
正文:
2.2 IDoom3Token与IDoom3Tokenizer接口的实现
在上一节中我们声明了IDoom3Token接口和IDoom3Tokenizer接口,本节我们来看一下这两个接口的具体实现过程。我们会发现接口和实现类之间的微妙关系,既:接口规定了要做什么,接口的实现类则规定了应该怎么去做。
2.2.1 Doom3Token类成员变量的声明
首先我们来看一下IDoom3Token接口的实现类,在TypeScript中使用implements关键字来实现一个接口,代码如下所示:
class Doom3Token implements IDoom3Token {
private _type : ETokenType ; // 标识当前token的类型 : NONE / STRING / NUMBER
private _charArr : string [ ] = [ ] ; // 字符串数组
private _val : number ; // 如果当前的token类型是NUMBER,则会设置该数值,如果是字符串类型,就忽略该变量
}
上面代码很简单,但是也有几个值得我们关注的地方:
- 在TypeScript / JavaScript中,并没有char这个数据类型,都是使用string类型来表示单个字符,我们在变量_charArr中存放的实际是char(一个字符)类型的数据。
- 在TypeScript中,有两种声明和实列化(内存分配)类型数组的方式,第一种就是我们上面所使用的方式,另外一种可以使用_charArray : Array < number > = new Array < string > ( )的方式,笔者更喜欢第一种方式来声明类型数组变量,简洁明了少写几个字。
- 我们会看到在声明IDoom3Token接口时使用了export关键字来导出接口,但是在实现类Doom3Token中并没有使用export关键字。这是接口的一个很棒的特性:我们只想暴露(export)接口(interface),我们想隐藏类(class)的实现,第三方调用时,只关心接口是怎么使用的,不需要知道具体类是怎么实现的。
- TypeScript支持public / protected / private 三个级别的访问修饰符,如果你没有在成员变量前声明访问修饰符,在默认情况下,被定义为public级别。关于三个访问修饰符的区别如下:
- 被public访问修饰符修饰的成员变量或方法能够被所有类访问。
- 被protected访问修饰符修饰的成员变量或方法既能被定义它的类也能被继承它的子类访问。
- 被private访问修饰符修饰的成员变量或方法只能被定义它的类访问,也就是说不能在声明它的类的外部访问。
2.2.2 Doom3Token类变量初始化的问题
接下来继续来看一下构造函数(constructor关键字)和reset函数,代码如下:
public constructor ( ) {
this . _charArr . length = 0 ;
this . _type = ETokenType . NONE ;
this . _val = 0.0 ;
}
public reset ( ) : void {
this . _charArr . length = 0 ;
this . _type = ETokenType . NONE ;
this . _val = 0.0 ;
}
我们会发现constructor中的代码和reset中的代码一模一样,那么读者可能会问,为什么不在constructor中直接调用reset函数呢?
其实这里涉及到TypeScript对成员变量初始化的时机点问题。大家可以试一下,如果我们在constructor中调用reset函数,TypeScript编译器会报“xxx属性没有初始化表达式,且未在构造函数中明确赋值。”的错误,如下图2.2所示。
从上述错误描述中我们可以知道,TypeScript对于成员变量的初始化有两个时机点,第一个时机点是在成员变量声明时立即进行赋值(初始化),如private _charArr : string [ ] = [ ] ; 这句代码所示,这种称为初始化表达式。
如果不在成员变量声明时立即赋值的话,那么就只能是在constructor构造函数中进行变量赋值(初始化)。但是你会发现,有时候延迟初始化或重新初始化是很有必要的一种操作。幸运的是,从TypeScript 2.7版本开始支持使用!(感叹号)来进行变量的显示断言赋值声明,我们来修改一下代码,看一下效果,具体代码如下:
// 使用!操作符来进行显示断言赋值声明
private _val ! : number ;
private _type ! : ETokenType ;
public constructor ( ) {
// this . _charArr . length = 0 ;
// this . _type = ETokenType . NONE ;
// this . _val = 0.0 ;
this . reset ( ) ;
}
我们会发现,TypeScript不再报初始化的错误了,是不是很棒的感觉。这是一个很有用的功能,可以让我们灵活的处理变量初始化的问题,因此值得在这里花点时间讨论一下。还是需要强调一点,在使用该变量前一定要初始化变量。
2.2.3 IDoom3Token接口方法的实现
接下来我们看一下Doom3Token类的其他几个接口方法的实现,具体代码如下:
// 使用get关键字来定义属性,get定义只读属性,set定义只写属性
public get type ( ) : ETokenType {
return this . _type ;
}
//获取当前token的字符串值
public getString ( ) : string {
// _charArr数组中存放的都是单个字符序列,例如[ d , o , o , m , 3 ]
// 我们可以使用数组的join方法将字符串联成字符串
// 下面会使用join方法后,会返回doom3这个字符串
return this . _charArr . join ( "" ) ;
}
// 获取当前token的浮点值
public getFloat ( ) : number {
return this . _val ;
}
// 获取当前token的int类型值
public getInt ( ) : number {
// 使用parserInt函数
// 第一个参数是一个字符串类型的数字表示
// 第二个参数是进制,我们一般用10进制
return parseInt ( this . _val . toString ( ) , 10 ) ;
}
我们来看一个字符串比较的接口方法的实现,具体代码如下所示:
public isString ( str : string ) : boolean {
let count : number = this . _charArr . length ;
// 字符串长度不相等,肯定不等
if ( str . length !== count ) {
return false ;
}
// 遍历每个字符
for ( let i : number = 0 ; i < count ; i++ ) {
// _charArr数组类型中每个char和输入的string类型中的每个char进行严格比较(!==操作符而不是!=)
// 只要任意一个char不相等,意味着整个字符串都不相等
if ( this . _charArr [ i ] !== str [ i ] ) {
return false ;
}
}
// 完全相等
return true ;
}
2.2.4 Doom3Token类的非接口方法实现
至此我们介绍了所有的接口方法的实现以及涉及到的、与TypeScript相关的语言要点,这些接口方法都是被第三方调用的,我们还要增加一些方法,这些方法由实现的内部类(例如IDoom3Tokenizer的实现类Doom3Tokenizer)所调用,但是它们并不需要被公开给第三方使用,下面我们就关注这些方法,具体代码如下所示:
// 下面三个非接口方法被IDoom3Tokenizer接口的实现类Doom3Tokenizer所使用
// 将一个char添加到_charArr数组的尾部
public addChar ( c : string ) : void {
this . _charArr . push ( c ) ;
}
// 设置数字,并将类型设置为NUMBER
public setVal ( num : number ) : void {
this . _val = num ;
this . _type = ETokenType . NUMBER ;
}
//设置类型
public setType ( type : ETokenType ) : void {
this . _type = type ;
}