在面向对象语言中,类是一种面向对象计算机编程语言的构造,是创建对象的蓝图,描述了所创建的对象共同的属性和方法。
一、类的属性与方法
1.1 类的成员属性和静态属性
在 TypeScript 中,我们可以通过 class
关键字来定义一个类:
class Person {
name: string; // 成员属性
constructor(name: string) { // 类的构造函数
this.name = name;
}
}
在以上代码中,我们使用 class
关键字定义了一个 Person
类,该类含有一个名为 name
的成员属性。其实 TypeScript 中的类是一个语法糖(所谓的语法糖就是在之前的某个语法的基础上改变了一种写法,实现的功能相同,但是写法不同了,主要是为了让开发人员在使用过程中更方便易懂。),若设置编译目标为 ES5 将会产生以下代码:
"use strict";
var Person = /** @class */ (function () {
function Person(name) {
this.name = name;
}
return Person;
}());
类除了可以定义成员属性外,还可以通过 static
关键字定义静态属性:
class Person {
static cid: string = "exe";
name: string; // 成员属性
constructor(name: string) { // 类的构造函数
this.name = name;
}
}
那么成员属性与静态属性有什么区别呢?在回答这个问题之前,我们先来看一下编译生成的 ES5 代码:
"use strict";
var Person = /** @class */ (function () {
function Person(name) {
this.name = name;
}
Person.cid = "exe";
return Person;
}());
观察以上代码可知,成员属性是定义在类的实例上,而静态属性是定义在构造函数上。
1.2 类的成员方法和静态方法
在 TS 类中,我们不仅可以定义成员属性和静态属性,还可以定义成员方法和静态方法,具体如下所示:
class Person {
static cid: string = "exe";
name: string; // 成员属性
static printCid() { // 定义静态方法
console.log(Person.cid);
}
constructor(name: string) { // 类的构造函数
this.name = name;
}
say(words: string) :void { // 定义成员方法
console.log(`${this.name} says:${words}`);
}
}
那么成员方法与静态方法有什么区别呢?同样,在回答这个问题之前,我们先来看一下编译生成的 ES5 代码:
"use strict";
var Person = /** @class */ (function () {
function Person(name) {
this.name = name;
}
Person.printCid = function () {
console.log(Person.cid);
};
Person.prototype.say = function (words) {
console.log(this.name + " says\uFF1A" + words);
};
Person.cid = "exe";
return Person;
}());
由以上代码可知,成员方法会被添加到构造函数的原型对象上,而静态方法会被添加到构造函数上。
1.3 类成员方法重载
函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。 在定义类的成员方法时,我们也可以对成员方法进行重载:
class Person {
constructor(public name: string) {}
say(): void;
say(words: string): void;
say(words?: string) :void { // 方法重载
if(typeof words === "string") {
console.log(`${this.name} says:${words}`);
} else {
console.log(`${this.name} says:Nothing`);
}
}
}
let p1 = new Person("Semlinker");
p1.say();
p1.say("Hello TS");
如果想进一步了解函数重载的话,可以继续阅读 是时候表演真正的技术了 - TS 分身之术 这一篇文章。
二、访问器
在 TypeScript 中,我们可以通过 getter
和 setter
方法来实现数据的封装和有效性校验,防止出现异常数据。
let passcode = "Hello TypeScript";
class Employee {
private _fullName: string = "";
get fullName(): string {
return this._fullName;
}
set fullName(newName: string) {
if (passcode && passcode == "Hello TypeScript") {
this._fullName = newName;
} else {
console.log("Error: Unauthorized update of employee!");
}
}
}
let employee = new Employee();
employee.fullName = "Semlinker";
在以上代码中,对于私有的 _fullName
属性,我们通过对外提供 getter
和 setter
来控制该属性的访问和修改。
三、类的继承
继承(Inheritance)是一种联结类与类的层次模型。指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系。通过类的继承,我们可以实现代码的复用。
继承是一种 is-a 关系:
在 TypeScript 中,我们可以通过 extends
关键字来实现类的继承:
3.1 父类
class Person {
constructor(public name: string) {}
public say(words: string) :void {
console.log(`${this.name} says:${words}`);
}
}
3.2 子类
class Developer extends Person {
constructor(name: string) {
super(name);
this.say("Learn TypeScript")
}
}
const p2 = new Developer("semlinker");
// 输出: "semlinker says:Learn TypeScript"
因为 Developer
类继承了 Person
类,所以我们可以在 Developer
类的构造函数中调用 say
方法。需要注意的是,在 TypeScript 中使用 extends
时,只能继承单个类:
class Programmer {}
// Classes can only extend a single class.(1174)
class Developer extends Person, Programmer {
constructor(name: string) {
super(name);
this.say("Learn TypeScript")
}
}
虽然在 TypeScript 中只允许单继承,但却允许我们实现多个接口。具体的使用示例如下所示:
interface CanSay {
say(words: string) :void
}
interface CanWalk {
walk(): void;
}
class Person implements CanSay, CanWalk {
constructor(public name: string) {}
public say(words: string) :void {
console.log(`${this.name} says:${words}`);
}
public walk(): void {
console.log(`${this.name} walk with feet`);
}
}
此外,除了可以继承具体的实现类之外,在实现继承时,我们还可以继承抽象类。
四、抽象类
使用 abstract
关键字声明的类,我们称之为抽象类。抽象类不能被实例化,因为它里面包含一个或多个抽象方法。 所谓的抽象方法,是指不包含具体实现的方法:
abstract class Person {
constructor(public name: string){}
abstract say(words: string) :void;
}
// Cannot create an instance of an abstract class.(2511)
const lolo = new Person(); // Error
抽象类不能被直接实例化,我们只能实例化实现了所有抽象方法的子类。具体如下所示:
class Developer extends Person {
constructor(name: string) {
super(name);
}
say(words: string): void {
console.log(`${this.name} says ${words}`);
}
}
const lolo = new Developer("lolo");
lolo.say("I love ts!"); // 输出:lolo says I love ts!
五、类访问修饰符
在 TS 类型中,我们可以使用 public
、protected
或 private
来描述该类属性和方法的可见性。
5.1 public
public 修饰的属性或者方法是公有的,可以在任何地方被访问到,默认所有的属性或者方法都是 public:
class Person {
constructor(public name: string) {}
public say(words: string) :void {
console.log(`${this.name} says:${words}`);
}
}
5.2 protected
protected 修饰的属性或者方法是受保护的,它和 private 类似,不同的地方是 protected 成员在派生类中仍然可以访问。
class Person {
constructor(public name: string) {}
public say(words: string) :void {
console.log(`${this.name} says:${words}`);
}
protected getClassName() {
return "Person";
}
}
const p1 = new Person("lolo");
p1.say("Learn TypeScript"); // Ok
// Property 'getClassName' is protected and only accessible within class 'Person' and its subclasses.
p1.getClassName() // Error
由以上错误信息可知,使用 protected
修饰符修饰的方法,只能在当前类或它的子类中使用。
class Developer extends Person {
constructor(name: string) {
super(name);
console.log(`Base Class:${this.getClassName()}`);
}
}
const p2 = new Developer("semlinker"); // 输出:"Base Class:Person"
5.3 private
private 修饰的属性或者方法是私有的,只能在类的内部进行访问。
class Person {
constructor(private id: number, public name: string) {}
}
const p1 = new Person(28, "lolo");
// Property 'id' is private and only accessible within class 'Person'.(2341)
p1.id // Error
p1.name // OK
由以上错误信息可知,使用 private
修饰符修饰的属性,只能在当前类内部访问。但真的是这样么?其实这只是 TS 类型检查器给我们的提示,在运行时我们还是可以访问 Person
实例的 id
属性。不相信的话,我们来看一下编译生成的 ES5 代码:
"use strict";
var Person = /** @class */ (function () {
function Person(id, name) {
this.id = id;
this.name = name;
}
return Person;
}());
var p1 = new Person(28, "lolo");
5.4 私有字段
针对上面的问题,TypeScript 团队在 3.8 版本就开始支持 ECMAScript 私有字段,使用方式如下:
class Person {
#name: string;
constructor(name: string) {
this.#name = name;
}
}
let semlinker = new Person("semlinker");
// Property '#name' is not accessible outside class 'Person' because it has a private identifier.
semlinker.#name // Error
那么 ECMAScript 私有字段 跟 private
修饰符相比,有什么特别之处么?这里我们来看一下编译生成的 ES2015 代码:
"use strict";
var __classPrivateFieldSet = // 省略相关代码
var _Person_name;
class Person {
constructor(name) {
_Person_name.set(this, void 0);
__classPrivateFieldSet(this, _Person_name, name, "f");
}
}
_Person_name = new WeakMap();
let semlinker = new Person("Semlinker");
观察以上的结果可知,在处理私有字段时使用到了 ES2015 新增的 WeakMap 数据类型,如果你对 WeakMap 还不了解的话,可以阅读 你不知道的 WeakMap 这篇文章。下面我们来总结一下,私有字段与常规属性(甚至使用 private
修饰符声明的属性)不同之处:
- 私有字段以
#
字符开头,有时我们称之为私有名称; - 每个私有字段名称都唯一地限定于其包含的类;
- 不能在私有字段上使用 TypeScript 可访问性修饰符(如 public 或 private);
- 私有字段不能在包含的类之外访问,甚至不能被检测到。
六、类表达式
TypeScript 1.6 添加了对 ES6 类表达式的支持。类表达式是用来定义类的一种语法。和函数表达式相同的一点是,类表达式可以是命名也可以是匿名的。如果是命名类表达式,这个名字只能在类体内部才能访问到。
类表达式的语法如下所示([] 方括号表示是可选的):
const MyClass = class [className] [extends] {
// class body
};
基于类表达式的语法,我们可以定义一个 Point 类:
let Point = class {
constructor(public x: number, public y: number) {}
public length() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
}
let p = new Point(3, 4);
console.log(p.length()); // 输出:5
需要注意在使用类表达式定义类的时候,我们也可以使用 extends
关键字。篇幅有限,这里就不展开介绍了,感兴趣的小伙伴可以自行测试一下。
七、泛型类
在类中使用泛型也很简单,我们只需要在类名后面,使用 <T, ...>
的语法定义任意多个类型变量,具体示例如下:
class Person<T> {
constructor(
public cid: T,
public name: string
) {}
}
let p1 = new Person<number>(28, "Lolo");
let p2 = new Person<string>("exe", "Semlinker");
接下来我们以实例化 p1
为例,来分析一下其处理过程:
- 在实例化
Person
对象时,我们传入 number
类型和相应的构造参数; - 之后在
Person
类中,类型变量 T
的值变成 number
类型; - 最后构造函数
cid
的参数类型也会变成 number
类型。
相信看到这里一些读者会有疑问,我们什么时候需要使用泛型呢?通常在决定是否使用泛型时,我们有以下两个参考标准:
- 当你的函数、接口或类将处理多种数据类型时;
- 当函数、接口或类在多个地方使用该数据类型时。
八、构造签名
在 TypeScript 接口中,你可以使用 new
关键字来描述一个构造函数:
interface Point {
new (x: number, y: number): Point;
}
以上接口中的 new (x: number, y: number)
我们称之为构造签名,其语法如下:
ConstructSignature:new
TypeParametersopt (
ParameterListopt )
TypeAnnotationopt
在上述的构造签名中,TypeParametersopt
、ParameterListopt
和 TypeAnnotationopt
分别表示:可选的类型参数、可选的参数列表和可选的类型注解。那么了解构造签名有什么用呢?这里我们先来看个例子:
interface Point {
new (x: number, y: number): Point;
x: number;
y: number;
}
class Point2D implements Point {
readonly x: number;
readonly y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
const point: Point = new Point2D(1, 2); // Error
对于以上的代码,TypeScript 编译器(v4.4.3)会提示以下错误信息:
Type 'Point2D' is not assignable to type 'Point'.
Type 'Point2D' provides no match for the signature 'new (x: number, y: number): Point'.
要解决这个问题,我们就需要把对前面定义的 Point
接口进行分离:
interface Point {
x: number;
y: number;
}
interface PointConstructor {
new (x: number, y: number): Point;
}
完成接口拆分之后,除了前面已经定义的 Point2D
类之外,我们又定义了一个 newPoint
工厂函数,该函数用于根据传入的 PointConstructor
类型的构造函数,来创建对应的 Point 对象。
class Point2D implements Point {
readonly x: number;
readonly y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
function newPoint( // 工厂方法
pointConstructor: PointConstructor,
x: number,
y: number
): Point {
return new pointConstructor(x, y);
}
const point: Point = newPoint(Point2D, 1, 2);
九、抽象构造签名
在 TypeScript 4.2 版本中引入了抽象构造签名,用于解决以下的问题:
type ConstructorFunction = new (...args: any[]) => any;
abstract class Utilities {}
// Type 'typeof Utilities' is not assignable to type 'ConstructorFunction'.
// Cannot assign an abstract constructor type to a non-abstract constructor type.
let UtilityClass: ConstructorFunction = Utilities; // Error.
由以上的错误信息可知,我们不能把抽象构造器类型分配给非抽象的构造器类型。针对这个问题,我们需要使用 abstract
修饰符:
declare type ConstructorFunction = abstract new (...args: any[]) => any;
需要注意的是,对于抽象构造器类型,我们也可以传入具体的实现类:
declare type ConstructorFunction = abstract new (...args: any[]) => any;
abstract class Utilities {}
class UtilitiesConcrete extends Utilities {}
let UtilityClass: ConstructorFunction = Utilities; // Ok
let UtilityClass1: ConstructorFunction = UtilitiesConcrete; // Ok
而对于 TypeScript 4.2 以下的版本,我们可以通过以下方式来解决上面的问题:
type Constructor<T> = Function & { prototype: T }
abstract class Utilities {}
class UtilitiesConcrete extends Utilities {}
let UtilityClass: Constructor<Utilities> = Utilities;
let UtilityClass1: Constructor<UtilitiesConcrete> = UtilitiesConcrete;
介绍完抽象构造签名,最后我们来简单介绍一下 class type 与 typeof class type 的区别。
十、class type 与 typeof class type
class Person {
static cid: string = "exe";
name: string; // 成员属性
static printCid() { // 定义静态方法
console.log(Person.cid);
}
constructor(name: string) { // 类的构造函数
this.name = name;
}
say(words: string) :void { // 定义成员方法
console.log(`${this.name} says:${words}`);
}
}
// Property 'say' is missing in type 'typeof Person' but required in type 'Person'.
let p1: Person = Person; // Error
let p2: Person = new Person("Semlinker"); // Ok
// Type 'Person' is missing the following properties from type 'typeof Person': prototype, cid, printCid
let p3: typeof Person = new Person("Lolo"); // Error
let p4: typeof Person = Person; // Ok
通过观察以上的代码,我们可以得出以下结论:
- 当使用
Person
类作为类型时,可以约束变量的值必须为 Person
类的实例; - 当使用
typeof Person
作为类型时,可以约束变量的值必须包含该类上的静态属性和方法。
此外,需要注意的是 TypeScript 使用的是 结构化 类型系统,与 Java/C++ 所采用的 名义化 类型系统是不一样的,所以以下代码在 TS 中是可以正常运行的:
class Person {
constructor(public name: string) {}
}
class SuperMan {
constructor(public name: string) {}
}
let p1: SuperMan = new Person("Semlinker"); // Ok
好的,在日常工作中,TypeScript 类比较常见的知识,就介绍到这里,感谢阿宝哥的分享!
十一、参考资源
- TypeScript 1.6
- how-to-use-classes-in-typescript