摘要:前面讲泛型的时候,提到了接口。和泛型一样,接口也是目前中并不存在的语法。不过可不吃这一套,所以这里通过注释关闭了对该接口的命名检查。这样的接口不能由类实现。
前面讲 泛型 的时候,提到了接口。和泛型一样,接口也是目前 JavaScript 中并不存在的语法。
由于泛型语法总是附加在类或函数语法中,所以从 TypeScript 转译成 JavaScript 之后,至少还存在类和函数(只是去掉了泛型定义,类似 Java 泛型的类型擦除)。然而,如果在某个 .ts 文件中只定义了接口,转译后的 .js 文件将是一个空文件——接口被完全“擦除”了。
那么,TypeScript 中为什么要出现接口语法?而对于没接触过强类型语法的 JSer 来说,接口到底是个什么东西?
什么是接口现实生活中我们会遇到这么一个问题:出国旅游之前,往往需要了解目的地的电源插座的情况:
是什么形状,是三插还是双插,是平插还是圆插?
如果形状相同,电压多少,110V 还是 220V 或者 380V?
直流电还是交流电?
大家都知道,国内的电源插头常见的有两种,三平插(比如多数笔记本电脑电源插头)和双平插(比如多数手机电源插头),家用电压都是 220V。但是近年来电子产品与国际接轨,电源适配器和充电器一般都支持 100~220V 电压。
那么上面就出现了两类标准,一类是插座的标准,另一类是插头的标准。如果这两类标准一样,我们就可以提包上路,不用担心到地方后手机充不上电,电脑找不到合适电源的问题。但是,如果标准不一样,就必须去买个转换插头,甚至是带变压功能的转换插头。
这里提到的转换插头在软件开发中属于“适配器模式”,这里不深研。我们要研究的是插座和插头的标准。插座就是留在墙上的接口,它有自身的标准,而插头为了能使用这个插座,就必须符合它的标准,换句话说,得匹配接口。工业上这像插座这样的标准必须成文、审批、公布并执行,而编程上的接口也类似,需要定义接口、类型检查(编译器)、公布文档,实现接口。
所以回到 TypeScript,我们以关键字 interface,用类似于 class 声明的语法在定义接口 (还记得声明类型一文中提到的类成员声明吗)。所以一个接口看起来可能是这样的
interface INamedLogable { name: string; log(...args: any[]); }通过实例讲接口
假设我们的业务中有这样一部分 JavaScript 代码
function doWith(logger) { console.log(`[Logger] ${logger.name}`); logger.log("begin to do"); // ... logger.log("all done"); } doWith({ name: "jsLogger", log(...args) { console.log(...args); } })翻译成 TypeScript
我们还不懂接口,所以先定义一个类,包含 name 属性和 log() 方法。有了这个类就可以在 doWith() 和其它定义中使用它来进行类型约束(检查)。
class JsLogger { name: string; constructor(name: string) { this.name = name; } log(...args: any[]) { console.log(...args); } }
然后定义 doWith:
function doWith(logger: JsLogger) { console.log(`[Logger] ${logger.name}`); logger.log("begin to do"); // ... logger.log("all done"); }
调用示例:
const logger = new JsLogger("jsLogger"); doWith(logger);给 log() 方法加点料
上面的示例中,输出的日志只有日志内容本身,但是我们希望能在日志信息每行前面缀上日志名称,比如像这样的输出
[jsLogger] begin to do
所以我们从 JsLogger 继承出来一个 PoweredJsLogger 来用:
class PoweredJsLogger extends JsLogger { log(...args: any[]) { console.log(`[${this.name}]`, ...args); } } const logger = new PoweredJsLogger("jsLogger"); doWith(logger);换个第三方 Logger
甚至我们可以换个第三方 Logger,与 JsLogger 毫无关系,但成员定义相同
function doWith(logger: JsLogger) { console.log(`[Logger] ${logger.name}`); logger.log("begin to do"); // ... logger.log("all done"); } const logger = new AnotherLogger("oops"); doWith(logger);
你以为它会报错?没有,它转译正常,运行正常,输出
[Logger] oops [Another(oops)] begin to do [Another(oops)] all done
看到这个结果,Java 和 C# 程序员要抓狂了。不过 JSer 觉得这没什么啊,我们平时经常这么干。
从类 (class) 声明接口理论上来说,接口是一个抽象概念,类是一个更具体的抽象概念——是的,类不是实体 (instance),从类产生的对象才是实体。一般情况下,我们的设计过程是从具体到抽象,但开发(编程)过程正好相反,是从抽象到具体。所以一般在开发过程中都是先定义接口,再定义实现这个接口的类。
当然有例外,我相信多数开发者会有相反的体验,尤其是一边设计一边开发的时候:先根据业务需要定义类,再从这个类抽象出接口,定义接口并声明之前的类实现这个接口。如果接口元素(比如:方法)发生变化,往往也是先在类中实现,再进行抽象补充到接口定义中。这种情况下我们多么希望能直接从类生成接口……当然有工具可以实现这个过程,但多数语言本身并不支持——别再问我原因,刚才已经讲过了。
不过 TypeScript 带来了不一样的体验,我们可以从类声明接口,比如这样
interface ILogger extends JsLogger { // 还可以补充其它接口元素 }
这里定义的 ILogger 和最前面定义的 INamedLogable 具有相同的接口元素,是一样的效果。
为什么 TypeScript 支持这种反向的定义……也许真的只是为了方便。但是对于大型应用开发来说,这并不见得是件好事。如果以后因为某些原因需要为 JsLogger 添加公共方法,那就悲剧了——所有实现了 ILogger 接口的类都得实现这个新加的方法。也许以后某个版本的 TypeScript 会处理这个问题,至少现在 Java 已经找到办法了,这就是 Java 8 带来的默认方法,而且 C# 马上也要实现这一特性了 。
回到上面的问题现在回到上面的问题,为什么向 doWith() 传入 AnotherLogger 对象毫不违和,甚至连个警告都没有。
前面我们已经提到了“鸭子辨型法”,对于 doWith(logger: JsLogger) 来说,它需要的并不真的是 JsLogger,而是 interface extends JsLogger {}。只要传入的这参数符合这个接口约束,方法体内的任何语句都不会产生语法错误,语法上绝对没有问题。因此,传入 AnotherLogger 不会有问题,它所隐含的接口定义完全符合 ILogger 接口的定义。
然而,语义上也许会有些问题,这也是我作为一个十多年经验的静态语言使用者所不能完全理解的。有可能这是 TypeScript 为了适应动态的 JavaScript 所做出的让步,也有可能这是 TypeScript 特意引入的特性。我对多数动态语言和函数式语言并不了解,但我相信,这肯定不是 TypeScript 首创。
TypeScript 接口详述上面大量的内容只是为了将大家通过 class 的定义引入到对 interface 的了解。但是接口到底该怎么定义?
常规接口常规接口的定义和类的定义几乎没有区别,上面已经存在例子,归纳起来需要注意几点:
使用 interface 关键字;
接口名称一般按规范前缀 I;
接口中不包含实现
不对成员变量赋初始值
没有构造函数
没有方法体
而对接口的实现可以通过 implemnets 关键字,比如
class MyLogger implements INamedLogable { name: string; log(...args: any[]) { console.log(...args); } }
这是显式地实现,还有隐式的。
const myLogger: INamedLogable = { name: "my-loader", log(...args: any[]) { console.log(...args); } };
另外,在所有声明接口类型的地方传值或赋值,TypeScript 会通过对接口元素一一对比来对传入的对象进行检查。
函数类型接口曾经我们定义一个函数类型,是使用 type 关键字,以类似 Lambda 的语法来定义。比如需要定义一个参数是 number,返回值是 string 的函数类型:
// 声明类型 type NumberToStringFunc = (n: number) => string; // 定义符合这个类型的 hex const hex: NumberToStringFunc = n => n.toString(16);
现在可以用接口语法来定义
// tslint:disable-next-line:interface-name interface NumberToStringFunc { (n: number): string; } const hex: NumberToStringFunc = n => n.toString(16);
这种定义方式和 Java 8 的函数式接口语法类似,而且由于它表示一个函数类型,所以一般不会前缀 I,而是后缀 Func(有参) 或者 Action(无参)。不过 TSLint 可不吃这一套,所以这里通过注释关闭了 TSLint 对该接口的命名检查。
这样的接口不能由类实现。上例中的 hex 是直接通过一个 Lambda 实现的。它还可以通过函数、函数表达式来实现。另外,它可以扩展为混合类型的接口。
混合类型接口JSer 们应该经常会用到一种技巧,定义一个函数,再为这个函数赋值某些属性——这没毛病,JavaScript 的函数本身就是对象,而 JavaScript 的对象可以动态修改。最常见的例子应该就是 jQuery 和 Lodash 了。
这样的类型在 TypeScript 中就通过混合类型接口来定义,这次直接引用官方文档的示例:
interface Counter { (start: number): string; interval: number; reset(): void; } function getCounter(): Counter { let counter =接口继承function (start: number) { }; counter.interval = 123; counter.reset = function () { }; return counter; } let c = getCounter(); c(10); c.reset(); c.interval = 5.0;
前面我们提到可以从类声明接口,其语法采用 extends 关键字,所以说成是继承也并无不可。
另外,接口还可以继承自其它接口,比如
interface INewLogger: ILogger { suplier: string; }
接口还允许从多个接口继承,比如上面提到的 INamedLogable 可以拆分一下
interface INamed { name: string; } interface ILogable { log(...args: any[]); } interface INamedLogable extends INamed, ILogable {}
这样定义 INamedLogable 是不是更合理一些?
后记不管什么语言,接口的主要目的是为了在供应者和消费者之前创建一个契约,其意义更倾向于设计而非程序本身,所以接口在各种设计模式中应用非常广泛。不要为了接口而接口,在设计需要的时候使用它。对复杂的应用来说,定义一套好的接口很有必要,但是对于一些小程序来说,似乎并无必要。
相关阅读
从 JavaScript 到 TypeScript - 模块化和构建
从 JavaScript 到 TypeScript - 声明类型
从 JavaScript 到 TypeScript - 泛型
关注作者的公众号“边城客栈” →
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/85173.html
摘要:要为变量或者常量指定类型也很简单,就是在变量常量名后面加个冒号,再指定类型即可,比如声明函数是类型,即返回值是类型声明参数是类型声明是无返回值的声明是这段代码演示了对函数类型参数类型和变量类型地声明。变量函数参数和返回值需要申明类型。 从 JavaScript 语法改写为 TypeScript 语法,有两个关键点,一点是类成员变量(Field)需要声明,另一点是要为各种东西(变量、参数...
摘要:接口前端程序员很难理解的点也是一门面向对象的语言,但是中它是基于原型实现的,中使用了类,这样会更清晰的体会到面向对象这一说法,但是实际在中的面向对象更加完整,它跟这些语言一样,通过接口和类去完整的面向对象编程。 从入门到放弃的java 初中时自学过JAVA,学了大概一个多月吧, 学了一个多月,看视频这些,后面放弃了编程。 依稀记得,那段日子极度苦逼,我想如果当时是学javaScrip...
摘要:接口前端程序员很难理解的点也是一门面向对象的语言,但是中它是基于原型实现的,中使用了类,这样会更清晰的体会到面向对象这一说法,但是实际在中的面向对象更加完整,它跟这些语言一样,通过接口和类去完整的面向对象编程。 从入门到放弃的java 初中时自学过JAVA,学了大概一个多月吧, 学了一个多月,看视频这些,后面放弃了编程。 依稀记得,那段日子极度苦逼,我想如果当时是学javaScrip...
摘要:当你陷在一个中大型项目中时应用日趋成为常态,没有类型约束类型推断,总有种牵一发而动全身的危机和束缚。总体而言,这些付出相对于代码的健壮性和可维护性,都是值得的。目前主流的都为的开发提供了良好的支持,比如和。参考资料中文文档 文章博客地址:http://pinggod.com/2016/Typescript/ TypeScript 是 JavaScript 的超集,为 JavaScrip...
阅读 2704·2021-11-25 09:43
阅读 2086·2021-11-24 09:39
阅读 1958·2021-11-17 09:33
阅读 2752·2021-09-27 14:11
阅读 1846·2019-08-30 15:54
阅读 3225·2019-08-26 18:27
阅读 1265·2019-08-23 18:00
阅读 1813·2019-08-23 17:53