接口与类
为什么需要接口?
我们来看一下这个代码,对于眼神不好使的人来说简直就是遭罪,当然我这里只是简单的给了几个属性,假如有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个属性,一个是string的 host 和 number 的 port
此时我们把接口理解为合同,而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中可以访问得到protected的surname,也就是说被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'记得继承Dad的Son必须要先调用super(),super()表示父类的构造器,先有父亲,后有儿子。
对于属性修饰符你可以这么理解。
class 代表着一个家族成员,extends 表示血缘关系,就像上面的父亲与儿子。
private 是属于家族成员的私有物品,私人空间,别人是不能看到的,除非自己告诉别人,通过方法返回。
protected 表示家族资产,比如姓氏,某一宝物,古董。
public 表示共有资产,谁想拿,去问class的示例拿就好了。
可选属性
这个比较简单,就是一个?就行,表示可传,可不传。
它的语义就是强制需要传递这个参数吗?不强制需求。

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

哪怕我们不传也是不会报错的。
这些修饰符,既然可以修饰类,那必然可以修饰接口上面的属性。
假如我们需要一个可以添加属性的 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类型的year、month、day参数,返回一个实现了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类型的year、month、day参数,返回一个实现了MyDate接口的类。
Last updated