类成员
字段
创建一个没有任何属性的类:
class Point{}
const point = new Point();
添加属性:
class Point2{
x: number; // 报错
y: number; // 报错
}
TS 会进行检测,要求属性必须初始化,否则会报错:
class Point2{ // 创建类
x: number = 0; // 创建属性
y: number = 0; // 创建属性
}
直接初始化时,可以省略类型声明,因为 TS 的编译器可以自动推断出类型:
class Point3 {
x = 0; // 推断出的类型为 number
y = 0; // 推断出的类型为 number
}
除了直接初始化,也可以通过构造器初始化:
class Point4 {
x: number;
y: number;
constructor(x: number, y: number) { // 创建构造函数
this.x = x; // 创建属性
this.y = y; // 创建属性
}
}
如果类属性没有指定类型,也没有初始化,会被认为具有any类型,并且编辑器会有警告提示:
class Point5{
x; // 警告,成员“x”隐式地具有“any”类型。
y; // 警告,成员“y”隐式地具有“any”类型。
}
特殊的,如果类属性是由外部程序(比如第三方库)初始化的,为了绕开 TS 编辑器的报错信息,可以:
class Point6{
x!: number;
y!: number;
}
属性名称后添加!即可。
readonly
如果属性被标记为readonly,则只能在声明时初始化或在构造器中赋值,不能再其他地方被修改:
class Person{
readonly name: string = 'new person';
constructor(name?: string){
if(name){
this.name = name;
}
}
setName(name: string): void{
this.name = name; // 报错,无法为“name”赋值,因为它是只读属性
}
}
const p = new Person('Tom');
console.log(p.name);
p.name = 'Jerry'; // 报错,无法为“name”赋值,因为它是只读属性
构造函数
构造函数与普通函数类似,也可以进行重载:
class Point7 {
readonly x: number;
readonly y: number;
constructor(x: number, y: number);
constructor(x: string, y: string);
constructor(x: number | string, y: number | string) {
if (typeof x === 'string') {
this.x = Number(x)
}
else {
this.x = x
}
if (typeof y === 'string') {
this.y = Number(y)
}
else {
this.y = y
}
}
}
const p2 = new Point7(1, 2);
const p3 = new Point7('1', '2');
console.log(p2);
console.log(p3);
与普通函数不同的地方是:
-
不能使用类型参数。
-
不能为返回值添加类型注解。
super 调用
如果存在继承关系,子类的构造函数中必须先使用super()调用父类的构造函数:
class Animal{}
class Dog extends Animal{
owner: string;
constructor(owner: string){
super();
this.owner = owner;
}
}
const dog = new Dog('Tom');
方法
类中的方法与普通函数类似,需要注意的是,方法中使用类的其他属性都需要使用this.进行访问,否则使用的会是外部空间的变量:
const name = 'Bruce';
class Person2 {
name: string;
age: number;
constructor(name: string, age: number = 20) {
this.name = name;
this.age = age;
}
test(){
console.log(name); // 这里的 name 是 Bruce
}
}
Getter/Setter
TS 支持为类属性添加 Getter/Setter:
class Person3{
_name: string = '';
get name(): string{
console.log('getter');
return this._name;
}
set name(value: string){
console.log('setter');
this._name = value;
}
}
const p7 = new Person3();
p7.name = 'Bruce';
console.log(p7.name);
添加了 Getter/Setter 的属性名称与 Getter/Setter 的名称不能相同,习惯上,添加了 Getter/Setter 的属性名称以_开头(这种命名风格有点像 Python 的受保护属性)。
Getter 的返回值类型注解和 Setter 的参数类型注解都不是必须的,缺省时前者会以实际返回类型为准,后者会以属性类型为准。
Getter/Setter 的用途在于对已有代码的扩展,比如为已有属性添加读取或赋值时的额外功能,所以单纯的读取和改写操作(比如示例中的)的 Getter/Setter 是无意义的,此时应该公开属性,而不是使用 Getter/Setter。
如果只有 Getter 没有 Setter,TS 会认为对应的属性是readonly。
从 TS 4.3 开始,Setter 可以接收与原始属性类型不同的参数:
class Person4 {
_name: string = '';
set name(value: string | number) {
if (typeof value === 'number') {
if (value > 0) {
this._name = 'Tom';
}
else {
this._name = 'Jerry';
}
}
else {
this._name = value;
}
}
get name(): string {
return this._name;
}
}
let person4 = new Person4();
person4.name = 1;
console.log(person4.name); // Tom
person4.name = -1;
console.log(person4.name); // Jerry
索引签名
像对象类型那样,类同样可以添加索引签名:
class CheckList {
[key: string]: boolean | ((key: string) => boolean);
check(key: string): boolean {
return this[key] as boolean;
}
}
继承
implements
implements表明某个类符合某个接口定义:
interface Flyable {
fly(): void;
}
class Bird implements Flyable {
fly() {
console.log('Bird is flying');
}
}
const bird = new Bird();
bird.fly();
TS 中implements仅能说明类与目标接口是否类型匹配,和其他语言中的 OOP 概念略有不同,比如:
interface PointInterface{
x: number;
y?: number;
}
class Point8 implements PointInterface{
x: number = 0;
}
const point2 = new Point8();
point2.x = 10;
point2.y = 20; // 报错,类型“Point8”上不存在属性“y”
这里PointInterface中y是可选属性,而Point8虽然实现了PointInterface接口,但并没有定义y属性(连可选属性都没有)。虽然看上去有点古怪,但这依然符合条件,因为PointInterface接口中的y是可选的。
最终的效果就是Point8中没有y属性,但依然实现了PointInterface接口,最终point2.y调用就会报错。
extends
TS 中可以从一个类派生出它的子类:
class Animal2 {
age: number = 1;
eat(food: string) {
console.log('吃:' + food);
}
}
class Dog2 extends Animal2 {
bark() { }
}
方法重写
TS 中子类同样可以重写父类的方法:
class Animal2 {
age: number = 1;
eat(food: string) {
console.log('吃:' + food);
}
}
class Cat2 extends Animal2 {
meow() { }
eat(food: string, where?: string) {
if(where === undefined){
super.eat(food);
}
else{
console.log('吃:' + food + ',在:' + where);
}
}
}
const cat1 = new Cat2();
cat1.eat('猫粮');
cat1.eat('猫粮', '猫窝');
重写时方法签名并不需要完全一致,只要子类的方法签名“兼容”父类的方法签名即可,通常是使用可选参数并使用super.xxx调用父类的方法,就像上面示例中做的。
之所以这样做,是要允许下面这样的调用:
const cat2: Animal2 = new Cat2();
cat2.eat('猫粮');
仅类型字段声明
看一个示例:
class Animal3{
readonly kind: string = '';
}
class Cat3 extends Animal3{
readonly kind: string = '猫科';
}
class AnimalHouse3{
hold: Animal3;
constructor(animal: Animal3){
this.hold = animal;
}
}
class CatHouse3 extends AnimalHouse3{
constructor(cat: Cat3){
super(cat);
}
}
const cat3 = new Cat3();
console.log(cat3);
const catHouse3 = new CatHouse3(cat3);
console.log(catHouse3);
console.log(catHouse3.hold); // 这里 catHourse3.hold 是 Animal3 类型
catHouse3.hold属性是从AnimalHouse3继承的,因此其类型是Animal3,我们可以重写这个属性:
class CatHouse3 extends AnimalHouse3{
declare hold: Cat3;
// ...
}
但要注意,这里需要添加declare关键字,表明这里仅是声明一个字段,否则这里会因为编译器没有找到初始化而报错。
初始化顺序
class Parent{
age: number = 1;
constructor(){
console.log(this.age);
}
}
class Child extends Parent{
age: number = 2;
constructor(){
super();
console.log(this.age);
}
}
const child = new Child();
上面这个示例会先输出1再输出2。
这是因为 JS 中,对象的初始化顺序是:
-
基类字段初始化。
-
基类的构造函数被调用。
-
子类的字段初始化。
-
子类的构造函数被调用。
成员可见性
TS 提供和其他语言类似的成员可见性标识符:
class Parent2{
private privateAttr: number = 1;
protected protectedAttr: number = 2;
public publicAttr: number = 3;
defaultAttr: number = 4;
}
const parent2 = new Parent2();
console.log(parent2.defaultAttr);
console.log(parent2.publicAttr);
console.log(parent2.protectedAttr); // 报错
console.log(parent2.privateAttr); // 报错
与Java不同,TS 中如果没有指定可见性,默认的可见性是public,这也和 JS 的默认行为一致。
如果存在继承关系,且子类重写了父类方法,可以放大该方法的可见性:
class parent3{
protected func1(): void{
console.log('parent.func1');
}
}
class Child3 extends parent3{
public func1(): void{
console.log('child.func1');
}
}
const child3 = new Child3();
child3.func1();
最后要强调的是,TS 是一种预处理语言,它只在将 TS 编译为 JS 的阶段生效,并不会影响 JS 的执行阶段,因此这些成员可见性标识符只能在编码阶段提供访问控制,并不能实际控制运行阶段(JS)的数据访问安全。
静态成员
TS 中同样可以为类定义静态成员,包括静态属性和静态方法,这些静态成员可以通过类名访问:
class Parent4{
static staticAttr: number = 1;
static staticFunc(): void{
console.log('staticFunc');
}
}
console.log(Parent4.staticAttr);
Parent4.staticFunc();
静态成员同样可以使用访问控制标识符:
class Parent5{
private static staticAttr: number = 1;
}
console.log(Parent5.staticAttr); // 报错
静态成员同样可以被继承和重写:
class Parent6{
static staticAttr: number = 1;
static staticFunc(): void{
console.log('parent.staticFunc', this.staticAttr);
}
}
class Child6 extends Parent6{
static staticAttr: number = 2;
static staticFunc(): void{
console.log('child.staticFunc', this.staticAttr);
}
}
Parent6.staticFunc(); // parent.staticFunc 1
Child6.staticFunc(); // child.staticFunc 2
特殊静态名称
某些特殊名称是不能被定义为静态成员的:
class MyClass{
static name = 'MyClass';
// 报错,静态属性“name”与构造函数“MyClass”的内置属性函数“name”冲突
static length = 10;
// 静态属性“length”与构造函数“MyClass”的内置属性函数“length”冲突
}
这是因为类原型(prototype)实际上是一个函数类型(Function),它本身拥有一些属性,这些属性是不能被覆盖的。
没有静态类
TS 和 JS 中不存在静态类(static class xxx{}),但在大部分情况下不会影响使用。在 TS 和 JS 中,函数本身就是顶层元素,你可以在最外层定义函数并在任何地方使用,而不需要用静态类包围和定义:
function myFunc() {
console.log('myFunc');
}
class MyClass2 {
func1() {
myFunc();
}
}
const myClass2 = new MyClass2();
myClass2.func1();
静态初始化块
可以在类中添加静态初始化块:
class MyClass3 {
static a: number;
static {
MyClass3.a = 1;
}
}
console.log(MyClass3.a);
泛型类
类同样可以使用类型参数:
class Box<T>{
value: T;
constructor(value: T) {
this.value = value;
}
}
const box1 = new Box('hello'); // 类型推断为 Box<string>
这种情况下类型参数会根据new时候的构造器参数类型进行推断。
静态成员中的类型参数
不能将类类型参数用于静态成员,这是一个错误:
class MyClass4<T> {
static a: T; // 报错,静态成员不能引用类类型参数
}
这也很容易理解,因为类类型参数只能通过创建实例时候(new xxx(...))的构造器参数类型进行推断,这和静态成员(类实例)无关。
this
JS 的 this 在某些时候会有一些与预期不符的表现:
class MyClass5{
name: string = 'MyClass5';
getName(): string {
return this.name;
}
}
const myClass5 = new MyClass5();
class MyClass6{
name: string = 'MyClass6';
getName = myClass5.getName;
}
const myClass6 = new MyClass6();
console.log(myClass6.getName()); // MyClass6
MyClass5中有一个方法getName,返回类属性name,MyClass6中同样有一个getName方法,不过没有直接定义,而是通过MyClass5的实例被赋值。
在最终调用MyClass6的实例的getName方法时,你可能期望输出的是MyClass5,而结果是MyClass6。因为在这种情况下,因为调用的实例是MyClass6的实例,因此此时的this关联的也是MyClass6的实例。
箭头函数
看一个例子:
class MyClass7{
name: string = 'MyClass7';
getName(): string {
return this.name;
}
}
const myClass7 = new MyClass7();
const getName = myClass7.getName;
console.log(getName());
// 报错,TypeError: Cannot read properties of undefined (reading 'name')
这里报错的原因和上面说的一样,因为getName函数是通过赋值直接关联到MyClass7实例的getName方法,因此调用时(getName()),this指向的是getName这个函数对象,而这个函数对象没有定义name属性。
注意,这里是一个隐蔽的运行时报错,编译阶段 TS 没有任何错误提示。
如果要解决这个问题,可以使用箭头函数定义类方法:
class MyClass8 {
name: string = 'MyClass8';
getName = () => {
return this.name;
}
}
const myClass8 = new MyClass8();
const getName2 = myClass8.getName;
console.log(getName2());
在箭头函数中,this只会指向所属的类实例,而不会因为赋值操作变成其它对象。
但是箭头函数存在其它问题:
class Parent7 {
name: string = 'Parent7';
getName = () => {
return this.name;
}
}
class Child7 extends Parent7 {
name: string = 'Child7';
getName = () => {
super.getName();
// 报错,父类字段“getName”无法通过 super 在子类中访问
return this.name;
}
}
就像示例展示的,如果父类的方法使用箭头函数定义,就不能在子类中使用super.xxx的方式获取该方法。
this 参数
另一种权衡后的解决方法是在需要使用this的方法中,添加一个特殊的this参数:
class Parent8 {
name: string = 'parent8';
getName(this: Parent8) {
return this.name;
}
}
const parent8 = new Parent8();
const getName3 = parent8.getName;
console.log(getName3());
// 报错,类型为“void”的 “this” 上下文不能分配给类型为“Parent8”的方法的 “this”。
这个参数比较特殊,TS 可以用它检测实际指向是否与预期类型相符,如果不符合就会报错。与之前的报错相比,此时报错会发生在编译阶段(IDE 会提示),而不是运行阶段,这相当有意义。
当然,这个特殊的this参数对于 JS 是没有意义的,理所当然的,编译后的 JS 代码中这个参数是不存在的。
特殊类型 this
TS 中,有一个特殊类型this,它表示当前类的类型:
class Parent9 {
name: string = 'parent9';
setName(newName: string){ // 这里推断出的返回值类型是 this
this.name = newName;
return this;
}
}
与固定的类型Parent9相比,this类型是动态的,比如:
class Child9 extends Parent9 {
}
const child9 = new Child9();
const child9Name = child9.setName('child9'); // 推断类型为 Child9
可以看到,在扩展的子类中,this会变成具体的子类类型。
除了将this应用于返回值,同样可以应用于参数,看一个示例:
class MyNumber{
value: number;
constructor(value: number) {
this.value = value;
}
compare(other: MyNumber): -1 | 0 | 1 {
return this.value === other.value ? 0 : this.value > other.value ? 1 : -1;
}
}
class SubMyNumber extends MyNumber {
name: string = 'SubMyNumber2';
constructor(value: number) {
super(value);
}
}
const myNumber = new MyNumber(1);
const subMyNumber = new SubMyNumber(2);
console.log(myNumber.compare(subMyNumber));
console.log(subMyNumber.compare(myNumber));
这个例子中,compare的参数other使用了基类类型,这很自然,其子类自然也符合基类类型,因此可以互相比较。
但如果使用this,就会变得很微妙:
class MyNumber2{
value: number;
constructor(value: number) {
this.value = value;
}
compare(other: this): -1 | 0 | 1 {
return this.value === other.value ? 0 : this.value > other.value ? 1 : -1;
}
}
class SubMyNumber2 extends MyNumber2 {
name: string = 'SubMyNumber2';
constructor(value: number) {
super(value);
}
}
const myNumber2 = new MyNumber2(1);
const subMyNumber2 = new SubMyNumber2(2);
console.log(myNumber2.compare(subMyNumber2));
console.log(subMyNumber2.compare(myNumber2));
// 报错,类型“MyNumber2”的参数不能赋给类型“SubMyNumber2”的参数
此时子类的compare方法只能接收子类(SubMyNumber2)类型的参数,而不能接收基类类型。
要注意,TS 的类型比较是基于鸭子类型,因此如果子类和基类属性完全一致,比如子类没有一个多余的
name属性,上面的代码就不会报错。
基于 this 的类型守卫
可以使用 this 定义一些方法作为对象的类型守卫,以缩小对象的类型:
class Animal4 {
kind: string = '';
isDog(): this is Dog4 {
return this.kind === 'dog';
}
isCat(): this is Cat4 {
return this instanceof Cat4;
}
}
class Dog4 extends Animal4 {
kind: 'dog' = 'dog';
bark(): void {
console.log('bark');
}
}
class Cat4 extends Animal4 {
kind: 'cat' = 'cat';
meow(): void {
console.log('meow');
}
}
const animal4: Animal4 = new Cat4();
if (animal4.isDog()) {
animal4.bark(); // 类型推断为 Dog4
} else if (animal4.isCat()) {
animal4.meow(); // 类型推断为 Cat4
}
另一个this类型守卫的应用是避免对可选成员的显式判断:
class Box10<T> {
value?: T;
hasValue(): this is { value: T } {
return this.value !== undefined;
}
}
const box10 = new Box10<string>();
box10.value = 'hello';
if (box10.hasValue()) {
console.log(box10.value.toUpperCase());
}
参数属性
TS 提供一种特殊语法,成员属性可以不在类中声明,直接在构造函数中作为参数进行声明,这些参数会被正确设置为属性,并且可以使用访问修饰符:
class Person8{
constructor(public name: string, public age: number) {
}
}
const p8 = new Person8('Alice', 18);
console.log(p8.name, p8.age);
类表达式
类表达式与类声明基本一致,唯一的区别是没有类名,我们需要使用其绑定的变量名创建实例:
const Person9 = class {
constructor(public name: string, public age: number) {
}
}
const p9 = new Person9('Alice', 18);
console.log(p9.name, p9.age);
构造函数签名
工具类型InstanceType可以获取类实例的类型:
class Person10 {
constructor(public name: string, public age: number) {
}
}
type PersonInstanceType = InstanceType<typeof Person10>;
function printPerson(person: PersonInstanceType){
console.log(person.name, person.age);
}
const p10 = new Person10('Alice', 20);
printPerson(p10);
抽象类
TS 中可以定义抽象类和抽象方法:
abstract class Conveyance{
abstract start(): void;
abstract stop(): void;
move(): void{
this.start();
this.stop();
}
}
class Car extends Conveyance{
start(): void{
console.log('Car started');
}
stop(): void{
console.log('Car stopped');
}
}
class Bike extends Conveyance{
start(): void{
console.log('Bike started');
}
stop(): void{
console.log('Bike stopped');
}
}
const car = new Car();
car.move();
const bike = new Bike();
bike.move();
抽象构造签名
假设需要定义一个工厂函数创建抽象类的子类,具体创建哪个子类通过参数传递类类型:
function careteConveyance(ConceyanceType: typeof Conveyance): Conveyance{
return new ConceyanceType(); // 报错,无法创建抽象类的实例
}
const car2 = careteConveyance(Car);
const bike2 = careteConveyance(Bike);
这里会报错,因为Conveyance是一个抽象类,不能使用new ConceyanceType()。
可以换一种方式:
function careteConveyance(ConceyanceType: new () => Conveyance): Conveyance {
return new ConceyanceType();
}
const car2 = careteConveyance(Car);
const bike2 = careteConveyance(Bike);
car2.move();
bike2.move();
类之间的关系
再次强调,TS 判断某个类是否是另一个类(IS 关系),是通过鸭子类型判断的。因此即使两个类没有显式继承关系,也可能隐式具备 IS 关系:
class Person11 {
name: string = '';
age: number = 0;
}
class Person12 {
name: string = '';
age: number = 0;
}
const person12: Person12 = new Person11();
最特别的是,空对象{}不具备任何成员,因此它可以作为任何对象的基类:
class Empty { }
function dealEmpty(empty: Empty): void {
console.log(empty);
}
dealEmpty(new Empty());
dealEmpty(new Person11());
dealEmpty({ age: 18 });
当然,不建议这么做。
The End.
参考资料

文章评论