LeapFE

LeapFE 查看完整檔案

北京編輯  |  填寫畢業院校好未來  |  勵步 編輯填寫個人主網站
編輯
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 個人簡介什么都沒有

個人動態

LeapFE 收藏了問題 · 2月23日

js有辦法能獲取唯一標識嗎?

PC端,想做一個同一設備防止重復注冊的功能。
想過用ip的方法,但是現在ip是動態的重啟可能會變。
也想過獲取mac地址但是瀏覽器有兼容性。
至于獲取手機驗證碼,自己的項目,預算有限。。。

所以有好的解決方法么?

LeapFE 關注了問題 · 2月23日

js有辦法能獲取唯一標識嗎?

PC端,想做一個同一設備防止重復注冊的功能。
想過用ip的方法,但是現在ip是動態的重啟可能會變。
也想過獲取mac地址但是瀏覽器有兼容性。
至于獲取手機驗證碼,自己的項目,預算有限。。。

所以有好的解決方法么?

關注 9 回答 6

LeapFE 贊了文章 · 2月22日

5個不常提及的HTML技巧

2021年你需要知道的HTML標簽和屬性

Web開發人員都在廣泛的使用HTML。無論你使用什么框架或者選擇哪個后端語言,框架在變,但是HTML始終如一。盡管被廣泛使用,但還是有一些標簽或者屬性是大部分開發者不熟知的。雖然現在有很多的模版引擎供我們使用,但是我們還是需要盡可能的熟練掌握HTML內容,就像CSS一樣。

在我看來,最好盡可能使用HTML特性來實現我們的功能,而不是使用JavaScript實現相同的功能,盡管我承認編寫HTML可能會是重復的和無聊的。

盡管許多開發人員每天都在使用HTML,但他們并沒有嘗試改進自己的項目,也沒有真正利用HTML的一些鮮為人知的特性。

下面這5個通過HTML標簽/屬性實現的功能我覺得需要了解一下:

圖片懶加載

圖片懶加載可以幫助提升網站的性能和響應能力。圖片懶加載可以避免立即加載那些不在屏幕中立即顯示的圖片素材,當用戶滾動臨近圖片時再去開始加載。

換言之,當用戶滾動到圖片出現時再進行加載,否則不加載。這就降低了屏幕內容展示過程中的圖片素材的請求數量,提升了站點性能。

往往我們都是通過javascript來實現的,通過監聽頁面滾動事件來確定加載對應的資源。但是,在不完全考慮兼容性的場景下,我們其實可以直接通過HTML來直接實現。

注:本篇的提到的標簽和屬性的兼容性需要大家根據實際場景來選取是否使用

可以通過為圖片文件添加loading="lazy"的屬性來實現:

<img data-original="image.png" loading="lazy" alt="lazy" width="200" height="200" />

輸入提示

當用戶在進行輸入搜索功能時,如果能夠給出有效的提示,這會大大提升用戶體驗。輸入建議和自動完成功能現在到處可見,我們可以使用Javascript添加輸入建議,方法是在輸入框上設置事件偵聽器,然后將搜索到的關鍵詞與預定義的建議相匹配。

其實,HTML也是能夠讓我們來實現預定義輸入建議功能的,通過<datalist>標簽來實現。需要注意的是,使用時這個標簽的id屬性需要和input元素的list屬性一致。

<label for="country">請選擇喜歡的國家:</label>
<input list="countries" name="country" id="country">
<datalist id="countries">
  <option value="UK">
  <option value="Germany">
  <option value="USA">
  <option value="Japan">
  <option value="India">
  <option value=“China”>
</datalist>

Picture標簽

你是否遇到過在不同場景或者不同尺寸的設備上面的時候,圖片展示適配問題呢?我想大家都遇到過。

針對只有一個尺寸的圖片素材的時候,我們往往可以通過CSS的object-fit屬性來進行裁切適配。但是有些時候需要針對不同的分辨率來顯示不同尺寸的圖片的場景的時候,我們是否可以直接通過HTML來實現呢?

HTML提供了<picture>標簽,允許我們來添加多張圖片資源,并且根據不同的分辨率需求來展示不同的圖片。

<picture>
  <source media="(min-width:768px)" srcset="med_flower.jpg">
  <source media="(min-width:495px)" srcset="small_flower.jpg">
  <img data-original="high_flower" style="width: auto;" />
</picture>

我們可以定義不同區間的最小分辨率來確定圖片素材,這個標簽的使用有些類似<audio><video>標簽。

Base URL

當我們的頁面有大量的錨點跳轉或者靜態資源加載時,并且這些跳轉或者資源都在統一的域名的場景時,我們可以通過<base>標簽來簡化這個處理。
例如,我們有一個列表需要跳轉到微博的不同大V的主頁,我們就可以通過設置<base>來簡化跳轉路徑

<head>
  <base  target="_blank">  
</head>
<body>
  <a href="jackiechan">成龍</a>
  <a href="kukoujialing">賈玲</a>
</body>

<base>標記必須具有hreftarget屬性。

頁面重定向(刷新)

當我們希望實現一段時間后或者是立即重定向到另一個頁面的功能時,我們可以直接通過HTML來實現。

我們經常會遇到有些站點會有這樣一個功能,“5s后頁面將跳轉”。這個交互可以嵌入到HTML中,直接通過<meta>標簽,設置http-equiv="refresh"來實現

<meta http-equiv="refresh" content="4; URL='https://google.com' />

這里content屬性指定了重定向發生的秒數。值得一提的是,盡管谷歌聲稱這種形式的重定向和其他的重定向方式一樣可用,但是使用這種類型的重定向其實并不是那么的優雅,往往會顯得很突兀。
因此,最好在某些特殊的情況下使用它,比如在長時間用戶不活動之后再重定向到目標頁面。

后記

HTML和CSS是非常強大的,哪怕我們僅僅使用這兩種技術也能創建出一些奇妙的網站。雖然它們的使用量很大很普遍,還是有很多的開發者并沒有真正的深入了解他們,還有很多的內容需要我們深入的去學習和理解,實踐,有很多的技巧等待著我們去發現。

查看原文

贊 37 收藏 28 評論 2

LeapFE 關注了問題 · 2月21日

安裝包出現問題,求解決

報錯的問題:遇到個這樣的問題。
怎么解決比較好呢?
圖片描述

關注 3 回答 1

LeapFE 贊了文章 · 2月21日

使用 mask 實現視頻彈幕人物遮罩過濾

經??匆恍?LOL 比賽直播的小伙伴,肯定都知道,在一些彈幕網站(Bilibili、虎牙)中,當人物與彈幕出現在一起的時候,彈幕會“巧妙”的躲到人物的下面,看著非常的智能。

簡單的一個截圖例子:

image

其實,這里是運用了 CSS 中的 MASK 屬性實現的。

mask 簡單用法介紹

之前在多篇文章都提到了 mask,比較詳細的一篇是 -- 奇妙的 CSS MASK,本文不對 mask 的基本概念做過多講解,向下閱讀時,如果對一些 mask 的用法感到疑惑,可以再去看看。

這里只簡單介紹下 mask 的基本用法:

最基本,使用 mask 的方式是借助圖片,類似這樣:

