本文针对TypeScript 与ES5、ES6进行比较,并进一步进行面向对象设计
TypeScript
由微软(Microsoft)开发的一种开源的编程语言。由于JavaScript语言本身的局限性,难以胜任和维护大型项目开发,因此,微软开发了TypeScript
编程语言,通过在JavaScript的基础上添加静态类型定义构建而成,其设计目标是开发大型应用,它可以通过TypeScript编译器或Babel
编译成纯的JavaScript,主要是ES5,编译出来的 JavaScript 可以运行在任何浏览器上。
TypeScript 是 JavaScript 的超集,扩展了 JavaScript 的语法,因此现有的 JavaScript 代码可与 TypeScript 一起工作无需任何修改,TypeScript 通过类型注解提供编译时的静态类型检查。
TypeScript 可处理已有的 JavaScript 代码,并只对其中的 TypeScript 代码进行编译。
TypeScript 既是一种语言,又是一组用于生成 JavaScript 的工具,是一个帮助开发人员编写企业级 JavaScript 的开源项目。TypeScript 语言和编译器使 JavaScript 的开发更接近于传统的面向对象设计体验。
TypeScript 是一种给 JavaScript 添加特性的语言扩展。语法上,TypeScript 很类似于 JScript .NET,添加了对静态类型、经典的面向对象语言特性,如类、继承、接口和命名空间等,支持了微软对 ECMAScript 语言标准的实现。
TypeScript 通过类型批注
提供静态类型,以在编译时启动类型检查。
function minus(num1:number, num2:number):number{
return num1 - num2;
}
对于基本类型的批注是 number,bool 和 string。而动态类型的结构则是 any 类型。
类型批注
可以被导出到一个单独的声明文件,以让使用类型的已被编译为 JavaScript 的 TypeScript 脚本的类型信息可用。批注可以为一个现有的 JavaScript 库声明,就像已经为 Node.js 和 jQuery 所做的那样。
当一个 TypeScript 脚本被编译时,有一个产生作为编译后的 JavaScript 的组件的一个接口而起作用的声明文件 (具有扩展名 .d.ts) 的选项。在这个过程中编译器基本上带走所有的函数和方法体而仅保留所导出类型的批注。当第三方开发者从 TypeScript 中使用它时,由此产生的声明文件就可以被用于描述一个 JavaScript 库或模块导出的虚拟的 TypeScript 类型。声明文件的概念类似于 C/C++ 中头文件的概念。
declare module '*.js' {
const content: any
export default content
}
类型声明文件可以为已存在的 JavaScript 库手写接口声明。TypeScript 增加了对 ECMAScript 6 标准特性的支持。
TypeScript 支持集成了类型批注的 ECMAScript6 的类。
在TypeScript里,接口的作用就是为类型命名和为你的代码或第三方代码定义契约。
TypeScript 支持ECMAScript6 的模块的概念。
随着TypeScript和ES6里引入了类,在一些场景下需要额外的特性来支持标注或修改类及其成员。 装饰器(Decorators
)为在类的声明及成员上通过元编程语法添加标注提供了一种方式。
TypeScript 目前支持 ECMAScript 3、ECMAScript 5、ECMAScript 6的标准,微软团队承诺在任何新版本的 TypeScript 编译器中都遵循 ECMAScript 标准,因此随着新版本的采用,TypeScript 语言和编译器也会随之效仿。
TypeScript支持与JavaScript几乎相同的数据类型,此外还提供了实用的枚举类型。
判断真假的数据类型,其值就是简单的true
/false
值,在JavaScript和TypeScript里称为boolean
。
let isTrue: boolean = false;
与JavaScript一样,TypeScript里的所有数字都是浮点数,代表双精度 64 位浮点值,可以是整数,也可以是小数(分数)。这些浮点数的类型是 number
。除了支持十进制和十六进制字面量,TypeScript还支持ES6中引入的二进制和八进制字面量。
let d: number = 25; // 十进制
let h: number = 0xb30f; // 十六进制
let b: number = 0b0101; // 二进制
let o: number = 0o744; // 八进制
可以使用双引号"
或单引号'
扩起来的文本数据表示字符串。使用 string
表示字符串类型。
let name: string = "zcj";
TypeScript像JavaScript一样可以操作数组元素。 有两种方式可以定义数组。 第一种,使用数组泛型Array<元素类型>
:
let list: Array<string> = ['zhang','li'];
第二种,可以在元素类型后面加上[]
,表示由此类型元素组成的一个数组:
let list: string[] = ['zhang','li'];
元组属于数组的一种,允许表示一个已知元素的数组,元组中的元素类型不必相同。
let val: [string, number];
val = ['zhang', 5];
TypeScript中的枚举类型是对JavaScript标准数据类型的一个补充,使其可以为一组数值赋予友好的名称。通过关键字enum
来声明:
enum Lang {C, C++, Java, Python, C#, JavaScript, TypeScript, GO, SQL}
let l: Lang = Lang.TypeScript;
在编程阶段,为还不清楚类型的变量指定一个类型,其值可能来自于动态的内容,此时不希望类型检查器对这些值进行检查而是直接让其通过编译阶段的检查。可以使用 any
来标记这些变量的数据类型。
let val: any = 16;
val = 'zcj';
val = true;
void
类型与any
类型比较,它表示没有任何类型。用于标识方法返回值的类型,表示该方法没有返回值。
function hello(): void {
console.log("hi, 早上好!");
}
TypeScript有两种各自的类型分别称为null
和undefined
。 null
是一个只有一个值的特殊类型,表示一个空对象引用。而undefined
表示一个没有赋值的变量。
对于那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式,通过never
类型来表示。
function error(message: string): never {
throw new Error(message);
}
除了number,string,boolean,symbol,null或undefined等原始类型之外,通过使用object
类型,可以更好的表示像Object.create这样的类型数据。
TypeScript支持ES6的类设计,使用基于类的面向对象的方式。允许开发者使用其特性,并且编译后的JavaScript可以在所有主流浏览器和平台上运行。
class Person {
constructor(name) {
this.name = name;
}
giveBirth() {
console.log('给出'+this.name+'的出生日期');
}
}
class Student extends Person {
constructor(name, homework) {
super(name);
this.homework = homework;
}
doHomework() {
console.log('Student 做作业:'+this.homework);
}
}
class PartTimeStudent extends Student {
constructor(name, homework, department) {
super(name, homework);
this.department = department;
}
startWorking() {
console.log('PartTimeStudent 开始工作:' + this.department);
}
}
const ps = new PartTimeStudent('张三','编程','麦当劳');
ps.giveBirth();
ps.doHomework();
ps.startWorking();
该例展示了最基本的继承:子类从父类中继承了属性和方法。 Person
、Student
和PartTimeStudent
分别承担了父类与子类的角色,而Student
既承担了子类角色,又承担了父类的角色。子类通过 extends
关键字继承父类。子类也通常被称作派生类,父类也通常被称作基类,有时父类也称为超类。
所有基类对象的属性和功能, 派生类对象都可以使用继承来获取。继承是面向对象编程的基石之一, 继承意味着一个对象使用另一个对象作为其基本类型,从而继承了该基本对象的所有特征。
TypeScript 引入了public
和private
访问修饰符来将类变量和方法函数标记为公共或私有。 此外,还可以使用 protected
访问修饰符。任何调用代码都可以访问公共类属性。 考虑以下代码:
class AbcWithPublic {
public id: number;
}
let obj = new AbcWithPublic();
obj.id = 5;
在这里,定义了一个名为 AbcWithPublic 的类,它有一个名为 id 的属性。 然后创建这个类的一个实例,obj,并将值 5 分配给这个实例的 id 属性。 现在来探索将属性标记为私有将如何影响对该属性的访问,如下所示:
class AbcWithPrivate {
private id: number;
constructor(_id : number) {
this.id = _id;
}
}
let obj = new AbcWithPrivate(5);
obj.id = 15;
在这里,定义了一个名为 AbcWithPrivate 的类,它有一个名为 id
的属性,现在它已被标记为私有。 这个类还有一个构造函数,也可以称为构造方法,它接受一个名为 _id
的参数,并将这个参数的值赋给 id
属性。 请注意,在此分配中使用了 this.id 语法。
然后创建这个类的一个实例,命名为 obj ,然后尝试将数值 15 赋值给私有 id 属性。 不过,此代码将生成以下错误:
error TS2341: Property 'id' is private and
only accessible within class 'AbcWithPrivate'.
正如从错误消息中看到的那样,TypeScript 不允许在类本身之外的实例中分配该类的 id 属性,由于已将其标记为private
(私有)。 请注意: 可以在类中的方法函数中为 id 属性赋值,就像在构造方法函数constructor
中所做的那样。
标注:
默认情况下,类函数是
public。 没有为属性或函数指定
private访问修饰符将导致它们的访问级别默认为
public。 类也可以将函数和属性标记为
protected。
Constructor
)访问修饰符:TypeScript 引入的构造函数Constructor
,允许直接在构造函数中使用访问修饰符指定参数。 考虑以下代码:
class AbcWithConstructor {
constructor(public id: number, private name: string){
}
}
let myObj = new AbcWithConstructor(6, "张三");
console.log(`myObj id: ${myObj.id}`);
console.log(`myObj.name: ${myObj.name}`);
此代码片段定义了一个名为 AbcWithConstructor
的类。 构造函数使用两个参数——数字类型的 id
和字符串类型的name
。 但是请注意,对于 id
的访问修饰符为 public
,对于 name
的为 private
。 这种标记会自动在 AbcWithConstructor
类上创建一个公共 id
属性和一个私有name
属性。
然后创建一个名为 myObj
的变量并将 AbcWithConstructor 类的一个新实例分配给它。 一旦这个类被实例化,它就会自动拥有两个属性:一个是 number
类型的 id
属性,它是公共(public
)的,以及一个 string
类型的 name
属性,它是私有(private
)的。 然而,编译前面的代码会产生一个 TypeScript 编译错误:
error TS2341: Property 'name' is private and only accessible within class 'AbcWithConstructor'.
这个错误告诉我们自动属性name
被声明为private
,因此在类本身之外的代码是不可用的。
除了 public
、private
和 protected
访问修饰符之外,还可以将类属性标记为readonly
(只读)。 这意味着,一旦设置了属性的值,就不能被类本身或类的任何使用者修改。 只有一个地方可以设置只读属性,这是在构造函数本身内。 考虑以下代码:
class AbcWithReadOnly {
readonly name: string;
constructor(_name : string) {
this.name = _name;
}
setReadOnly(_name: string) {
this.name = _name;
}
}
此时,我们定义了一个名为 AbcWithReadOnly
的类,它具有string
类型的name
属性,该属性已用 readonly
关键字标记修饰。 构造函数constructor
正在设置这个值。 然后我们定义了第二个名为 setReadOnly
的函数方法,试图在其中设置这个只读属性。 此代码将发生以下编译错误:
error TS2540: Cannot assign to 'name' because it is a constant or a read-only property.
这个错误消息说明,可以设置只读属性的唯一位置是在构造函数方法constructor
中。
ES5中引入了属性访问器的概念。 访问器只是一个函数
,当类的使用者设置属性或检索属性时调用它。 这意味着可以检测到类的使用者何时获取或设置属性,这可以用作其他逻辑的触发机制。 为了使用访问器,我们创建了一对 get 和 set 函数(具有相同的函数名)来访问一个内部属性。 这个概念通过以下代码示例来理解:
class AbcWithAccessors {
private _zcj : string;
get zcj() {
console.log(`内部 get zcj()`);
return this._zcj;
}
set zcj(value:string) {
console.log(`内部 set zcj()`);
this._zcj = value;
}
}
这个类有一个私有的 _zcj
属性和两个函数,都称为 zcj
。 这些函数中的第一个以 get 关键字为前缀。 当类的使用者检索或读取属性时,将调用此 get 函数。 在该示例中,get 函数将调试消息记录到控制台,然后简单地返回内部 _zcj
属性的值。
这些函数中的第二个以 set 关键字为前缀并接受一个值参数。 当类的使用者分配值或设置属性时,将调用此 set 函数。 在该示例中,只是将消息记录到控制台,然后设置内部 _zcj
属性。 请注意,内部 _zcj
属性是私有的,因此不能在类本身之外访问。
现在可以使用这个类了,代码如下:
let obj = new AbcWithAccessors();
obj.zcj = 'zhao cong jun';
console.log(`zcj 属性被设置为 ${AbcWithAccessors.zcj}`);
注意:
以上代码编译时,使用以下编译命令,否则会提示Accessors are only available when targeting ECMAScript 5 and higher.
错误信息。
tsc accessors.ts -t es5 // 以上代码保存文件名为:accessors.ts
// 编译的目标文件名为:accessors.js
在这里,创建了该类的一个实例,并将其命名为 obj
。 请注意如何不使用名为 get
和 set
的两个独立函数。 只是将它们用作单个 zcj
属性。
当我们给这个属性赋值时,ES5 的运行时会调用 set zcj(value)
函数,当我们检索访问到这个属性时,运行时会调用 get zcj()
函数。 这段代码的输出如下:
内部 set zcj()
内部 get zcj()
zcj 属性被设置为 zhao cong jun
使用 getter
和 setter
函数可以让我们挂钩到类属性,并在访问这些类属性时执行代码。此功能仅在使用 ECMAScript 5 及更高版本时可用。 请注意,某些浏览器不支持 ECMAScript 5(例如 IE 8),并且在尝试使用类访问器时会导致 JavaScript 运行时错误。
类可以具有静态属性。 如果一个类的属性被标记为静态,那么这个类的每个实例都将具有相同的属性值。 换句话说,同一个类的所有实例都将共享静态属性。 考虑以下代码:
class AbcWithStaticProperty {
static count = 0;
calculateCount() {
AbcWithStaticProperty.count ++;
}
}
此类定义在类属性计数上使用 static
关键字。 它有一个名为 calculateCount
的函数,用于递增静态计数属性。 请注意 calculateCount
函数中的语法。 通常,会使用 this
关键字来访问类的属性。 但是,这里需要引用属性的全名,包括类名,即AbcWithStaticProperty.count
,才能访问该属性。 然后就可以使用这个类了,代码如下:
let obj1 = new AbcWithStaticProperty();
console.log(`AbcWithStaticProperty.count = ${AbcWithStaticProperty.count}`);
obj1.calculateCount();
console.log(`AbcWithStaticProperty.count = ${AbcWithStaticProperty.count}`);
let obj2 = new AbcWithStaticProperty();
obj2.calculateCount();
console.log(`AbcWithStaticProperty.count = ${AbcWithStaticProperty.count}`);
此代码片段以名为 obj1
的 AbcWithStaticProperty
类的新实例开始。 然后将 AbcWithStaticProperty.count
的值记录到控制台。 再然后在这个类的实例上调用 calculateCount
函数,并再次将 AbcWithStaticProperty..count
的值记录到控制台。 接着再创建这个类的另一个实例,命名为 obj2
,调用其 calculateCount
函数,并将 AbcWithStaticProperty.count
的值记录到控制台。 此代码段将输出以下内容:
AbcWithStaticProperty.count = 0
AbcWithStaticProperty.count = 1
AbcWithStaticProperty.count = 2
这个输出告诉我们的是,名为AbcWithStaticProperty..count
的静态属性确实在同一个类的两个实例之间共享,即 obj1
和 obj2
。 它从 0 开始,并在调用 obj1.calculateCount
时递增。 当创建该类的第二个实例时,它也为该类实例保留其原始值 1。 当 obj2
更新此计数时,它也会为 obj1
更新。
静态函数是可以在类上调用而无需先创建类的实例的函数。 这些函数在本质上几乎是全局的,但必须通过在函数名前加上类名来调用。 考虑以下类的定义:
class AbcWithStaticFunction {
static hello() {
console.log(`Hello World!`);
}
}
AbcWithStaticFunction.hello();
此类定义包括一个名为 hello
的函数,它被标记为static
。 从代码的最后一行可以看出,可以调用这个函数,而无需新建 AbcWithStaticFunction 类的实例。 此时可以直接调用该函数,只要在其前面加上类名即可。
在处理大型项目时,尤其是在处理外部库时,可能会出现两个类同名
的情况,这显然会导致编译错误。 TypeScript 使用命名空间的概念来解决这些情况。用于命名空间的代码语法如下所示:
namespace MySpace {
class First {
}
export class Second {
name: string;
}
}
这里使用了namespace
关键字定义了一个命名空间,并将这个命名空间称为MySpace
。 命名空间声明类似于类声明,因为它的范围由左大括号和右大括号限定,即 {
意味着命名空间开启,而 }
意味着命名空间关闭。 这个命名空间中定义了两个类。 这些类被命名为First
和 Second
。使用命名空间时,类的定义声明在命名空间之外是不可见的,除非使用 export
关键字明确导出。 要创建在命名空间中定义的类,必须使用完整的命名空间名称来引用该类。 创建这些类的实例如下:
let first = new MySpace.First();
let second = new MySpace.Second();
在这里正在创建的 First
类的实例和Second
类的实例。 请注意,需要如何使用完整的命名空间名称才能正确引用这些类,即 new MySpace.Second()
。 由于First
类没有使用 export
关键字,所以这段代码编译时会产生如下错误:
error TS2339: Property 'First' does not exist on type 'typeof MySpace'.
想要避免此类错误,需要在First
类的前面使用 export
关键字,完整代码如下:
namespace MySpace {
export class First {
}
export class Second {
name: string;
}
}
let first = new MySpace.First();
let second = new MySpace.Second();
此时,使用完整的命名空间名称创建类的实例
的代码没有变化,每个类(以命名空间名称为前缀
)都被编译器视为一个单独的类名。即使在不同的命名空间
中使用相同的类名也不会导致编译错误。
抽象类,有时称为抽象基类,通常用于提供一组基本功能或属性,这些功能或属性在一组相似类之间共享。抽象类是一种允许在相似对象组之间重用代码的技术。抽象类需要使用关键字abstract
来声明,其中的部分(不是全部
)函数方法也需要使用关键字abstract
来定义,考虑以下类的设计:
abstract class Employee {
public id: number;
public name: string;
abstract getInfos(): string;
public printInfos() {
console.log(this.getInfos());
}
}
class Engineer extends Employee {
getInfos(): string {
return `id : ${this.id}, name : ${this.name}`;
}
}
class Manager extends Engineer {
public engineers: Engineer[];
getInfos() : string {
return super.getInfos()
+ `, 工程师人数 ${this.engineers.length}`;
}
}
在这里,定义了一个名为 Employee
的抽象类,它包括一个 id
和 name
属性,对于工程师、经理等雇员都是通用的。然后定义了所谓的抽象函数方法名称为 getInfos
。使用抽象函数方法意味着从这个抽象类派生的任何类都必须实现这个函数方法。接着定义了一个 printInfos
函数方法来记录这个 Employee
的详细信息到控制台。请注意
如何从抽象类中调用抽象函数方法 getInfos
的。这意味着在 printInfos
函数方法中的代码将调用派生类中函数方法的实际实现。
这里解释了为什么抽象类是无法实例化的类的原因,就是因为抽象类中至少含有一个抽象函数方法的定义,否则就不能声明为抽象类。而所谓的抽象函数方法的定义,此处只对函数方法名进行声明,并没有函数方法体的实现。这是抽象类不能实例化的真正原因。而其抽象方法的实现需要在其派生类中重写完成。
第二个类,名为 Engineer
,扩展了 Employee
类。 因此,它必须实现已在基类中标记为抽象的 getInfos
函数方法。 它返回 Engineer
的 id
和 name
属性的字符串表示形式。
接下来,有一个派生自 Engineer
的名为 Manager
的类。 因此,这个 Manager
类也有一个 id
和 name
属性,但有一个名为 engineers
的额外属性。 因为这个类已经派生自Engineer
,所以不一定需要再次实现函数方法 getInfos
。 它可以简单地使用 Engineer
类提供的 getInfos
函数方法的版本。 但是请注意,实际上已经在 Manager
类中重写了这个 getInfos
函数方法。 该函数通过 super
关键字调用基类 getInfos
函数方法,然后添加一些关于其 engineers
属性的额外信息。 现在来看看当创建和使用这些类时会发生什么,如下:
let engineer = new Engineer();
engineer.id = 1;
engineer.name = "张工";
engineer.printInfos();
在此,创建了一个 Engineer
类的实例,命名为engineer
,设置了它的 id
和 name
属性,并从抽象类中调用了printInfos
函数方法。 而抽象类将调用在 Engineer
类中提供的 getInfos
函数方法的实现,因此将以下内容输出到控制台:
id : 1, name : 张工
现在以类似的方式使用 Manager
类:
let manager = new Manager();
manager.id = 2;
manager.name = "李经理";
manager.engineers = new Array();
manager.engineers.push(engineer)
manager.printInfos();
在这里,创建了一个 Manager
类的实例,命名为manager
,并像以前一样设置它的 id
和 name
属性。 因为这个类也有一个engineers
数组,所以将engineers
属性设置为一个空白数组,并将engineer
对象添加其中。 但是请注意,当在最后一行调用抽象类 printInfos
函数方法时会发生什么:
id : 1, name : 张工
id : 2, name : 李经理, 工程师人数 1
抽象类的printInfos
函数方法调用派生类的getInfos
函数方法的实现。 因为 Manager
类还重写了一个 getInfos
函数方法,所以抽象类会在 Manager
实例上调用这个函数。 然后 Manager
实例上的 getInfos
函数方法调用基类(super
)实现,即 Engineer
实例的getInfos函数方法,如代码
super.getInfos() `所示。 然后,它会附加有关工程师人数的信息。
使用抽象类和继承能够以更简洁和更可重用的方式编写代码。 抽象、继承是良好的面向对象设计的基础。 正如读者所看到的,TypeScript
语言能够帮助编写良好、干净的 JavaScript
代码。
接口提供了一种机制,用来定义对象必须实现的属性和方法。因此,它是一种定义自定义类型的方法。 当我们已经了解了 TypeScript
的语法,用于将变量强类型化为一种基本类型,例如字符串或数字。 使用这种语法,还可以将变量强类型为接口类型。 这意味着变量必须具有与接口中描述的相同的属性。 如果一个对象依附于一个接口,则称该对象实现了该接口。 接口是使用 interface
关键字来定义。考虑以下 TypeScript
代码:
interface IResponse {
id: number;
myurl: string;
}
从一个名为 IResponse
的接口开始,它有一个 id
和一个 myurl
属性。 id 属性被强类型为 number
类型,myurl
属性为 string
类型。 然后可以将此接口定义应用于变量,如下所示:
let res : IResponse;
res = { id: 19044405, myurl : "http://wwww.ischoolcode.cn" };
在此处,定义了一个名为 res
的变量,并将其强类型化为 IResponse
类型。 然后创建一个对象实例,并为对象的属性赋值。 请注意,IResponse
接口同时定义了 id
和 myurl
属性,因此,两者都必须存在。
接口定义还可以包括可选属性。 考虑以下接口定义:
interface IResponse {
id: number;
myurl?: string;
}
定义的IResponse
接口,它有一个 number
类型的 id
属性和一个名为 myurl
的 string
类型的可选属性。 注意可选属性的语法,位于myurl
属性后的?
字符用于指定该属性是可选的。 因此可以使用这个接口定义,如下:
let idOnly : IResponse = { id: 1904440501 };
let idAndUrl : IResponse = { id: 1904440502, myurl : "http://wwww.ischoolcode.cn" };
idAndUrl = idOnly;
这里有两个变量,它们都实现了 IResponse
接口。 第一个名为 idOnly
的变量只是指定了 id
属性。 这是有效的 TypeScript
,因为已将 IResponse
接口的 myurl
属性标记为可选。 第二个变量称为 idAndUrl
,它指定了 id
和 myurl
属性。
请注意此代码段的最后一行。 因为这两个变量都实现了IResponse
接口,所以可以将它们相互分配。 如果不使用具有可选属性的接口定义,此代码通常会导致错误。
接口是 TypeScript 的编译时语言功能,编译器不会从包含在 TypeScript 项目中的接口生成任何 JavaScript 代码。 接口仅由编译器用于编译步骤期间的类型检查。
接口也是描述类行为的一种方式。 如果期望接口提供某些行为,接口可以被视为类必须实现的一种契约。与类一样,接口在处理函数方法时遵循相同的规则。 更新 IResponse
接口定义,为新的函数方法编写其函数声明定义,如下所示:
interface IResponse {
id: number;
myurl: string;
print(): string;
func1(arg: any): any;
func2(arg?: number);
func3(arg?: number);
func4(...args: number[]);
func5(callback: (id: number) => string);
}
此接口定义包括 id
和 myurl
属性以及一个print
函数方法声明。有一个 func1
函数方法签名的声明,以及 func2
函数方法声明,它定义显示了如何在接口中使用可选参数。然而,func3
函数方法的接口声明定义与其将要进行的实现类的定义略有不同。该接口定义了实现类的形状,因此函数方法声明中不能包含参数变量的值。因此,将 func3
函数方法声明中的arg
参数定义为可选参数,并将其默认值的设置留给实现类去执行。func4
函数方法的声明定义中包含的参数...args
具有其余参数语法。而 func5
函数方法的声明显示了如何定义回调函数方法签名。
它们看起来很像实际的类函数方法,但没有函数体的实现。 请记住,接口只是定义了函数方法的声明,其实现要留给它的实现类来完成。
我们的实现类定义现在使用 implements
关键字来实现 IResponse
接口。实现类中必须全部完成接口中所声明的函数方法的实现任务(函数方法体的编写
)。其中所有函数方法都遵循的语法和规则,简单回顾如下:
any
关键字来放松强类型作为以上规则的示例,考虑以下代码:
class MyResponse implements IResponse {
id: number;
myurl: string;
constructor(idArg: number, urlArg: string);
constructor(idArg: string, urlArg: string);
constructor(idArg: any, urlArg: any) {
if (typeof idArg === "number") {
this.id = idArg;
}
this.myurl = urlArg;
}
print(): string {
return "id:" + this.id + " url:" + this.myurl;
}
func1(arg: any): any {
this.id = arg;
}
func2(arg?: number) {
if (arg) {
this.id = arg;
}
}
func3(arg: number = 19044405) {
this.id = arg;
}
func4(...args: number []) {
if (args.length > 0) {
this.id = args[0];
}
}
func5( callback: (id: number) => string ) {
callback(this.id);
}
}
首先,要注意的是构造函数
。 这里的的实现类的定义为构造函数使用函数重载
,允许使用数字和字符串或两个字符串来构造类的实例。 以下代码显示了将如何使用这些构造函数定义:
let mr1 = new MyResponse(19044405, "http://wwww.ischoolcode.cn");
let mr2 = new MyResponse("19044405", "http://wwww.ischoolcode.cn");
let mr3 = new MyResponse(true, "爱校码.中国");
mr1
变量使用构造函数的数字、字符串
变体,而 mr2
变量使用字符串、字符串
变体。 mr3
变量将产生编译错误,因为这里不允许构造函数使用布尔值、布尔值变体。
在使用重载构造函数时必须小心。当调用构造函数的字符串、字符串
变体时会发生什么,在构造函数中,将 idArg 参数的值分配给类的 id 属性,即使已经定义了类的id
属性为number
类型,但是当用字符串调用构造函数时,这显然不是一个数字类型。 在这种情况下,TypeScript 不会产生编译错误,并且不会自动尝试将值“19044405”转换为数字。 在需要这种类型的功能的情况下,需要使用类型保护来确保类型安全:
constructor(idArg: any, urlArg: any) {
if (typeof idArg === "number") {
this.id = idArg;
}
this.myurl = urlArg;
}
在这里,引入了类型保护,以确保仅在 idArg
参数的类型实际上是数字时才分配 id
属性(其类型为数字类型)。
其次,来看看该类实现的其余函数方法,从 func1
函数开始:
mr1.func1(true);
mr1.func1({ id: 1904, name: "ischoolcode" });
此示例中的第一个调用是使用布尔值参数调用 func1
函数方法,第二个调用是使用任意对象参数。 这两次函数方法调用都是有效的,因为参数 arg
1是使用any
类型定义的。
下一个,是 func2
函数方法:
mr1.func2(1904);
mr1.func2();
在这里,首先使用单个参数调用 func2
函数方法,然后不使用任何参数调用之。 同样,这些调用是有效的,因为 arg
参数被标记为可选。
下一个,是 func3
函数方法:
mr1.func3(1904);
mr1.func3();
对 于func3
函数方法的这两个调用都是有效的。 第一次调用将覆盖默认值 ,而第二次调用(不带参数)将使用默认值 。
下一个,是 func4
函数方法:
mr1.func4(1904,19044, 190444);
mr1.func4(1904,19044, 190444, 1904440, 19044405);
这里的 func4
函数方法可以使用任意数量的参数调用,因为使用其余参数语法
将这些参数保存在一个数组中。 这两个调用都是有效的。
最后,看一下 func5
函数方法:
function myCallback(id: number): string {
return id.toString();
}
mr1.func5(myCallback);
此段代码显示名为 myCallback
的函数的定义,该函数与 func5
函数方法所需的回调签名相匹配。 这允许将 myCallback
作为参数传递给 func5
函数方法。
在结束抽象类与接口这个小节之际,来比较一下抽象类与接口的情况,当抽象类当中的所有函数方法都是抽象方法的时候,其作用就相当于接口了,只不过接口中的函数方法声明省略了关键字abstract。也可以认为接口是抽象类的特例。
面向接口编程是设计模式当中的重要理念,程序员应该针对接口编程,而不是针对其实现编程。 这意味着程序
是使用接口作为对象之间定义的交互来构建的。 通过对接口进行编程,客户端对象不知道其依赖对象的内部逻辑,因此对更改更具弹性。 通过定义一个接口,我们可以开始构建夯实一个 API
,该 API
描述一个对象提供什么功能、应该如何使用它,以及多个对象如何相互交互。
多态性,是指在编程中指定契约并让许多不同类型实现该契约
的能力。 使用实现某些契约
的类的代码不需要知道具体实现的细节。 考虑以下代码:
interface IVehicle {
moveTo(x: number, y: number);
}
class Car implements IVehicle {
moveTo(x: number, y: number) {
console.log('开车去 ' + x + ' ' + y);
}
}
class SportsCar extends Car {
constructor() {
super();
console.log('参加比赛');
}
}
class Airplane implements IVehicle {
moveTo(x: number, y: number) {
console.log('飞往 ' + x + ' ' + y);
}
}
class Satellite implements IVehicle {
moveTo(x: number) {
console.log('定位 ' + x);
}
}
function navigate(vehicle: IVehicle) {
vehicle.moveTo(88.3545677, 23.5623836);
}
const car = new SportsCar();
navigate(car);
const airplane = new Airplane();
navigate(airplane);
const satellite = new Satellite();
navigate(satellite);
这里的代码展示了 TypeScript
中的多态性。导航函数navigate
接受与 IVehicle
接口兼容的任何类型。具体来说,这意味着任何针对IVehicle
接口的实现类具有名为 moveTo
的函数方法,该方法最多接受两个 number
类型的参数。注意:
如果一个方法接受的参数较少,则它在结构上与其兼容。在许多语言中,即使方法体中没有使用冗余参数,也将被迫指定它,但在 TypeScript 中,可以省略它。如果契约
指定了参数,调用代码仍然可以传递参数,这保留了多态性。
Car
实现了IVehicle
接口,SportsCar
继承自 Car
,因此它也实现了 IVehicle
接口。 Airplane
也实现 IVehicle
接口,并且重写了 moveTo
方法。不过 Satellite
类重写moveTo
方法时,只能控制“x”
坐标,这种类型仍然兼容,因为 TypeScript 中允许使用具有较少参数的类型。基于其结构接受兼容类型是 TypeScript 结构类型系统的一个特性。
代码执行结果如下:
参加比赛
开车去 88.3545677 23.5623836
飞往 88.3545677 23.5623836
定位 88.3545677
博文最后更新时间: