摘要:前面講泛型的時候,提到了接口。和泛型一樣,接口也是目前中并不存在的語法。不過可不吃這一套,所以這里通過注釋關(guān)閉了對該接口的命名檢查。這樣的接口不能由類實(shí)現(xiàn)。
前面講 泛型 的時候,提到了接口。和泛型一樣,接口也是目前 JavaScript 中并不存在的語法。
由于泛型語法總是附加在類或函數(shù)語法中,所以從 TypeScript 轉(zhuǎn)譯成 JavaScript 之后,至少還存在類和函數(shù)(只是去掉了泛型定義,類似 Java 泛型的類型擦除)。然而,如果在某個 .ts 文件中只定義了接口,轉(zhuǎn)譯后的 .js 文件將是一個空文件——接口被完全“擦除”了。
那么,TypeScript 中為什么要出現(xiàn)接口語法?而對于沒接觸過強(qiáng)類型語法的 JSer 來說,接口到底是個什么東西?
什么是接口現(xiàn)實(shí)生活中我們會遇到這么一個問題:出國旅游之前,往往需要了解目的地的電源插座的情況:
是什么形狀,是三插還是雙插,是平插還是圓插?
如果形狀相同,電壓多少,110V 還是 220V 或者 380V?
直流電還是交流電?
大家都知道,國內(nèi)的電源插頭常見的有兩種,三平插(比如多數(shù)筆記本電腦電源插頭)和雙平插(比如多數(shù)手機(jī)電源插頭),家用電壓都是 220V。但是近年來電子產(chǎn)品與國際接軌,電源適配器和充電器一般都支持 100~220V 電壓。
那么上面就出現(xiàn)了兩類標(biāo)準(zhǔn),一類是插座的標(biāo)準(zhǔn),另一類是插頭的標(biāo)準(zhǔn)。如果這兩類標(biāo)準(zhǔn)一樣,我們就可以提包上路,不用擔(dān)心到地方后手機(jī)充不上電,電腦找不到合適電源的問題。但是,如果標(biāo)準(zhǔn)不一樣,就必須去買個轉(zhuǎn)換插頭,甚至是帶變壓功能的轉(zhuǎn)換插頭。
這里提到的轉(zhuǎn)換插頭在軟件開發(fā)中屬于“適配器模式”,這里不深研。我們要研究的是插座和插頭的標(biāo)準(zhǔn)。插座就是留在墻上的接口,它有自身的標(biāo)準(zhǔn),而插頭為了能使用這個插座,就必須符合它的標(biāo)準(zhǔn),換句話說,得匹配接口。工業(yè)上這像插座這樣的標(biāo)準(zhǔn)必須成文、審批、公布并執(zhí)行,而編程上的接口也類似,需要定義接口、類型檢查(編譯器)、公布文檔,實(shí)現(xiàn)接口。
所以回到 TypeScript,我們以關(guān)鍵字 interface,用類似于 class 聲明的語法在定義接口 (還記得聲明類型一文中提到的類成員聲明嗎)。所以一個接口看起來可能是這樣的
interface INamedLogable { name: string; log(...args: any[]); }通過實(shí)例講接口
假設(shè)我們的業(yè)務(wù)中有這樣一部分 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() 和其它定義中使用它來進(jìn)行類型約束(檢查)。
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"); }
調(diào)用示例:
const logger = new JsLogger("jsLogger"); doWith(logger);給 log() 方法加點(diǎn)料
上面的示例中,輸出的日志只有日志內(nèi)容本身,但是我們希望能在日志信息每行前面綴上日志名稱,比如像這樣的輸出
[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 毫無關(guān)系,但成員定義相同
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);
你以為它會報(bào)錯?沒有,它轉(zhuǎn)譯正常,運(yùn)行正常,輸出
[Logger] oops [Another(oops)] begin to do [Another(oops)] all done
看到這個結(jié)果,Java 和 C# 程序員要抓狂了。不過 JSer 覺得這沒什么啊,我們平時經(jīng)常這么干。
從類 (class) 聲明接口理論上來說,接口是一個抽象概念,類是一個更具體的抽象概念——是的,類不是實(shí)體 (instance),從類產(chǎn)生的對象才是實(shí)體。一般情況下,我們的設(shè)計(jì)過程是從具體到抽象,但開發(fā)(編程)過程正好相反,是從抽象到具體。所以一般在開發(fā)過程中都是先定義接口,再定義實(shí)現(xiàn)這個接口的類。
當(dāng)然有例外,我相信多數(shù)開發(fā)者會有相反的體驗(yàn),尤其是一邊設(shè)計(jì)一邊開發(fā)的時候:先根據(jù)業(yè)務(wù)需要定義類,再從這個類抽象出接口,定義接口并聲明之前的類實(shí)現(xiàn)這個接口。如果接口元素(比如:方法)發(fā)生變化,往往也是先在類中實(shí)現(xiàn),再進(jìn)行抽象補(bǔ)充到接口定義中。這種情況下我們多么希望能直接從類生成接口……當(dāng)然有工具可以實(shí)現(xiàn)這個過程,但多數(shù)語言本身并不支持——別再問我原因,剛才已經(jīng)講過了。
不過 TypeScript 帶來了不一樣的體驗(yàn),我們可以從類聲明接口,比如這樣
interface ILogger extends JsLogger { // 還可以補(bǔ)充其它接口元素 }
這里定義的 ILogger 和最前面定義的 INamedLogable 具有相同的接口元素,是一樣的效果。
為什么 TypeScript 支持這種反向的定義……也許真的只是為了方便。但是對于大型應(yīng)用開發(fā)來說,這并不見得是件好事。如果以后因?yàn)槟承┰蛐枰獮?JsLogger 添加公共方法,那就悲劇了——所有實(shí)現(xiàn)了 ILogger 接口的類都得實(shí)現(xiàn)這個新加的方法。也許以后某個版本的 TypeScript 會處理這個問題,至少現(xiàn)在 Java 已經(jīng)找到辦法了,這就是 Java 8 帶來的默認(rèn)方法,而且 C# 馬上也要實(shí)現(xiàn)這一特性了 。
回到上面的問題現(xiàn)在回到上面的問題,為什么向 doWith() 傳入 AnotherLogger 對象毫不違和,甚至連個警告都沒有。
前面我們已經(jīng)提到了“鴨子辨型法”,對于 doWith(logger: JsLogger) 來說,它需要的并不真的是 JsLogger,而是 interface extends JsLogger {}。只要傳入的這參數(shù)符合這個接口約束,方法體內(nèi)的任何語句都不會產(chǎn)生語法錯誤,語法上絕對沒有問題。因此,傳入 AnotherLogger 不會有問題,它所隱含的接口定義完全符合 ILogger 接口的定義。
然而,語義上也許會有些問題,這也是我作為一個十多年經(jīng)驗(yàn)的靜態(tài)語言使用者所不能完全理解的。有可能這是 TypeScript 為了適應(yīng)動態(tài)的 JavaScript 所做出的讓步,也有可能這是 TypeScript 特意引入的特性。我對多數(shù)動態(tài)語言和函數(shù)式語言并不了解,但我相信,這肯定不是 TypeScript 首創(chuàng)。
TypeScript 接口詳述上面大量的內(nèi)容只是為了將大家通過 class 的定義引入到對 interface 的了解。但是接口到底該怎么定義?
常規(guī)接口常規(guī)接口的定義和類的定義幾乎沒有區(qū)別,上面已經(jīng)存在例子,歸納起來需要注意幾點(diǎn):
使用 interface 關(guān)鍵字;
接口名稱一般按規(guī)范前綴 I;
接口中不包含實(shí)現(xiàn)
不對成員變量賦初始值
沒有構(gòu)造函數(shù)
沒有方法體
而對接口的實(shí)現(xiàn)可以通過 implemnets 關(guān)鍵字,比如
class MyLogger implements INamedLogable { name: string; log(...args: any[]) { console.log(...args); } }
這是顯式地實(shí)現(xiàn),還有隱式的。
const myLogger: INamedLogable = { name: "my-loader", log(...args: any[]) { console.log(...args); } };
另外,在所有聲明接口類型的地方傳值或賦值,TypeScript 會通過對接口元素一一對比來對傳入的對象進(jìn)行檢查。
函數(shù)類型接口曾經(jīng)我們定義一個函數(shù)類型,是使用 type 關(guān)鍵字,以類似 Lambda 的語法來定義。比如需要定義一個參數(shù)是 number,返回值是 string 的函數(shù)類型:
// 聲明類型 type NumberToStringFunc = (n: number) => string; // 定義符合這個類型的 hex const hex: NumberToStringFunc = n => n.toString(16);
現(xiàn)在可以用接口語法來定義
// tslint:disable-next-line:interface-name interface NumberToStringFunc { (n: number): string; } const hex: NumberToStringFunc = n => n.toString(16);
這種定義方式和 Java 8 的函數(shù)式接口語法類似,而且由于它表示一個函數(shù)類型,所以一般不會前綴 I,而是后綴 Func(有參) 或者 Action(無參)。不過 TSLint 可不吃這一套,所以這里通過注釋關(guān)閉了 TSLint 對該接口的命名檢查。
這樣的接口不能由類實(shí)現(xiàn)。上例中的 hex 是直接通過一個 Lambda 實(shí)現(xiàn)的。它還可以通過函數(shù)、函數(shù)表達(dá)式來實(shí)現(xiàn)。另外,它可以擴(kuò)展為混合類型的接口。
混合類型接口JSer 們應(yīng)該經(jīng)常會用到一種技巧,定義一個函數(shù),再為這個函數(shù)賦值某些屬性——這沒毛病,JavaScript 的函數(shù)本身就是對象,而 JavaScript 的對象可以動態(tài)修改。最常見的例子應(yīng)該就是 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 關(guān)鍵字,所以說成是繼承也并無不可。
另外,接口還可以繼承自其它接口,比如
interface INewLogger: ILogger { suplier: string; }
接口還允許從多個接口繼承,比如上面提到的 INamedLogable 可以拆分一下
interface INamed { name: string; } interface ILogable { log(...args: any[]); } interface INamedLogable extends INamed, ILogable {}
這樣定義 INamedLogable 是不是更合理一些?
后記不管什么語言,接口的主要目的是為了在供應(yīng)者和消費(fèi)者之前創(chuàng)建一個契約,其意義更傾向于設(shè)計(jì)而非程序本身,所以接口在各種設(shè)計(jì)模式中應(yīng)用非常廣泛。不要為了接口而接口,在設(shè)計(jì)需要的時候使用它。對復(fù)雜的應(yīng)用來說,定義一套好的接口很有必要,但是對于一些小程序來說,似乎并無必要。
相關(guān)閱讀
從 JavaScript 到 TypeScript - 模塊化和構(gòu)建
從 JavaScript 到 TypeScript - 聲明類型
從 JavaScript 到 TypeScript - 泛型
關(guān)注作者的公眾號“邊城客棧” →
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/85173.html
摘要:要為變量或者常量指定類型也很簡單,就是在變量常量名后面加個冒號,再指定類型即可,比如聲明函數(shù)是類型,即返回值是類型聲明參數(shù)是類型聲明是無返回值的聲明是這段代碼演示了對函數(shù)類型參數(shù)類型和變量類型地聲明。變量函數(shù)參數(shù)和返回值需要申明類型。 從 JavaScript 語法改寫為 TypeScript 語法,有兩個關(guān)鍵點(diǎn),一點(diǎn)是類成員變量(Field)需要聲明,另一點(diǎn)是要為各種東西(變量、參數(shù)...
摘要:接口前端程序員很難理解的點(diǎn)也是一門面向?qū)ο蟮恼Z言,但是中它是基于原型實(shí)現(xiàn)的,中使用了類,這樣會更清晰的體會到面向?qū)ο筮@一說法,但是實(shí)際在中的面向?qū)ο蟾油暾@些語言一樣,通過接口和類去完整的面向?qū)ο缶幊獭? 從入門到放棄的java 初中時自學(xué)過JAVA,學(xué)了大概一個多月吧, 學(xué)了一個多月,看視頻這些,后面放棄了編程。 依稀記得,那段日子極度苦逼,我想如果當(dāng)時是學(xué)javaScrip...
摘要:接口前端程序員很難理解的點(diǎn)也是一門面向?qū)ο蟮恼Z言,但是中它是基于原型實(shí)現(xiàn)的,中使用了類,這樣會更清晰的體會到面向?qū)ο筮@一說法,但是實(shí)際在中的面向?qū)ο蟾油暾@些語言一樣,通過接口和類去完整的面向?qū)ο缶幊獭? 從入門到放棄的java 初中時自學(xué)過JAVA,學(xué)了大概一個多月吧, 學(xué)了一個多月,看視頻這些,后面放棄了編程。 依稀記得,那段日子極度苦逼,我想如果當(dāng)時是學(xué)javaScrip...
摘要:當(dāng)你陷在一個中大型項(xiàng)目中時應(yīng)用日趨成為常態(tài),沒有類型約束類型推斷,總有種牽一發(fā)而動全身的危機(jī)和束縛。總體而言,這些付出相對于代碼的健壯性和可維護(hù)性,都是值得的。目前主流的都為的開發(fā)提供了良好的支持,比如和。參考資料中文文檔 文章博客地址:http://pinggod.com/2016/Typescript/ TypeScript 是 JavaScript 的超集,為 JavaScrip...
閱讀 2703·2021-11-25 09:43
閱讀 2085·2021-11-24 09:39
閱讀 1954·2021-11-17 09:33
閱讀 2750·2021-09-27 14:11
閱讀 1840·2019-08-30 15:54
閱讀 3224·2019-08-26 18:27
閱讀 1264·2019-08-23 18:00
閱讀 1810·2019-08-23 17:53