{
    /* Image values */
    mask: url(mask.png);                       /* 使用位圖來做遮罩 */
    mask: url(masks.svg#star);                 /* 使用 SVG 圖形中的形狀來做遮罩 */
}

當然,使用圖片的方式后文會再講。借助圖片的方式其實比較繁瑣,因為我們首先還得準備相應的圖片素材,除了圖片,mask 還可以接受一個類似 background 的參數,也就是漸變。

類似如下使用方法:

{
    mask: linear-gradient(#000, transparent)                      /* 使用漸變來做遮罩 */
}

那該具體怎么使用呢?一個非常簡單的例子,上述我們創造了一個從黑色到透明漸變色,我們將它運用到實際中,代碼類似這樣:

下面這樣一張圖片,疊加上一個從透明到黑色的漸變,

{
    background: url(image.png) ;
    mask: linear-gradient(90deg, transparent, #fff);
}

image

應用了 mask 之后,就會變成這樣:

image

這個 DEMO,可以先簡單了解到 mask 的基本用法。

這里得到了使用 mask 最重要結論:添加了 mask 屬性的元素,其內容會與 mask 表示的漸變的 transparent 的重疊部分,并且重疊部分將會變得透明。

值得注意的是,上面的漸變使用的是 linear-gradient(90deg, transparent, #fff),這里的 #fff 純色部分其實換成任意顏色都可以,不影響效果。

CodePen Demo -- 使用 MASK 的基本使用

使用 mask 實現人物遮罩過濾

了解了 mask 的用法后,接下來,我們運用 mask,簡單實現視頻彈幕中,彈幕碰到人物,自動被隱藏過濾的例子。

首先,我簡單的模擬了一個召喚師峽谷,以及一些基本的彈幕:

mask1

方便示意,這里使用了一張靜態圖,表示了召喚師峽谷的地圖,并非真的視頻,而彈幕則是一條一條的 <p> 元素,和實際情況一致。偽代碼大概是這樣:

<!-- 地圖 -->
<div class="g-map"></div>
<!-- 包裹所有彈幕的容器 -->
<div class="g-barrage-container">
    <!-- 所有彈幕 -->
    <div class="g-barrage">6666</div>
    ...
    <div class="g-barrage">6666</div>
</div>

為了模擬實際情況,我們再用一個 div 添加一個實際的人物,如果不做任何處理,其實就是我們看視頻打開彈幕的感受,人物被視頻所遮擋:

mask2

注意,這里我添加了一個人物亞索,并且用 animation 模擬了簡單的運動,在運動的過程中,人物是被彈幕給遮擋住的。

接下來,就可以請出 mask 了。

我們利用 mask 制作一個 radial-gradient ,使得人物附近為 transparent,并且根據人物運動的 animation,給 mask 的 mask-position 也添加上相同的 animation 即可。最終可以得到這樣的效果:

.g-barrage-container {
    position: absolute;
    mask: radial-gradient(circle at 100px 100px, transparent 60px, #fff 80px, #fff 100%);
    animation: mask 10s infinite alternate;
}

@keyframes mask {
    100% {
        mask-position: 85vw 0;
    }
}

mask3

實際上就是給放置彈幕的容器,添加一個 mask 屬性,把人物所在的位置標識出來,并且根據人物的運動不斷的去變換這個 mask 即可。我們把 mask 換成 background,原理一看就懂。

  • 把 mask 替換成 background 示意圖:

mask4

background 透明的地方,即 mask 中為 transparent 的部分,實際就是彈幕會被隱藏遮罩的部分,而其他白色部分,彈幕不會被隱藏,正是完美的利用了 mask 的特性。

其實這項技術和視頻本身是無關的,我們只需要根據視頻計算需要屏蔽掉彈幕的位置,得到相應的 mask 參數即可。如果去掉背景和運動的人物,只保留彈幕和 mask,是這樣的:

mask6

需要明確的是,使用 mask,不是將彈幕部分給遮擋住,而是利用 mask,指定彈幕容器之下,哪些部分正常展示,哪些部分透明隱藏。

最后,完整的 Demo 你可以戳這里:

CodePen Demo -- mask 實現彈幕人物遮罩過濾

實際生產環境中的運用

當然,上面我們簡單的還原了利用 mask 實現彈幕遮罩過濾的效果。但是實際情況比上述的場景復雜的多,因為人物英雄的位置是不確定的,每一刻都在變化。所以在實際生產環境中,mask 圖片的參數,其實是由后端實時對視頻進行處理計算出來的,然后傳給前端,前端再進行渲染。

對于運用了這項技術的直播網站,我們可以審查元素,看到包裹彈幕的容器的 mask 屬性,每時每刻都在發生變化:

mask5

返回回來的其實是一個 SVG 圖片,大概長這個樣子:

image

這樣,根據視頻人物的實時位置變化,不斷計算新的 mask,再實時作用于彈幕容器之上,實現遮罩過濾。

最后

本文到此結束,希望對你有幫助 :),本文介紹了 CSS mask 的一個實際生產環境中,非常有意義的一次實踐,也表明很多新的 CSS 技術,運用得當,還是能給業務帶來非常有益的幫助的。

想 Get 到最有意思的 CSS 資訊,千萬不要錯過我的公眾號 -- iCSS前端趣聞 ??

gzh_small.png

更多精彩 CSS 技術文章匯總在我的 Github -- iCSS ,持續更新,歡迎點個 star 訂閱收藏。

如果還有什么疑問或者建議,可以多多交流,原創文章,文筆有限,才疏學淺,文中若有不正之處,萬望告知。

查看原文

贊 42 收藏 22 評論 7

LeapFE 發布了文章 · 2月19日

現代代碼審查:Google案例研究

摘要

使用基于工具的輕量級代碼檢查代碼更改(又名現代代碼審查)已成為廣泛的規范,應用于各種開源和產業系統。在本文中,我們對Google的現代代碼審查進行了探索性研究。 Google很早就引入了代碼審查并經過多年的發展;我們的研究揭示了為什么Google引入了這種做法并分析了當前的狀態,經過數十年的代碼變更和數百萬條代碼審核。通過12次訪談,對44位受訪者的調查以及分析的900萬條已審核變更的審核日志,我們調查了Google進行代碼審查的動機,當前做法,以及開發人員的滿意度和挑戰。

1.引言

對等代碼審查,是除了作者以外的開發者們對源代碼進行手動檢查,被認為是對提高軟件項目質量的一種有價值的工具。在 1976年,Fagan正式制定了高度結構化的代碼審查流程——代碼檢查。多年來,研究人員提供了有關代碼檢查的優勢的證據,特別是在發現缺陷上,但麻煩的是費時,這種方法的同步性特點阻礙了它在實踐中的推廣?,F今,大多數組織都采用更輕量級的代碼審查實踐來限制檢查效率低下?,F代代碼審查是(1)非正式(與Fagan風格相反),(2)基于工具的,(3)異步,(4)聚焦于審查代碼更改。

一個開放的研究挑戰是:在這種新穎的背景下,了解哪些實踐代表了寶貴而有效的審查方法。 Rigby和Bird定量分析了來自跨領域軟件項目以及組織的代碼審查數據,發現五個高度趨同的方面,他們猜想其他的項目也有這個規律性。Rigby和Bird的分析基于有價值的廣泛的觀點(分析了多個來自不同的環境的項目)。對于由Basili倡導的知識實證體系的發展,同樣重要的是要考慮對單個案例進行分析的集中和縱向的觀點。本文擴展了Rigby和Bird的工作,著重于審查實踐和特色,主要是:在Google成立,一家公司擁有數十年的代碼審查的歷史和大量的每日審查借鑒。本文可以(1)對從業人員規定進行代碼審查和(2)吸引研究人員想了解和支持這一新穎過程的人。

從Google非常早期的歷史開始,代碼審查就已經成為軟件開發中必不可少的一部分;因為它很早就引入了,已經成為了Google文化的一個核心部分。在Google,代碼審查的過程和工具經過反復完善了十多年,應用在每天全球數十個辦公室中,超過25,000名開發人員的超過20,000行源代碼的更改。

我們以探索性調查形式進行分析聚焦于代碼審查的三個方面,并擴展了Rigby和Bird的成果:(1)驅動代碼審查的動機,(2)當前實踐,以及(3)開發人員對代碼審查的感受,專注于特定審查遇到的挑戰(審核過程中的中斷)和滿意度。我們的研究方法結合了來自多個數據源的輸入:對Google開發人員進行12次半結構化的訪談;一個內部調查,發送給了最近做個變更審查的工程師,并收到44條回復;一組來自兩年間的Google代碼審查工具產生的900萬條評論的數據。

我們發現:相比其他情況,Google的流程明顯更輕松,基于一位審閱者,快速迭代,小更改,以及集成緊密代碼審查工具。由于圍繞代碼審查發生的交互是復雜的,中斷審查仍然存在。不過,開發人員認為此過程很有價值,確信當規模很大的時候,可以很好地發揮作用,并且,進行這個過程有若干原因,也取決于作者與審查者之間的關系。最后,我們發現關于使用代碼審查工具,不僅可以協作審查,而且還有個一個很重要的發現:代碼審查可以作為一種教學工具。

2.背景和相關工作

我們描述了文獻研究中的審查過程,然后我們詳細介紹這些融合代碼審查的做法流程。

2.1 代碼審查過程和情景

代碼檢查。軟件檢查是第一個正式的代碼審查流程。這個高度結構化的過程涉及計劃,概述,準備,檢查會議,返工和跟進。代碼檢查的目標是在同步檢查會議中發現缺陷,作者和審稿人坐在同一會議室內來檢查代碼更改。 Kollanus和Koskinen匯編了有關代碼檢查的最新文獻調查研究。他們發現絕大多數關于代碼檢查的研究本質上是經驗性的。代碼檢查作為一種發現缺陷的技術的整體價值和促使檢查者讀代碼的價值存在共識??傮w而言,與Internet的普及和異步代碼審查流程的增長,自2005年以來,代碼檢查的研究有所下降。

通過電子郵件進行異步審查。直到2000年代后期,大多數大型OSS項目都采用一種遠程異步審查的形式,這依賴于補丁發送的通訊渠道,如郵件列表和問題跟蹤系統。在這種情況下,項目成員評估貢獻的補丁程序并通過這些渠道請求修改。當補丁被認為質量足夠高時,核心開發人員會將其提交給代碼庫。受信任的提交者可能具有提交到審查過程,而不是進行提交前的審查。 Rigby等人,是最早在這種環境下進行廣泛工作的人之一;他們發現,這種類型的審查“與[代碼檢查]幾乎沒有共同點,只是相信同行會有效地發現軟件缺陷” 。 Kononenko等人,分析了這種情況,并且發現審查響應時間和接受程度和社會因素相關,例如,審查者的工作量和改變作者的經驗,這些是代碼檢查所無法反映的。

基于工具的審查。為了使修補程序審查的過程結構更加合理,OSS和工業設置中出現了幾種工具。這些工具支持審閱過程的持久工作:(1)補丁的作者將其提交給代碼審閱工具,(2)審閱者可以看到建議代碼與更改的差異,(3)可以與作者和其他審稿人就特定的話題展開討論,然后(4)作者可以提出修改意見,以解決評審者的意見。此反饋周期將持續到每個人都滿意或補丁被丟棄為止。不同的項目使用了他們的工具來支持他們的過程。 Microsoft使用CodeFlow,該工具跟蹤每個人(作者或審閱者)的狀態以及他們在進程中的位置(簽名,等待,審閱); CodeFlow不會阻止作者未經批準而提交更改,并支持在評審線程中聊天。 Google的Chromium項目(以及其他幾個OSS項目)依賴于外部可用的Gerrit;在Chromium中,只有經過審閱者的明確批準并自動確認更改不會破壞構建,更改才會合并到master分支中。在Gerrit中,未分配審查者也可以發表評論。 VMware開發了開源的ReviewBoard,它將靜態分析集成到審查過程中。這種集成依賴于變更作者手動請求分析,并且已經證明可以提高代碼審查的質量。 Facebook的代碼審查系統Phabricator,使審查者可以“接管”更改并自行提交,并提供了掛鉤以進行自動靜態分析或持續的構建/測試集成。

在基于工具審查的背景下,研究人員調查了代碼更改接受或響應時間與更改后的代碼和作者的特征之間的關系,以及審閱者之間的協議。 根據工業和OSS開發人員的意見,還進行了定義什么構成良好的代碼審查。

基于分叉模式的開發模型。在GitHub上,拉請求的過程中,開發人員想要對現有的git倉庫進行更改,就要對其fork進行更改。在發出拉請求后,會出現在拉請求的列表中展示項目中的問題,任何可以看到該項目的人都可以看到。 Gousios等人對拉請求集成者和貢獻者的實踐和碰到的問題進行了定性研究,發現與基于工具的代碼審查有類似的方法。

2.2 代碼審查中的融合實踐

Rigby和Bird提出了第一項也是最重要的工作,這些工作試圖跨幾個代碼審查過程和上下文確定融合的實踐。 他們考慮了使用基于電子郵件審查的OSS項目,使用Gerrit的OSS項目,使用基本代碼審查工具的AMD項目以及使用CodeFlow的Microsoft。 他們分析了這些項目的過程和數據,以描述多個角度,例如迭代開發,審查者選擇和審查討論。 他們確定了所有已考慮項目都融合到的五種現代代碼審查實踐(表1)。 我們將使用其ID(例如CP1)來引用這些做法。 基本上,他們在快速,輕量級過程(CP1,CP2,CP3)方面達成了一致,很少有人參與(CP4)進行小組問題解決(CP5)。

id融合實踐
CP1當時同行審查遵循輕量級,靈活的流程
CP2審查要提早(在提交更改之前),快速且頻繁地進行
CP3變更范圍很小
CP4兩名審查者找大量的缺陷
CP5審查已從發現缺陷活動變為小組解決問題活動

3.方法論

本節描述了我們研究的問題和背景;它還概述了我們的研究方法及其局限性。

3.1 研究的問題

這項研究的總體目標是調查Google的現代代碼審查,這一過程涉及成千上萬的開發人員,并且經過了十多年的改進。 為此,我們進行了探索性調查,圍繞三個主要研究問題進行了結構設計。

RQ1:Google進行代碼審查的動機是什么?Rigby和Bird發現動機是現代代碼審查的融合特征之一(CP5)。 在此可以了解,動機和期望推動了Google的代碼審查。 特別是,我們既考慮了引入現代代碼審查的歷史原因(因為Google是最早使用現代代碼審查的公司之一),也考慮了當前的期望。

RQ2:Google的代碼審查實踐是做什么? Rigby和Birdregard根據流程(CP1),速度和頻率(CP2),分析變更的大?。–P3)和審閱者數量(CP4)來執行流程本身。 我們對Google的這些方面進行了分析,以調查與以前的研究相比,對于具有更長代碼審查歷史,明確文化和大量審查記錄的公司來說,同樣的發現是否成立。

RQ3:Google開發人員如何看待代碼審查?最后,在我們的最后一個研究問題中,我們有興趣了解Google開發人員如何看待其公司中已實施的現代代碼審查。為了更好的理解實踐(因為感知驅動行動)和指導未來的研究,這個探索很必需。我們聚焦于兩方面:一些特定的審查,比如:開發人員有中斷審查的經歷;在面臨一些挑戰時開發人員對審查是否滿意。

3.2 研究的背景

我們簡要描述了關于我們方法論的研究背景。 有關Google代碼審查過程和工具的詳細說明,請參見第5.1節。

Google的大多數軟件開發都在一個整體的源代碼存儲庫中(mono-repo),通過內部版本控制系統訪問。 由于Google代碼審查是必需的,因此,每次向Google源代碼控制系統提交代碼,都先要使用CRITIQUE進行代碼審查,CRITIQUE是一個內部開發的,集中的,基于Web的代碼檢查工具。 開發工作流程基于Google的整體代碼庫,包括代碼審查過程,是非常統一的。就像第2節中描述的工具一樣,CRITIQUE允許審查者看到提議更改代碼和開始討論前明確的代碼行處。CRITIQUE提供了大量的登錄功能;記錄開發者使用該工具的交互(包括打開工具,查看差異,創建評論和接受更改)。

3.3 研究的方法

為了回答我們的研究問題,我們采用定性和定量相結合的方法,該方法結合了以下幾種來源的數據:在與Google從事軟件開發工作的員工進行的半結構化訪談,來自代碼審查工具的日志以及對其他員工的調查。我們使用訪談作為一種工具來收集有關進行代碼審查(RQ1)動機的多樣性(與頻率相對比)的數據,并激發開發人員對代碼審查及其挑戰(RQ3)的理解。 我們使用CRITIQUE的日志來量化和描述當前的審查實踐(RQ2)。最后,我們使用調查來確認訪談(RQ1)中出現的代碼審查的多種動機,和激發開發人員對過程的滿意度的因素。

會談。我們與選定的Google員工進行了一系列面對面的半結構化訪談,每次訪談大約需要1個小時。最初的可能參加者是使用滾雪球采樣法來選擇的,首先是論文作者所知道的開發人員。從此庫中,選擇參與者以確保團隊的分散,技術領域,工作角色,公司內部的時間長度以及在代碼審核過程中的角色。訪談腳本包括有關代碼審查的動機,最近審查/撰寫的變更,以及最佳/最差審查經歷的問題。 在每次訪談之前,我們都會回顧參與者的審查歷史,并找到要在訪談中會討論的更改; 我們選擇這些更改,是根據互動次數,參與對話的人數,以及是否有很多令人驚訝審查點評。在訪談的觀察部分中,要求參與者在審閱即將發生的變更時思考,并提供一些明確的信息,例如開始審閱的切入點。 訪談一直持續到達到飽和,并且訪談提出了大致相似的概念。 總體而言,我們對從Google工作1個月到10年(平均5年),軟件工程和站點可靠性工程的員工進行了12次面試。他們包括技術主管,經理和個人貢獻者。每次訪談涉及三到四個人:參與者和2-3個受訪者(其中兩個是本文的作者)。采訪由一名采訪者實時轉錄,而另一名采訪者提出問題。

采訪數據的開放編碼。為了確定采訪數據中出現的廣泛主題,我們進行了一次開放編碼。 兩位作者討論了訪談筆錄,以確立共同的主題,然后將其轉換為編碼方案。 然后,另一位作者對討論的注釋進行了封閉編碼,注釋是對確認的主題。我們對其中不止一個采訪進行了迭代,直到我們就該計劃達成協議。 我們還跟蹤了上下文(審稿人與作者之間的關系)中提到的這些主題。問題設計和分析過程的結合意味著我們可以討論結果中的穩定主題,但不能有意義地討論發生的相對頻率。

審查數據的分析。我們定量的分析數據,數據是代碼審查過程中使用CRITIQUE產生的日志。我們主要關注Rigby和Bird發現的融合實踐(CP)相關的指標。為了方便對比,我們不考慮沒有審查者的更改,因為我們對有明確代碼審查過程的更改感興趣。我們將“審閱者”視為批準代碼更改的任何用戶,而不論更改作者是否明確要求他們進行審閱。我們使用基于名稱的啟發式的方法來自動化流程產生的更改。我們專門關注Google主要代碼庫中發生的更改。我們還排除了在研究時尚未落實的更改,以及我們的差異工具報告的源碼零行變化量的更改,例如,僅修改二進制文件的更改。在Google,平均每個工作日,提交了約20,000個符合上述過濾條件的更改。 我們的最終數據集包括2014年1月至2016年7月,由25,000多名作者和審閱者創建的符合這些標準的大約900萬項更改,以及從2014年9月至2016年7月之間的所有更改中收集的大約1300萬條審查評論。

調查。我們創建了一個在線調查表,并發送給了98位最近提交了代碼更改的工程師。代碼更改已經過審核,因此我們定制了調查表,以詢問受訪者如何看待代碼審核,關于他們最近的特定更改;這種策略使我們可以減輕召回偏見,但仍可以收集全面的數據。 該調查包括三個關于收到的評論的價值的Likertscale問題,一個關于評論對其更改影響的多項選擇(基于訪談產生的期望)和一個可選的“其他”回答,以及一個開放式- 最終提出質疑,以引起受訪者對所收到的評論,代碼評論工具和/或整個過程的意見。 我們收到了44份有效的調查問卷答復(45%的答復率,在軟件工程研究中被認為很高了)。

3.4 有效性和局限性的威脅

我們描述了研究方法所帶來的對有效性和工作成果局限性的威脅,以及為緩解這些挑戰所采取的行動。

內部有效性——可信度。關于評論數據的定量分析,我們使用啟發式方法從定量分析中濾除機器人撰寫的更改,但這些啟發式方法可能允許某些機器人撰寫的更改; 我們對此進行了緩解,因為我們僅包括具有人工審核者的由機器人撰寫的更改。關于定性調查,我們使用了開放式編碼來分析受訪者的答案。該編碼可能會受到編寫該編碼的作者的經驗和動機的影響,盡管會通過讓多個編碼人員參與來減輕這種偏見。決定參加我們的訪談并自由選擇調查的員工決定這樣做,從而引入了自我選擇偏見的風險。 因此,對于不選擇參與的開發人員而言,結果可能會有所不同;為減輕此問題,我們將訪談和調查中的信息相結合。 此外,我們使用雪球抽樣方法來確定要面試的工程師,這有抽樣偏差的風險。盡管我們試圖通過面試具有各種工作角色和職責的開發人員來減輕這種風險,但我們訪談的開發人員可能有其他因素在整個公司中并不適用。 為了減輕主持人的接受偏見,參與定性數據收集的研究人員不屬于CRITIQUE團隊。 社會可取性偏見可能已經影響了答案,使其更適合Google文化。 但是,在Google鼓勵人們批評和改進發現的工作流程,從而減少這種偏見。 最后,我們沒有采訪與專家評審員(例如安全評審)進行交互的研究科學家或開發人員,因此我們的結果偏向于一般開發人員。

通用性——可移植性。我們的結果可能無法推廣到其他情況,而是我們對多年實踐和數百萬次細化檢查后,仍會發生的多種多樣的做法和審查中斷感興趣。鑒于基本代碼檢查機制在多個公司和OSS項目中的相似性, 有理由認為,如果審查過程達到相同的成熟度并使用可比較的工具,則開發人員將具有類似的經驗。

4.結果:動力

在我們的第一個研究問題中,我們首先要研究導致這一過程的原因,從而尋求理解開發人員在Google進行代碼審查時的動機和期望。

4.1 一切如何開始

Google的代碼審查最早是由第一批員工之一引入的; 本文的第一作者采訪了該員工(以下簡稱 E),以更好地理解代碼審查及其演變的最初動機。E 解釋了代碼審查引入的主要推動力是:迫使開發人員編寫其他開發人員可以理解的代碼 ; 這被認為很重要,因為代碼必須作為未來開發人員的老師。Google在代碼審查中的引入標志著從研究代碼庫(已優化為快速原型開發)向生產代碼庫的過渡,在此基礎上考慮未來工程師閱讀源代碼。代碼審查也被認為能夠確保不止一個人熟悉每一段代碼,從而增加了知識在公司中的駐留機會。

E 重申了這樣一個概念,即盡管審閱者發現錯誤是很棒的,但在Google引入codereview的首要原因是為了提高代碼的可理解性和可維護性。但是,除了最初進行代碼審查的教育動機外,E 解釋說,開發人員的三個其他好處很快就在內部對開發人員變得顯而易見:檢查樣式和設計的一致性;確保足夠的測試;通過確保沒有任何開發人員可以在沒有監督的情況下提交任意代碼來提高安全性。

4.2 目前的期望

通過對訪談數據進行編碼,我們確定了Google開發人員期望從代碼審查中獲得的四個關鍵主題:教育,維護規范,把關事故預防。 教育從代碼審查中學習或學習,并與引入代碼審查的最初原因保持一致; 規范是指組織對自由選擇的偏好(例如格式或API使用模式); 網守涉及圍繞源代碼,設計選擇或其他工件的邊界的建立和維護; 事故是指引入錯誤,缺陷或其他與質量相關的問題。

這些是審查過程中的主要主題,但是代碼審查也用于追溯歷史。開發人員在審查過程完成后對其進行評估;代碼審查可以瀏覽歷史記錄 代碼更改的內容,包括發生了什么注釋以及更改如何演變。 我們還注意到開發人員使用代碼回顧歷史來了解錯誤的引入方式。 從本質上講,代碼審查使將來的變更審核成為可能。

在我們的調查中,我們進一步驗證了這種編碼方案。 他們可以選擇四個主題中的一個或多個主題和/或自己撰寫。 較早確定的四個主題中的每個主題都是在特定代碼審查的背景下由8至11個受訪者選擇的,因此,可以更加確信上述編碼方案與開發人員對代碼審查價值的理解相一致。

盡管這些期望可以覆蓋以前在Microsoft [4]上獲得的期望,但正如我們的參與者所解釋的那樣,Google的主要重點是教育以及代碼的可讀性和可理解性,這與歷史動因相吻合。 因此,關注點與Rigby和Bird的關注點不一致(即,小組解決問題的活動)[33]。

發現1. Google進行代碼審查的期望并不以解決問題為中心。 Google引入審核,目的是確保代碼的可讀性和可維護性。 當今的開發人員除了維護規范,跟蹤歷史記錄,保護措施和預防事故外,還從教育角度進行了了解。發現缺陷受到歡迎,但不是唯一的重點。

如前所述,在對訪談筆錄進行編碼時,我們還跟蹤了提到主題的評論上下文,我們發現這些不同主題的相對重要性取決于作者與評論者之間的關系(圖1)。例如,維護工程師與具有不同資歷的工程師(項目負責人,專家可讀性審閱者或“新”團隊成員)之間的規范沖突,而與同伴或其他團隊相比則少一些,而看門人和事故預防則是主要的。具有廣泛的價值,并包含多種不同的關系。

圖1. 關系圖,描述了哪些評論期望主題主要出現在特定作者/評論者上下文中。

發現2.對Google進行特定代碼審查的期望取決于作者與審查者之間的工作關系。

5.結果:實踐

在我們的第二個研究問題中,我們描述了代碼重審過程,并將其定量方面的內容與先前工作中發現的趨同做法進行了比較[33]。

5.1 描述審查過程

Google的代碼審查與兩個概念相關:所有權和可讀性。 我們首先介紹它們,然后描述審閱過程的流程,然后得出內部審閱工具CRITIQUE與其他審閱工具不同的特點。

所有權。Google代碼庫以樹結構排列,其中每個目錄都由一組人員明確擁有。 盡管任何開發人員都可以提議對代碼庫的任何部分進行更改,但是相關目錄(或父目錄)的所有者必須在提交更改之前對其進行審核和批準; 甚至目錄所有者也要在提交之前檢查其代碼。

可讀性。Google定義了一個稱為可讀能力的概念,該概念很早就引入了,以確保代碼庫中的代碼風格和規范保持一致。 開發人員可以使用特定語言獲得可讀性認證。 為了應用可讀性,開發人員將更改發送給一組有可讀能力的審閱者。 一旦這些審閱者確信開發人員了解某種語言的代碼風格和最佳實踐,便會為開發人員授予該語言的可讀性。 每次更改都必須由具有所使用語言可讀性證明的人員編寫或審閱。

代碼審查流程。審查流程與評論緊密結合,其工作方式如下:

1。 創建:作者開始修改,添加或刪除某些代碼; 一旦準備好,他們就會進行更改。

2。 預覽:作者然后使用CRITIQUE來查看更改的差異,并查看自動代碼分析器的結果(例如,來自Tricorder [36])。 準備就緒后,作者將更改發送給一個或多個審閱者。

3。 評論:審閱者可以在Web UI中查看差異,并隨時起草評論。 程序分析結果(如果存在)也對審閱者可見。未解決的評論顯示為變更作者必須解決的操作項目。已解決的評論包括可選或信息性評論,可能不需要變更作者采取任何行動。

4。 解決反饋:作者現在可以通過更新更改或通過回復評論來處理注釋。更新更改后,作者將上載新快照。 作者和審閱者可以查看任意一對快照之間的差異,以了解發生了什么變化。

5。 批準:解決所有評論后,評論者會批準該更改并將其標記為“ LGTM”(對我來說很好 Looks Good To Me)。 要最終進行更改,開發人員通常必須至少獲得一名審閱者的批準。通常,只需一名審閱者即可滿足上述所有權和可讀性要求。

我們嘗試量化“輕量級”審閱的方式(CP1)。 我們通過檢查變更作者郵寄了一組可解決以前未解決的注釋的評論來衡量評論中來回的次數。我們假設一個迭代對應于一個作者解決某個評論的一個實例; 零重復意味著作者可以立即提交。我們發現所有更改中有80%以上最多涉及解決評論的重復。

建議審閱者。要確定最佳的人來重新審閱更改,CRITIQUE依靠一種工具來分析變更并建議可能的審閱者。 此工具確定滿足更改中所有文件的審閱要求所需的最小審閱者集。 請注意,通常只需要一名審閱者,因為更改通常是由擁有文件查詢所有權和/或可讀權的人創作的。 該工具對最近編輯和/或審閱所包含文件的審閱者進行優先級排序。 由于尚未建立新的團隊成員,因為他們尚未建立審核/編輯歷史記錄,因此已明確添加為他們。 未分配的審閱者還可以對更改發表評論(并可能批準)。尋找審閱者的工具支持通常僅在文件更改超出特定團隊的情況下才需要。 在一個團隊內,開發人員知道向誰發送更改。 為了將可能發送給團隊中任何人的更改,許多團隊使用一種系統,該系統將循環發送到團隊電子郵件地址的審閱分配給配置的團隊成員,同時考慮到審閱負載和休假。

代碼分析結果。CRITIQUE將代碼分析結果顯示為注釋以及人工注釋(盡管顏色不同)。分析人員(或審閱者)可以提供建議的編輯,這些編輯可以被提議,也可以通過評論應用于變更。 為了在更改提交之前審核更改,Google的開發還包括預提交掛鉤:檢查失敗需要開發人員顯式覆蓋以啟用提交的地方。 提交前檢查包括基本的自動樣式檢查和運行與變更相關的自動測試套件。 所有預提交檢查的結果在代碼查看工具中可見。 通常,會自動觸發預提交檢查。 這些檢查是可配置的,以便團隊可以強制實施特定于項目的不變量,并自動將電子郵件列表添加到更改中,以提高意識和透明度。 除了預先提交結果外,CRITIQUE還可以通過Tricorder [36]顯示各種自動代碼分析的結果,這些分析可能不會阻止提交更改。 分析結果包括簡單的樣式檢查,更復雜的基于編譯器的分析通過以及特定于項目的檢查。 目前,Tricorder包括110個分析儀,其中5個是用于數百次附加檢查的插件系統,總共可分析30多種語言。

發現3. Google代碼審核過程與輕量級和靈活的融合做法保持一致。 但是,與其他研究過的系統相比,所有權和可讀權是明確的,并且起著關鍵作用。 審閱工具包括審閱者推薦和代碼分析結果。

5.2 量化審核流程

我們復制了Rigby和Bird發現的CP2-4的定量分析,以便將這些實踐與Google融合的特征進行比較。

審查頻率和速度。Rigby和Bird發現快節奏的迭代開發也適用于現代代碼審查:在他們的項目中,開發人員的工作間隔非常短。 為了找到答案,他們分析了評論的頻率和速度。

在Google,就頻率而言,我們發現處于中位數上的開發者每周大約進行3次更改,而80%的開發者每周進行少于7次更改。 同樣,開發人員每周審核的變更中位數為4,而80%的審閱者每周審核的變更少于10。 在速度方面,我們發現開發人員必須等待對其更改的初步反饋,對于較小的更改,平均時間少于一小時,對于較大的更改,平均時間少于5小時。 整個審閱過程的總體(所有代碼大?。┲兄笛舆t小于4小時。 這比Rigby和Bird [33]報告的平均批準時間要低得多,AMD的批準時間中位數為17.5小時,Chrome OS為15.7小時,三個Microsoft項目為14.7、19.8和18.9小時。 另一項研究發現,微軟批準的平均時間為24小時[14]。

審查規模。Rigby和Bird認為,只有通過較小的變更來審查并隨后分析審查規模,才能實現快速審查時間。在Google,正在考慮的更改中,超過35%僅修改一個文件,而大約90%的修改少于10個文件。超過10%的更改僅修改一行代碼,而修改的行數的中位數為24。更改位數的中位數顯著低于Rigby和Bird對AMD(44行),Lucent(263行)和Bing等公司的報告。 ,Microsoft的Office和SQLServer(在這些界限之間的某個位置),但符合開放源代碼項目中的更改大小[33]。

審查者和評論的數量。甚至在經過深入研究的代碼檢查中,研究人員的最佳人數一直存在爭議[37]。 Rigby和Bird調查了所考慮的項目是否收斂到了類似數量的參與評審人員。 他們發現這個數字是兩個,無論是否明確邀請了審閱者(例如,在Microsoft項目中,邀請的中位數最多為4個審閱者),或者是否公開廣播了更改以進行審閱[33]。

相比之下,在Google中,只有不到25%的更改擁有多于一名審閱者,而超過99%的更改最多具有五名審閱者,中位審閱者人數為1。較大的更改通常平均會擁有更多的審閱者。 但是,即使平均變化非常大,平均也需要不到兩名審稿人。

Rigby和Bird還發現“當活躍于[超過2]位審稿人時,有關更改的評論數量最少” [33],并得出結論,兩名審稿人發現缺陷的最佳數量。 在Google,情況有所不同:審閱人數越多,對更改的評論平均數就越多。 此外,每次更改的平均注釋數隨行數的變化而增加,對于大約1250行的更改,每個更改的最高注釋數為12.5。 大于此的更改通常包含自動生成的代碼或較大的刪除,從而導致平均注釋數較低。

發現4.與先前調查的其他項目相比,Google的代碼審查已將審查過程融合到一個過程中,該過程的審查速度顯著加快且變更幅度較小。 此外,與其他項目中的兩名審閱者相比,一名審閱者通常被認為足夠。

6.結果:開發人員的看法

我們最后一個研究問題是通過Google的代碼審查來調查開發人員的挑戰和滿足感。

6.1 Google的代碼審查中斷

以前的研究調查了整個審查過程中的挑戰[4,26],并提供了令人信服的證據,這也被我們作為工程師的經驗所證實,理解要審查的代碼是一個主要障礙。 為了拓寬我們的經驗知識體系,我們在這里集中討論特定審查(“審查中斷”)中遇到的挑戰,例如延誤或分歧。

對我們的訪談數據的分析提出了五個主要主題。 前四個主題認為審查中斷在過程中的相關因素有:

距離:受訪者從兩個角度感知代碼審閱的距離:地理(即作者與審閱者之間的物理距離)和組織(例如不同團隊或不同角色之間的物理距離)。這兩種類型的距離都被認為是導致審閱過程延遲或導致誤解的原因。

社會互動:受訪者認為代碼審閱中的交流有可能從兩個方面導致問題:措辭和權力。 措辭是指有時作者對評論發表敏感的事實。 對評論的情感分析提供了證據,表明帶有負面語氣的評論不太可能有用[11]。 權利是指使用代碼審查過程來誘使他人改變自己的行為; 例如,拖延審核或保留批準。 措辭或權利在審查中,可能會使開發人員對檢查過程感到不舒服或沮喪。

審查主題:訪談提到了關于代碼審查是否是重新審查某些方面的最合適上下文(尤其是設計審查)的分歧。 這導致期望值不匹配(例如,某些團隊希望大多數設計在第一次審閱之前完成,其他團隊希望在審閱中討論設計),這可能導致參與者之間以及過程中產生摩擦。

背景:受訪者讓我們看到,由于不知道是什么導致了這種變化,所以會產生誤解; 例如,如果變更的理由是解決生產問題的緊急解決方案或“有個不錯的改進”。 預期結果的不匹配會導致延遲或沮喪。

最后一個主題是工具本身:

定制化:一些團隊對代碼審查有不同的要求,例如,關于需要多少審查者。 這是技術上的審查中斷,因為批評中并不總是支持任意定制,并且可能引起對這些政策的誤解。 根據反饋,CRITIQUE最近發布了一項新功能,該功能允許更改作者要求所有審閱者簽名。

6.2 滿意度和時間投入

為了了解已確定問題的重要性,我們使用了調查的一部分來調查代碼審查是否總體上被認為是有價值的。

我們發現(表2)在Google內部代碼審查被普遍認為是有價值和有效的–所有受訪者都同意代碼審查很有價值的說法。我們對CRITIQUE進行的內部滿意度調查反映了這種觀點:97%的開發人員對此感到滿意。

在特定變化的背景下,情緒變化更大。最不滿意的答復與很小的更改(1個字或2行)或與實現某些其他目標所需的更改(例如,從源代碼的更改觸發過程)相關。但是,大多數受訪者認為,他們所做的更改的反饋量是適當的。在這3個中,有8位受訪者認為注釋無濟于事,并指出,所審查的更改是小的配置更改,對代碼審核沒有影響。只有2位受訪者表示評論中有bug。

badgood
對于此更改,審核過程很好地利用了我的時間24141113
總的來說,我認為Google的代碼審查很有價值0001430
對于此更改,反饋量為223450

表2. 用戶滿意度調查結果

為了根據滿意度對答案進行情境化,我們還調查了開發人員花費在審閱代碼上的時間。為了準確量化審閱者所花費的時間,我們跟蹤了開發人員與CRITIQUE的互動(例如,打開選項卡,查看了差異,評論,批準了更改),以及其他工具來估算開發人員每周花費多長時間來審核代碼。我們將開發人員交互的順序分組為一定的時間段,將“審閱會話”視為與變更提交者以外的其他開發人員進行的,與同一未提交變更相關的交互順序,每次相隔不超過10分鐘。 從2016年10月開始的五周內,所有審核會話所花的總小時數,然后計算每周每位用戶的平均值,過濾出我們在過去五周內都沒有數據的用戶。 我們發現開發人員平均花費3.2(平均每周2.6個小時)來審查更改。 與OSS項目的6.4小時/周的自我報告時間相比,這個數字很低[10]。

發現5.盡管經過了多年的改進,但Google的代碼審查仍面對審查中斷。 這些主要與評論周圍發生的互動的復雜性有關。 但是,開發人員強烈認為代碼審查是一個有價值的過程,開發人員每周花費大約3個小時進行審查。

7.討論

我們討論了這項調查中出現的主題,這些主題可以啟發從業人員建立代碼審查流程,并激發研究人員在未來的調查中。

7.1 真正輕量級的過程

現代代碼審查的誕生是它減輕了繁瑣的代碼檢查的負擔[4]; 實際上,Rigby和Bird在他們對整個系統的調查中都證實了這一特征(CP1)。 在Google,代碼審查已匯聚到一個更加輕量級的過程,開發人員發現該過程既有價值又可以很好地利用他們的時間。

Google的審查時間中位數比其他項目要短得多。 我們假定這些差異是由于Google在代碼審查方面的文化(嚴格的審查標準和對快速審閱時間的期望)。 此外,審稿人人數也有很大差異(其中一個在Google中,而其他兩個在其他項目中);我們認為擁有一名審閱者可以使審閱變得快速而輕便。

審閱時間短和審閱者人數少可能是由于代碼審閱是開發人員工作流程中必不可少的一部分;它們也可能源于小的更改。 OSS項目的中位數變化范圍從11到32行變化,具體取決于項目。 在公司中,此更改大小通常較大,有時高達263行。 我們發現Google的更改大小與OSS更接近:大多數更改很小。變更的大小分布是代碼審查過程質量的重要因素。 先前的研究發現,隨著更改大小的增加,有用評論的數量會減少,審閱延遲會增加。 大小也會影響開發人員對代碼審查過程的理解; Mozilla投稿人的調查發現,開發人員認為與大小相關的因素對審核延遲的影響最大。 Google確認變更大小與評論質量之間的相關性,并強烈鼓勵開發人員進行小的增量更改(大刪除和自動重構除外)。 這些發現和我們的研究支持審查小的更改的價值以及對研究和工具的需求,以幫助開發人員創建如此小的獨立代碼更改以進行審查。

7.2 實踐中的軟件工程研究

Google進行代碼審查的部分做法與軟件工程研究中提出的做法保持一致。 例如,微軟公司對代碼所有權的一項研究發現,應認真審查較小貢獻者所做的更改,以提高代碼質量。 我們發現,此概念是在Google上通過要求所有者批準而強制實施的。 同樣,以前的研究表明,通常有一個變更的審閱者將負責檢查代碼是否符合常規。 可讀性使此過程更加明確。 在下文中,我們將重點介紹使其成為“下一代代碼審查工具” 的CRITIQUE的功能。

審查者的建議。研究人員發現,對審稿代碼具有先驗知識的審稿人會提供更多有用的評論,因此工具可以為審稿人選擇提供支持。我們已經看到,審閱者推薦功能受工具支持,從而優先考慮那些最近編輯/審閱受審文件的人員。這證實了最近的研究,即頻繁的審稿人對模塊的發展做出了巨大的貢獻,應與頻繁的編輯者一并納入。在Google中,實際上,找到合適的審稿人似乎沒有問題,實際上,實施推薦的模型很簡單,因為它可以以編程方式識別所有者。這與其他用于標識審閱者的提議的工具相反,審閱者已經審閱了具有相似名稱的文件或考慮了諸如審閱中包含的評論數量之類的功能。 Google的工作重點是處理審稿人的工作量和暫時缺勤(與Microsoft的研究一致)。

靜態分析集成。對88位Mozilla開發人員進行的定性研究發現,靜態分析集成是代碼審查最常用的功能。 自動進行的分析使審閱者可以專注于更改的可理解性和可維護性,而不會因為瑣碎的評論(例如關于格式)而分心。 我們在Google的調查向我們展示了在代碼審閱工具中進行靜態分析集成的實際含義。CRITIQUE為分析作者集成了反饋渠道:審閱者可以選擇在分析生成的評論上單擊“請修復”,以表示作者應修復該問題,作者或審稿人都可以單擊“無用”以標記對審閱過程無用的分析結果。具有較高“無用”點擊率的分析儀已固定或禁用。我們發現,這種反饋循環對于維持開發人員對分析結果的信任至關重要。

協作審查之外的審查工具。最后,我們找到了有力的證據,表明CRITIQUE的使用超出了審查代碼。 變更作者使用CRITIQUE來檢查差異并瀏覽分析工具的結果。 在某些情況下,代碼審查是變更開發過程的一部分:一個審查者可能會發送未完成的變更,以便決定如何完成實施。此外,開發人員還使用CRITIQUE來檢查提交的更改的歷史,只要這些更改被批準即可。 這與Sutherland和Venolia設想的將代碼審查數據用于開發的有益用法相一致。 將來的工作可以調查代碼審查工具的這些意外的和潛在的有影響的非審查使用。

7.3 知識傳播

知識轉移是Rigby和Bird提出的主題。 為了衡量由于代碼審查而導致的知識轉移,他們從先驗工作的角度出發,通過測量變更,審查和兩個集合的不同文件數來衡量專業知識,以更改的文件數為依據 。他們發現開發人員通過代碼審查了解更多文件。

在Google,知識轉移是代碼審查教育動機的一部分。 我們試圖通過查看評論和編輯/審閱的文件來量化此效果。 隨著開發人員積累了在Google工作的經驗,他們對其更改發表的評論平均減少了(圖2)。在過去一年內開始工作的Google開發人員,每次更改的注釋通常多于兩倍。先前的工作發現,作者認為來自審閱者的注釋無用,而無用注釋的數目則隨著經驗的增加而減少。 我們假設評論的減少是由于審閱者在使用代碼庫建立譜系關系時需要詢問較少的問題的結果,并佐證了代碼審閱的教育方面可能隨著時間的流逝而得到回報的假說。此外,我們可以看到, 由Google的工程師編輯和審查的文件,以及這兩個集合的結合,隨著資歷的增加而增加(圖3),并且看到的文件總數明顯大于編輯的文件數。 在公司工作(以3個月為增量),然后計算他們已編輯和審閱的文件數。在以后的工作中,更好地了解審閱文件如何影響開發人員的流利性將是很有意思的。

圖2. 審查者的評論和開發者在Google的任職年限

圖3. 全職員工隨時間查看(編輯或審閱,或兩者都有)的不同文件的數量。

8.結論

我們的研究發現,代碼審查是Google開發工作流程的重要方面。擔任所有職務的開發人員都將其視為提供多種好處的環境,并且在此上下文中,開發人員可以相互學習代碼庫,維護團隊代碼庫的完整性以及搭建,建立和發展確保代碼庫可讀性和一致性的規范。開發人員報告說,他們對審查代碼的要求感到滿意。大部分更改很小,只有一名審閱者,除了提交授權外沒有其他評論。在一周中,有70%的更改在郵寄出去進行初審后不到24小時內就會提交。這些特征使代碼審查比其他采用類似過程的項目更輕便。此外,我們發現Google在其實踐中包含了一些研究思想,從而使當前研究趨勢的實踐意義顯而易見。

原文地址:https://dl.acm.org/doi/10.114...

查看原文

贊 11 收藏 4 評論 0

LeapFE 發布了文章 · 2020-10-30

使用狄克斯特拉算法求地鐵最少用時路徑

什么是“圖”

?

圖”由節點和邊組成。上面的地鐵線路圖中從“芍藥居”出發到“太陽宮”需要3分種可以用下“圖”表示?!皥D”中描述了“A”和“B”互為鄰節點,其中3代表從節點“A”到“B”那條邊的權重,邊有權重的圖稱為“加權圖”,不帶權重的圖稱為“非加權圖”。邊上的剪頭代表只能從A到B且需要的成本為3,這種邊代有方向的圖稱為“有向圖”。

??

如果“A”能到“B”同時“B”也可以到”A”且成本同樣為3則稱為“無向圖”

??

如果存在節“C”使得“A”到 “B”,“B”可以到“C”,“C”又可以到“A”則稱“A”、“B”、“C”為一個“環”。 無向圖中每一條邊最可看為一個環。

狄克斯特拉算法

  1. 狄克斯特拉算法的試用范圍
  • 它用于計算加權圖中的最短路徑
  • 只適用于有向無環圖,(算法中會屏蔽環路)??
  • 不能將它用于包含負權邊(邊的權重為負值)的圖

2.算法流程

  1. 算法舉例

有如下“有向加權圖”, 我們要從“起點”出發到“終點”。

首先需要四個表,用于存儲相關信息。

表一: 用于存儲“圖”信息?“圖”信息

表二: 用于存儲每結點從起點出發的最小成本, 開始時只有“起點”成本為0

表三:最小開銷路徑上每結點的父結點

表四:記錄結點處理狀態

算法流程如下:

1)??????從表二及表四中找出最小開銷的未處理節點,開始時只有“起點”

2)??????從表一中看到從起點出發可以到達A和B開銷分別為5和3,更新表二

??

3)??????更新表三記錄當前到達A、B點的最小開銷父結點為起點

??

4)??????更新表四記錄已處理過起點,完成對一個節點的處理

