为什么需要接口?

我们来看一下这个代码,对于眼神不好使的人来说简直就是遭罪,当然我这里只是简单的给了几个属性,假如有20个属性呢?20个使用这种结构的函数呢?

  1. function somefunc1({ x = 0, y = 0 }: { x: number, y: number }) {
  2. // ...
  3. }
  4. function somefunc2({ x = 0, y = 0, z = 0 }: { x: number, y: number, z: number }) {
  5. // ...
  6. }

一切需要复制粘贴的代码,都可以通过代码去解决。

于是我们有了接口,就像神说,要有光一样亮趟。

  1. function somefunc1({ x = 0, y = 0 }: pointer2d) {
  2. // ...
  3. }
  4. function somefunc2({ x = 0, y = 0, z = 0 }: pointer3d) {
  5. // ...
  6. }
  7. interface pointer2d {
  8. x: number;
  9. y: number;
  10. }
  11. interface pointer3d extends pointer2d {
  12. z: number;
  13. }

接口的作用就是去描述结构的形态

我们可以把 interface 做为语文里面的总结。

  1. interface pointer2d {
  2. x: number;
  3. y: number;
  4. }

总结一下,其中二维坐标系点需要2个属性,一个是number类型的 x,一个是number类型的 y

  1. function somefunc1({ x = 0, y = 0 }: pointer2d) {
  2. // ...
  3. }

somefunc1 需要传入一个像二维坐标系点一样的对象。

连起来完整的就是,somefunc1 需要传入一个像二维坐标系点一样的对象,二维坐标系点需要2个属性,一个是number类型的 x,一个是number类型的 y

而 extends 就是总结的总结了。

  1. interface pointer3d extends pointer2d {
  2. z: number;
  3. }

读作,总结一下,pointer3d首先要像pointer2d一样,需要2个属性,一个是number类型的 x,一个是number类型的 y。同时还需要一个新的属性 z

也可以理解为在阐述的基础上继续阐述,pointer2d是论点一,比如说,吃蔬菜的好处在这个论点给你说清楚了之后,继续升入讲pointer3d也就是,什么样的蔬菜包含什么样的维生素,就像这样由浅入深的阐述你的变量的结构形态。

描述类

上面的例子描述了函数传入对象,必须拥有的字段,这一次我们来正经的描述一下对象。

  1. interface Db{
  2. host: string;
  3. port: number;
  4. }

这个Db接口描述了,必须要有2个属性,一个是stringhostnumberport

此时我们把接口理解为合同,而implements就是履行。

  1. interface -> 合同
  2. Db -> 合同名称
  3. implements -> 履行

因为 MySQL 类 履行了 Db 合同,是不是要执行里面的条款呀?

这里我们没有执行我们合同里面的条款,所以这里报错了,提示你的host条款上哪去了?接口与类 - 图1

当我们做好 host 之后,编译器检测到你还有条框没有执行,所以又报错了,这里它又发问了,你的 port 上哪去了?会不会写程序!!自己写的规定都没实现。

接口与类 - 图2

完善一下我们的代码

  1. interface Db {
  2. host: string;
  3. port: number;
  4. }
  5. class MySQL implements Db {
  6. host: string;
  7. port: number;
  8. constructor(host: string, port: number) {
  9. this.host = host;
  10. this.port = port;
  11. console.log('正在连接 ' + this.host + ":" + this.port + " 的数据库....")
  12. }
  13. }
  14. let mysql = new MySQL('localhost', 3306);

结果

  1. 正在连接 localhost:3306 的数据库....

属性修饰符

修饰符就想形容词一样,表示对属性的一些修饰,比如好看的,和难看的,以及夹在中间好难看的。

readonly

当我们去描述一个类的时候,我们想要让某一个字段,只能被读取,而不能被修改,就像宪法一样,可看不可改。

同样你也可以把它理解为属性常量。

  1. interface Person{
  2. readonly IdCard: string; // 身份证号
  3. }
  4. class Person implements Person{
  5. readonly IdCard: string = "42xxxxxxxxxxxxxxx";
  6. constructor(){}
  7. }

像只读属性,我们初始化的时候就必须要给它赋值。

