第3章〓面向对象 早期JavaScript(ES5前)使用构造函数和原型(prototype)链来实现面向对象编程,没有类的概念,从ES6开始引入了类,面向对象编程开始变得方便和直观。而TypeScript本身就是一个面向对象的编程语言,完全支持类、接口、继承等语法。通过本章的相关概念阐述和代码示例学习,读者应能理解并掌握TypeScript面向对象编程相关概念和技术。 面向对象编程(Object Oriented Programming,OOP),是通过对象的方式把现实世界映射到计算机模型的一种编程方法。相比于面向过程编程(Procedure Oriented Programming,POP),面向对象编程更适合分析和解决复杂项目问题。 简单而言,对象就是现实中的实体,类就是现实中的分类。比如,现在要实现一个员工管理系统。公司里有张珊、李思等员工。员工是一种分类,在面向对象编程中就是类,而张珊和李思这些具体实体在面向对象编程中就是对象。 视频讲解 3.1类 类是面向对象编程的核心。 类是对象的抽象,是用于创建对象的模板。没有通过类创建的对象,就无法借助映射、模拟问题域中的实体来解决项目问题。 面向对象编程时,通常在项目的问题域中分析现实中的实体,将同类实体的特征、属性、功能、行为等抽象出来,形成类结构。 3.1.1类结构 在TypeScript中,类可以看成由名字、属性、函数组成的一个封装结构体。 注意,在类中的函数习惯上被称作方法(method)。但本质上方法还是函数,只是函数在类中的别称而已。为便于陈述,本书不再区分方法和函数,统一称之为“函数”。 定义类结构的语法如下: class 类名 { 修饰符 属性名 : 类型 constructor(参数名 : 类型, ...) { 构造体 } 修饰符 函数名(参数名 : 类型, ...) { 函数体 } } 类用关键字class声明; 建议类名首字母大写; 类可以没有属性和函数,也可以有多个; 构造函数是用于创建对象的特殊函数,构造函数名用constructor表示。 【例31】定义员工类 1.class Employee { 2. name : string 3. salary : number 4. constructor(name : string, salary : number){ 5.this.name = name 6.this.salary = salary 7.} 8.addSalary(increase : number){ 9.this.salary += increase 10. } 11.} 12.let zhangShan : Employee = new Employee('张珊',5000) 13.console.log(zhangShan); 14.zhangShan.addSalary(1000) 15.console.log(zhangShan.salary); 第1~11行,用class关键字定义了员工类Employee,类的结构体则封装在一对花括号{}中。 第2~3行,定义了类的两个属性: string类型的name和number类型的salary。 第4~7行,constructor()为构造函数,是用于创建对象的特殊函数。如12行new Employee('张珊',5000)就是用该构造函数创建员工类Employee的对象zhangShan(变量名首字母小写)。注意,在第5、6行中,this.name和this.salary与定义构造函数时传入的参数name和salary不同,“this.变量名”代表当前对象的属性。第5、6行的作用是通过传入的name和salary参数值初始化对象的两个属性值。关于构造函数的更多细节,可参考3.1.5节内容。 第8~10行,定义addSalary()函数,其功能是通过传入参数increase的值来改变属性salary的值。 第12行,用关键字new加类名,调用构造函数来创建类Employee的对象zhangShan,同时设置对象zhangShan的两个属性name、salary的值分别为'张珊'和5000。 第13行,用console.log()函数输出对象zhangShan的值。 第14行,调用addSalary()函数,用于改变对象zhangShan的salary属性值。 第15行,用console.log()函数输出zhangShan的salary属性值。 执行结果为: Employee { name: '张珊', salary: 5000 } 6000 3.1.2属性 属性在编程中常常被称为字段(field)或成员变量(member variable)。 属性值一般用于表示实体的具体状态或特征。如定义一个员工类,员工有编号、姓名、性别、基本工资等特征值,也具备是否为应届生、是否在职等状态值,这些都可以在员工类中用属性来表示。 1. 属性定义需要初始化 属性若不是可选类型,则定义时应该初始化(定义时直接赋值),或在构造函数中进行初始化赋值,否则会出现语法错误。 【例32】未初始化属性值会出现语法错误 1.class Pet { 2.name : string 3.constructor(){ 4.} 5.} 第2行,未对属性name赋值,也没有在第3~4行的构造函数中赋值,会造成编译时报错: Property 'name' has no initializer and is not definitely assigned in the constructor. 解决以上语法问题有3种方式: 定义属性时直接赋值、在构造函数中对属性赋值、设置属性为可选类型。 【例33】未初始化属性值语法错误,解决方式一: 定义属性时直接赋值 1.class Pet{ 2.name : string = 'unknown' 3.constructor(){} 4.} 第2行,定义属性name时直接为其赋值'unknown',不再出现语法错误。 【例34】未初始化属性值语法错误,解决方式二: 在构造函数中对属性赋值 1.class Pet{ 2.name : string 3.constructor(name : string){ 4.this.name = name 5.} 6.} 第4行,在构造函数中为属性name赋予参数值。 【例35】未初始化属性值语法错误,解决方式三: 设置属性为可选类型 1.class Pet{ 2.name ?: string 3.constructor(){} 4.} 5.let pet = new Pet() 6.console.log(pet) 第2行,用问号?设置属性为可选类型,这样属性就不需要初始化了。当然,没有初始化相当于该属性不存在。因此,执行第6行语句不会输出属性name,执行结果为: Pet {} 2. 静态属性 除了用于描述对象状态和特征的属性外,还有能描述类的静态特征的属性,即静态属性。 普通属性可被称为实例属性或对象属性,必须先实例化对象,方能使用。而静态属性是属于“类”本身的属性,用static关键字声明,无须实例化对象,通过类名就可调用。 【例36】用static关键字声明静态属性并通过类名调用 1.class MathTool{ 2. static pi:number = 3.14 3.} 4.MathTool.pi = 3.1415 5.console.log(MathTool.pi) 第2行,用关键字static声明静态属性pi。 第4~5行,用“类名.静态属性”方式调用MathTool类的静态属性pi。 执行结果为: 3.1415 3.1.3函数 函数是复用代码的最基本单位。 对TypeScript而言,函数可单独定义,也可以定义在类中。与属性类似,定义在类中的函数也分为实例和静态两种。实例函数属于对象,必须先创建对象才能使用; 静态函数属于类,用static声明,通过类名直接调用。 【例37】在类中定义实例函数 1.class MathTool { 2.els : number[] = [] 3.max(...els : number[]) : number { 4.els.sort() 5.return els[els.length-1] 6.} 7.} 8.let mt = new MathTool() 9.mt.els = [1,4,2,3,2] 10.let max = mt.max(1,4,2,3,2) 11.console.log(max) 第3~7行,定义MathTool类的实例函数max()。 第8行,创建MathTool类的对象mt。 第10行,用“对象名.实例函数”方式调用对象mt的实例函数max()。 执行结果为: 4 【例38】在类中定义静态函数 1.class MathTool{ 2.static max(...els:number[]):number{ 3.els.sort() 4.return els[els.length-1] 5.} 6.} 7.let max = MathTool.max(1,4,2,3,2) 8.console.log(max) 第2行,用关键字static声明静态函数max()。 第7行,用“类名.静态函数”方式调用类MathTool的静态函数max()。 执行结果为: 4 3.1.4存储器与访问器 存储器(setter)和访问器(getter),就是用来获取和设置属性值的特殊函数。 如果外界可以随意访问属性,就会引发安全问题,这就是引入存储器和访问器来间接访问属性的原因。另外,存储器和访问器实际上为函数结构,因此还可在内部实现一些额外的逻辑功能。 访问器用关键字get定义,存储器用关键字set定义。 【例39】定义存储器和访问器 1.class Emp { 2.constructor(){} 3.private _name : string 4.get name() : string{//访问器 5.return this._name 6.} 7.set name(name : string){//存储器,不允许有返回类型 8.this._name = name 9.} 10.} 11.let zs = new Emp() 12.zs.name = "张珊" //实际调用 setter存储器: set name("张珊") 13.console.log(zs.name) //实际调用 getter访问器: get name() 第4~6行,用get定义访问器,访问器对属性_name做了简单返回。 第7~9行,用set定义存储器,将输入参数赋值给属性_name。注意,存储器不允许标注返回值类型,即便是无返回值也不允许使用关键字void。 第12~13行,分别调用存储器set name("张珊")和访问器get name()。 执行结果为: 张珊 注意,如果编译时有如下报错: TS1056: Accessors are only available when targeting ECMAScript 5 and higher. 是因为编译的ES版本不支持访问器语法,此时可指定支持该语法的ES版本进行编译,如: tsc -t es5 notes.ts 访问器和函数一样,也可以是静态的。 【例310】定义静态的存储器和访问器 1.class Emp { 2. private static _count : number = 0 3. constructor(){ Emp._count++ } 4. static get count() : number{//定义静态访问器 5.return Emp._count 6. } 7. static set count(num : number){//定义静态存储器 8.Emp._count=num 9. } 10.} 11.console.log(Emp.count)//0, 实际调用静态访问器: get count() 12.new Emp() 13.console.log(Emp.count) //1, 实际调用静态访问器: get count() 14.Emp.count = 9 //1, 实际调用静态存储器: set count(num : number) 15.console.log(Emp.count) //9, 实际调用静态访问器: get count() 第4~6行,用关键字static定义静态访问器 get count(),用于返回类Emp的静态属性_count的值。 第7~19行,用关键字static定义静态存储器set count(num : number),用于设置Emp的静态属性_count的值。 第11行、第13行、第15行,代码Emp.count实际调用了静态访问器get count()。 第14行,代码Emp.count=9实际调用了静态存储器set count(num : number)。 执行结果为: 0 1 9 3.1.5构造函数 构造函数又称构造器(constructor),简称构造,是用于创建对象的特殊函数。 对象又称实例。用构造函数创建对象的过程,即为类的实例化过程。 TypeScript没有指定构造函数时,系统会生成一个默认的无参构造函数。TypeScript构造函数用constructor命名,使用new关键字加类名会调用构造函数进行对象的创建。 通常,在实例化对象的过程中会同时初始化其属性值。在构造函数中,可通过关键字this来访问当前对象的属性和函数。 【例311】用构造函数对属性进行初始化 1.class Dog{ 2. name : string 3. constructor( name : string ){ 4. this.name = name 5. } 6.} 7.let doggie = new Dog("doggie") 8.console.log(doggie) 第3~5行,用关键字constructor定义了Dog类的构造函数。this.name使用关键字this引用了name属性,因此this.name=name是用初始化属性name的值为输入参数name赋值。 第7行,用关键字new调用构造函数,创建Dog类对象doggie。注意,在调用构造函数时,使用了类名Dog而非constructor; 传入的实际参数值"doggie"将初始化对象的name属性值。 第8行,用console.log()函数输出对象doggie的信息。 执行结果为: Dog { name: 'doggie' } 注意,若构造函数只对属性进行初始化,则可使用“初始化属性速记写法”,令代码更简洁易读。 【例312】对属性进行初始化,构造函数可使用“初始化属性速记写法” 1.class Cat{ 2.constructor(public name : string ){} 3.} 4.let kitty = new Cat('kitty') 5.console.log(kitty) 第2行,构造函数中有形式参数name,但在花括号{}中没有明确对属性name赋值,实际上,Cat类甚至没有声明过name属性。 第4行,通过构造函数创建对象kitty。 第5行,用console.log()函数输出对象kitty。 执行结果为: Cat { name: 'kitty' } Cat对象内多了属性name,且值被初始化了。这说明第2行构造函数的写法是种初始化属性的缩略结构。它允许在不声明属性的情况下,构造函数写入的参数将成为实例属性,并进行初始化操作。 注意,第2行中构造函数的参数上有访问修饰符public(也可为protected或private),若不写访问修饰符,是不会生成相应属性和对属性进行初始化赋值的。 视频讲解 3.2对象 在TypeScript中,对象可以被视为包含一组键值对的实例,因此,对象可以用字面方式(即使用花括号{})来创建。对象作为类的实例,当然可以通过类的构造函数方式来创建; 另外,所有对象都是Object的子类对象,因此对象也可以使用new Object()方式创建。 3.2.1对象概述 对象是现实问题域中的实体在计算机程序中的映射。 现实中的实体都有自己的特征(或状态)和行为(或功能),如通讯录应用中的李思同学。李思有特征: 编号“002”、姓名“李思”、性别“男”、电话号码“138××××0686”、联系地址“北京市双清路30号”; 李思也有行为: 修改电话号码、修改联系地址。 在TypeScript中,对象可被看成包含一组键值对的实例。其值可以是原始类型,也可以是数组、函数、对象等。 TypeScript映射实体时,一般将实体的特征、状态转换为属性,将行为、功能转换为函数。映射实体李思同学为对象lisi,可直观地表示为例313所示代码。 【例313】映射实体李思同学为对象lisi 1.let lisi = { 2.sno : '002', 3.name : '李思', 4.sex : '男', 5.tel : '138××××0686', 6.addr : '北京市双清路30号', 7.changeTel : function(tel : string) : void { 8.this.tel = tel 9.}, 10.changeAddr : function(addr : string) : void { 11.this.addr = addr 12.} 13.} 实体也可以是相对抽象的事物。以实体“圆”为例: 其半径值为该实体“圆”的特征、计算半径和计算面积就是该实体“圆”的2个功能。 【例314】实体可以是抽象事物,比如“圆” 1.let circle = { 2.radius : 10, 3.getCircumference : function() : number{ 4.return 2 * 3.14 * this.radius 5.}, 6.getArea : function() : number{ 7.return 3.14 * this.radius * this.radius 8.} 9.} 3.2.2创建对象 实际上,在TypeScript中创建对象有多种方式。除了直接用字面方式创建对象外,还可以用new Object()方式创建,或用构造函数创建。 1. 字面方式创建对象,在定义结构的同时创建对象 【例315】用字面方式创建对象 1.enum Color{Red, Blonde, Brown, While, Black, Gray} 2.let doggie = { 3.name : 'awang', 4.color : Color.Blonde, 5.birth : new Date(2020,3,6), 6.skills : [], 7.eat : function(){}, 8.bark : () => {} 9.} 10.console.log(doggie.birth) 第2~9行,用字面方式创建对象,内部包含一组键值对。 第3~6行,定义4个不同类型的属性,分别是string、枚举、日期和数组类型。 第7~8行,定义2个函数,第7行为普通函数、第8行为箭头函数。 第10行,用console.log()函数输出对象doggie的信息。 执行结果为: { name: 'awang', color: 1, birth: 2020-04-05T16:00:00.000Z, skills: [], eat: [Function: eat], bark: [Function: bark] } 从运行结果看,字面方式成功创建了doggie对象。 2. 用new Object()方式创建空对象,再按需追加对象属性和函数 在TypeScript中,Object类是所有类的终极父类,new Object()就是用Object类的构造函数创建一个空对象。 【例316】用new Object()方式创建对象 1.let dogB : any = new Object()//any类型 2.dogB.name = "awang" 3.dogB.eat = function(){} 4.dogB.eat() 第1行,用new Object()创建对象dogB。注意,应声明dogB的类型为any,否则后面为对象动态添加属性或函数时会出现如下报错: Property 'name' does not exist on type 'Object' 第2~3行,分别动态添加属性和函数。 第4行,调用动态增加的eat()函数。 以上new Object()写法还可以用Object.create(null)方式等价替代。 【例317】用Object.create(null)方式创建对象 1.const dogC = Object.create(null) 2.dogC.name = "awang" 3.dogC.eat = function(){} 4.dogC.eat() 3. 由类的构造函数创建对象 3.1.5节已经介绍过,可使用类的构造函数创建对象。 【例318】用类的构造函数创建对象 1.class Dog{ 2.constructor(public name:string ){} 3.} 4.let doggie = new Dog('aWang') 5.console.dir(doggie ) 第1~3行,定义Dog类,内部构造函数使用“初始化属性速记写法”对属性name进行初始化。 第4行,用关键字new调用构造函数,创建了Dog类对象doggie。 执行结果为: Dog { name: 'aWang' } 视频讲解 3.3继承 继承是面向对象编程中实现“类扩展”的机制,是类层次上的代码复用。 继承就相当于将父类的属性和函数直接定义到了子类中,子类可直接使用这些继承的属性和函数。 3.3.1继承语法 在TypeScript中,用关键字extends指明继承关系。 继承的基础语法结构如下所示: class 子类 extends 父类 { 类结构体(属性、构造、函数) } 父类(parent class)又被称为基类(base class)、超类(super class),子类(sub class)又被称为派生类(derived class)。 【例319】子类继承父类 1.class Animal{ 2.constructor(public name : string){} 3.move(){ 4.console.log(this.name+' moved') 5.} 6.} 7.class Dog extends Animal{} 8.const doggie = new Dog('Kipper') 9.console.log(doggie) 10.doggie.move() 第1~6行,定义Animal类,该类包含一个构造函数和一个普通函数。其中第2行是构造函数,该构造函数使用了“初始化属性速记写法”,会为Animal类加上对属性name。 第7行,用关键字extends实现Dog类对Animal类的继承。此处,子类Dog中没有定义属性和函数,但实际上通过继承,相当于Dog类有了来自父类的属性name和move()函数。 第8行,用构造函数创建了doggie对象。 第9~10行,用console.log()函数输出对象doggie的信息,并调用对象doggie的move()函数。 执行结果为: Dog { name: 'Kipper' } Kipper moved 从结果看,子类Dog继承了父类Animal的属性name和move()函数。 3.3.2单继承 在TypeScript中,类是单继承的,即只能继承一个类,不支持继承多个类。 【例320】继承多个父类导致编译时报错 1.class Father{} 2.class Mother{} 3.class Son extends Father, Mother{} 第3行,Son类继承了两个父类,显然继承多个父类会导致语法出错。将鼠标移至下画波浪线处,可观察到相应的错误提示,如图31所示。 图31继承多个父类导致语法出错 虽然类不允许多继承,但是允许A类继承B类,B类继承C类……这种链式的继承满足单继承语法要求。 【例321】类的单继承“链” 1.class Animal{//动物 2.constructor(public name:string){} 3.} 4.class Mammal extends Animal{//哺乳动物继承动物 5.static breastFeed = true 6.constructor(name:string) { 7.super(name) 8.} 9.} 10.class Panda extends Mammal{ //熊猫继承哺乳动物 11.eatBamboo():void{} 12.constructor(name:string) { 13.super(name) 14.} 15.} 16.console.log(Panda.breastFeed) 17.let panpan = new Panda('PanPan') 18.console.log(panpan.name) 第1~3行,定义Animal类。 第2行,使用构造函数的“初始化属性速记写法”,为Animal类添加属性name。 第4~9行,定义继承Animal类的Mammal类,并增加1个静态属性breastFeed。 第10~15行,定义继承Mammal类的Panda类,并增加1个eatBamboo()函数。 第16行,用Panda类直接调用来自其父类Mammal的静态属性breastFeed。 第17行,用构造函数创建Panda对象,并通过参数初始化来自父类Animal的属性name。 第18行,通过panpan.name,访问panpan对象继承自Animal类的name属性。 执行结果为: true PanPan 这说明,TypeScript是允许单链继承的,即子类可继承父类及其“祖先类”的属性和函数。 3.3.3函数覆盖与多态 1. 函数覆盖 继承后,若子类中定义了与父类函数签名完全相同的函数(名称相同,参数个数和类型也相同),则被称为函数的覆盖(override)或重写。 【例322】函数覆盖 1.class Animal{ 2.eat() : void { 3.console.log('animal eated') 4.} 5.} 6.class Dog extends Animal{ 7.eat() : void { 8.console.log('dog eated') 9.} 10.} 11.let doggie = new Dog() 12.doggie.eat() 第1~10行,分别定义父类Animal和子类Dog。父类和子类中都有eat()函数,并且函数名和参数形式都相同(都没有参数)。此时子类中的eat()就覆盖了父类中的eat()函数。 第11~12行,创建子类Dog的对象doggie,并调用eat()函数。注意,此时执行的eat()函数应该是子类中的函数。 执行结果为: dog eated 从结果看,调用的确实是子类中的函数。若要调用从父类继承的eat()函数,可使用关键字super。 【例323】使用关键字super调用从父类继承的函数 1.class Dog extends Animal{ 2.eat(): void { 3.console.log('dog eated') 4.super.eat() 5.} 6.} 第4行,使用关键字super调用eat()函数,该函数为从父类继承的。 注意,关键字super代表父类对象,因此可用super调用父类对象的属性和函数。 执行结果为: dog eated animal eated 2. 多态 在掌握了函数覆盖的基础上,可进一步深入理解多态特征。 多态(polymorphism)是面向对象编程的一个重要特征。在多态中,同一个函数名可以在不同的类中具有不同的实现。当调用实例的属性和函数时,会根据实例的实际类型进行动态调用,而不是根据声明类型进行调用。 多态行为使开发人员能够以统一的方式处理不同类的对象,而无须关心具体的对象类型。合理应用多态能够提高代码的重用性和可维护性。 【例324】多态特征: 根据实例的“实际类型”调用相应的函数 1.class Shape{//图形 2.draw(){ console.log('draw a shape'); } 3.} 4.class Circle extends Shape{//圆 5.draw(){ console.log('draw a circle'); } 6.} 7.class Square extends Shape{ //正方形 8.draw(){ console.log('draw a square'); } 9.} 10.class ShapeTool{ //工具类 11.static doDraw(shape:Shape){ 12.shape.draw() 13.} 14.} 15.let s1 : Shape = new Circle() 16.ShapeTool.doDraw(s1) 17.ShapeTool.doDraw(new Square()) 第1~9行,分别定义3个类,其中Shape是父类,Circle和Square都是Shape的子类。3个类中都有draw()函数,子类中的draw()函数覆盖了父类中的draw()函数。 第10~14行,定义工具类ShapeTool,内部有静态doDraw()函数。注意,参数shape的类型为父类Shape,在函数内调用的是shape的draw()函数。 第15~16行,创建对象s1。注意,s1的声明类型是Shape,但实际类型为Circle。当执行ShapeTool.doDraw(s1)时,传入s1,因此执行s1.draw()时,执行的是实际类型Circle的函数,而非声明类型Shape的函数。这正是多态的表现。 第17行,创建Square对象并传入ShapeTool的doDraw()函数,同样执行的是实际类型Square的函数,而非声明类型Shape的函数。 执行结果为: draw a circle draw a square 3.3.4this与super 前面几个章节中多次提到两个关键字: this和super。这里系统地总结一下两者的区别和相应的使用场合。 this代表当前对象本身,而super代表父类对象。 1. 访问属性和函数 this用于访问当前对象中的属性和函数,当属性和函数不存在时,会自动调用继承自父类对象的属性和函数。使用super则可直接调用父类对象中的属性和函数。 【例325】用this和super分别调用自身的函数和父类函数 1.class Parent{ 2.doSth() : void { 3.console.log('parent do sth'); 4.} 5.} 6.class Derived extends Parent{ 7.doSth() : void { 8.console.log('derived do sth') 9.} 10. test(){ 11.this.doSth() 12.super.doSth() 13.} 14.} 15.let obj = new Derived() 16.obj.test() 第1~5行,定义Parent类,该类内部有一个doSth()函数。 第6~14行,定义Parent类的子类Derived,子类的内部也定义了一个doSth()函数,此外还有一个测试this和super关键字用法的test()函数。注意,在test()中,用this.doSth()和super.doSth()分别调用了本类对象的函数和父类对象的函数。 执行结果为: derived do sth parent do sth 2. 用super调用父类的构造函数 在子类的构造函数中,可使用super调用父类的构造函数。需要注意的是,在子类的构造函数中,用super调用构造函数的代码必须放在有效的第一行上。 【例326】用super调用父类构造 1.class Tag { 2.constructor(public name : string){} 3.} 4.class Img extends Tag{ 5.src : string 6.constructor(name : string, src : string){ 7.super(name) 8.this.src = src 9.} 10.} 11.let img = new Img('logo', 'img/logo.png') 12.console.log(img) 第1~3行,定义类Tag。第2行定义构造函数,该构造函数使用“初始化属性速记写法”对属性name进行初始化。 第4~10行,定义类Tag的子类Img,所以类Img会继承类Tag的属性name。 第5行,在类Img中增加属性src。 第6~10行,定义类Img的构造函数。其中第7行super(name)就是调用父类的构造函数,因此会对属性name进行初始化; 第8行对属性src进行初始化。 注意,super()必须放在子类的构造函数的第一行,否则会报错。这是因为,创建本类对象前,需先创建父类对象,以便能继承父类对象的属性。 第11~12行,通过构造函数创建类Img的对象img,并用console.log()函数输出对象img的信息。 执行结果为: Img { name: 'logo', src: 'img/logo.png' } 视频讲解 3.4抽象类 抽象类(abstract class)是一种特殊的类。和普通类一样,抽象类可以有属性和函数,但不允许用构造函数直接创建对象。 抽象类通常作为其他类的父类存在,它的主要作用是为子类提供一个通用的模板,定义一些共同的属性和函数。子类需要实现(重写)在抽象类中声明的抽象函数才能创建对象。 在抽象类中,可以定义抽象函数和普通函数。抽象函数是没有具体实现的函数,只有函数签名,因此子类必须实现这些抽象函数。而普通函数则可以有具体的实现。 在class前加关键字abstract来定义抽象类。 【例327】定义抽象类 1.abstract class Shape { 2.public abstract Draw() : void 3.} 4.let s = new Shape()//抽象类不能直接创建,会报错 第1行,关键字class前加了关键字abstract,说明该类是抽象类。 第2行,在函数前加abstract,说明该函数是抽象函数。所谓抽象函数,就是只有函数签名,没有函数体的函数。 注意,若类内部有抽象函数,则相当于类结构部分抽象,那么类在整体上就是个抽象类,此时,必须在class前加abstract关键字,将该类定义为抽象类。反之,抽象类中的函数可以都是非抽象的,并不要求一定要存在抽象函数。 此外,抽象函数是不能加花括号{}的,因为花括号{}代表“实现”,这样的函数就不能被称作抽象函数了。 第4行,创建抽象类Shape的对象,会有如下报错: Cannot create an instance of an abstract class. 这说明抽象类无法用构造函数直接创建对象,但可以通过子类进行间接创建。 【例328】通过子类间接创建抽象类对象 1.abstract class Shape { 2.constructor(public name : string){} 3.public abstract draw(): void 4.} 5.class Circle extends Shape{ 6.public draw() : void { 7.console.log('draw a circle') 8.} 9.constructor(){ 10.super('circle') 11.} 12.} 13.let c = new Circle() 14.console.log(c) 第1~4行,用关键字abstract定义抽象类Shape。 第5~12行,定义了抽象类Shape的子类Circle。 第6~8行,在子类Circle中,对抽象父类Shape中的抽象函数draw()进行实现。 注意,子类中若不实现继承的抽象函数,则子类在整体上就是抽象类,必须用关键字abstract标注该子类为抽象类,否则就会出现语法错误。 第9~11行,子类Circle定义了自己的构造函数,并用关键字super调用父类构造函数来初始化继承的name属性。 第13~14行,创建子类Circle的对象c。注意,在创建子类过程中,会先创建父类对象。虽然此时父类是抽象的,但这并不妨碍它的对象被创建出来。用console.log()函数输出对象c的信息时,可观察到继承自抽象父类的name属性值,如下所示: Circle { name: 'circle' } 3.5接口 接口(interface)可被视为更为彻底的抽象类。 抽象类中允许有具体实现函数,甚至可以全部为具体实现函数。而接口本身并不包含任何具体实现函数,仅用于声明必须实现哪些函数,函数的具体功能由子类实现。为此,接口的子类又被称为接口的实现类。 3.5.1定义接口 在接口中,可以定义属性和抽象函数。另外,与类的单一继承不同,接口可以继承多个必接口。 定义接口的语法如下所示: interface 接口名 [ extends 接口1, 接口2 …] { [属性…] [抽象函数…] } 关键字interface用于定义接口; 关键字extends用于继承父接口; 接口中可定义属性和抽象函数。 接口是定义了功能的契约,对子类起规范作用,即实现接口的具体子类必须实现接口中的所有抽象函数。 【例329】用关键字interface定义接口IShape 1.interface IShape{ 2. name:string 3. // pi:number = 3.14 4. //static pi:number 5. draw():void 6.} 第1行,用关键字interface定义接口IShape。 第2行,声明属性name。 注意,在接口中,属性不能初始化赋值,如第3行会出错。另外,在接口中不允许声明静态属性,如第4行会出错。 第5行,声明抽象函数draw()。注意,接口函数默认就是抽象的,接口中的函数不用加也不能加abstract关键字。 3.5.2接口实现类 接口是定义了功能的契约。当类通过关键字implements声明要实现接口时,必须具体化接口中所有的抽象函数和属性,否则说明该类违背了实现接口的契约,会出错。 【例330】接口的实现 1.interface IShape { 2.getCircumference() : number//声明抽象函数 3.getArea() : number //声明抽象函数 4.} 5.class Circle implements IShape{ 6.constructor(public radius : number){} 7.getCircumference() : number { //实现抽象函数 8.return 2*Math.PI*this.radius 9.} 10.getArea() : number {//实现抽象函数 11.return Math.PI*this.radius*this.radius 12.} 13.} 14.class Square implements IShape{ 15.constructor(public sideLen : number){} 16.getCircumference() : number { //实现抽象函数 17.return 4*this.sideLen 18.} 19.//遗漏getArea()实现 20.} 第1~4行,定义接口IShape,在接口中声明两个抽象函数。 第5~13行,定义接口IShape的实现类Circle。关键字implements声明子类要实现接口。因为在类Circle中实现了接口IShape所有声明的函数,因此语法没有问题。 第14~20行,定义接口IShape的实现类Square。因为遗漏了getArea()函数的实现,因此语法会出错,如下所示: Class 'Square' incorrectly implements interface 'IShape'. Property 'getArea' is missing in type 'Square' but required in type 'IShape'. 3.5.3接口多继承 在TypeScript中,类之间是单继承的,但接口允许多继承。一旦继承,则子接口将继承所有父接口中的属性和抽象函数。 【例331】接口的多继承 1.interface Flyable { Fly() : void } 2.interface Singable { Sing() : void } 3.interface IfcBird extends Flyable, Singable {} 第3行,IfcBird接口用关键字extends继承了两个接口,接口间用逗号分隔。 【例332】实现接口时,它继承的所有函数都得实现 1.interface Flyable { fly() : void } 2.interface Singable { sing() : void } 3.interface IfcBird extends Flyable, Singable { jump() : void } 4.class Parrot implements IfcBird { 5.fly() : void {} 6.sing() : void {} 7.jump() :void{} 8.} 第3行,通过多继承,接口IfcBird继承了Flyable和Singable两个接口,同时声明了抽象函数jump()。 第4~8行,类Parrot实现了IfcBird接口,而接口IfcBird又继承了Flyable和Singable两个接口。因此Parrot类需实现3个接口中的全部3个函数。 3.6实战闯关——面向对象 针对面向对象编程,需掌握的重点知识和技能为: 类的设计、属性设置、类的继承、函数覆盖、接口设计等。 【实战31】类设计 定义一个商品类,要求如下: 商品属性包括商品编号、商品名称、商品所在分类编号、商品价格、商品图片URL。 商品函数包括修改商品分类编号、修改商品价格、修改商品图片URL。 【实战32】属性设置 定义一个员工类。类中有实例属性name(姓名)和salary(工资),还有静态属性count(员工数)。当创建一个员工对象时,count的值需加1。 【实战33】父类与子类的继承和函数覆盖 定义父类Shape(图形): 内有whoAmI()函数,输出“我是一个图形”。 定义类Shape的子类Square(正方形): 内有代表边长的width属性,以及用于计算面积的getArea()函数。 在Square中也定义一个whoAmI()函数,输出“我是一个边长为width的正方形”,其中的width用width属性值代替。 【实战34】接口与实现类 定义接口ICustomer,它有4个属性: name(姓名)、tel(联系电话)、addr(联系地址)和level(客户级别),还有一个用于获取折扣率的抽象函数getDiscountRate()。 定义接口ICustomer的两个实现类Customer和VIP: Customer类,设置Level的默认值为0,实现getDiscountRate()函数返回1。 VIP类,设置Level的默认值为1,实现getDiscountRate()函数返回0.95。 【实战35】接口的继承 定义接口Point,它有2个代表坐标的属性: x和y,都为number类型。 定义接口Point3D,在Point3D内有3个代表坐标的属性x、y和z,都为number类型。注意,其中x和y属性需继承自接口Point。 声明Point3D类型变量p3,将3个属性值分别设置为1、2和3。