??

5)??????(第二輪)從表二及表四中找出未處理過的最小開銷的節點“B”(到達成本3)

6)??????從表一中看到從B出發可以到達節點A和終點開銷分別為1和4

  • 由于從B到A的開銷1加B的當前的最小到達開銷3小于表二中A現有的最小開銷5所以更新表二A的最小開銷為4并更新表三中A節點的最小到達開銷父節點為B。
  • 在表二中添加“終點”開銷為7 (B到達開銷3加B到終點開銷4)
  • 表三中添加終點父結點為B

??

??

7)??????記錄B點已處理過

??

8)??????(第三輪)從表二及表四中找出未處理過的最小開銷的節點“A”

9)??????從點A出表可到達終點,點A當前最小到達成本為4 加上A到終點的開銷1小于表二中終點當前的最小開銷,所以更新表二中終點的開銷為5 并更新表三中終點父節點為A?

??

10)??????記錄A點已處理

??

11)??????(第四輪) 從表二及表四中找出未處理過的最小開銷的節點:“終點“

12)??????由于終點無指向結點無需再處理,支接標記已處理完成終點

13)??????(第五輪)已無未處理結點完成操作

14)??????最終結果

從表二中我們知道終點的最小到達開銷為5

從表三中我們可以從終點的父結點一路推出最小開銷路徑為: 終點 < A < B < 起點