从下面编译好的js代码里面可以看出,interface并不会产生任何实际代码

  1. var Person = (function () {
  2. function Person() {
  3. this.IdCard = "42xxxxxxxxxxxxxxx";
  4. }
  5. return Person;
  6. }());

当然我们不仅可以用interface去描述带有构造器的class,我们还可以直接描述通过字面量{}构造的类变量;

  1. interface Person{
  2. readonly IdCard: string; // 身份证号
  3. }
  4. let person: Person = { IdCard:'43xxxxxxxxx' }

而生成的代码依旧不含有任何与interface相关的东西。

  1. var person = { IdCard: '43xxxxxxxxx' };

private 与 protected

private 表示私有的变量,不能被其他任何访问,只归自己管。接口与类 - 图3

从这里可以看到,父亲的私房钱,只归自己管,哪怕儿子继承了父亲也不行,在儿子的构造器里面,仅仅可以取得到surname姓氏。

同时我们可以看到,在Son中可以访问得到protectedsurname,也就是说被protected修饰的是可以被继承的。

public 和 默认的

修改一下我们的代码,增加一个public的属性,和一个没有任何修饰的属性。接口与类 - 图4

从结果可以看出,继承,可以继承除了private的所有。

而对于通过new创建的实例来说。

接口与类 - 图5

我们可以看到,实例只能访问public和默认的属性,同时也说明了默认就是public,所以public可以省略不写。

完整的代码如下

  1. class Dad{
  2. protected surname; // 姓氏
  3. private private_money; // 私房钱
  4. public public_something;
  5. default_something;
  6. constructor(){}
  7. }
  8. class Son extends Dad {
  9. constructor() {
  10. super()
  11. }
  12. }
  13. let d = new Dad()
  14. d.public_something = 'some_thing';
  15. d.default_something = 'default_thing'
  16. let s = new Son()
  17. s.public_something = 'some_thing';
  18. s.default_something = 'default_thing'

记得继承DadSon必须要先调用super()super()表示父类的构造器,先有父亲,后有儿子。

对于属性修饰符你可以这么理解。

class 代表着一个家族成员,extends 表示血缘关系,就像上面的父亲与儿子。

private 是属于家族成员的私有物品,私人空间,别人是不能看到的,除非自己告诉别人,通过方法返回

protected 表示家族资产,比如姓氏,某一宝物,古董。

public 表示共有资产,谁想拿,去问class的示例拿就好了。

可选属性

这个比较简单,就是一个?就行,表示可传,可不传。

它的语义就是强制需要传递这个参数吗?不强制需求。

接口与类 - 图6

这里我们定义了一个可选的name属性,当我们在IdCard后面的属性,继续加的时候,编辑器自动提示了有一个nameproperty(属性)。

接口与类 - 图7

哪怕我们不传也是不会报错的。

这些修饰符,既然可以修饰类,那必然可以修饰接口上面的属性。

假如我们需要一个可以添加属性的 interface 怎么办呢?

接口与类 - 图8

getPerson 这个函数要求我们传入的对象需要符合Person合同,当我们添加一些其他没有在合同里面定义的条款的时候,就会报错。

所以我们需要修改一下我们的合同,给它定义一下对于新增的条款,有些什么限制。

  1. interface Person{
  2. readonly IdCard: string; // 身份证号
  3. name?: string;
  4. [propName : string]: any;
  5. }
  6. let person: Person = { IdCard:'43xxxxxxxxx' }
  7. function getPerson(p: Person) {
  8. console.log(p);
  9. }
  10. getPerson({ IdCard: 's', b : 2 })

我们在js语言中,访问一个对象的属性可以通过.去访问,还可以通过['属性名']这样的形式去访问。

接口与类 - 图9接口与类 - 图10

我们可以看到,这2种都是有代码提示的,说明都是可行的。再看一看我们interface里面的[propName : string]: any;

他们之间存在着[]的联系,放心这不是卡巴斯基与巴基斯坦的巴基联系,而是很大的联系。

[propName : string]: any;

[] 里面就限定了属性名的类型,而后面的any就限定了属性值的类型。

这个propName是可以随意修改的。

接下来我们看看不正经的代码。

接口与类 - 图11

这是不是没有问题?这个还算正常吧,限定了为 number,也没报错。

但是得到的结果是'0',不要大惊小怪这是正常的。

  1. { '0': 2, IdCard: 's' }

