接口与类

优质
小牛编辑
133浏览
2023-12-01

为什么需要接口?

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

function somefunc1({ x = 0, y = 0 }: { x: number, y: number }) {
    // ...
}

function somefunc2({ x = 0, y = 0, z = 0 }: { x: number, y: number, z: number }) {
    // ...
}

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

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

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

function somefunc2({ x = 0, y = 0, z = 0 }: pointer3d) {
    // ...
}

interface pointer2d {
    x: number;
    y: number;
}

interface pointer3d extends pointer2d {
    z: number;
}

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

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

interface pointer2d {
    x: number;
    y: number;
}

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

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

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

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

而 extends 就是总结的总结了。

interface pointer3d extends pointer2d {
    z: number;
}

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

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

描述类

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

interface Db{
    host: string;
    port: number;
}

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

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

interface -> 合同
Db  -> 合同名称
implements -> 履行

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

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

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

完善一下我们的代码

interface Db {
    host: string;
    port: number;
}

class MySQL implements Db {
    host: string;
    port: number;

    constructor(host: string, port: number) {
        this.host = host;
        this.port = port;
        console.log('正在连接 ' + this.host + ":" + this.port + " 的数据库....")
    }
}

let mysql = new MySQL('localhost', 3306);

结果

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

属性修饰符

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

readonly

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

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

interface Person{
    readonly IdCard: string; // 身份证号
}


class Person implements Person{
    readonly IdCard: string = "42xxxxxxxxxxxxxxx";
    constructor(){}
}

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

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

var Person = (function () {
    function Person() {
        this.IdCard = "42xxxxxxxxxxxxxxx";
    }
    return Person;
}());

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

interface Person{
    readonly IdCard: string; // 身份证号
}

let person: Person =  { IdCard:'43xxxxxxxxx' }

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

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

private 与 protected

private 表示私有的变量,不能被其他任何访问,只归自己管。

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

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

public 和 默认的

修改一下我们的代码,增加一个public的属性,和一个没有任何修饰的属性。

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

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

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

完整的代码如下

class Dad{
    protected surname; // 姓氏
    private private_money; // 私房钱
    public public_something;
    default_something;
    constructor(){}
}

class Son extends Dad {
    constructor() {
        super()
    }
}

let d = new Dad()
d.public_something = 'some_thing';
d.default_something = 'default_thing'

let s = new Son()
s.public_something = 'some_thing';
s.default_something = 'default_thing'

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

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

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

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

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

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

可选属性

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

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

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

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

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

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

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

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

interface Person{
    readonly IdCard: string; // 身份证号
    name?: string;
    [propName : string]: any;
}

let person: Person =  { IdCard:'43xxxxxxxxx' }

function getPerson(p: Person) {
    console.log(p);
}

getPerson({ IdCard: 's', b : 2 })

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

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

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

[propName : string]: any;

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

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

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

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

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

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

我们在 Chrome 里面测试一下 JS

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

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

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

接下来我们看看这个

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

并且装换出来的 js 代码还是原样。

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

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

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

描述函数

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

interface Db {
    host: string;
    port: number;
}

interface InitFunc{
    (options: Db) : string;
}


let myfunc : InitFunc = function (opts: Db) {
    return '';
}

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

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

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

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

描述可实例化

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

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

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

interface MyDateInit {
    new (year: string, month: string, day: string) : MyDate;
}

interface MyDate {
    year: string;
    month: string;
    day: string;
}

class DateClass implements MyDate {
    year: string;
    month: string;
    day: string;
    constructor(year: string, month: string, day: string) {
        this.year = year;
        this.month = month;
        this.day = day;
        return this;
    }
}

function getDate(Class: MyDateInit, { year, month, day }) : MyDate{
    return new Class(year, month, day);
}

getDate(DateClass, { year: '2017', month: '12',day: '1' });

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

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

MyDate 描述的就是我们的类

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

具体原因是,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() 真正意义上面的描述的是以下代码,描述的是一个函数方法。

var DateClass = (function () {
    function DateClass(year, month, day) {
        this.year = year;
        this.month = month;
        this.day = day;
        return this;
    }
    return DateClass;
}());

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

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

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

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

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

interface MyDateInit2 {
    (year: string, month: string, day: string) : MyDate
}

let ExamplePlus : MyDateInit2 = function(year: string, month: string, day: string) : MyDate{
    this.year = year
    this.month = month
    this.day = day
    return this as MyDate;
};

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

interface test{
    constructor(year: string, month: string, day: string);
}

// 错误例子
// class a1 implements test{
//  constructor(year: string, month: string, day: string){
//  }
// }

// 错误例子
// let a2 : test = class test{
//  constructor(year: string, month: string, day: string){
//  }
// }

let a3 : test = {
    constructor(year: string, month: string, day: string){

    }
}

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

interface test{
    constructor(year: string, month: string, day: string): void;
}

let a3 : test = {
    constructor(year: string, month: string, day: string){

    }
}

let cc = new a3.constructor('', '', '')

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

let some : MyDateInit = class SomeDate implements MyDate {
    year: string;
    month: string;
    day: string;
    constructor(year: string, month: string, day: string) {

    }
}

描述混合类型

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

interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}

function getCounter(): Counter {
    let counter = <Counter>function (start: number) {console.log('start is ' + start)};
    counter.interval = 123;
    counter.reset = function () {console.log('do you want reset counter?')};
    return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

console.dir(c)

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

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

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

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

interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}
declare function getCounter(): Counter;
declare let c: Counter;

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

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

interface couterInstance{
    start: number;
}

interface Counter {
    (start: number): couterInstance;
    interval: number;
    reset(): void;
}

function getCounter(): Counter {
    let counter = <Counter>function (start: number) {
        console.log('start is ' + start)
        this.start = start;
    };
    counter.interval = 123;
    counter.reset = function () {console.log('do you want reset counter?')};
    return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

console.dir(c)

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

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

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

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

总结

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

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

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

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

读代码:

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

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