4.代碼實現(TypeScript)

/** * 狄克斯特拉查找結果 */
export interface DijkstraFindResult<T_node> {      
  /** 差找的圖 */      
  graph: Map<T_node, Map<T_node, number>>;      
  /** 開始節點 */      
  startNode: T_node;      
  /** 結束節點 */      
  endNode: T_node;      
  /** 是否找到 */      
  isFind: boolean;      
  /** 最小成本路徑節點鏈*/      
  parents: Map<T_node, T_node>;      
  /** 結果路徑 */      
  path: T_node[];      
  /** 每節點最小到達成本 */      
  arriveCosts: Map<T_node, number>;
}

/** 
* 查找未處理過的最小成本節點 
* @param costs key:節點信息, value:當前到達成本 
* @param processed key:節點信息 value: 是否已處理過 
*/
function findMinCostNode<T_node>(  costs: Map<T_node, number>,  processed: Map<T_node, boolean>): T_node | null {  
  var minCost: number = Number.MAX_VALUE;  
  var minCostNode: T_node | null = null;  
  for (const [node, cost] of costs) {    
      if (cost < minCost && !processed.get(node)) {      
          minCost = cost;      
          minCostNode = node;    
      }  
  }  
  return minCostNode;
}
/** 
* 返回從開始節點到結束節點路徑 
* @param endNode 結束節點 
* @param parents key:節點A  value:節點A父節點 
*/
function getPath<T_node>(  endNode: T_node,  parents: Map<T_node, T_node>): T_node[] {  
  let path = [endNode];  
  let nParent = parents.get(endNode);  
  while (nParent) {    
      path.push(nParent);    
      nParent = parents.get(nParent);  
  }  
  path.reverse();  
  return path;
}




/** 
* 狄克斯特拉查找(找出成本最短路徑) 
* - 用于加權(無負權邊)有向圖無環圖 
* @param graph 要查找的"圖", Map<節點 ,Map<相鄰節點,到達成本>> 
* @param startNode 開始節點 
* @param endNode 結束節點 
*/
export function dijkstraFind<T_node>(  
  graph: Map<T_node, Map<T_node, number>>,  
  startNode: T_node,  
  endNode: T_node): DijkstraFindResult<T_node> {  
  /** 到節點最小成本 * k:節點 * v:從出發點到節點最小成本 */  
  let arriveCosts: Map<T_node, number> = new Map();  
  /** 最小成本路徑父節點 k:節點A v: 節點A在最小成本路徑上的父節點 */  
  let parents: Map<T_node, T_node> = new Map();  
  /** 已處理節點  k: 節點  v: 是否已處理過 */  
  let processedNode: Map<T_node, boolean> = new Map();  
  // 設置起點成本為零  
  arriveCosts.set(startNode, 0);  
  // 當前節點  
  let currentNode: T_node | null = startNode;  
  // 當前節點到達成本  
  let currentNodeCost: number = 0;  
  // 當前節點鄰節點  
  let neighbors: Map<T_node, number>;  
  let isFind: boolean = false;  
  while (currentNode) {    
      // 標記是否找到目標結點    
     if (currentNode === endNode) isFind = true;    
     // 這里costs中一定會有node對映值所以強制轉型成number    
     currentNodeCost = <number>arriveCosts.get(currentNode);    
     neighbors = graph.get(currentNode) || new Map();    
     //遍歷鄰節點更新最小成本    
     for (const [neighborNode, neighborCost] of neighbors) {      
         // 鄰節點之前算出的最小到達成本      
         let tmpPrevMinCost = arriveCosts.get(neighborNode);      
         let prevCost: number =  tmpPrevMinCost === undefined ? Number.MAX_VALUE : tmpPrevMinCost;      
         // 鄰節點經過當前節點的成本      
         let newCost = currentNodeCost + neighborCost;      
         // 如果經當前結點成本更小,更新成本記錄及鄰節點最小成本路徑父結點      
         if (newCost < prevCost) {        
             arriveCosts.set(neighborNode, newCost);        
             parents.set(neighborNode, <T_node>currentNode);      
         }    
     }    
     // 記錄已處理結點    
     processedNode.set(<T_node>currentNode, true);    
     // 找出下一個未處理的可到達最小成本結點    
     currentNode = findMinCostNode(arriveCosts, processedNode);  
 }  
 // 從起始點到終點路徑  
 let path: T_node[] = [];  
 if (isFind) {    
     path = getPath(endNode, parents);  
 }  
 return {    
     isFind: isFind,    
     path: path,    
     graph: graph,    
     arriveCosts: arriveCosts,    
     parents: parents,    
     startNode: startNode,    
     endNode,  
 };
} //eof dijkstraFind


// 測試

function objToMap(obj: any): Map<string, number> {  
  let map: Map<string, number> = new Map();  
  for (let k in obj) {    
      map.set(k, obj[k]);  
  }  
  return map;
}

/** 圖 */
const graph: Map<string, Map<string, number>> = new Map();
graph.set("start", objToMap({ a: 5, b: 3 }));
graph.set("a", objToMap({ end: 1 }));
graph.set("b", objToMap({ a: 1, end: 4 }));
graph.set("end", new Map());

let result = dijkstraFind(graph, "start", "end");
console.log(result);

// 輸出
/*
{  
  isFind: true,  
  path: [ 'start', 'b', 'a', 'end' ],  
  graph: Map {    
      'start' => Map { 'a' => 5, 'b' => 3 },    
      'a' => Map { 'start' => 5, 'end' => 1, 'b' => 1 },    
      'b' => Map { 'start' => 3, 'end' => 4, 'a' => 1 },    
      'end' => Map { 'a' => 1, 'b' => 4 }  
  },  
  arriveCosts: Map { 
      'start' => 0, 
      'a' => 4, 
      'b' => 3, 
      'end' => 5 
  },  
  parents: Map { 
      'a' => 'b', 
      'b' => 'start', 
      'end' => 'a' 
  },  
  startNode: 'start',  
  endNode: 'end'
}

*/

求地鐵兩站間最小用時路徑

把上例中的“圖”看成一個地換線路圖:現在我們要人A站到D站

??

將狄克斯特拉算法應用于地鐵圖對比上面的例子有幾個問題.

問題1:????? 地鐵為一個無向圖,如A可以到B,B也可以到A ,所以描述圖信息時雙向的圖信息都 要錄入,如:

??

問題2:圖中第條邊都是一個環,且如A,B,C也可組成一個環是否會對結果產生影響?

不會,因為算法中每次選出的處理節點都是到達成本最小的節點,只有從這個節出發到下一個節點成本更底時才會更新最小成本表和父節點表,且處理過的結點不會再次處理。

問題3: 如何處理換乘線路用時問題?

如:1號線換5號線需要2分種, 5號線換2號線要1分鐘。

上圖中我們可以看出不考慮換乘從A到D的最少用時路徑為:

A > B > C > D

如果算上換乘線路時間最短用時路徑為: ?????

A > C > D

那么如何處理呢?我們可以把換乘站內的換乘路徑看成一個局部的圖并將其嵌入地鐵圖中,如:

?

上圖中B結點由 B_A,B_D, ?B_C 三個結點代替。其中 B_A到B_C,B_D 到B_C 權重相同(也可以不同)代表從1號線換5號線用時2分鐘,B_A到B_D權重為0代表從A經由B到D不需要換乘。將上圖作為新的算法輸入數據就可算出考慮換乘用時的最少用時路徑。

參考:

《算法圖解》【美】Aditya Dhargava

注:

狄克斯特拉算法部分主要參考算法圖解

查看原文

贊 8 收藏 7 評論 0

LeapFE 發布了文章 · 2020-10-30

使用QuickType工具從json自動生成類型聲明代碼

一、QuickType 工具功能簡介