我们在 Chrome 里面测试一下 JS

接口与类 - 图12

其实 js 对象是不允许你设置属性名为number的,他会自动转换为字符串。

此时我们再添加一个属性,这报错了。非常正常是吧。

interface 仅仅只是在 ts 层面起了作用。

接口与类 - 图13

接下来我们看看这个接口与类 - 图14

诶,这就很奇怪了,我明明给的是number为什么不报错呢?

并且装换出来的 js 代码还是原样。接口与类 - 图15

其实答案就是隐身转换。数字可以转换成字符串,而字符串不一定能转换成数字,比如'xx1'怎么转成数字,请问 x该转成几?

也就是说,你所给的类型只要可以转换成合同里面的类型,我就认为你是对的,我比较明智,是个老司机,我懂你各种隐含意思。

还有一点就是,对于你声明的属性,假如你不用字符串的''去包裹起来,js 编译器会帮你去做,从下面的代码你可以看出来。

接口与类 - 图16

描述函数

描述函数,我们只能使用一个变量接受匿名函数的引用,而不能通过function去创建一个具体有名称的函数。

  1. interface Db {
  2. host: string;
  3. port: number;
  4. }
  5. interface InitFunc{
  6. (options: Db) : string;
  7. }
  8. let myfunc : InitFunc = function (opts: Db) {
  9. return '';
  10. }

有了前面的经验,理解这个应该不难。

我们之前提到过()代表函数调用,对于这个 interface,我们这样读。

InitFunc接口规定,它的函数调用需要传递一个Db合同、约束的options对象,返回一个string类型的值。

同时你也可以看到interface 只是约束类型并不约束你的变量名。

描述可实例化

正常的理所当然的,我们会认为下面的代码是正确的。

这是初学者经常会犯的错误。

接口与类 - 图17

错误的代码我就不放上来了,免得误导大家,现在把正确的代码放在这里。

  1. interface MyDateInit {
  2. new (year: string, month: string, day: string) : MyDate;
  3. }
  4. interface MyDate {
  5. year: string;
  6. month: string;
  7. day: string;
  8. }
  9. class DateClass implements MyDate {
  10. year: string;
  11. month: string;
  12. day: string;
  13. constructor(year: string, month: string, day: string) {
  14. this.year = year;
  15. this.month = month;
  16. this.day = day;
  17. return this;
  18. }
  19. }
  20. function getDate(Class: MyDateInit, { year, month, day }) : MyDate{
  21. return new Class(year, month, day);
  22. }
  23. getDate(DateClass, { year: '2017', month: '12',day: '1' });

通常描述一个类的构造器和字段方法是分开来的。

MyDateInit 描述的就是我们的构造器

MyDate 描述的就是我们的类

就像前面的描述函数一样,没法用function somefunc(){}去接受实现合同一样。同样我们没办法在class someClass上面去实现构造器的合同。

接口与类 - 图18

具体原因是,interface描述的是实例化后的类,也就是{name:'some',age:22}这样的类型。而 constructor 方法是属于静态方法。

你可以理解new Class()其实就是去调用了Class.constructor()方法,是的没错,是方法

重点是方法2个字,new (year: string, month: string, day: string) : MyDate; 在这段代码里面是不是有()这个符号,这个代表着方法,而new则代表着可实例化的意思。

合起来就是, MyDateInit描述了一个可以实例化的对象,并且它的构造器需要一个string类型的yearmonthday参数,返回一个实现了MyDate接口的类。

从 js 源码上可以看出,这个 new() 真正意义上面的描述的是以下代码,描述的是一个函数方法。

  1. var DateClass = (function () {
  2. function DateClass(year, month, day) {
  3. this.year = year;
  4. this.month = month;
  5. this.day = day;
  6. return this;
  7. }
  8. return DateClass;
  9. }());

同时我也尝试这么写,通过function去构造,我发现约束类型不对。

接口与类 - 图19

我写出来的类型签名是下面这个, 而 => 代表着函数的返回值。

  1. (year: string, month: string, day: string) => MyDate

相当于编译器告诉我,我描述的是类的构造器,你把一个函数给我是怎么个意思?是不是想打架!来啊,心平气和的干一架~ 二营长,把他x的意大利炮拉出来!!

