类的定义
虽然JavaScript语言支持了类,但其本质上仍是函数,类是一种语法糖。TypeScript语言对JavaScript中的类进行了扩展,为其添加了类型支持,如实现接口、泛型类等。
与函数声明不同的是,类声明不会被提升,因此必须先声明后使用。示例如下:
在使用类声明时,不允许声明同名的类,否则将产生错误
类表达式
类表达式是另一种定义类的方式,它的语法如下所示:
如果在类表达式中定义了类名,则该类名只能够在类内部使用,在类外不允许引用该类名。示例如下:
成员变量
在类中定义成员变量的方法如下所示:
除了在成员变量声明中设置初始值,我们还可以在类的构造函数中设置成员变量的初始值
--strictPropertyInitialization
TypeScript提供了“--strictPropertyInitialization”编译选项来帮助严格检查未经初始化的成员变量。需要注意的是,“--strictPropertyInitialization”编译选项必须与“--strictNullChecks”编译选项同时启用,否则“--strictPropertyInitialization”编译选项将不起作用
若启用了“--strictPropertyInitialization”编译选项并且仅在构造函数中对成员变量进行了初始化操作,那么需要在构造函数中直接进行赋值操作。如果通过在构造函数中调用某个方法,进而在该方法中间接地初始化成员变量,那么编译器将无法检测到该初始化操作,因此会产生编译错误
此例中,我们在构造函数中调用了init方法对成员变量a进行了初始化,但是编译器却无法检测到成员变量a已经被初始化。在一些场景中,我们确实想要通过调用某些方法来初始化类的成员变量。这时可以使用非空类型断言“!”来通知编译器该成员变量已经进行初始化,以此来避免产生编译错误
readonly 属性
在声明类的成员变量时,在成员变量名之前添加readonly修饰符能够将该成员变量声明为只读的
此例中,只读成员变量a在声明时进行了初始化,只读成员变量b在构造函数中进行了初始化,而只读成员变量c没有进行初始化,因此将产生编译错误。
成员函数
成员函数也称作方法,声明成员函数与在对象字面量中声明方法是类似的。示例如下:
成员存取器
如果一个类属性同时定义了get方法和set方法,那么get方法的返回值类型必须与set方法的参数类型一致,否则将产生错误。示例如下
如果一个类属性同时定义了get方法和set方法,那么get方法和set方法必须具有相同的可访问性。例如,不允许将get方法定义为公有的,而将set方法定义为私有的
索引成员
- 类的索引成员会在类的类型中引入索引签名。索引签名包含两种,分别为字符串索引签名和数值索引签名
- 类中所有的属性和方法必须符合字符串索引签名定义的类型。同时,只有当类具有类似数组的行为时,数值索引签名才有意义。
成员可访问性
TypeScript为类成员提供了以下三种可访问性修饰符:
- public:类的公有成员没有访问限制,可以在当前类的内部、外部以及派生类的内部访问。在默认情况下,类的所有成员都是公有成员。因此,在定义公有成员时也可以省略public修饰符。
- protected:类的受保护成员允许在当前类的内部和派生类的内部访问,但是不允许在当前类的外部访问。类的受保护成员使用protected修饰符标识。
- private:类的私有成员只允许在当前类的内部被访问,在当前类的外部以及派生类的内部都不允许访问。类的私有成员使用private修饰符标识。
构造函数
与普通函数相同,在构造函数中也可以定义可选参数、默认值参数和剩余参数。但是构造函数不允许定义返回值类型,因为构造函数的返回值类型永远为类的实例类型
在构造函数上也可以使用可访问性修饰符。它描述的是在何处允许使用该类来创建实例对象。在默认情况下,构造函数是公有的。如果将构造函数设置成私有的,则只允许在类的内部创建该类的对象。例如,下例中Singleton类的构造函数是私有的,因此只允许在Singleton类内部创建该类的实例对象
与函数重载类似,构造函数也支持重载。我们将没有函数体的构造函数声明称为构造函数重载,同时将定义了函数体的构造函数声明称为构造函数实现。构造函数重载可以存在零个或多个,而构造函数实现只能存在一个。示例如下:
参数成员
TypeScript提供了一种简洁语法能够把构造函数的形式参数声明为类的成员变量,它叫作参数成员。在构造函数参数列表中,为形式参数添加任何一个可访问性修饰符或者readonly修饰符,该形式参数就成了参数成员,进而会被声明为类的成员变量。 示例如下:
继承
在定义类时可以使用extends关键字来指定要继承的类,具体语法如下所示:
在该语法中,我们将BaseClass叫作基类,将DerivedClass叫作派生类,派生类继承了基类。有时候,我们也将基类称作父类,将派生类称作子类。
当派生类继承了基类后,就自动继承了基类的非私有成员。例如,下例中Circle类继承了Shape类。因此,Circle类获得了Shape类的color和switchColor公有成员。我们可以在Circle类的实例对象上访问color成员变量和调用switchColor成员函数。示例如下:
在派生类中可以重写基类的成员变量和成员函数。在重写成员变量和成员函数时,需要在派生类中定义与基类中同名的成员变量和成员函数。示例如下:
-
在派生类中,可以通过super关键字来访问基类中的非私有成员。当派生类和基类中存在同名的非私有成员时,在派生类中只能通过super关键字来访问基类中的非私有成员,无法使用this关键字来引用基类中的非私有成员。
-
若派生类重写了基类中的受保护成员,则可以将该成员的可访问性设置为受保护的或公有的。也就是说,在派生类中只允许放宽基类成员的可访问性。
-
在派生类的构造函数中必须调用基类的构造函数,否则将不能正确地实例化派生类。在派生类的构造函数中使用“super()”语句就能够调用基类的构造函数。
-
在派生类的构造函数中,引用了this的语句必须放在“super()”调用的语句之后,否则将产生编译错误,因为在基类初始化之前访问类的成员可能会产生错误。示例如下:
TypeScript中的类仅支持单继承,不支持多继承。也就是说,在extends语句中只能指定一个基类。
TypeScript允许接口继承类。若接口继承了一个类,那么该接口会继承基类中所有成员的类型。例如,下例中接口B继承了类A。因此,接口B中包含了string类型的成员x和方法类型y。示例如下:
在接口继承类时,接口不但会继承基类的公有成员类型,还会继承基类的受保护成员类型和私有成员类型。如果接口从基类继承了非公有成员,那么该接口只能由基类或基类的子类来实现。示例如下:
实现接口
虽然一个类只允许继承一个基类,但是可以实现一个或多个接口。在定义类时,使用implements语句能够声明类所实现的接口。当实现多个接口时,接口名之间使用逗号“,”分隔。下例中,类C实现了接口A和接口B:
如果类的定义中声明了要实现的接口,那么这个类就需要实现接口中定义的类型成员。下例中,Circle类声明了要实现Shape 和Color两个接口。因此,在Circle类中需要实现两个接口中定义的类型成员color和area。示例如下:
静态成员
类的定义中可以包含静态成员。类的静态成员不属于类的某个实例,而是属于类本身。类的静态成员使用static关键字定义,并且只允许通过类名来访问。
- 类的静态成员也可以定义不同的可访问性,如public、private和protected。
- 类的public静态成员和protected静态成员也可以被继承。
抽象类和抽象成员
定义抽象类时,只需要在class关键字之前添加abstract关键字即可。示例如下:
抽象类与具体类的一个重要区别是,抽象类不能被实例化。也就是说,不允许使用new运算符来创建一个抽象类的实例。示例如下:
抽象类的作用是作为基类使用,派生类可以继承抽象类。示例如下:
- 抽象类也可以继承其他抽象类。
- 抽象类中允许(通常)包含抽象成员,也允许包含非抽象成员。
在抽象类中允许声明抽象成员,抽象成员不允许包含具体实现代码。示例如下:
如果一个具体类继承了抽象类,那么在具体的派生类中必须实现抽象类基类中的所有抽象成员。因此,抽象类中的抽象成员不能声明为private,否则将无法在派生类中实现该成员。示例如下:
this 类型
在类中存在一种特殊的this类型,它表示当前this值的类型。我们可以在类的非静态成员的类型注解中使用this类型。例如,下例中add()方法和subtract()方法的返回值类型为this类型。第20行,我们可以链式调用add()方法和subtract()方法,因为它们返回的是当前实例对象。示例如下:
注意,this类型不允许应用于类的静态成员。
类类型
类声明将会引入一个新的命名类型,即与类同名的类类型。
在定义一个类时,实际上我们定义了一个构造函数。随后,我们可以使用new运算符和该构造函数来创建类的实例。我们可以将该类型称作类的构造函数类型,在该类型中也包含了类的静态成员类型。