QuickType 是一款可以根據 json 文本生成指定語言(如 Type Script,C++,,Java,C#,Go 等)類型聲明代碼的工具。

例如我們在寫接口調用處理收到響應數據的邏輯時一般分為如下兩步: 1.根據接口返回的 JSON 格式寫一個對應的類型 2.寫 JSON 格式驗證與解析邏輯來根據收到的數據生成對應的類對象

使用 QuickType 工具就可以根據 JSON 文本幫助我們自動生成以上兩部分的代碼。
以如下 JSON 為例:
json代碼

使用 QuickType 生成 TypeScript 語言的接口聲明代碼如下:

生成TypeScript

二、QuickType 工具的使用

可以通過桌面應用、web 頁、 IDE 插件、命令行 4 種方式使用 QuickType 工具。
其中 web 頁(https://app.quicktype.io/ 可能被墻)和桌面應用使用方式基本一致這里不做介紹。

1.桌面應用方式(僅 mac OS)

打開 App Store 搜索 “Paste JSON as Code”下載安裝即可
圖片.png

軟件使用很簡單,軟件時時生成目標代碼,按如下步驟操作:

  • 在左側選擇原始數據的類型
  • 輸入原始數據
  • 修改要生成的類名,
  • 在右側選擇要生成的目標語言,并進行配置(每種語言的可配置項不同)

圖片.png

1.以 IDE 擴展方式使用

QuickType 提供了 Xcode,VSCode, Visual Studio 三種開發工具的擴展。下載地址如下:

下面以 VSCode 擴展的安裝與使用為例

2.1 安裝 vscode 擴展 Paste JSON as Code

  • 打開 Visual Studio Code 軟件進入擴展商店
  • 搜索 Paste JSON as Code
  • 點擊 install 進行安裝

圖片.png

2.2 在 VSCode 中使用 Paste JSON as Code 擴展

vscode 中 Paste JSON 有兩種使用方式。

方式 1: 將剪切板中的 JSON 內容直接生成目標代碼插入到當前編輯文件中,流程如下:

  • 選擇并拷貝【control(win)/command(mac)+c】要生成目標代碼的 JSON 文本
  • 打開要插入類型聲明代碼的文件,用鼠標點擊要插入代碼的位置(擴展會自動根據文件擴展名決定生成目標代碼的語言)
  • 打開 VSCode 命令框【按 control(win)/command(mac) + shift +p】輸入 “>Paste JSON as Types”后回車
  • 根據提示輸入要生成的類型名稱,回車后會在當前文檔插入聲明代碼。
    如下圖:

圖片.png

方式 2:編輯 JSON 文件時時生成類型聲明文件,流程如下:

  • 在 vscode 打開 json 文本文件
  • 打開 VSCode 命令框【按 control(win)/command(mac) + shift +p】輸入 “>Open quicktype for JSON”
  • 編輯區域會顯示一個名為 QuickType.xx 的目標語言文件,文件內容會隨著你對 json 文件的編輯跟新。(默認生成代碼語言可能不是你想要的)
  • 設置目標語言類型:打開 VSCode 命令框【按 control(win)/command(mac) + shift +p】輸入 “>Set quicktype target language”
  • 在打開的下拉列表中選擇生成代碼語言完成語言切換

圖片.png

3.以命令行方式使用

安裝流程:

  • 安裝 node 環境
  • 全局安裝 quicktype npm 包
    命令行下輸入: npm install –g quicktype

quicktype 命令

# 查看幫助
quicktype

# json字符串生成C# 聲明
echo '{ "name": "David" }' | quicktype -l csharp

# json字符串生成Go類聲名文件 ints.go
echo '[1, 2, 3]' | quicktype -o ints.go

# 從json文件生成swift類文件
quicktype person.json -o Person.swift

# 可選參數
quicktype \
  --src person.json \
  --src-lang json \
  --lang swift \
  --top-level Person \
  --out Person.swift

# 從返回 JSON 的接口生成 java類文件
quicktype https://api.somewhere.com/data -o Data.java
查看原文

贊 3 收藏 2 評論 0

LeapFE 發布了文章 · 2020-10-28

webpack、gulp、rollup、tsc/babel 使用對比

本文檔主要介紹四種工具的特點, 包括優點、缺點、 輸入、輸出、能夠處理的文件類型,針對不同文件類型的處理方式, 以及其適用場景。

Rollup

簡介

Rollup 是一個模塊打包工具, 可以將我們按照 ESM (ES2015 Module) 規范編寫的源碼構建輸出如下格式:

  • IIFE: 自執行函數, 可通過 <script> 標簽加載
  • AMD: 通過 RequireJS 加載
  • CommonJS: Node 默認的模塊規范, 可通過 Webpack 加載
  • UMD: 兼容 IIFE, AMD, CJS 三種模塊規范
  • ESM: ES2015 Module 規范, 可用 Webpack, Rollup 加載

優點:

支持動態導入。

支持tree shaking。僅加載模塊里用得到的函數以減小文件大小。

Scope Hoisting。?rollup可以將所有小文件生成到一個大文件中,所有代碼都在同一個函數作用域里:,?不會像 Webpack 那樣用很多函數來包裝模塊。

沒有其他冗余代碼, 執行很快。除了必要的 cjs, umd 頭外,bundle 代碼基本和源碼差不多,也沒有奇怪的 __webpack_require__, Object.defineProperty 之類的東西,

缺點:

不支持熱更新功能;對于commonjs模塊,需要額外的插件將其轉化為es2015供rollup 處理;無法進行公共代碼拆分。

輸入:

options.input? 單/多文件入口點

輸出:

rollup支持生成 iife、cjs、amd 、esm、umd格式的文件; 單/多js文件輸出

文件資源處理:?

rollup 通過插件來編譯處理各類靜態資源:

  • rollup-plugin-typescript2
  • rollup-plugin-babel
  • rollup-plugin-uglify
  • rollup-plugin-commonjs
  • rollup-plugin-postcss
  • rollup-plugin-img
  • rollup-plugin-json

基本使用參考

?https://www.cnblogs.com/tugenhua0707/p/8179686.html

適用場景:

由純js開發的第三方庫; 需要生成單一的umd文件的場景

案例:

純js/ts編寫的第三方庫:

React、Vue

UI組件庫?evergreen

使用 babel 將 js/ts 編譯成? esm 和 cjs 格式的模塊文件, 使用 rollup 將庫打包成? umd 格式的?evergreen.min.js 和?evergreen.js ,? 打包出來的代碼比較干凈。

gulp

簡介

前端構建工具,gulp是基于Nodejs,自動化地完成 javascript、coffee、sass、less、html/image、css 等文件的測試、檢查、合并、壓縮、格式化、瀏覽器自動刷新、部署文件生成,并監聽文件在改動后重復指定的這些步驟。

借鑒了Unix操作系統的管道(pipe)思想,前一級的輸出,直接變成后一級的輸入,使得在操作上非常簡單。

gulp基于流式操作,通過各種 Transform Stream 來實現文件不斷處理 輸出。

優點:

gulp文檔簡單,學習成本低,使用簡單;對大量源文件可以進行流式處理,借助插件,可以對文件類型進行多種操作處理。

缺點

不支持tree-shaking、熱更新、代碼分割等。 gulp 對 js 模塊化方案無能為力,只是對靜態資源做流式處理,處理之后并未做有效的優化整合。

輸入:

輸入(gulp.src) js,ts,scss,less 等源文件

輸出:

對輸入源文件依次執行打包(bundle)、編譯(compile)、壓縮、重命名等處理后輸出(gulp.dest)到指定目錄中去

適用場景:

靜態資源密集操作型場景,主要用于css、圖片等靜態資源的處理操作。

文件處理:

gulp通過各種中間件處理靜態資源的編譯:

案例:

antd

gulp + webpack + tsc / babel

gulp的作用主要是打包流程管理, 拷貝文件(less/ts/ts類型聲明文件),處理less, 拷貝并轉譯less 為css。

tsc及babel 則用于轉譯 靜態ts文件, 逐個輸出到指定目錄es/lib目錄下

webpack主要用于模塊化處理,將打包后的模塊編譯到 dist下的? antd.js? ?antd.min.js 以及及其他css文件等。

Webpack

簡介:

Webpack 是一種前端資源模塊化管理和打包 工具。它可以將許多松散的模塊按照依賴和規則打包成符合生產環境部署的前端資源。還可以將按需加載的模塊進行代碼分割,等到實際需要的時候再異步加載。

優點:

基本之前gulp 可以進行的操作處理,現在webpack也都可以做。同時支持熱更新,支持tree shaking 、Scope Hoisting、動態加載、代碼拆分、文件指紋、代碼壓縮、靜態資源處理等,支持多種打包方式。(優點有很多,在這不做過多贅述)

缺點:

不支持 打包出esm格式的代碼 (打包后的代碼再次被引用時tree shaking 困難), 打包后亢余代碼多,配置較為復雜。

輸入:

入口文件 js/ts

輸出

js、css、 img等靜態資源文件

適用場景:

應用程序開發

案例:

react-bootstrap

react-bootstrap 使用babel進行tsx文件的編譯,并且按照原有目錄輸出到 lib esm/cjs目錄下;

同時使用shell 工具 拷貝 TS類型聲明文件 到對應目錄;

對于umd文件,則采用webpack打包生成了? react-bootstrap.min.js 及 react-bootstrap.js 輸出到dist下。

打包umd方式非常簡單,但文件中保留了許多webpack使用的到的冗余代碼。生成效果不如上述 的?evergreen?純凈。

tsc / babel

簡介

tsc/babel 可以將 ts 代碼編譯 js 代碼。支持編譯成 esm、cjs、amd 格式的文件

優點:

編譯速度快,可以保留原有的目錄相對位置,分目錄保存各模塊的代碼,便于按需引用加載;

缺點:

只對語言本身進行編譯轉換,不支持tree shaking 等高級功能。

輸入:

ts/js 文件

輸出:

ts/ts對應的js文件,且一一對應

案例分析:

tsc/babel常與其他工具配合使用

打包使用方式推薦

第三方js類庫:

  1. ?rollup + 插件 (推薦)
  2. ?babel/tsc + uglifyjs
  3. ?webpack

UI類庫開發(按需加載)

生成esm?? tsc/babel ?+ gulp

生成cjs? ? ?tsc/babel + gulp

生成umd? ?rollup (js + css的合并文件)

開發應用程序

webpack + loader + plugin

上述打包方式各有其特點,根據當前需求及開發便利,酌情選擇打包編譯方式。

查看原文

贊 4 收藏 4 評論 0

LeapFE 贊了文章 · 2020-09-17

「1.8W字」一份不可多得的 TS 學習指南

阿寶哥第一次使用 TypeScript 是在 Angular 2.x 項目中,那時候 TypeScript 還沒有進入大眾的視野。然而現在學習 TypeScript 的小伙伴越來越多了,本文阿寶哥將從 16 個方面入手,帶你一步步學習 TypeScript,感興趣的小伙伴不要錯過。

image

一、TypeScript 是什么

TypeScript 是一種由微軟開發的自由和開源的編程語言。它是 JavaScript 的一個超集,而且本質上向這個語言添加了可選的靜態類型和基于類的面向對象編程。

TypeScript 提供最新的和不斷發展的 JavaScript 特性,包括那些來自 2015 年的 ECMAScript 和未來的提案中的特性,比如異步功能和 Decorators,以幫助建立健壯的組件。下圖顯示了 TypeScript 與 ES5、ES2015 和 ES2016 之間的關系:

1.1 TypeScript 與 JavaScript 的區別

TypeScriptJavaScript
JavaScript 的超集用于解決大型項目的代碼復雜性一種腳本語言,用于創建動態網頁
可以在編譯期間發現并糾正錯誤作為一種解釋型語言,只能在運行時發現錯誤
強類型,支持靜態和動態類型弱類型,沒有靜態類型選項
最終被編譯成 JavaScript 代碼,使瀏覽器可以理解可以直接在瀏覽器中使用
支持模塊、泛型和接口不支持模塊,泛型或接口
社區的支持仍在增長,而且還不是很大大量的社區支持以及大量文檔和解決問題的支持

1.2 獲取 TypeScript

命令行的 TypeScript 編譯器可以使用 npm 包管理器來安裝。

1.安裝 TypeScript
$ npm install -g typescript
2.驗證 TypeScript
$ tsc -v 
# Version 4.0.2
3.編譯 TypeScript 文件
$ tsc helloworld.ts
# helloworld.ts => helloworld.js

當然,對剛入門 TypeScript 的小伙伴來說,也可以不用安裝 typescript,而是直接使用線上的 TypeScript Playground 來學習新的語法或新特性。通過配置 TS Config 的 Target,可以設置不同的編譯目標,從而編譯生成不同的目標代碼。

下圖示例中所設置的編譯目標是 ES5:

(圖片來源:https://www.typescriptlang.or...

1.3 典型 TypeScript 工作流程

如你所見,在上圖中包含 3 個 ts 文件:a.ts、b.ts 和 c.ts。這些文件將被 TypeScript 編譯器,根據配置的編譯選項編譯成 3 個 js 文件,即 a.js、b.js 和 c.js。對于大多數使用 TypeScript 開發的 Web 項目,我們還會對編譯生成的 js 文件進行打包處理,然后在進行部署。

1.4 TypeScript 初體驗

新建一個 hello.ts 文件,并輸入以下內容:

function greet(person: string) {
  return 'Hello, ' + person;
}

console.log(greet("TypeScript"));

然后執行 tsc hello.ts 命令,之后會生成一個編譯好的文件 hello.js

"use strict";
function greet(person) {
  return 'Hello, ' + person;
}
console.log(greet("TypeScript"));

觀察以上編譯后的輸出結果,我們發現 person 參數的類型信息在編譯后被擦除了。TypeScript 只會在編譯階段對類型進行靜態檢查,如果發現有錯誤,編譯時就會報錯。而在運行時,編譯生成的 JS 與普通的 JavaScript 文件一樣,并不會進行類型檢查。

二、TypeScript 基礎類型

2.1 Boolean 類型

let isDone: boolean = false;
// ES5:var isDone = false;

2.2 Number 類型

let count: number = 10;
// ES5:var count = 10;

2.3 String 類型

let name: string = "semliker";
// ES5:var name = 'semlinker';

2.4 Symbol 類型

const sym = Symbol();
let obj = {
  [sym]: "semlinker",
};

console.log(obj[sym]); // semlinker 

2.5 Array 類型

let list: number[] = [1, 2, 3];
// ES5:var list = [1,2,3];

let list: Array<number> = [1, 2, 3]; // Array<number>泛型語法
// ES5:var list = [1,2,3];

2.6 Enum 類型

使用枚舉我們可以定義一些帶名字的常量。 使用枚舉可以清晰地表達意圖或創建一組有區別的用例。 TypeScript 支持數字的和基于字符串的枚舉。

1.數字枚舉
enum Direction {
  NORTH,
  SOUTH,
  EAST,
  WEST,
}

let dir: Direction = Direction.NORTH;

默認情況下,NORTH 的初始值為 0,其余的成員會從 1 開始自動增長。換句話說,Direction.SOUTH 的值為 1,Direction.EAST 的值為 2,Direction.WEST 的值為 3。

以上的枚舉示例經編譯后,對應的 ES5 代碼如下:

"use strict";
var Direction;
(function (Direction) {
  Direction[(Direction["NORTH"] = 0)] = "NORTH";
  Direction[(Direction["SOUTH"] = 1)] = "SOUTH";
  Direction[(Direction["EAST"] = 2)] = "EAST";
  Direction[(Direction["WEST"] = 3)] = "WEST";
})(Direction || (Direction = {}));
var dir = Direction.NORTH;

當然我們也可以設置 NORTH 的初始值,比如:

enum Direction {
  NORTH = 3,
  SOUTH,
  EAST,
  WEST,
}
2.字符串枚舉

在 TypeScript 2.4 版本,允許我們使用字符串枚舉。在一個字符串枚舉里,每個成員都必須用字符串字面量,或另外一個字符串枚舉成員進行初始化。

enum Direction {
  NORTH = "NORTH",
  SOUTH = "SOUTH",
  EAST = "EAST",
  WEST = "WEST",
}

以上代碼對應的 ES5 代碼如下:

"use strict";
var Direction;
(function (Direction) {
    Direction["NORTH"] = "NORTH";
    Direction["SOUTH"] = "SOUTH";
    Direction["EAST"] = "EAST";
    Direction["WEST"] = "WEST";
})(Direction || (Direction = {}));

通過觀察數字枚舉和字符串枚舉的編譯結果,我們可以知道數字枚舉除了支持 從成員名稱到成員值 的普通映射之外,它還支持 從成員值到成員名稱 的反向映射:

enum Direction {
  NORTH,
  SOUTH,
  EAST,
  WEST,
}

let dirName = Direction[0]; // NORTH
let dirVal = Direction["NORTH"]; // 0

另外,對于純字符串枚舉,我們不能省略任何初始化程序。而數字枚舉如果沒有顯式設置值時,則會使用默認規則進行初始化。

3.常量枚舉

除了數字枚舉和字符串枚舉之外,還有一種特殊的枚舉 —— 常量枚舉。它是使用 const 關鍵字修飾的枚舉,常量枚舉會使用內聯語法,不會為枚舉類型編譯生成任何 JavaScript。為了更好地理解這句話,我們來看一個具體的例子:

const enum Direction {
  NORTH,
  SOUTH,
  EAST,
  WEST,
}

let dir: Direction = Direction.NORTH;

以上代碼對應的 ES5 代碼如下:

"use strict";
var dir = 0 /* NORTH */;
4.異構枚舉

異構枚舉的成員值是數字和字符串的混合:

enum Enum {
  A,
  B,
  C = "C",
  D = "D",
  E = 8,
  F,
}

以上代碼對于的 ES5 代碼如下:

"use strict";
var Enum;
(function (Enum) {
    Enum[Enum["A"] = 0] = "A";
    Enum[Enum["B"] = 1] = "B";
    Enum["C"] = "C";
    Enum["D"] = "D";
    Enum[Enum["E"] = 8] = "E";
    Enum[Enum["F"] = 9] = "F";
})(Enum || (Enum = {}));

通過觀察上述生成的 ES5 代碼,我們可以發現數字枚舉相對字符串枚舉多了 “反向映射”:

console.log(Enum.A) //輸出:0
console.log(Enum[0]) // 輸出:A

2.7 Any 類型

在 TypeScript 中,任何類型都可以被歸為 any 類型。這讓 any 類型成為了類型系統的頂級類型(也被稱作全局超級類型)。

let notSure: any = 666;
notSure = "semlinker";
notSure = false;

any 類型本質上是類型系統的一個逃逸艙。作為開發者,這給了我們很大的自由:TypeScript 允許我們對 any 類型的值執行任何操作,而無需事先執行任何形式的檢查。比如:

let value: any;

value.foo.bar; // OK
value.trim(); // OK
value(); // OK
new value(); // OK
value[0][1]; // OK

在許多場景下,這太寬松了。使用 any 類型,可以很容易地編寫類型正確但在運行時有問題的代碼。如果我們使用 any 類型,就無法使用 TypeScript 提供的大量的保護機制。為了解決 any 帶來的問題,TypeScript 3.0 引入了 unknown 類型。

2.8 Unknown 類型

就像所有類型都可以賦值給 any,所有類型也都可以賦值給 unknown。這使得 unknown 成為 TypeScript 類型系統的另一種頂級類型(另一種是 any)。下面我們來看一下 unknown 類型的使用示例:

let value: unknown;

value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = null; // OK
value = undefined; // OK
value = new TypeError(); // OK
value = Symbol("type"); // OK

value 變量的所有賦值都被認為是類型正確的。但是,當我們嘗試將類型為 unknown 的值賦值給其他類型的變量時會發生什么?

let value: unknown;

let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
let value5: string = value; // Error
let value6: object = value; // Error
let value7: any[] = value; // Error
let value8: Function = value; // Error

unknown 類型只能被賦值給 any 類型和 unknown 類型本身。直觀地說,這是有道理的:只有能夠保存任意類型值的容器才能保存 unknown 類型的值。畢竟我們不知道變量 value 中存儲了什么類型的值。

現在讓我們看看當我們嘗試對類型為 unknown 的值執行操作時會發生什么。以下是我們在之前 any 章節看過的相同操作:

let value: unknown;

value.foo.bar; // Error
value.trim(); // Error
value(); // Error
new value(); // Error
value[0][1]; // Error

value 變量類型設置為 unknown 后,這些操作都不再被認為是類型正確的。通過將 any 類型改變為 unknown 類型,我們已將允許所有更改的默認設置,更改為禁止任何更改。

2.9 Tuple 類型

眾所周知,數組一般由同種類型的值組成,但有時我們需要在單個變量中存儲不同類型的值,這時候我們就可以使用元組。在 JavaScript 中是沒有元組的,元組是 TypeScript 中特有的類型,其工作方式類似于數組。

元組可用于定義具有有限數量的未命名屬性的類型。每個屬性都有一個關聯的類型。使用元組時,必須提供每個屬性的值。為了更直觀地理解元組的概念,我們來看一個具體的例子:

let tupleType: [string, boolean];
tupleType = ["semlinker", true];

在上面代碼中,我們定義了一個名為 tupleType 的變量,它的類型是一個類型數組 [string, boolean],然后我們按照正確的類型依次初始化 tupleType 變量。與數組一樣,我們可以通過下標來訪問元組中的元素:

console.log(tupleType[0]); // semlinker
console.log(tupleType[1]); // true

在元組初始化的時候,如果出現類型不匹配的話,比如:

tupleType = [true, "semlinker"];

此時,TypeScript 編譯器會提示以下錯誤信息:

[0]: Type 'true' is not assignable to type 'string'.
[1]: Type 'string' is not assignable to type 'boolean'.

很明顯是因為類型不匹配導致的。在元組初始化的時候,我們還必須提供每個屬性的值,不然也會出現錯誤,比如:

tupleType = ["semlinker"];

此時,TypeScript 編譯器會提示以下錯誤信息:

Property '1' is missing in type '[string]' but required in type '[string, boolean]'.

2.10 Void 類型

某種程度上來說,void 類型像是與 any 類型相反,它表示沒有任何類型。當一個函數沒有返回值時,你通常會見到其返回值類型是 void:

// 聲明函數返回值為void
function warnUser(): void {
  console.log("This is my warning message");
}

以上代碼編譯生成的 ES5 代碼如下:

"use strict";
function warnUser() {
  console.log("This is my warning message");
}

需要注意的是,聲明一個 void 類型的變量沒有什么作用,因為在嚴格模式下,它的值只能為 undefined

let unusable: void = undefined;

2.11 Null 和 Undefined 類型

TypeScript 里,undefinednull 兩者有各自的類型分別為 undefinednull。

let u: undefined = undefined;
let n: null = null;

2.12 object, Object 和 {} 類型

1.object 類型

object 類型是:TypeScript 2.2 引入的新類型,它用于表示非原始類型。

// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
  create(o: object | null): any;
  // ...
}

const proto = {};

Object.create(proto);     // OK
Object.create(null);      // OK
Object.create(undefined); // Error
Object.create(1337);      // Error
Object.create(true);      // Error
Object.create("oops");    // Error
2.Object 類型

Object 類型:它是所有 Object 類的實例的類型,它由以下兩個接口來定義:

  • Object 接口定義了 Object.prototype 原型對象上的屬性;
// node_modules/typescript/lib/lib.es5.d.ts
interface Object {
  constructor: Function;
  toString(): string;
  toLocaleString(): string;
  valueOf(): Object;
  hasOwnProperty(v: PropertyKey): boolean;
  isPrototypeOf(v: Object): boolean;
  propertyIsEnumerable(v: PropertyKey): boolean;
}
  • ObjectConstructor 接口定義了 Object 類的屬性。
// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
  /** Invocation via `new` */
  new(value?: any): Object;
  /** Invocation via function calls */
  (value?: any): any;
  readonly prototype: Object;
  getPrototypeOf(o: any): any;
  // ···
}

declare var Object: ObjectConstructor;

Object 類的所有實例都繼承了 Object 接口中的所有屬性。

3.{} 類型

{} 類型描述了一個沒有成員的對象。當你試圖訪問這樣一個對象的任意屬性時,TypeScript 會產生一個編譯時錯誤。

// Type {}
const obj = {};

// Error: Property 'prop' does not exist on type '{}'.
obj.prop = "semlinker";

但是,你仍然可以使用在 Object 類型上定義的所有屬性和方法,這些屬性和方法可通過 JavaScript 的原型鏈隱式地使用:

// Type {}
const obj = {};

// "[object Object]"
obj.toString();

2.13 Never 類型

never 類型表示的是那些永不存在的值的類型。 例如,never 類型是那些總是會拋出異?;蚋揪筒粫蟹祷刂档暮瘮当磉_式或箭頭函數表達式的返回值類型。

// 返回never的函數必須存在無法達到的終點
function error(message: string): never {
  throw new Error(message);
}

function infiniteLoop(): never {
  while (true) {}
}

在 TypeScript 中,可以利用 never 類型的特性來實現全面性檢查,具體示例如下:

type Foo = string | number;

function controlFlowAnalysisWithNever(foo: Foo) {
  if (typeof foo === "string") {
    // 這里 foo 被收窄為 string 類型
  } else if (typeof foo === "number") {
    // 這里 foo 被收窄為 number 類型
  } else {
    // foo 在這里是 never
    const check: never = foo;
  }
}

注意在 else 分支里面,我們把收窄為 never 的 foo 賦值給一個顯示聲明的 never 變量。如果一切邏輯正確,那么這里應該能夠編譯通過。但是假如后來有一天你的同事修改了 Foo 的類型:

type Foo = string | number | boolean;

然而他忘記同時修改 controlFlowAnalysisWithNever 方法中的控制流程,這時候 else 分支的 foo 類型會被收窄為 boolean 類型,導致無法賦值給 never 類型,這時就會產生一個編譯錯誤。通過這個方式,我們可以確保

controlFlowAnalysisWithNever 方法總是窮盡了 Foo 的所有可能類型。 通過這個示例,我們可以得出一個結論:使用 never 避免出現新增了聯合類型沒有對應的實現,目的就是寫出類型絕對安全的代碼。

三、TypeScript 斷言

3.1 類型斷言

有時候你會遇到這樣的情況,你會比 TypeScript 更了解某個值的詳細信息。通常這會發生在你清楚地知道一個實體具有比它現有類型更確切的類型。

通過類型斷言這種方式可以告訴編譯器,“相信我,我知道自己在干什么”。類型斷言好比其他語言里的類型轉換,但是不進行特殊的數據檢查和解構。它沒有運行時的影響,只是在編譯階段起作用。

類型斷言有兩種形式:

1.“尖括號” 語法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
2.as 語法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

3.2 非空斷言

在上下文中當類型檢查器無法斷定類型時,一個新的后綴表達式操作符 ! 可以用于斷言操作對象是非 null 和非 undefined 類型。具體而言,x! 將從 x 值域中排除 null 和 undefined 。

那么非空斷言操作符到底有什么用呢?下面我們先來看一下非空斷言操作符的一些使用場景。

1.忽略 undefined 和 null 類型
function myFunc(maybeString: string | undefined | null) {
  // Type 'string | null | undefined' is not assignable to type 'string'.
  // Type 'undefined' is not assignable to type 'string'. 
  const onlyString: string = maybeString; // Error
  const ignoreUndefinedAndNull: string = maybeString!; // Ok
}
2.調用函數時忽略 undefined 類型
type NumGenerator = () => number;

function myFunc(numGenerator: NumGenerator | undefined) {
  // Object is possibly 'undefined'.(2532)
  // Cannot invoke an object which is possibly 'undefined'.(2722)
  const num1 = numGenerator(); // Error
  const num2 = numGenerator!(); //OK
}

因為 ! 非空斷言操作符會從編譯生成的 JavaScript 代碼中移除,所以在實際使用的過程中,要特別注意。比如下面這個例子:

const a: number | undefined = undefined;
const b: number = a!;
console.log(b); 

以上 TS 代碼會編譯生成以下 ES5 代碼:

"use strict";
const a = undefined;
const b = a;
console.log(b);

雖然在 TS 代碼中,我們使用了非空斷言,使得 const b: number = a!; 語句可以通過 TypeScript 類型檢查器的檢查。但在生成的 ES5 代碼中,! 非空斷言操作符被移除了,所以在瀏覽器中執行以上代碼,在控制臺會輸出 undefined。

3.3 確定賦值斷言

在 TypeScript 2.7 版本中引入了確定賦值斷言,即允許在實例屬性和變量聲明后面放置一個 ! 號,從而告訴 TypeScript 該屬性會被明確地賦值。為了更好地理解它的作用,我們來看個具體的例子:

let x: number;
initialize();
// Variable 'x' is used before being assigned.(2454)
console.log(2 * x); // Error

function initialize() {
  x = 10;
}

很明顯該異常信息是說變量 x 在賦值前被使用了,要解決該問題,我們可以使用確定賦值斷言:

let x!: number;
initialize();
console.log(2 * x); // Ok

function initialize() {
  x = 10;
}

通過 let x!: number; 確定賦值斷言,TypeScript 編譯器就會知道該屬性會被明確地賦值。

四、類型守衛

類型保護是可執行運行時檢查的一種表達式,用于確保該類型在一定的范圍內。 換句話說,類型保護可以保證一個字符串是一個字符串,盡管它的值也可以是一個數值。類型保護與特性檢測并不是完全不同,其主要思想是嘗試檢測屬性、方法或原型,以確定如何處理值。目前主要有四種的方式來實現類型保護:

4.1 in 關鍵字

interface Admin {
  name: string;
  privileges: string[];
}

interface Employee {
  name: string;
  startDate: Date;
}

type UnknownEmployee = Employee | Admin;

function printEmployeeInformation(emp: UnknownEmployee) {
  console.log("Name: " + emp.name);
  if ("privileges" in emp) {
    console.log("Privileges: " + emp.privileges);
  }
  if ("startDate" in emp) {
    console.log("Start Date: " + emp.startDate);
  }
}

4.2 typeof 關鍵字

function padLeft(value: string, padding: string | number) {
  if (typeof padding === "number") {
      return Array(padding + 1).join(" ") + value;
  }
  if (typeof padding === "string") {
      return padding + value;
  }
  throw new Error(`Expected string or number, got '${padding}'.`);
}

typeof 類型保護只支持兩種形式:typeof v === "typename"typeof v !== typename,"typename" 必須是 "number", "string", "boolean""symbol"。 但是 TypeScript 并不會阻止你與其它字符串比較,語言不會把那些表達式識別為類型保護。

4.3 instanceof 關鍵字

interface Padder {
  getPaddingString(): string;
}

class SpaceRepeatingPadder implements Padder {
  constructor(private numSpaces: number) {}
  getPaddingString() {
    return Array(this.numSpaces + 1).join(" ");
  }
}

class StringPadder implements Padder {
  constructor(private value: string) {}
  getPaddingString() {
    return this.value;
  }
}

let padder: Padder = new SpaceRepeatingPadder(6);

if (padder instanceof SpaceRepeatingPadder) {
  // padder的類型收窄為 'SpaceRepeatingPadder'
}

4.4 自定義類型保護的類型謂詞

function isNumber(x: any): x is number {
  return typeof x === "number";
}

function isString(x: any): x is string {
  return typeof x === "string";
}

五、聯合類型和類型別名

5.1 聯合類型

聯合類型通常與 nullundefined 一起使用:

const sayHello = (name: string | undefined) => {
  /* ... */
};

例如,這里 name 的類型是 string | undefined 意味著可以將 stringundefined 的值傳遞給sayHello 函數。

sayHello("semlinker");
sayHello(undefined);

通過這個示例,你可以憑直覺知道類型 A 和類型 B 聯合后的類型是同時接受 A 和 B 值的類型。此外,對于聯合類型來說,你可能會遇到以下的用法:

let num: 1 | 2 = 1;
type EventNames = 'click' | 'scroll' | 'mousemove';

以上示例中的 1、2'click' 被稱為字面量類型,用來約束取值只能是某幾個值中的一個。

5.2 可辨識聯合

TypeScript 可辨識聯合(Discriminated Unions)類型,也稱為代數數據類型或標簽聯合類型。它包含 3 個要點:可辨識、聯合類型和類型守衛。

這種類型的本質是結合聯合類型和字面量類型的一種類型保護方法。如果一個類型是多個類型的聯合類型,且多個類型含有一個公共屬性,那么就可以利用這個公共屬性,來創建不同的類型保護區塊。

1.可辨識

可辨識要求聯合類型中的每個元素都含有一個單例類型屬性,比如:

enum CarTransmission {
  Automatic = 200,
  Manual = 300
}

interface Motorcycle {
  vType: "motorcycle"; // discriminant
  make: number; // year
}

interface Car {
  vType: "car"; // discriminant
  transmission: CarTransmission
}

interface Truck {
  vType: "truck"; // discriminant
  capacity: number; // in tons
}

在上述代碼中,我們分別定義了 Motorcycle、 CarTruck 三個接口,在這些接口中都包含一個 vType 屬性,該屬性被稱為可辨識的屬性,而其它的屬性只跟特性的接口相關。

2.聯合類型

基于前面定義了三個接口,我們可以創建一個 Vehicle 聯合類型:

type Vehicle = Motorcycle | Car | Truck;

現在我們就可以開始使用 Vehicle 聯合類型,對于 Vehicle 類型的變量,它可以表示不同類型的車輛。

3.類型守衛

下面我們來定義一個 evaluatePrice 方法,該方法用于根據車輛的類型、容量和評估因子來計算價格,具體實現如下:

const EVALUATION_FACTOR = Math.PI; 

function evaluatePrice(vehicle: Vehicle) {
  return vehicle.capacity * EVALUATION_FACTOR;
}

const myTruck: Truck = { vType: "truck", capacity: 9.5 };
evaluatePrice(myTruck);

對于以上代碼,TypeScript 編譯器將會提示以下錯誤信息:

Property 'capacity' does not exist on type 'Vehicle'.
Property 'capacity' does not exist on type 'Motorcycle'.

原因是在 Motorcycle 接口中,并不存在 capacity 屬性,而對于 Car 接口來說,它也不存在 capacity 屬性。那么,現在我們應該如何解決以上問題呢?這時,我們可以使用類型守衛。下面我們來重構一下前面定義的 evaluatePrice 方法,重構后的代碼如下:

function evaluatePrice(vehicle: Vehicle) {
  switch(vehicle.vType) {
    case "car":
      return vehicle.transmission * EVALUATION_FACTOR;
    case "truck":
      return vehicle.capacity * EVALUATION_FACTOR;
    case "motorcycle":
      return vehicle.make * EVALUATION_FACTOR;
  }
}

在以上代碼中,我們使用 switchcase 運算符來實現類型守衛,從而確保在 evaluatePrice 方法中,我們可以安全地訪問 vehicle 對象中的所包含的屬性,來正確的計算該車輛類型所對應的價格。

5.3 類型別名

類型別名用來給一個類型起個新名字。

type Message = string | string[];

let greet = (message: Message) => {
  // ...
};

六、交叉類型

在 TypeScript 中交叉類型是將多個類型合并為一個類型。通過 & 運算符可以將現有的多種類型疊加到一起成為一種類型,它包含了所需的所有類型的特性。

type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };

let point: Point = {
  x: 1,
  y: 1
}

在上面代碼中我們先定義了 PartialPointX 類型,接著使用 & 運算符創建一個新的 Point 類型,表示一個含有 x 和 y 坐標的點,然后定義了一個 Point 類型的變量并初始化。

6.1 同名基礎類型屬性的合并

那么現在問題來了,假設在合并多個類型的過程中,剛好出現某些類型存在相同的成員,但對應的類型又不一致,比如:

interface X {
  c: string;
  d: string;
}

interface Y {
  c: number;
  e: string
}

type XY = X & Y;
type YX = Y & X;

let p: XY;
let q: YX;

在上面的代碼中,接口 X 和接口 Y 都含有一個相同的成員 c,但它們的類型不一致。對于這種情況,此時 XY 類型或 YX 類型中成員 c 的類型是不是可以是 stringnumber 類型呢?比如下面的例子:

p = { c: 6, d: "d", e: "e" }; 

q = { c: "c", d: "d", e: "e" }; 

為什么接口 X 和接口 Y 混入后,成員 c 的類型會變成 never 呢?這是因為混入后成員 c 的類型為 string & number,即成員 c 的類型既可以是 string 類型又可以是 number 類型。很明顯這種類型是不存在的,所以混入后成員 c 的類型為 never。

6.2 同名非基礎類型屬性的合并

在上面示例中,剛好接口 X 和接口 Y 中內部成員 c 的類型都是基本數據類型,那么如果是非基本數據類型的話,又會是什么情形。我們來看個具體的例子:

interface D { d: boolean; }
interface E { e: string; }
interface F { f: number; }

interface A { x: D; }
interface B { x: E; }
interface C { x: F; }

type ABC = A & B & C;

let abc: ABC = {
  x: {
    d: true,
    e: 'semlinker',
    f: 666
  }
};

console.log('abc:', abc);

以上代碼成功運行后,控制臺會輸出以下結果:

由上圖可知,在混入多個類型時,若存在相同的成員,且成員類型為非基本數據類型,那么是可以成功合并。

七、TypeScript 函數

7.1 TypeScript 函數與 JavaScript 函數的區別

TypeScriptJavaScript
含有類型無類型
箭頭函數箭頭函數(ES2015)
函數類型無函數類型
必填和可選參數所有參數都是可選的
默認參數默認參數
剩余參數剩余參數
函數重載無函數重載

7.2 箭頭函數

1.常見語法
myBooks.forEach(() => console.log('reading'));

myBooks.forEach(title => console.log(title));

myBooks.forEach((title, idx, arr) =>
  console.log(idx + '-' + title);
);

myBooks.forEach((title, idx, arr) => {
  console.log(idx + '-' + title);
});
2.使用示例
// 未使用箭頭函數
function Book() {
  let self = this;
  self.publishDate = 2016;
  setInterval(function () {
    console.log(self.publishDate);
  }, 1000);
}