经过这样修改之后,就可以正常运行了。也就是去掉new

  1. interface MyDateInit2 {
  2. (year: string, month: string, day: string) : MyDate
  3. }
  4. let ExamplePlus : MyDateInit2 = function(year: string, month: string, day: string) : MyDate{
  5. this.year = year
  6. this.month = month
  7. this.day = day
  8. return this as MyDate;
  9. };

哪怕你想通过限定constructor的参数,来限制构造器依旧是不行的。

  1. interface test{
  2. constructor(year: string, month: string, day: string);
  3. }
  4. // 错误例子
  5. // class a1 implements test{
  6. // constructor(year: string, month: string, day: string){
  7. // }
  8. // }
  9. // 错误例子
  10. // let a2 : test = class test{
  11. // constructor(year: string, month: string, day: string){
  12. // }
  13. // }
  14. let a3 : test = {
  15. constructor(year: string, month: string, day: string){
  16. }
  17. }

ts 虽然支持直接 new function 但是,function 必须是返回值为void的函数。

  1. interface test{
  2. constructor(year: string, month: string, day: string): void;
  3. }
  4. let a3 : test = {
  5. constructor(year: string, month: string, day: string){
  6. }
  7. }
  8. let cc = new a3.constructor('', '', '')

其实关于new()最贴切与最精简的例子还行下面的代码。

  1. let some : MyDateInit = class SomeDate implements MyDate {
  2. year: string;
  3. month: string;
  4. day: string;
  5. constructor(year: string, month: string, day: string) {
  6. }
  7. }

描述混合类型

混合类型通常出现在第三方 js 库的 d.ts 文件上面,在我们写d.ts文件的时候可能需要。

  1. interface Counter {
  2. (start: number): string;
  3. interval: number;
  4. reset(): void;
  5. }
  6. function getCounter(): Counter {
  7. let counter = <Counter>function (start: number) {console.log('start is ' + start)};
  8. counter.interval = 123;
  9. counter.reset = function () {console.log('do you want reset counter?')};
  10. return counter;
  11. }
  12. let c = getCounter();
  13. c(10);
  14. c.reset();
  15. c.interval = 5.0;
  16. console.dir(c)

我们把官网的代码拿下来,小小的修改了一下。

Counter 描述的是一个函数,并且它有静态的interval属性,和静态的reset方法。

getCounter 就是一个工厂函数,每次访问他,都可以得到一个被Counter接口修饰的函数。

并且我们通过tsc -d生成一下我们的d.ts文件。

  1. interface Counter {
  2. (start: number): string;
  3. interval: number;
  4. reset(): void;
  5. }
  6. declare function getCounter(): Counter;
  7. declare let c: Counter;

假如我们想要定义实例方法呢?

counter 函数返回一个我们定好的接口即可

  1. interface couterInstance{
  2. start: number;
  3. }
  4. interface Counter {
  5. (start: number): couterInstance;
  6. interval: number;
  7. reset(): void;
  8. }
  9. function getCounter(): Counter {
  10. let counter = <Counter>function (start: number) {
  11. console.log('start is ' + start)
  12. this.start = start;
  13. };
  14. counter.interval = 123;
  15. counter.reset = function () {console.log('do you want reset counter?')};
  16. return counter;
  17. }
  18. let c = getCounter();
  19. c(10);
  20. c.reset();
  21. c.interval = 5.0;
  22. console.dir(c)

编译之后,新建一个index.html文件

  1. <meta charset="utf-8">
  2. <script src="./interface.js"></script>

用浏览器打开它,打开控制台,把鼠标移动到函数的上面,右键,选择store as global variable

接口与类 - 图20

通过new去实例化这个函数,我们就可以看到有一个 start属性

接口与类 - 图21

总结

我希望这个总结你来写,写与不写选择都在于你。

就像有的人会选择最坎坷的走向优秀的道路,而有的人不会。

最好把故事、场景都自己再阐述一篇,代码都读一篇。

故事: public 就是公共资产,private 就是私人物品,比如爸爸的私房钱,protected 就是家族资产。

读代码:

  1. new (year: string, month: string, day: string) : MyDate

表示可以实例化的对象,并且它的构造器需要一个string类型的yearmonthday参数,返回一个实现了MyDate接口的类。