其他应用与注意事项
除上述使用方式外,类还可以用于初始化、赋值等。
类的初始化顺序
在某些情况下,代码的执行顺序会让人疑惑。例如,对于以下代码,读者可以先试着自行推出输出结果。
class A {
name = "A";
constructor() {
console.log("this is " + this.name);
}
}
class B extends A {
name = "B";
}
let b = new B();
执行代码后,输出结果是 “this is A”。这着实让人疑惑,this
看上去应该指向的是类 B
的实例,但 this
的 name
属性返回了字符串 “A”。
如果理解 TypeScript 中类的实例化顺序,就知道原因了。类的实例化顺序如下。
-
初始化父类属性,赋予默认值。
-
执行父类构造函数。
-
初始化子类属性,赋予默认值。
-
执行子类构造函数。
由于父类的构造函数是实例化的第二步,在执行 console.log("this is " +this.name)
时,只有父类属性被初始化,并被赋予了默认值,因此输出结果为 “this is A”。
那么子类的构造函数去哪里了呢?不妨这么理解,虽然类 B
继承了类 A
,没有重写类 A
的构造函数,但它隐式拥有自己的构造函数,因此以上示例代码中类 B
实际上等同于以下代码。
class B extends A {
name = "B";
constructor() {
super();
}
}
参数属性
TypeScript 还支持参数属性,目的是使属性定义更便于使用。但它会破坏代码结构,降低代码的可读性。因此这里只做简单介绍,但并不推荐使用。
例如,以下代码在 Task
类中定义了 taskName
、Priority
和 informations
这 3 个属性,这些属性可以通过构造函数传入具体值。可以看出,这种写法并不方便,对于每一个属性,都需要在构造函数中指定值。
class Task {
public taskName: string;
protected prority: number;
private infomations: string[];
constructor(taskName: string, prority: number, infomations: string[]) {
this.taskName = taskName;
this.prority = prority;
this.infomations = infomations;
}
}
TypeScript 支持一种简化的参数属性写法,将属性定义与值都写到构造函数中。以下 Task
类的代码与上一一段代码的作用一模一样,但它更简洁。
class Task {
constructor(
public taskName: string,
protected prority: number,
private infomations: string[],
) { }
}
两种写法在调用方式上也没有任何差别,示例代码如下。
let task1: Task = new Task("Function1 Coding", 1, ["需要单元测试", "需要重构", "用指定算法实现"]);
console.log(task1.taskName); // 输出"Fuction1 Coding"
类表达式
如同函数一样,类也可以以表达式的形式进行声明,示例代码如下。
const Person = class {
constructor(public name: string) { };
selfIntroduction() { console.log('I'm ${this.name}') };
};
这同样是一种不推荐的用法,因为类表达式会将类的定义赋给变量,但变量本身不会被 TypeScript 识别为一种类型。例如,以下代码将引起编译错误。
//编译错误:"Person"表示值,但在此处用作类型。是否指"类型 Person"?ts(2749)
let person1: Person = new Person("Nick");
以表达式形式声明的类只能通过以下方式实例化,不能在声明时指明类型。
let person2 = new Person("Nick");
person2.selfIntroduction();
不够严格的类
在 TypeScript 中,类本身不是严格的类型,而只是一种结构上的约束。如果两个类具有相同或类似的结构,那么 TypeScript 可以将其识别为相同的类型。
例如,以下代码首先声明了 A
和 B
两个类,它们拥有完全一样的属性,接着,声明了一个变量 c
,该变量虽然声明为 A
类型,但实际上它为 B
的实例。这段代码在 TypeScript 中也能正常编译和执行。
class A {
x: string;
y: number;
}
class B {
x: string;
y: number;
}
let c: A = new B();
A
和 B
两个类甚至不需要结构完全一致,只要 B
包含 A
的所有成员,即使 B
比 A
的成员更多,也可以将 B
的实例赋给 A
类型的变量。
class A {
x: string;
y: number;
}
class B {
x: string;
y: number;
z: boolean;
}
let c: A = new B();
除以上比较独特的场景之外,TypeScript 中还有一个场景需要注意。例如,以下代码首先声明了一个没有任何成员的 EmptyClass
类,然后声明了一个 test()
函数,对该函数需要传入 EmptyClass
类型的参数,但在实际调用时,EmptyClass
类型会被识别为空对象类型 “{}
”,因此该函数可以传入任何类型的对象。
class EmptyClass { }
function test(obj: EmptyClass) {
console.log(obj);
}
//以下代码均不会引起编译错误
test({});
test(test);
test({ a: 1 });
class NormalClass { name: string; }
test(new NormalClass());
instanceof运算符
instanceof
运算符主要用于判断一个对象是否是指定类型的实例。如果左侧的对象是右侧类型的实例,则返回 true
;否则,返回 false
。该运算符主要用于引用类型,无法用于原始类型。
该运算符的语法如下。
x instanceof 引用类型名称
例如,以下代码首先声明了 A
和 B
两个类。虽然它们的结构一致,但是它们实际上是不同的类型。接着,声明了一个变量 a
,并将其值指定为 A
的实例。然后,分别输出 a instanceof A
和 a instanceof B
的判断结果。可以发现,程序准确地判断出变量 a
的值是 A
类的实例。
class A { }
class B { }
let a = new A();
console.log(a instanceof A); //输出true
console.log(a instanceof B); //输出false