TypeScript:类(Classes)

时间:2022-07-07 05:15:33

返回TypeScript手册总目录


传统的Javascript关注的是函数(function)和基于原型(prototype-based)的继承作为构建可重复使用组件的基本方式,但是与更舒服地使用面向对象的方式比较,这可能让程序员感到有点难受了。在面向对象中,类继承了功能, 然后对象从类中产生。从下一代版本的JS(ECMAScript6)开始,JS程序员可以使用基于类的面向对象的方式来构建应用。在TS中,我们允许程序员不用等待下一代JS出来,现在就使用这些技术,并且把它们向下编译成可在所有主流浏览器和平台上运行的JS。

先来看一个简单的基于类的例子:

class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello," + this.greeting;
}
} var user = new Greeter("World!");

document.writeln(user.greet());

如果你熟悉Java或者C#的话,那么这段代码对你而言很简单。首先,声明了一个类,该类有三个成员:包括一个属性greeting,一个构造函数和一个方法greet。

你会发现,在我们使用类的某个成员时,是通过this关键字来访问的,这表明它是一个成员访问。

倒数第二行我们使用new关键字创建了一个Greeter类的对象,这会调用我们之前定义的构造函数,使用Greeter模型创建了一个新的对象,然后运行构造函数进行初始化。

继承

在TS中,我们使用通用的面向对象模式。当然,在基于类的编程中,最基本模式之一就是使用继承这一特征来扩展已存在的类来创建一个新类。

来看一个例子:

class Animal {
name: string;
constructor(theName: string) {
this.name = theName;
}
move(meters: number = 0) {
alert(this.name + "移动了" + meters+"米");
}
}
class Snake extends Animal {
constructor(name: string) {
super(name);
}
move(meters = 5) {
alert("我是蛇,我在滑行...");
super.move(meters);
}
}
class Horse extends Animal {
constructor(name: string) { super(name) }
move(meters = 45) {
alert("我是马,我在奔跑!");
super.move(meters);
}
} var snakeObj = new Snake("眼镜蛇");

var horseObj: Animal = new Horse("千里马");

snakeObj.move();

horseObj.move(1000);

这个例子覆盖了TS中相当一部分继承特征。我们这里使用了extends关键字来创建一个子类。你可以看到Snake和Horse类都是Animal的子类,从而获得了父类的访问权。

这个例子也说明了,如果子类需要,就可以重写父类中的方法。例子中Snake和Horse类都创建了一个move方法,并且重写了来自Animal的move方法,这样就把父类的特有功能给了每个子类。

Private/Public修饰符

默认Public

你可能已经注意到在上面的例子中,我们没有使用关键字public就能使类的成员可见。像C#语言就必须要求每一个成员显式地标明public才是可见的。在TS中,每一个成员默认都是public的。

你仍然可以把成员标记为private,目的是你可以控制它在你的类之外是公共可见的。我们可以这样写之前的Animal类:

class Animal {
private name: string;
constructor(theName: string) {
this.name = theName;
}
move(meters: number = 0) {
alert(this.name + "移动了" + meters+"米");
}
}

理解private

TS是一个结构型系统。当比较两个不同的类型时,不用考虑它们来自哪里,如果它们的每一个成员的类型是兼容的,那么我们就说这两个类型是兼容的。

当比较具有private成员的类型时,我们就要具体分析了。对于两个兼容的类型而言,如果其中一个类有一个private成员,那么另外一个必须要有一个产生自相同声明的private成员(换言之,共用同一个声明)。

来通过一个实例来更好地理解:

class Animal {
private name: string;
constructor(theName: string) {
this.name = theName;
}
}
class Snake extends Animal {
constructor(name: string) {
super(name);
}
} class Employee {

private name: string;

constructor(theName: string) {

this.name = theName;

}

} var animalObj = new Animal("公鸡");

var snakeObj = new Snake("蛇");

var emloyeeObj = new Employee("雇员");

animalObj = snakeObj;

animalObj = emloyeeObj;//报错,因为Employee类有name的私有声明,不能把它复值给animalObj的name属性

在这个例子中,我们有三个类,其中”Snake”是Animal的子类。我们也创建了一个看上去和Animal样子一样的新类”Employee“。我们也创建了这些类的 实例,并把它们相互赋值看会发生什么。因为Animal和Snake共享同一个Animal中的声明”private name:string“,所以它们是兼容的。然而,这对Employee不是这样。当尝试将一个Employee对象赋值给Animal时,会得到一个类型不兼容的错误。虽然Employee也有一个名为name的私有成员,但是它和Animal中的那个私有成员不是同一个。

参数属性

通过创建参数类型,public和private关键字也是创建和初始化类的成员快捷方式。该属性能够让你一步创建和初始化一个成员。看下面我们修改之前例子的代码。注意我们是如何不使用”theName“而在constructor上使用快捷的”private name:string”来创建并初始化name参数。

class Employee {
constructor(private name: string) {
}
move(meters: number = 0) {
alert(this.name + "移动了" + meters+"米");
}
}

用这种方式使用private来创建和初始化一个private成员,对于public也是同样的道理。