// 使用箭頭函數
function Book() {
  this.publishDate = 2016;
  setInterval(() => {
    console.log(this.publishDate);
  }, 1000);
}

7.3 參數類型和返回類型

function createUserId(name: string, id: number): string {
  return name + id;
}

7.4 函數類型

let IdGenerator: (chars: string, nums: number) => string;

function createUserId(name: string, id: number): string {
  return name + id;
}

IdGenerator = createUserId;

7.5 可選參數及默認參數

// 可選參數
function createUserId(name: string, id: number, age?: number): string {
  return name + id;
}

// 默認參數
function createUserId(
  name = "semlinker",
  id: number,
  age?: number
): string {
  return name + id;
}

在聲明函數時,可以通過 ? 號來定義可選參數,比如 age?: number 這種形式。在實際使用時,需要注意的是可選參數要放在普通參數的后面,不然會導致編譯錯誤。

7.6 剩余參數

function push(array, ...items) {
  items.forEach(function (item) {
    array.push(item);
  });
}

let a = [];
push(a, 1, 2, 3);

7.7 函數重載

函數重載或方法重載是使用相同名稱和不同參數數量或類型創建多個方法的一種能力。

function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: Combinable, b: Combinable) {
  // type Combinable = string | number;
  if (typeof a === 'string' || typeof b === 'string') {
    return a.toString() + b.toString();
  }
  return a + b;
}

在以上代碼中,我們為 add 函數提供了多個函數類型定義,從而實現函數的重載。在 TypeScript 中除了可以重載普通函數之外,我們還可以重載類中的成員方法。

方法重載是指在同一個類中方法同名,參數不同(參數類型不同、參數個數不同或參數個數相同時參數的先后順序不同),調用時根據實參的形式,選擇與它匹配的方法執行操作的一種技術。所以類中成員方法滿足重載的條件是:在同一個類中,方法名相同且參數列表不同。下面我們來舉一個成員方法重載的例子:

class Calculator {
  add(a: number, b: number): number;
  add(a: string, b: string): string;
  add(a: string, b: number): string;
  add(a: number, b: string): string;
  add(a: Combinable, b: Combinable) {
  if (typeof a === 'string' || typeof b === 'string') {
    return a.toString() + b.toString();
  }
    return a + b;
  }
}

const calculator = new Calculator();
const result = calculator.add('Semlinker', ' Kakuqo');

這里需要注意的是,當 TypeScript 編譯器處理函數重載時,它會查找重載列表,嘗試使用第一個重載定義。 如果匹配的話就使用這個。 因此,在定義重載的時候,一定要把最精確的定義放在最前面。另外在 Calculator 類中,add(a: Combinable, b: Combinable){ } 并不是重載列表的一部分,因此對于 add 成員方法來說,我們只定義了四個重載方法。

八、TypeScript 數組

8.1 數組解構

let x: number; let y: number; let z: number;
let five_array = [0,1,2,3,4];
[x,y,z] = five_array;

8.2 數組展開運算符

let two_array = [0, 1];
let five_array = [...two_array, 2, 3, 4];

8.3 數組遍歷

let colors: string[] = ["red", "green", "blue"];
for (let i of colors) {
  console.log(i);
}

九、TypeScript 對象

9.1 對象解構

let person = {
  name: "Semlinker",
  gender: "Male",
};

let { name, gender } = person;

9.2 對象展開運算符

let person = {
  name: "Semlinker",
  gender: "Male",
  address: "Xiamen",
};

// 組裝對象
let personWithAge = { ...person, age: 33 };

// 獲取除了某些項外的其它項
let { name, ...rest } = person;

十、TypeScript 接口

在面向對象語言中,接口是一個很重要的概念,它是對行為的抽象,而具體如何行動需要由類去實現。

TypeScript 中的接口是一個非常靈活的概念,除了可用于對類的一部分行為進行抽象以外,也常用于對「對象的形狀(Shape)」進行描述。

10.1 對象的形狀

interface Person {
  name: string;
  age: number;
}

let semlinker: Person = {
  name: "semlinker",
  age: 33,
};

10.2 可選 | 只讀屬性

interface Person {
  readonly name: string;
  age?: number;
}

只讀屬性用于限制只能在對象剛剛創建的時候修改其值。此外 TypeScript 還提供了 ReadonlyArray<T> 類型,它與 Array<T> 相似,只是把所有可變方法去掉了,因此可以確保數組創建后再也不能被修改。

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!

10.3 任意屬性

有時候我們希望一個接口中除了包含必選和可選屬性之外,還允許有其他的任意屬性,這時我們可以使用 索引簽名 的形式來滿足上述要求。

interface Person {
  name: string;
  age?: number;
  [propName: string]: any;
}

const p1 = { name: "semlinker" };
const p2 = { name: "lolo", age: 5 };
const p3 = { name: "kakuqo", sex: 1 }

10.4 接口與類型別名的區別

1.Objects/Functions

接口和類型別名都可以用來描述對象的形狀或函數簽名:

接口

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

interface SetPoint {
  (x: number, y: number): void;
}

類型別名

type Point = {
  x: number;
  y: number;
};

type SetPoint = (x: number, y: number) => void;
2.Other Types

與接口類型不一樣,類型別名可以用于一些其他類型,比如原始類型、聯合類型和元組:

// primitive
type Name = string;

// object
type PartialPointX = { x: number; };
type PartialPointY = { y: number; };

// union
type PartialPoint = PartialPointX | PartialPointY;

// tuple
type Data = [number, string];
3.Extend

接口和類型別名都能夠被擴展,但語法有所不同。此外,接口和類型別名不是互斥的。接口可以擴展類型別名,而反過來是不行的。

Interface extends interface

interface PartialPointX { x: number; }
interface Point extends PartialPointX { 
  y: number; 
}

Type alias extends type alias

type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };

Interface extends type alias

type PartialPointX = { x: number; };
interface Point extends PartialPointX { y: number; }

Type alias extends interface

interface PartialPointX { x: number; }
type Point = PartialPointX & { y: number; };
4.Implements

類可以以相同的方式實現接口或類型別名,但類不能實現使用類型別名定義的聯合類型:

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

class SomePoint implements Point {
  x = 1;
  y = 2;
}

type Point2 = {
  x: number;
  y: number;
};

class SomePoint2 implements Point2 {
  x = 1;
  y = 2;
}

type PartialPoint = { x: number; } | { y: number; };

// A class can only implement an object type or 
// intersection of object types with statically known members.
class SomePartialPoint implements PartialPoint { // Error
  x = 1;
  y = 2;
}
5.Declaration merging

與類型別名不同,接口可以定義多次,會被自動合并為單個接口。

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

const point: Point = { x: 1, y: 2 };

十一、TypeScript 類

11.1 類的屬性與方法

在面向對象語言中,類是一種面向對象計算機編程語言的構造,是創建對象的藍圖,描述了所創建的對象共同的屬性和方法。

在 TypeScript 中,我們可以通過 Class 關鍵字來定義一個類:

class Greeter {
  // 靜態屬性
  static cname: string = "Greeter";
  // 成員屬性
  greeting: string;

  // 構造函數 - 執行初始化操作
  constructor(message: string) {
    this.greeting = message;
  }

  // 靜態方法
  static getClassName() {
    return "Class name is Greeter";
  }

  // 成員方法
  greet() {
    return "Hello, " + this.greeting;
  }
}

let greeter = new Greeter("world");

那么成員屬性與靜態屬性,成員方法與靜態方法有什么區別呢?這里無需過多解釋,我們直接看一下編譯生成的 ES5 代碼:

"use strict";
var Greeter = /** @class */ (function () {
    // 構造函數 - 執行初始化操作
    function Greeter(message) {
      this.greeting = message;
    }
    // 靜態方法
    Greeter.getClassName = function () {
      return "Class name is Greeter";
    };
    // 成員方法
    Greeter.prototype.greet = function () {
      return "Hello, " + this.greeting;
    };
    // 靜態屬性
    Greeter.cname = "Greeter";
    return Greeter;
}());
var greeter = new Greeter("world");

11.2 ECMAScript 私有字段

在 TypeScript 3.8 版本就開始支持ECMAScript 私有字段,使用方式如下:

class Person {
  #name: string;

  constructor(name: string) {
    this.#name = name;
  }

  greet() {
    console.log(`Hello, my name is ${this.#name}!`);
  }
}

let semlinker = new Person("Semlinker");

semlinker.#name;
//     ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.

與常規屬性(甚至使用 private 修飾符聲明的屬性)不同,私有字段要牢記以下規則:

  • 私有字段以 # 字符開頭,有時我們稱之為私有名稱;
  • 每個私有字段名稱都唯一地限定于其包含的類;
  • 不能在私有字段上使用 TypeScript 可訪問性修飾符(如 public 或 private);
  • 私有字段不能在包含的類之外訪問,甚至不能被檢測到。

11.3 訪問器

在 TypeScript 中,我們可以通過 gettersetter 方法來實現數據的封裝和有效性校驗,防止出現異常數據。

let passcode = "Hello TypeScript";

class Employee {
  private _fullName: string;

  get fullName(): string {
    return this._fullName;
  }

  set fullName(newName: string) {
    if (passcode && passcode == "Hello TypeScript") {
      this._fullName = newName;
    } else {
      console.log("Error: Unauthorized update of employee!");
    }
  }
}

let employee = new Employee();
employee.fullName = "Semlinker";
if (employee.fullName) {
  console.log(employee.fullName);
}

11.4 類的繼承

繼承(Inheritance)是一種聯結類與類的層次模型。指的是一個類(稱為子類、子接口)繼承另外的一個類(稱為父類、父接口)的功能,并可以增加它自己的新功能的能力,繼承是類與類或者接口與接口之間最常見的關系。

繼承是一種 is-a 關系:

在 TypeScript 中,我們可以通過 extends 關鍵字來實現繼承:

class Animal {
  name: string;
  
  constructor(theName: string) {
    this.name = theName;
  }
  
  move(distanceInMeters: number = 0) {
    console.log(`${this.name} moved ${distanceInMeters}m.`);
  }
}

class Snake extends Animal {
  constructor(name: string) {
    super(name); // 調用父類的構造函數
  }
  
  move(distanceInMeters = 5) {
    console.log("Slithering...");
    super.move(distanceInMeters);
  }
}

let sam = new Snake("Sammy the Python");
sam.move();

11.5 抽象類

使用 abstract 關鍵字聲明的類,我們稱之為抽象類。抽象類不能被實例化,因為它里面包含一個或多個抽象方法。所謂的抽象方法,是指不包含具體實現的方法:

abstract class Person {
  constructor(public name: string){}

  abstract say(words: string) :void;
}

// Cannot create an instance of an abstract class.(2511)
const lolo = new Person(); // Error

抽象類不能被直接實例化,我們只能實例化實現了所有抽象方法的子類。具體如下所示:

abstract class Person {
  constructor(public name: string){}

  // 抽象方法
  abstract say(words: string) :void;
}

class Developer extends Person {
  constructor(name: string) {
    super(name);
  }
  
  say(words: string): void {
    console.log(`${this.name} says ${words}`);
  }
}

const lolo = new Developer("lolo");
lolo.say("I love ts!"); // lolo says I love ts!

11.6 類方法重載

在前面的章節,我們已經介紹了函數重載。對于類的方法來說,它也支持重載。比如,在以下示例中我們重載了 ProductService 類的 getProducts 成員方法:

class ProductService {
    getProducts(): void;
    getProducts(id: number): void;
    getProducts(id?: number) {
      if(typeof id === 'number') {
          console.log(`獲取id為 ${id} 的產品信息`);
      } else {
          console.log(`獲取所有的產品信息`);
      }  
    }
}

const productService = new ProductService();
productService.getProducts(666); // 獲取id為 666 的產品信息
productService.getProducts(); // 獲取所有的產品信息 

十二、TypeScript 泛型

軟件工程中,我們不僅要創建一致的定義良好的 API,同時也要考慮可重用性。 組件不僅能夠支持當前的數據類型,同時也能支持未來的數據類型,這在創建大型系統時為你提供了十分靈活的功能。

在像 C# 和 Java 這樣的語言中,可以使用泛型來創建可重用的組件,一個組件可以支持多種類型的數據。 這樣用戶就可以以自己的數據類型來使用組件。

設計泛型的關鍵目的是在成員之間提供有意義的約束,這些成員可以是:類的實例成員、類的方法、函數參數和函數返回值。

泛型(Generics)是允許同一個函數接受不同類型參數的一種模板。相比于使用 any 類型,使用泛型來創建可復用的組件要更好,因為泛型會保留參數類型。

12.1 泛型語法

對于剛接觸 TypeScript 泛型的讀者來說,首次看到 <T> 語法會感到陌生。其實它沒有什么特別,就像傳遞參數一樣,我們傳遞了我們想要用于特定函數調用的類型。

參考上面的圖片,當我們調用 identity<Number>(1) ,Number 類型就像參數 1 一樣,它將在出現 T 的任何位置填充該類型。圖中 <T> 內部的 T 被稱為類型變量,它是我們希望傳遞給 identity 函數的類型占位符,同時它被分配給 value 參數用來代替它的類型:此時 T 充當的是類型,而不是特定的 Number 類型。

其中 T 代表 Type,在定義泛型時通常用作第一個類型變量名稱。但實際上 T 可以用任何有效名稱代替。除了 T 之外,以下是常見泛型變量代表的意思:

  • K(Key):表示對象中的鍵類型;
  • V(Value):表示對象中的值類型;
  • E(Element):表示元素類型。

其實并不是只能定義一個類型變量,我們可以引入希望定義的任何數量的類型變量。比如我們引入一個新的類型變量 U,用于擴展我們定義的 identity 函數:

function identity <T, U>(value: T, message: U) : T {
  console.log(message);
  return value;
}

console.log(identity<Number, string>(68, "Semlinker"));

除了為類型變量顯式設定值之外,一種更常見的做法是使編譯器自動選擇這些類型,從而使代碼更簡潔。我們可以完全省略尖括號,比如:

function identity <T, U>(value: T, message: U) : T {
  console.log(message);
  return value;
}

console.log(identity(68, "Semlinker"));

對于上述代碼,編譯器足夠聰明,能夠知道我們的參數類型,并將它們賦值給 T 和 U,而不需要開發人員顯式指定它們。

12.2 泛型接口

interface GenericIdentityFn<T> {
  (arg: T): T;
}

12.3 泛型類

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
  return x + y;
};

12.4 泛型工具類型

為了方便開發者 TypeScript 內置了一些常用的工具類型,比如 Partial、Required、Readonly、Record 和 ReturnType 等。出于篇幅考慮,這里我們只簡單介紹 Partial 工具類型。不過在具體介紹之前,我們得先介紹一些相關的基礎知識,方便讀者自行學習其它的工具類型。

1.typeof

在 TypeScript 中,typeof 操作符可以用來獲取一個變量聲明或對象的類型。

interface Person {
  name: string;
  age: number;
}

const sem: Person = { name: 'semlinker', age: 33 };
type Sem= typeof sem; // -> Person

function toArray(x: number): Array<number> {
  return [x];
}

type Func = typeof toArray; // -> (x: number) => number[]
2.keyof

keyof 操作符是在 TypeScript 2.1 版本引入的,該操作符可以用于獲取某種類型的所有鍵,其返回類型是聯合類型。

interface Person {
  name: string;
  age: number;
}

type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join" 
type K3 = keyof { [x: string]: Person };  // string | number

在 TypeScript 中支持兩種索引簽名,數字索引和字符串索引:

interface StringArray {
  // 字符串索引 -> keyof StringArray => string | number
  [index: string]: string; 
}

interface StringArray1 {
  // 數字索引 -> keyof StringArray1 => number
  [index: number]: string;
}

為了同時支持兩種索引類型,就得要求數字索引的返回值必須是字符串索引返回值的子類。其中的原因就是當使用數值索引時,JavaScript 在執行索引操作時,會先把數值索引先轉換為字符串索引。所以 keyof { [x: string]: Person } 的結果會返回 string | number。

3.in

in 用來遍歷枚舉類型:

type Keys = "a" | "b" | "c"

type Obj =  {
  [p in Keys]: any
} // -> { a: any, b: any, c: any }
4.infer

在條件類型語句中,可以用 infer 聲明一個類型變量并且對它進行使用。

type ReturnType<T> = T extends (
  ...args: any[]
) => infer R ? R : any;

以上代碼中 infer R 就是聲明一個變量來承載傳入函數簽名的返回值類型,簡單說就是用它取到函數返回值的類型方便之后使用。

5.extends

有時候我們定義的泛型不想過于靈活或者說想繼承某些類等,可以通過 extends 關鍵字添加泛型約束。

interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

現在這個泛型函數被定義了約束,因此它不再是適用于任意類型:

loggingIdentity(3);  // Error, number doesn't have a .length property

這時我們需要傳入符合約束類型的值,必須包含必須的屬性:

loggingIdentity({length: 10, value: 3});
6.Partial

