TypeScript系列 基础篇(五) Classes 类
TypeScript 系列 基础篇(五) Classes 类
类 在 JavaScript
中出现于 ES2015
版本,TS
对 类 进行了全面支持,还加入了一些其它语法来增强类的表达能力,本文将详细聊一聊TS
中的类的知识,你知道的和不知道的,这里都有。
一、类的成员
1. 属性字段 (Fields
)
字段声明会为类添加创建一个公共的可写的实例属性。我们可以为字段添加类型注释,如果不添加,就会是 any
类型,当然这是我们不希望发生的。
1 | class Person { |
字段声明时可以赋初值,其类型会被 TS 自动推论,在实例化时会自动执行值的初始化。
1 | class Person { |
如果开启了严格属性初始化检查: strictPropertyInitialization
,则没有赋初值的字段必须在构造函数中初始化,不能在其它的方法中初始化,TS 不会去检测其它方法内的初始化。
1 | class Person { |
事实上,开启该检测是为了防止属性值为空带来的意外错误。我们可以使用非空断言来明确该属性不会为空,这样也不会报错。
1 | class Person { |
2. readonly
只读属性
添加了 readonly
修饰符的属性,将不允许在构造函数以外的地方中进行重新赋值。
1 | class Person { |
3. constructor
构造函数
构造函数接收实例化时传入的参数,可以提供参数默认值。在构造函数中进行类实例的初始化操作,可以分配属性值、调用类的方法等。
1 | class Person { |
还记得我们在Typescript 系列:(二)函数篇里讲的函数重载吗?构造函数自然也可以重载。注意构造函数的重载签名和实现签名是没有返回值类型的。
1 | class Person { |
4. 调用 super( )
我们知道,类可以通过 extends
关键字来继承一个基类。此时,我们在构造函数中使用 this
关键字之前需要先调用 super( )
,相当于调用了父类的构造函数。
1 | class Person { |
5. methods
方法
类里面的函数叫做方法。声明一个方法不要用 function
关键字。
1 | class Person { |
6. 存取器 setters/getters
和 JS
里没什么差别。
1 | class Person { |
对于存取器,TS
有几个特别的推论:
- 如果有
get
而没有set
,则该属性会被推论为readonly
只读属性; - 如果
setter
没有明确参数的类型,则会推论为getter
的返回值的类型; getter
和setter
的可见性保持一致。
7. 索引签名
类 也可以使用索引签名,和在对象类型里使用差不多。
1 | class Person { |
二、类的继承
1. implement
语句
使用 implements
语句检查类是否符合某接口规范。实现某个接口,则类中需要含有该接口的所有属性和方法才能通过检测。
1 | interface Person { |
可以同时实现多个接口:
1 | interface Person { |
注意 implements 语句只是检测类是否符合接口规范。
2. extends
语句
- 通过
extends
语句可以让类继承一个基类,获得它所有的属性和方法,还能定义自己的属性和方法。
1 | class Person { |
- 重写父类方法,可以通过
super.xx( )
来调用父类的方法。子类的方法需要能兼容父类的方法,包括参数数量、类型,以及返回值。
1 | class Person { |
- 字段类型声明
在父类的构造函数执行完之后,才会开始子类的初始化,期间可能改写来自父类的属性或方法。当 子类的某个属性 是 父类相应属性 的子类型时,这个过程就会浪费性能。可以通过 declare
关键字来声明字段类型,使其不受运行时效果的影响。
1 | interface Animal { |
- 初始化顺序
父类字段初始化 —> 父类构造函数执行 —> 子类字段初始化 —> 子类构造函数执行
3. 继承内置类型
继承内置类型,如Array
、Error
等,当在构造函数中调用super( )
之后,this
的原型指向会错误地指向super
的调用者,即Array
、Error
等内置类型。ES6
使用 new.target
来调整原型链,但是在 ES5
中却保证不了 new.target
的值。因此,我们在调用super()
之后,要手动调整原型链,让this
的原型指向我们新的类。 Object.setPrototypeOf( )
便是要用的方法 (不支持该方法的可以退一步使用Object.prototype.__proto__
)。
1 | class MsgError1 extends Error { |
需要注意,这种问题会一直传递下去,也就是说,以 MsgError2
为基类所创造的子类,也需要再次手动调整原型的指向。此外,不支持IE10
及更低的版本。
三、成员的可见性 Member Visibility
在 TS 中,实现了 public
,protected
,private
等修饰符来实现成员的可见性。
1. public
public
修饰符用来定义公开成员,这也是默认的成员可见性,当没有写可见性修饰符时,就默认是 public
。被声明为public
的成员,可在任何地方访问。太简单了就不给栗子了。
2. protected
被 protected
修饰的成员只能在类或者其子类中访问,无法通过实例来访问。
1 | class Person { |
在子类中,如果我们通过字段重新声明了基类中的 protected
成员,则会将其在子类中变为 public
成员,除非重新加上 protected
修饰符:
1 | class Person { |
3. private
被 private
修饰的成员只能在类中访问,无法通过实例来访问,也无法在其子类中访问。
1 | class Person { |
但是在TS
中支持在类中通过同类的其它实例获取该实例上的private
成员:
1 | class Person { |
需要注意,成员可见性仅在 TS
的类型检查时有效。一旦代码被编译为 JS
代码,则在JS
中,可以通过类实例查看原本在TS
是 pretected
或 private
的成员。另外 JS
的私有修饰符 “#
“ 可以实现在编译后依然是私有成员。因此,如果要实现通过私有化来保护成员,应使用闭包、WeakMap
或私有字段 “#
“ 等手段。
四、静态成员 static
首先我们要明确一点,类本身也是一个对象。我们通过static
修饰符可以将某个成员变成静态成员。静态成员与类的实例无关,而是被挂到类对象本身,可以与实例成员重名,且静态方法中的 this
指向类对象本身,我们通过类对象本身来访问类成员。
1 | class Person { |
也许你会好奇我为什么用 _name
而不是name
,事实上不是我不使用,而是不能使用。稍后你会得到答案。
静态成员也可以使用 public
、protected
、private
等修饰符。类似的,protected
静态属性 只能由类或子类中的静态成员访问;private
静态成员只能由 类 中的静态成员访问。
1 | class Person { |
静态成员可以被子类继承:
1 | class Person { |
特殊的静态名字:name
,由于存在内置静态属性 Function.name
,因此我们在给静态属性命名时,不能使用name
,否则会发生冲突。
1 | class Person { |
五、静态域
我将类中的 static blocks
称为静态域,通过 static { }
声明一块区域,在该区域编写的语句能够自动执行,且能访问私有属性 如 “#name
“。因此,可以在静态域中书写静态成员做初始化逻辑。这里想不出什么好的栗子,就搬运了官网的:
1 | class Foo { |
六、泛型类
在执行new
操作时,泛型类的类型参数也会由传入的参数来进行推论。
1 | class Person<T> { |
泛型类可以像泛型接口一样进行泛型约束以及指定类型参数的默认值。大家都能明白的吧,就不给栗子了。
静态成员无法享用泛型:
1 | class Person<T> { |
这是因为每个静态成员都只有一个,而实例成员在每个实例上都存在一个。假若静态成员能享用泛型,那么我们new
一个实例a
,传入类型string
,此时静态属性_name
类型为string
;我们new
一个实例b
,传入类型number
,那么此时静态属性_name
的类型是啥呢?string
亦或number
? 显然都不合理。所以静态成员无法使用类型参数。
七、运行时的 this
Ts
中的this
指向和 JS
保持一致,因此有时候我们需要防止成员丢失this
上下文。
1. 使用箭头函数
1 | class Person { |
但是这也需要权衡利弊:
- 这样做能保证
setName
方法的this
永远正确地指向实例本身; - 使用中这种方式定义的方法不会挂载原型上,而是会被添加到每一个实例上,因此会占用更多的内存;
- 同样,其子类无法通过
super
,setName
来调用父类的setName
方法,因为无法在原型链上找到;
2. 使用 this
参数
如同在TS
的函数中将this
作为参数,为其指定类型一样,在类的方法中也可以如此这般。
1 | class Person { |
这种方式很好地弥补了箭头函数的不足,唯一的缺点就是习惯了 JS
思维的同学可能会试图通过其它对象来调用该方法,显然这样是不会成功的。
八、 把 this
作为 类型
首先要说,这玩意儿非常有用。在TS
的类中,this
可以作为一种特殊的类型,由当前的类进行动态推论。
1 | class Person { |
这里得setName
返回了this
,这个this
是表示实例值,其类型被推论为 this
,这个this
表示类型。this
类型就会在setName
调用时被动态推论为当前的类。这样的好处是在子类中可以也自动推论为子类。例如我们在Person
类的实例中调用setName
,返回值的类型就是Person
;如果在Person
的子类Manager
的实例中调用setName
,返回值的类型则是Person
的子类Manage
r:
1 | class Person { |
基于 this
类型的 类型守卫:和在函数中一样,我们可以在类或接口的方法的返回值的类型的位置使用this is Type
来进行类型缩减。写个最简单的栗子吧,实在是懒癌犯了 qwq
。
1 | class Person { |
九、参数属性
参数属性是TS
提供的一个非常方便的语法。在构造函数的参数前加上 public
、protected
、private
或者readonly
等修饰符,就可以把普通参数变为参数属性。参数属性既是构造函数的参数,又会作为实例属性自动被添加到实例上,且在传参时自动进行赋值,无需在函数体内进行赋值操作。
1 | class Person { |
十、类表达式
类似函数表达式,没啥说的,直接上栗子:
1 | const Person = class { |
十一、abstract 抽象类及其成员
含有抽象成员的类为抽象类。抽象类和抽象成员都需要在前面加上 abstract
修饰符。抽象类不能使用 new
进行实例化,而是用来作为基类,声明一些抽象方法或抽象属性,其子类需要实现所有这些方法或属性。
1 | abstract class Person { |
十二、类成员之间的关系
和其它类型一样,类之间也是通过结构来进行比较的,当拥有相同的成员,则可以相互替换;当一个类A
中含有另一个类B
的所有成员,尽管没有显示地通过 extends
继承,类 A
依然会被认为是类 B
的子类。
1 | class Person { |
这看起来很直观简单,不过少数情况下会看起来有些 emmm
,怪怪的。拿个官方栗子来:
1 | class Empty {} |
关于类的知识就分享到这里啦,下一篇 分享 TS
中的 模块 的内容,不见不散!