访问器

TS支持getters和setters作为访问一个对象成员拦截方式。这对于每个对象上的一个成员是如何被访问的提供了一种细粒度控制的方式。

下面使用get和set来转换一个类。首先,先不使用setter和getter来开始这个例子。

class Employee {
fullName: string;
}
var obj = new Employee();
obj.fullName = "tkb至简";
if(obj.fullName){
alert(obj.fullName);
}

虽然允许人们直接随意地设置fullName相当方便,但是如果人们突发奇想就可以改变名字,这会让我们很困惑。

在下面这个版本中,我们允许用户修改雇员之前,先要检测确保用户有一个有效的安全密码。我们可以使用会检测安全密码的set来取代直接访问fullName。我们添加一个相应的get来允许之前的例子继续无缝运行。

var passcode = "123456";
class Employee {
private _fullName: string;
get fullName(): string {
return this._fullName;
}
set fullName(newName: string) {
if (passcode && passcode == "123456") {
this._fullName = newName;
} else {
alert("您没有权限修改雇员信息!");
}
}
}
var obj = new Employee();
obj.fullName = "tkb至简";
if (obj.fullName) {
alert(obj.fullName);
}

为了证明访问器现在正在检查安全码,我们可以修改安全码来看当安全码不匹配的时候,会不会弹出一个对话框说“您没有权限修改雇员信息!”。

修改passcode为12345时,弹出错误提醒:

TypeScript:类(Classes)

注意:访问器要求你必须将编译器设置为输出ECMAScript 5。

静态属性

说到这里,我们目前只讨论了类的实例成员,它们只有当类实例化时才会出现在对象上。我们也可以创建静态成员,它们对于类自身都是可见的而不是实例。下面的例子,我们在origin上使用了static关键字,因为它对于所有的grids都是通用的。每一个实例都可以通过附加的类名来访问这个值。和实例访问之前加上this关键字相似。

class Grid {
static orgin = { x: 0, yield: 0 };
calculateDistanceFromOrgin(point: { x: number,y:number }) {
var xDist = point.x - Grid.orgin.x;
var yDist = point.y - Grid.orgin.y;
return Math.sqrt(xDist * xDist + yDist * yDist) * this.scale;
}
constructor(public scale: number) {}
}
var grid1 = new Grid(1.0);//比例尺为1
var grid2 = new Grid(5.0);//比例尺为5
alert(grid1.calculateDistanceFromOrgin({ x: 10, y: 10 }));
alert(grid2.calculateDistanceFromOrgin({ x: 10, y: 10 }));

高级技术点

构造函数

在TS中声明一个类的时候,实际上是一次创建了多个声明。第一个是实例类的类型。

class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello," + this.greeting;
}
} var user:Greeter;
user = new Greeter("World!");

document.writeln(user.greet());

这里,当我们声明”var user:Greeter;”时,我们使用了Greeter作为Greeter类的实例的类型。这个对于来自面向对象语言的程序员来说几乎是第二个特性。

我们也通过调用构造函数创建其他值。这个函数是在我们new出类的实例的时候调用的。为了理解实践中它是什么样子,让我们通过上面的TS代码生成的JS代码来看一下:

var Greeter = (function () {
function Greeter(message) {
this.greeting = message;
}
Greeter.prototype.greet = function () {
return "Hello," + this.greeting;
};
return Greeter;
})();
var user = new Greeter("World!");
document.writeln(user.greet());

这里,“var user”将会被构造函数赋值。当我们调用new并运行这个函数的时候,我们得到了该类的一个实例。构造函数也包含了该类的所有静态成员。思考每个类的另外一种方式是类有实例的一面和静态的一面。

稍微修改一下例子看一下不同:

class Greeter {
static standardGreeting = "Hello,There";
greeting: string;
greet() {
if (this.greeting) {
return "Hello," + this.greeting;
} else {
return Greeter.standardGreeting;
}
}
} var user: Greeter;

user = new Greeter();

alert(user.greet()); var greetMaker: typeof Greeter=Greeter;

greetMaker.standardGreeting = "Hey,there!";

var user2: Greeter = new greetMaker();

alert(user2.greet());

这个例子中,“user”和之前运行相似。我们实例化Greeter类,然后使用了这个对象。这个之前已经看到过。

下一个,然后我们直接只用类。这里我们创建了一个新的变量叫做“greetMaker”。这个变量保持了类本身,或者说成是它的构造函数。这里我们使用了typeof Greeter,意思是“给我们Greeter类本身的类型”而不是类型的实例。或者,更准确地说,“给我叫做Greeter的标志的类型”,它是构造函数的类型。这个类型包含了Greeter的所有的静态成员和创建Greeter类实例的构造函数。

使用类作为接口

正如之前说的,一个类的声明创建了两样东西:代表类的实例的类型和构造函数。因为类创建了类型,所以你可以在你可以使用接口的地方使用它们,例如:

class Point {
x: number;
y: number;
} interface Point3D extends Point {

z: number;

}

var point3D: Point3D = { x: 1, y: 2, z: 3 };