接口与类
一、接口
在面向对象的语言中“接口”是个很重要的概念,它是对行为的抽象,而具体内容需要通过类实现,TypeScript 中的接口是一个非常灵活的概念,可以用来约束对象、函数以及类的结构和类型,是一种代码协作的契约,我们必须遵守而且不能改变。
1、对象类型接口
对象类型的接口用来设置对象需要存在的普通属性、可选属性和只读属性,另外还可以通过类型注解语法或 [propName: string]: any 来制定可以接受的其他任意额外属性,举个🌰:
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 interface Person { name : string bool?: boolean readonly timestamp : number readonly arr : ReadonlyArray <number > } let p1 : Person = { name : 'oliver' , bool : true , timestamp : + new Date (), arr : [1 , 2 , 3 ] } let p2 : Person = { age : 'oliver' , name : 123 } p1.timestamp = 123 p1.arr .pop ()
需要注意的是此处 ReadonlyArray<T>
类型,它与 Array<T>
相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改,ReadonlyMap<T>
和 ReadonlySet<T>
与之类似。
2、函数类型接口
TypeScript 中接口还可以用来规范函数的形状,列出参数列表及返回值类型的函数定义。写法如下:
1 2 3 4 5 6 7 let add : (x: number , y: number ) => number interface Add { (x : number , y : number ): number } let add : Add = (a, b ) => a + b
3、可索引类型接口
当我们不确定一个接口中有多少个属性时就可以使用可索引类型接口,接口可以通过字符串类型或数字类型索引:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 interface StringArr { readonly [index : number ]: string length : number } let arr1 : StringArr = ['hello' , 'world' ]arr1[1 ] = '' let arr2 : StringArr = [23 ,12 ,3 ,21 ]interface Names { [index : string ]: string } let names : Names = { '1' : 'xiaozhang' } interface Circle { [x : string ]: string [y : number ]: string }
4、混合类型接口
混合类型接口就是接口既可以定义一个函数,也可以像对象一样拥有属性和方法,因此往往可以用来描述一个函数接收什么参数,输出什么结果,同时这个函数有另外什么方法或属性之类的,举个🌰:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 interface Counter { (start : number ): void version : string add (): void } function getCounter ( ): Counter { let count = 0 function counter (start : number ) { count = start } counter.version = '0.0.1' counter.add = function ( ) { count++ } return counter } const c = getCounter ()c (10 ) c.version c.add ()
5、接口的继承
跟类一样,接口通过extend关键字继承,更新新的形状,比方说继承接口并生成新的接口,这个新的接口可以设定一个新的方法检查。举个🌰:
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 interface PersonInfoInterface { name : string age : number log?(): void } interface Student extends PersonInfoInterface { doHomework (): boolean } interface Teacher extends PersonInfoInterface { dispatchHomework (): void } let Alice : Teacher = { name : 'Alice' , age : 34 , dispatchHomework ( ) { console .log ('dispatched' ) } } let oliver : Student = { name : 'oliver' , age : 12 , log ( ) { console .log (this .name , this .age ) }, doHomework ( ) { return true } }
二、类
在面向对象语言中,类是一种面向对象计算机编程语言的构造,是创建对象的蓝图,描述了所创建的对象共同的属性和方法。
1、类的属性及方法
在 JavaScript 中我们通过 class 关键字来定义一个类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Greeter { static cname : string = "Greeter" ; greeting : string ; constructor (message: string ) { this .greeting = message; } static getClassName ( ) { return "Class name is Greeter" ; } greet ( ) { return "Hello, " + this .greeting ; } } let greeter = new Greeter ("world" );
那么成员属性与静态属性,成员方法与静态方法有什么区别呢?我们直接查看编译后的 ES5 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var Greeter = (function ( ) { function Greeter (message ) { this .greeting = message; } Greeter .getClassName = function ( ) { return "Class name is Greeter" ; }; Greeter .prototype .greet = function ( ) { return "Hello, " + this .greeting ; }; Greeter .cname = "Greeter" ; return Greeter ; }()); var greeter = new Greeter ("world" );
从编译后的代码我们不难看出成员属性会添加到类的实例上,成员方法会添加到类的原型对象上,因此对于类的实例而言是可调用的,而静态属性与静态方法都会只添加到类自身,只能被类自身调用。除此之外我们需要注意类的成员属性和方法还有 public、private 和 protected 可访问性修饰符,举个🌰:
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 34 35 36 37 class Dog { name : string private age : number protected sex : string constructor (name : string , age : number ) { this .name = name; this .age = age; this .sex = 'male' } private pri ( ) { console .log ('private' ) }; protected pro ( ) { console .log ('protected' ) } } let dog = new Dog ('wangcai' , 2 );console .log (dog.age );console .log (dog.sex );dog.pri (); dog.pro (); class Husky extends Dog { color : string constructor (name: string , age: number ) { super (name, age); this .color = 'yellow' ; this .pro (); this .pri (); } } Husky .pri ()Husky .pro ()
需要注意当我们给构造函数添加 private 修饰符时表示类既不可以被继承也不可以被实例化,当我们给构造函数添加 protected 修饰符时表示类不可被实例化只能被继承,常用于声明基类。
2、ECMA私有字段
在 TypeScript 3.8 版本就开始支持ECMAScript 私有字段,使用方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Person { #name : string ; constructor (name: string ) { this .#name = name; } greet ( ) { console .log (`Hello, my name is ${this .#name} !` ); } } let semlinker = new Person ("Semlinker" );semlinker.#name;
与常规属性(甚至使用private修饰符声明的属性)不同,私有字段具有以下规则:
私有字段以 # 字符开头,有时我们称之为私有名称;
每个私有字段名称都唯一的限定于其包含的类;
不能在私有字段上使用 TypeScript 可访问性修饰符(如 public 或 private);
私有字段不能在包含的类之外访问,甚至不能被检测到。
3、访问器
在 TypeScript 中,我们可以通过 getter 和 setter 方法来实现数据的封装和有效性校验,防止出现异常数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 let passcode = "Hello TypeScript" ;class Employee { private _fullName : string = '' ; get fullName (): string { return this ._fullName ; } set fullName (newName: string ) { if (passcode && passcode == "Hello TypeScript" ) { this ._fullName = newName; } else { console .log ("Error: Unauthorized update of employee!" ); } } } let employee = new Employee ();employee.fullName = "Semlinker" ; if (employee.fullName ) { console .log (employee.fullName ); }
4、类的继承
继承(Inheritance)是一种联结类与类的层次模型。指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系。在 TypeScript 中,我们通过 extends 关键字来实现继承,通过 super 关键字来调用父类的构造函数和方法,举个🌰:
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 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); } } let sam = new Snake ("Sammy the Python" );sam.move ();
5、抽象类
使用 abstract 关键字声明的类,我们称之为抽象类。抽象类不能被实例化,因为它里面包含一个或多个抽象方法,所谓的抽象方法,是指不包含具体实现的方法,抽象类的好处在于可以抽离出一些事物的共性,有利于代码的复用和扩展,举个🌰:
1 2 3 4 5 6 7 8 abstract class Person { constructor (public name: string ){} abstract say (words : string ) :void ; } const lolo = new Person ();
抽象类不能被直接实例化,我们只能实例化实现了所有抽象方法的子类。具体如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 abstract class Person { constructor (public name: string ){} abstract say (words : string ) :void ; } class Developer extends Person { constructor (name: string ) { super (name); } say (words : string ): void { console .log (`${this .name} says ${words} ` ); } } const lolo = new Developer ("lolo" );lolo.say ("I love ts!" );
6、基于抽象类实现多态
面向对象(OOP)语言的三大特性分别是:封装(Encapsulation)、继承(Inheritance)和多态(Polymorphism),多态是指由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如下面示例中 Cat 和 Dog 都继承自 Animal,但是分别实现了自己的 eat 方法。此时针对某一个实例,我们无需了解它是 Cat 还是 Dog,就可以直接调用 eat 方法,程序会自动判断出来应该如何执行 eat:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 abstract class Animal { abstract sleep (): void } class Dog { sleep ( ) { console .log ("dog sleep" ) } } let dog = new Dog ();class Cat { sleep ( ) { console .log ("cat sleep" ) } } let cat = new Cat ()let animals : Animal [] = [dog, cat]animals.forEach (i => { i.sleep () })
7、类的方法的重载
方法重载是指在同一个类中方法同名,参数不同(参数类型不同、参数个数不同或参数个数相同时参数的先后顺序不同),调用时根据实参的形式,选择与它匹配的方法执行操作的一种技术。所以类中成员方法满足重载的条件是:在同一个类中,方法名相同且参数列表不同,在以下示例中我们重载了 ProductService 类的 getProducts 成员方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class ProductService { getProducts (): void ; getProducts (id : number ): void ; getProducts (id?: number ) { if (typeof id === 'number' ) { console .log (`获取id为 ${id} 的产品信息` ); } else { console .log (`获取所有的产品信息` ); } } } const productService = new ProductService ();productService.getProducts (666 ); productService.getProducts ();
这里需要注意的是,当TypeScript编译器处理方法重载时,它会查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。另外在ProductService类中,getProducts(id?: number){}
并不是重载列表的一部分,因此对于getProducts
成员方法来说,我们只定义了两个重载方法。
三、类与接口的关系
1、类可以实现接口
如果你希望在类中使用必须要被遵循的接口(类)或别人定义的对象结构,可以使用 implements 关键字来确保其兼容性:
1 2 3 4 5 6 7 8 9 10 11 12 interface Human { name : string ; eat (): void ; } class Asian implements Human { name : string ; constructor (name : string ) { this .name = name; } eat ( ) {} }
类实现接口需要注意的有以下几点:
类实现接口的时候必须实现接口定义的所有属性,但是类可以定义接口之外自己的属性
接口只能约束类的公有成员
接口也不能约束类的构造函数
2、接口可以继承类
接口除了可以继承接口还可以继承类,相当于接口把类的成员都抽象了出来,也就是只有类的成员结构而没有具体的实现,举个🌰:
1 2 3 4 5 6 7 class Auto { state = 1 } interface AutoInterface extends Auto {} class C implements AutoInterface { state = 1 }
需要注意的是接口在抽离类的成员时不仅抽离了公共成员,还抽离了私有成员和受保护成员,举个🌰:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Auto { state = 1 private num = 12 } interface AutoInterface extends Auto {}class C implements AutoInterface { state = 1 ; } class C implements AutoInterface { state = 1 ; num = 14 }
参考资料
极客时间《TypeScript开发实战》专栏
《深入理解TypeScript》
一份不可多得的 TS 学习指南(1.8W字)
Typescript使用手册