Partial<T> 的作用就是將某個類型里的屬性全部變為可選項 ?。

定義:

/**
 * node_modules/typescript/lib/lib.es5.d.ts
 * Make all properties in T optional
 */
type Partial<T> = {
  [P in keyof T]?: T[P];
};

在以上代碼中,首先通過 keyof T 拿到 T 的所有屬性名,然后使用 in 進行遍歷,將值賦給 P,最后通過 T[P] 取得相應的屬性值。中間的 ? 號,用于將所有屬性變為可選。

示例:

interface Todo {
  title: string;
  description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
  return { ...todo, ...fieldsToUpdate };
}

const todo1 = {
  title: "Learn TS",
  description: "Learn TypeScript",
};

const todo2 = updateTodo(todo1, {
  description: "Learn TypeScript Enum",
});

在上面的 updateTodo 方法中,我們利用 Partial<T> 工具類型,定義 fieldsToUpdate 的類型為 Partial<Todo>,即:

{
   title?: string | undefined;
   description?: string | undefined;
}

十三、TypeScript 裝飾器

13.1 裝飾器是什么

  • 它是一個表達式
  • 該表達式被執行后,返回一個函數
  • 函數的入參分別為 target、name 和 descriptor
  • 執行該函數后,可能返回 descriptor 對象,用于配置 target 對象

13.2 裝飾器的分類

  • 類裝飾器(Class decorators)
  • 屬性裝飾器(Property decorators)
  • 方法裝飾器(Method decorators)
  • 參數裝飾器(Parameter decorators)

需要注意的是,若要啟用實驗性的裝飾器特性,你必須在命令行或 tsconfig.json 里啟用 experimentalDecorators 編譯器選項:

命令行

tsc --target ES5 --experimentalDecorators

tsconfig.json

{
  "compilerOptions": {
     "target": "ES5",
     "experimentalDecorators": true
   }
}

13.3 類裝飾器

類裝飾器聲明:

declare type ClassDecorator = <TFunction extends Function>(
  target: TFunction
) => TFunction | void;

類裝飾器顧名思義,就是用來裝飾類的。它接收一個參數:

  • target: TFunction - 被裝飾的類

看完第一眼后,是不是感覺都不好了。沒事,我們馬上來個例子:

function Greeter(target: Function): void {
  target.prototype.greet = function (): void {
    console.log("Hello Semlinker!");
  };
}

@Greeter
class Greeting {
  constructor() {
    // 內部實現
  }
}

let myGreeting = new Greeting();
(myGreeting as any).greet(); // console output: 'Hello Semlinker!';

上面的例子中,我們定義了 Greeter 類裝飾器,同時我們使用了 @Greeter 語法糖,來使用裝飾器。

友情提示:讀者可以直接復制上面的代碼,在 TypeScript Playground 中運行查看結果。

有的讀者可能想問,例子中總是輸出 Hello Semlinker! ,能自定義輸出的問候語么 ?這個問題很好,答案是可以的。

具體實現如下:

function Greeter(greeting: string) {
  return function (target: Function) {
    target.prototype.greet = function (): void {
      console.log(greeting);
    };
  };
}

@Greeter("Hello TS!")
class Greeting {
  constructor() {
    // 內部實現
  }
}

let myGreeting = new Greeting();
(myGreeting as any).greet(); // console output: 'Hello TS!';

13.4 屬性裝飾器

屬性裝飾器聲明:

declare type PropertyDecorator = (target:Object, 
  propertyKey: string | symbol ) => void;

屬性裝飾器顧名思義,用來裝飾類的屬性。它接收兩個參數:

  • target: Object - 被裝飾的類
  • propertyKey: string | symbol - 被裝飾類的屬性名

趁熱打鐵,馬上來個例子熱熱身:

function logProperty(target: any, key: string) {
  delete target[key];

  const backingField = "_" + key;

  Object.defineProperty(target, backingField, {
    writable: true,
    enumerable: true,
    configurable: true
  });

  // property getter
  const getter = function (this: any) {
    const currVal = this[backingField];
    console.log(`Get: ${key} => ${currVal}`);
    return currVal;
  };

  // property setter
  const setter = function (this: any, newVal: any) {
    console.log(`Set: ${key} => ${newVal}`);
    this[backingField] = newVal;
  };

  // Create new property with getter and setter
  Object.defineProperty(target, key, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true
  });
}

class Person { 
  @logProperty
  public name: string;

  constructor(name : string) { 
    this.name = name;
  }
}

const p1 = new Person("semlinker");
p1.name = "kakuqo";

以上代碼我們定義了一個 logProperty 函數,來跟蹤用戶對屬性的操作,當代碼成功運行后,在控制臺會輸出以下結果:

Set: name => semlinker
Set: name => kakuqo

13.5 方法裝飾器

方法裝飾器聲明:

declare type MethodDecorator = <T>(target:Object, propertyKey: string | symbol,          
  descriptor: TypePropertyDescript<T>) => TypedPropertyDescriptor<T> | void;

方法裝飾器顧名思義,用來裝飾類的方法。它接收三個參數:

  • target: Object - 被裝飾的類
  • propertyKey: string | symbol - 方法名
  • descriptor: TypePropertyDescript - 屬性描述符

廢話不多說,直接上例子:

function log(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
  let originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log("wrapped function: before invoking " + propertyKey);
    let result = originalMethod.apply(this, args);
    console.log("wrapped function: after invoking " + propertyKey);
    return result;
  };
}

class Task {
  @log
  runTask(arg: any): any {
    console.log("runTask invoked, args: " + arg);
    return "finished";
  }
}

let task = new Task();
let result = task.runTask("learn ts");
console.log("result: " + result);

以上代碼成功運行后,控制臺會輸出以下結果:

"wrapped function: before invoking runTask" 
"runTask invoked, args: learn ts" 
"wrapped function: after invoking runTask" 
"result: finished" 

下面我們來介紹一下參數裝飾器。

13.6 參數裝飾器

參數裝飾器聲明:

declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, 
  parameterIndex: number ) => void

參數裝飾器顧名思義,是用來裝飾函數參數,它接收三個參數:

  • target: Object - 被裝飾的類
  • propertyKey: string | symbol - 方法名
  • parameterIndex: number - 方法中參數的索引值
function Log(target: Function, key: string, parameterIndex: number) {
  let functionLogged = key || target.prototype.constructor.name;
  console.log(`The parameter in position ${parameterIndex} at ${functionLogged} has
    been decorated`);
}

class Greeter {
  greeting: string;
  constructor(@Log phrase: string) {
    this.greeting = phrase; 
  }
}

以上代碼成功運行后,控制臺會輸出以下結果:

"The parameter in position 0 at Greeter has been decorated" 

十四、TypeScript 4.0 新特性

TypeScript 4.0 帶來了很多新的特性,這里我們只簡單介紹其中的兩個新特性。

14.1 構造函數的類屬性推斷

noImplicitAny 配置屬性被啟用之后,TypeScript 4.0 就可以使用控制流分析來確認類中的屬性類型:

class Person {
  fullName; // (property) Person.fullName: string
  firstName; // (property) Person.firstName: string
  lastName; // (property) Person.lastName: string

  constructor(fullName: string) {
    this.fullName = fullName;
    this.firstName = fullName.split(" ")[0];
    this.lastName =   fullName.split(" ")[1];
  }  
}

然而對于以上的代碼,如果在 TypeScript 4.0 以前的版本,比如在 3.9.2 版本下,編譯器會提示以下錯誤信息:

class Person {
  // Member 'fullName' implicitly has an 'any' type.(7008)
  fullName; // Error
  firstName; // Error
  lastName; // Error

  constructor(fullName: string) {
    this.fullName = fullName;
    this.firstName = fullName.split(" ")[0];
    this.lastName =   fullName.split(" ")[1];
  }  
}

從構造函數推斷類屬性的類型,該特性給我們帶來了便利。但在使用過程中,如果我們沒法保證對成員屬性都進行賦值,那么該屬性可能會被認為是 undefined。

class Person {
   fullName;  // (property) Person.fullName: string
   firstName; // (property) Person.firstName: string | undefined
   lastName; // (property) Person.lastName: string | undefined

   constructor(fullName: string) {
     this.fullName = fullName;
     if(Math.random()){
       this.firstName = fullName.split(" ")[0];
       this.lastName =   fullName.split(" ")[1];
     }
   }  
}

14.2 標記的元組元素

在以下的示例中,我們使用元組類型來聲明剩余參數的類型:

function addPerson(...args: [string, number]): void {
  console.log(`Person info: name: ${args[0]}, age: ${args[1]}`)
}

addPerson("lolo", 5); // Person info: name: lolo, age: 5 

其實,對于上面的 addPerson 函數,我們也可以這樣實現:

function addPerson(name: string, age: number) {
  console.log(`Person info: name: ${name}, age: ${age}`)
}

這兩種方式看起來沒有多大的區別,但對于第一種方式,我們沒法設置第一個參數和第二個參數的名稱。雖然這樣對類型檢查沒有影響,但在元組位置上缺少標簽,會使得它們難于使用。為了提高開發者使用元組的體驗,TypeScript 4.0 支持為元組類型設置標簽:

function addPerson(...args: [name: string, age: number]): void {
  console.log(`Person info: name: ${args[0]}, age: ${args[1]}`);
}

之后,當我們使用 addPerson 方法時,TypeScript 的智能提示就會變得更加友好。

// 未使用標簽的智能提示
// addPerson(args_0: string, args_1: number): void
function addPerson(...args: [string, number]): void {
  console.log(`Person info: name: ${args[0]}, age: ${args[1]}`)
} 

// 已使用標簽的智能提示
// addPerson(name: string, age: number): void
function addPerson(...args: [name: string, age: number]): void {
  console.log(`Person info: name: ${args[0]}, age: ${args[1]}`);
} 

十五、編譯上下文

15.1 tsconfig.json 的作用

  • 用于標識 TypeScript 項目的根路徑;
  • 用于配置 TypeScript 編譯器;
  • 用于指定編譯的文件。

15.2 tsconfig.json 重要字段

  • files - 設置要編譯的文件的名稱;
  • include - 設置需要進行編譯的文件,支持路徑模式匹配;
  • exclude - 設置無需進行編譯的文件,支持路徑模式匹配;
  • compilerOptions - 設置與編譯流程相關的選項。

15.3 compilerOptions 選項

compilerOptions 支持很多選項,常見的有 baseUrl、 target、baseUrl、 moduleResolutionlib 等。

compilerOptions 每個選項的詳細說明如下:

{
  "compilerOptions": {

    /* 基本選項 */
    "target": "es5",                       // 指定 ECMAScript 目標版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
    "module": "commonjs",                  // 指定使用模塊: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
    "lib": [],                             // 指定要包含在編譯中的庫文件
    "allowJs": true,                       // 允許編譯 javascript 文件
    "checkJs": true,                       // 報告 javascript 文件中的錯誤
    "jsx": "preserve",                     // 指定 jsx 代碼的生成: 'preserve', 'react-native', or 'react'
    "declaration": true,                   // 生成相應的 '.d.ts' 文件
    "sourceMap": true,                     // 生成相應的 '.map' 文件
    "outFile": "./",                       // 將輸出文件合并為一個文件
    "outDir": "./",                        // 指定輸出目錄
    "rootDir": "./",                       // 用來控制輸出目錄結構 --outDir.
    "removeComments": true,                // 刪除編譯后的所有的注釋
    "noEmit": true,                        // 不生成輸出文件
    "importHelpers": true,                 // 從 tslib 導入輔助工具函數
    "isolatedModules": true,               // 將每個文件做為單獨的模塊 (與 'ts.transpileModule' 類似).

    /* 嚴格的類型檢查選項 */
    "strict": true,                        // 啟用所有嚴格類型檢查選項
    "noImplicitAny": true,                 // 在表達式和聲明上有隱含的 any類型時報錯
    "strictNullChecks": true,              // 啟用嚴格的 null 檢查
    "noImplicitThis": true,                // 當 this 表達式值為 any 類型的時候,生成一個錯誤
    "alwaysStrict": true,                  // 以嚴格模式檢查每個模塊,并在每個文件里加入 'use strict'

    /* 額外的檢查 */
    "noUnusedLocals": true,                // 有未使用的變量時,拋出錯誤
    "noUnusedParameters": true,            // 有未使用的參數時,拋出錯誤
    "noImplicitReturns": true,             // 并不是所有函數里的代碼都有返回值時,拋出錯誤
    "noFallthroughCasesInSwitch": true,    // 報告 switch 語句的 fallthrough 錯誤。(即,不允許 switch 的 case 語句貫穿)

    /* 模塊解析選項 */
    "moduleResolution": "node",            // 選擇模塊解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
    "baseUrl": "./",                       // 用于解析非相對模塊名稱的基目錄
    "paths": {},                           // 模塊名到基于 baseUrl 的路徑映射的列表
    "rootDirs": [],                        // 根文件夾列表,其組合內容表示項目運行時的結構內容
    "typeRoots": [],                       // 包含類型聲明的文件列表
    "types": [],                           // 需要包含的類型聲明文件名列表
    "allowSyntheticDefaultImports": true,  // 允許從沒有設置默認導出的模塊中默認導入。

    /* Source Map Options */
    "sourceRoot": "./",                    // 指定調試器應該找到 TypeScript 文件而不是源文件的位置
    "mapRoot": "./",                       // 指定調試器應該找到映射文件而不是生成文件的位置
    "inlineSourceMap": true,               // 生成單個 soucemaps 文件,而不是將 sourcemaps 生成不同的文件
    "inlineSources": true,                 // 將代碼與 sourcemaps 生成到一個文件中,要求同時設置了 --inlineSourceMap 或 --sourceMap 屬性

    /* 其他選項 */
    "experimentalDecorators": true,        // 啟用裝飾器
    "emitDecoratorMetadata": true          // 為裝飾器提供元數據的支持
  }
}

十六、TypeScript 開發輔助工具

16.1 TypeScript Playground

簡介:TypeScript 官方提供的在線 TypeScript 運行環境,利用它你可以方便地學習 TypeScript 相關知識與不同版本的功能特性。

在線地址:https://www.typescriptlang.or...

除了 TypeScript 官方的 Playground 之外,你還可以選擇其他的 Playground,比如 codepen.io、stackblitzjsbin.com 等。

16.2 TypeScript UML Playground

簡介:一款在線 TypeScript UML 工具,利用它你可以為指定的 TypeScript 代碼生成 UML 類圖。

在線地址:https://tsuml-demo.firebaseap...

16.3 JSON TO TS

簡介:一款 TypeScript 在線工具,利用它你可以為指定的 JSON 數據生成對應的 TypeScript 接口定義。

在線地址:http://www.jsontots.com/

除了使用 jsontots 在線工具之外,對于使用 VSCode IDE 的小伙們還可以安裝 JSON to TS 擴展來快速完成 JSON to TS 的轉換工作。

16.4 Schemats

簡介:利用 Schemats,你可以基于(Postgres,MySQL)SQL 數據庫中的 schema 自動生成 TypeScript 接口定義。

在線地址:https://github.com/SweetIQ/sc...

16.5 TypeScript AST Viewer

簡介:一款 TypeScript AST 在線工具,利用它你可以查看指定 TypeScript 代碼對應的 AST(Abstract Syntax Tree)抽象語法樹。

在線地址:https://ts-ast-viewer.com/

對于了解過 AST 的小伙伴來說,對 astexplorer 這款在線工具應該不會陌生。該工具除了支持 JavaScript 之外,還支持 CSS、JSON、RegExp、GraphQL 和 Markdown 等格式的解析。

16.6 TypeDoc

簡介:TypeDoc 用于將 TypeScript 源代碼中的注釋轉換為 HTML 文檔或 JSON 模型。它可靈活擴展,并支持多種配置。

在線地址:https://typedoc.org/

16.7 TypeScript ESLint

簡介:使用 TypeScript ESLint 可以幫助我們規范代碼質量,提高團隊開發效率。

在線地址:https://typescript-eslint.io/

TypeScript ESLint 項目感興趣且想在項目中應用的小伙伴,可以參考 “在Typescript項目中,如何優雅的使用ESLint和Prettier” 這篇文章。

能堅持看到這里的小伙伴都是 “真愛”,如果你還意猶未盡,那就來看看本人整理的 Github 上 1.8K+ 的開源項目:awesome-typescript。

https://github.com/semlinker/...

十七、參考資源

十八、推薦閱讀

查看原文

贊 141 收藏 108 評論 8

認證與成就

  • 獲得 187 次點贊
  • 獲得 1 枚徽章 獲得 0 枚金徽章, 獲得 0 枚銀徽章, 獲得 1 枚銅徽章

擅長技能
編輯

(??? )
暫時沒有

開源項目 & 著作
編輯

(??? )
暫時沒有

注冊于 2019-02-22
個人主頁被 3.5k 人瀏覽

一本到在线是免费观看_亚洲2020天天堂在线观看_国产欧美亚洲精品第一页_最好看的2018中文字幕 <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>