状态: 初稿
介绍
传统 JavaScript 开发者用函数和基于原型的继承构建可重用组件, 但有些程序员更喜欢面向对象方法 — 类多数功能由继承而来, 对象又参照类实例化. ECMAScript 2015 (也称 ECMAScript 6), 开始允许开发者按照面向对象的, 基于类的编程方法构建应用程序. TypeScript 用户无需等待新版本 JavaScript, 现在就可以使用这些技术, 编译器生成的 JavaScript 可在各主流浏览器和平台运行.
类
1 2 3 4 5 6 7 8 9 10 11 class Greeter { greeting : string ; constructor (message: string ) { this .greeting = message; } greet ( ) { return "Hello, " + this .greeting ; } } let greeter = new Greeter ("world" );
假如你有 Java 或 C# 经验, 这语法对你并不陌生. 我们创建了一个新类 Greeter
. 这个新类有三个成员, 分别是: 一个叫 greeting
的属性, 一个构造器, 一个成员函数(方法) greet
.
你也注意到在类定义中, 我们引用一些标识符要加 this.
前缀. 这表示一个对类成员的访问.
在最后一行, new
关键字创建了一个类的对象(实例).new
语句首先创建一个形体跟 Greeter
一致的对象, 然后调用类构造器方法初始化该对象.
继承
在 TypeScript 中, 我们可以运用许多常见面向对象模式. 继承是基于类编程最重要的模式之一, 其基本思想是: 创建新类, 继承旧类, 完善功能.
我们看个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Animal { move (distanceInMeters: number = 0 ) { console .log (`Animal moved ${distanceInMeters} m.` ); } } class Dog extends Animal { bark ( ) { console .log ('Woof! Woof!' ); } } const dog = new Dog ();dog.bark (); dog.move (10 ); dog.bark ();
这例子展现了继承最基本的能力: 子类继承父类的属性和方法. 这里, Dog
是一个用 extends
关键字继承基类 Animal
的派生类 . 派生类也叫子类 , 基类也叫父类
因为 Dog
完善了 Animal
的功能, 我们现在可以创建一个既能 move
, 又能 bark
(吠叫)的 Dog
实例.
再看个稍复杂点的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Animal { name : string ; constructor (theName: string ) { this .name = theName; } move (distanceInMeters: number = 0 ) { console .log (`${this .name} moved ${distanceInMeters} m.` ); } } class Snake extends Animal { constructor (name: string ) { super (name); } move (distanceInMeters = 5 ) { console .log ("Slithering..." ); super .move (distanceInMeters); } } class Horse extends Animal { constructor (name: string ) { super (name); } move (distanceInMeters = 45 ) { console .log ("Galloping..." ); super .move (distanceInMeters); } } let sam = new Snake ("Sammy the Python" );let tom : Animal = new Horse ("Tommy the Palomino" );sam.move (); tom.move (34 );
这个例子体现更多我们暂未涉及的继承特性.
第一, 每个拥有构造函数的子类必须显式调用 父类构造函数super()
. 第二, 在构造函数中, 要先调用 super()
, 再访问其他类成员. 以上规则很重要, 是 TypeScript 语法检查的一部分.
最后, 这个例子还展示了如何覆写 父类方法, 以创建子类特有版本. 这里不论是 Snake
, 还是 Horse
, 都根据自身特点覆写了父类 Animal
的 move
方法. 注意虽然 tom
的类型是 Animal
, 由于它的值是一个 Horse
, 调用 tom.move(34)
调用的是 Hosre
的覆写方法.
例子输出如下:
1 2 3 4 Slithering... Sammy the Python moved 5m. Galloping... Tommy the Palomino moved 34m.
访问修饰符
公有是默认状态
纵观所有例子, 我们在程序生命期间都不受限制地访问一个对象的任何成员. 如果熟悉其他语言中的类, 就知道我们需要 public
关键字; 比如说 C#, C# 要求你用 public
修饰想对外部可见的每个成员. 而在 TypeScript 中, 公有(public)是成员默认状态.
你还是可以用 public
显式修饰一个成员.Animal
类就像这样:
1 2 3 4 5 6 7 class Animal { public name : string ; public constructor (theName: string ) { this .name = theName; } public move (distanceInMeters: number ) { console .log (`${this .name} moved ${distanceInMeters} m.` ); } }
理解 private
如果一个成员是 private
的, 你将不能在所属类外部访问它. 例如:
1 2 3 4 5 6 class Animal { private name : string ; constructor (theName: string ) { this .name = theName; } } new Animal ("Cat" ).name ;
TypeScript 是一个结构化类型系统. 它在比较两个不同类型的时候, 不管两者出身, 只要它们所有成员是相容的, 它们自身就是相容的.
然而, 当参与比较的类型有 protected
或 private
成员时, 标准稍有不同. 要兼容一个含有 protected
, 或 private
成员的类, 要求另一个类也有对应 protected
或 private
成员, 而且两个 private
成员同源(即来自同一个父类).
来看实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Animal { private name : string ; constructor (theName: string ) { this .name = theName; } } class Rhino extends Animal { constructor ( ) { super ("Rhino" ); } } class Employee { private name : string ; constructor (theName: string ) { this .name = theName; } } let animal = new Animal ("Goat" );let rhino = new Rhino ();let employee = new Employee ("Bob" );animal = rhino; animal = employee;
上例, 我们有一个 Animal
和一个 Rhino
(犀牛), Rhino
是 Animal
的子类. 我们还有一个新类 Employee
, 单论形体, Employee
与 Animal
是相容的. 现在, 创建这些类的一些变量, 然后试试互相赋值会发生什么. 由于 Animal
和 Rhino
共享在 Animal
中定义的 private name: string
, 所以它们的 private
成员是同源的. 而对 Employee
, 则并非如此. 试图把 Employee
赋值给 Animal
时出错, 说两个类型不相容. 虽然 Employee
也有名为 name
的 private
成员, 但它不来自 Animal
.
理解 protected
protected
比 private
宽松, 你可以在所属类和派生类访问 protected
成员. 例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Person { protected name : string ; constructor (name: string ) { this .name = name; } } class Employee extends Person { private department : string ; constructor (name: string , department: string ) { super (name); this .department = department; } public getElevatorPitch ( ) { return `Hello, my name is ${this .name} and I work in ${this .department} .` ; } } let howard = new Employee ("Howard" , "Sales" );console .log (howard.getElevatorPitch ());console .log (howard.name );
可以看到的确不能在外部访问 Person
类的 name
成员, 而 Employee
派生自 Person
, 在 Employee
的成员方法访问就没问题,
你可以用 protected
修饰构造函数. 类的 protected
构造函数表示不能在类外创建类的实例, 但可以继承. 举例,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Person { protected name : string ; protected constructor (theName: string ) { this .name = theName; } } class Employee extends Person { private department : string ; constructor (name: string , department: string ) { super (name); this .department = department; } public getElevatorPitch ( ) { return `Hello, my name is ${this .name} and I work in ${this .department} .` ; } } let howard = new Employee ("Howard" , "Sales" );let john = new Person ("John" );
readonly 关键字
你可以用 readonly
关键字定义”只读”属性. 只读属性必须在定义时, 或在构造函数内获得初值.
1 2 3 4 5 6 7 8 9 class Octopus { readonly name : string ; readonly numberOfLegs : number = 8 ; constructor (theName : string ) { this .name = theName; } } let dad = new Octopus ("Man with the 8 strong legs" );dad.name = "Man with the 3-piece suit" ;
参数属性
上一个例子, 我们为类 Octopus
(章鱼) 定义了一个只读成员 name
, 然后给构造函数添加参数 theName
. 在构造函数内, 把 theName
赋值给 this.name
, 这是为了在构造函数结束后继续使用 theName
.构造属性 语法让你一次性创建, 初始化一个属性. 以下是上例简化后的 Octopus
类定义:
1 2 3 4 5 class Octopus { readonly numberOfLegs : number = 8 ; constructor (readonly name: string ) { } }
我们丢掉了 theName
, 构造函数参数 readonly name: string
同时定义并用实参初始化成员 name
. 成员定义和初始化由此统一为一体.
用访问修饰符 或 readonly
或两者组合修饰构造函数的一个参数来定义参数属性. 一个 private
参数属性对应类中一个 private
成员, 一个 protected
参数属性对应类中一个 protected
成员, 以此类推.
存取方法
TypeScript 存/取方法可以拦截对类成员的存/取操作. 借助该特性, 你可以自定义存/取方法精细控制如何访问类成员.
为获得直观的理解, 我们来改造一个类, 为它添加存取方法. 先从没有存/取方法的类开始:
1 2 3 4 5 6 7 8 9 class Employee { fullName : string ; } let employee = new Employee ();employee.fullName = "Bob Smith" ; if (employee.fullName ) { console .log (employee.fullName ); }
随意存取 fullName
的确很方便, 但为了数据的有效性, 你最好为存操作添加一些限制.
现在来改造 Employee
类, 首先 fullName
加入存方法, 确保它的长度符合后端数据库对应字段的长度要求. 如果过长就抛出异常通知客户程序.
为维持现有功能不变, 我们还添加了一个取方法, 未修改地读取出 fullName
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const fullNameMaxLength = 10 ;class Employee { private _fullName : string ; get fullName (): string { return this ._fullName ; } set fullName (newName: string ) { if (newName && newName.length > fullNameMaxLength) { throw new Error ("fullName has a max length of " + fullNameMaxLength); } this ._fullName = newName; } } let employee = new Employee ();employee.fullName = "Bob Smith" ; if (employee.fullName ) { console .log (employee.fullName ); }
现在把一个长度大于 10 的字符串赋值给 fullName
, 观察是否有异常抛出, 以验证我们的存方法在发挥作用.
存取方法若干说明:
第一, 为使用存取方法, 编译器输出不得低于 ECMAScript 5. 降级到 ECMAScript 3 是不受支持的. 第二, 缺失存方法的属性等同于只读属性. 在导出 .d.ts
时尤其有用, 你的用户只能 “看到” 该属性, 而不能修改它.
静态属性
到目前为止, 我们只探讨了类的实例成员, 实例方法依附类实例而存在. 同样地, 你可以为一个类创建静态成员, 静态成员无需创建类实例就能使用. 下例, 我们用 static
修饰变量 origin
, 所有 grid (Grid
的实例) 共享这个变量. 如同 this.
代表实例成员访问. 实例方法必须使用类名前缀访问静态成员, 这里我们前置 Grid.
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Grid { static origin = {x : 0 , y : 0 }; calculateDistanceFromOrigin (point: {x: number ; y: number ;} ) { let xDist = (point.x - Grid .origin .x ); let yDist = (point.y - Grid .origin .y ); return Math .sqrt (xDist * xDist + yDist * yDist) / this .scale ; } constructor (public scale : number ) { } } let grid1 = new Grid (1.0 ); let grid2 = new Grid (5.0 ); console .log (grid1.calculateDistanceFromOrigin ({x : 10 , y : 10 }));console .log (grid2.calculateDistanceFromOrigin ({x : 10 , y : 10 }));
抽象类
抽象类是一种类, 你可以为它创建派生类. 但不能直接实例化它. 与接口不同, 一个抽象类可能已包含某些成员的实现细节.abstract
关键字用来声明抽象类, 或抽象类内部的抽象方法.
1 2 3 4 5 6 abstract class Animal { abstract makeSound (): void ; move (): void { console .log ("roaming the earth..." ); } }
抽象类内部以 abstract
修饰的方法叫做抽象方法, 抽象类不实现抽象方法, 它的派生类要实现这些抽象方法. 抽象方法的定义语法与接口函数基本一致, 只声明函数的签名, 不提供实现(函数体). 然后用 abstract
关键字表示这是一个抽象方法, 最后添加可选的访问修饰符.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 abstract class Department { constructor (public name: string ) { } printName (): void { console .log ("Department name: " + this .name ); } abstract printMeeting (): void ; } class AccountingDepartment extends Department { constructor ( ) { super ("Accounting and Auditing" ); } printMeeting (): void { console .log ("The Accounting Department meets each Monday at 10am." ); } generateReports (): void { console .log ("Generating accounting reports..." ); } } let department : Department ; department = new Department (); department = new AccountingDepartment (); department.printName (); department.printMeeting (); department.generateReports ();
高级话题
构造器函数
你在 TypeScript 创建一个类的同时实际上创建了许多定义. 其一, 类实例的类型.
1 2 3 4 5 6 7 8 9 10 11 12 13 class Greeter { greeting : string ; constructor (message: string ) { this .greeting = message; } greet ( ) { return "Hello, " + this .greeting ; } } let greeter : Greeter ;greeter = new Greeter ("world" ); console .log (greeter.greet ());
当我们写下 let greeter: Greeter
, Greeter
将作为 Greeter
类的实例类型. 对其他面向对象程序员来说, 这几乎是第二性质.
其二, 我们还创建了一个为 new
操作符调用的构造器函数 . 这个函数作为值赋给一个变量. 我们可以在生成的 JavaScript 中看到它:
1 2 3 4 5 6 7 8 9 10 11 12 13 let Greeter = (function ( ) { function Greeter (message ) { this .greeting = message; } Greeter .prototype .greet = function ( ) { return "Hello, " + this .greeting ; }; return Greeter ; })(); let greeter;greeter = new Greeter ("world" ); console .log (greeter.greet ());
这里, let Greeter
把构造器函数赋值给 Greeter
. 当我们执行 new
操作调用构造器函数, 我们得到该类的一个实例. 构造器函数也包含所有类静态成员. 另一种看待类的方式是区分”实例面”和”静态面”.
对上例稍作修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Greeter { static standardGreeting = "Hello, there" ; greeting : string ; greet ( ) { if (this .greeting ) { return "Hello, " + this .greeting ; } else { return Greeter .standardGreeting ; } } } let greeter1 : Greeter ;greeter1 = new Greeter (); console .log (greeter1.greet ());let greeterMaker : typeof Greeter = Greeter ;greeterMaker.standardGreeting = "Hey there!" ; let greeter2 : Greeter = new greeterMaker ();console .log (greeter2.greet ());
在这个例子中, greeter1
的工作方式没有变化. 我们实例化 Greeter
, 得到一个实例. 没什么特别的.
接下来, 我们直接使用类本身. 首先创建一个名为 greeterMaker
的变量. 这个变量引用类本身, 或者说, 这个类的构造器函数. 它的类型是 typeof Greeter
, typeof Greeter
即 “把类 Greeter
的类型给我”, 而不是类实例的类型. 更准确地说, “给我标识符 Greeter
的类型”, 即构造器函数的类型. 它包括 Greeter
所有静态成员, 还有能创建 Greeter
类实例的构造器. 我们最后演示把 new
运算符作用在 greeterMaker
身上创建一个 Greeter
实例, 调用它的 greet
方法.
类作为接口
如上节所述, 类定义创建两个事物: 一个类实例类型, 一个构造器函数. 因为类创建类型, 你可以在能应用接口的地方应用类.
1 2 3 4 5 6 7 8 9 10 class Point { x : number ; y : number ; } interface Point3 d extends Point { z : number ; } let point3d : Point3 d = {x : 1 , y : 2 , z : 3 };