Linux中國

一篇缺失的 TypeScript 介紹

下文是 James Henry(@MrJamesHenry)所提交的內容。我是 ESLint 核心團隊的一員,也是 TypeScript 佈道師。我正在和 Todd 在 UltimateAngular 平台上合作發布 Angular 和 TypeScript 的精品課程。

本文的主旨是為了介紹我們是如何看待 TypeScript 的以及它在加強 JavaScript 開發中所起的作用。

我們也將儘可能地給出那些類型和編譯方面的那些時髦辭彙的準確定義。

TypeScript 強大之處遠遠不止這些,本篇文章無法涵蓋,想要了解更多請閱讀官方文檔,或者學習 UltimateAngular 上的 TypeScript 課程 ,從初學者成為一位 TypeScript 高手。

背景

TypeScript 是個出乎意料強大的工具,而且它真的很容易掌握。

然而,TypeScript 可能比 JavaScript 要更為複雜一些,因為 TypeScript 可能向我們同時引入了一系列以前沒有考慮過的 JavaScript 程序相關的技術概念。

每當我們談論到類型、編譯器等這些概念的時候,你會發現很快會變的不知所云起來。

這篇文章就是一篇為了解答你需要知道的許許多多不知所云的概念,來幫助你 TypeScript 快速入門的教程,可以讓你輕鬆自如的應對這些概念。

關鍵知識的掌握

在 Web 瀏覽器中運行我們的代碼這件事或許使我們對它是如何工作的產生一些誤解,「它不用經過編譯,是嗎?」,「我敢肯定這裡面是沒有類型的...」

更有意思的是,上述的說法既是正確的也是不正確的,這取決於上下文環境和我們是如何定義這些概念的。

首先,我們要作的是明確這些。

JavaScript 是解釋型語言還是編譯型語言?

傳統意義上,程序員經常將自己的程序編譯之後運行出結果就認為這種語言是編譯型語言。

從初學者的角度來說,編譯的過程就是將我們自己編輯好的高級語言程序轉換成機器實際運行的格式。

就像 Go 語言,可以使用 go build 的命令行工具編譯 .go 的文件,將其編譯成代碼的低級形式,它可以直接執行、運行。

# We manually compile our .go file into something we can run
# using the command line tool "go build"
go build ultimate-angular.go
# ...then we execute it!
./ultimate-angular

作為一個 JavaScript 程序員(這一刻,請先忽略我們對新一代構建工具和模塊載入程序的熱愛),我們在日常的 JavaScript 開發中並沒有編譯的這一基本步驟,

我們寫一些 JavaScript 代碼,把它放在瀏覽器的 <script> 標籤中,它就能運行了(或者在服務端環境運行,比如:node.js)。

好吧,因此 JavaScript 沒有進行過編譯,那它一定是解釋型語言了,是嗎?

實際上,我們能夠確定的一點是,JavaScript 不是我們自己編譯的,現在讓我們簡單的回顧一個簡單的解釋型語言的例子,再來談 JavaScript 的編譯問題。

解釋型計算機語言的執行的過程就像人們看書一樣,從上到下、一行一行的閱讀。

我們所熟知的解釋型語言的典型例子是 bash 腳本。我們終端中的 bash 解釋器逐行讀取我們的命令並且執行它。

現在我們回到 JavaScript 是解釋執行還是編譯執行的討論中,我們要將逐行讀取和逐行執行程序分開理解(對「解釋型」的簡單理解),不要混在一起。

以此代碼為例:

hello();
function hello(){
    console.log("Hello")
}

這是真正意義上 JavaScript 輸出 Hello 單詞的程序代碼,但是,在 hello() 在我們定義它之前就已經使用了這個函數,這是簡單的逐行執行辦不到的,因為 hello() 在第一行沒有任何意義的,直到我們在第二行聲明了它。

像這樣在 JavaScript 是存在的,因為我們的代碼實際上在執行之前就被所謂的「JavaScript 引擎」或者是「特定的編譯環境」編譯過,這個編譯的過程取決於具體的實現(比如,使用 V8 引擎的 node.js 和 Chome 就和使用 SpiderMonkey 的 FireFox 就有所不同)。

在這裡,我們不會在進一步的講解編譯型執行和解釋型執行微妙之處(這裡的定義已經很好了)。

請務必記住,我們編寫的 JavaScript 代碼已經不是我們的用戶實際執行的代碼了,即使是我們簡單地將其放在 HTML 中的 <script> ,也是不一樣的。

運行時間 VS 編譯時間

現在我們已經正確理解了編譯和運行是兩個不同的階段,那「 運行階段 Run Time 」和「 編譯階段 Compile Time 」理解起來也就容易多了。

編譯階段,就是我們在我們的編輯器或者 IDE 當中的代碼轉換成其它格式的代碼的階段。

運行階段,就是我們程序實際執行的階段,例如:上面的 hello() 函數就執行在「運行階段」。

TypeScript 編譯器

現在我們了解了程序的生命周期中的關鍵階段,接下來我們可以介紹 TypeScript 編譯器了。

TypeScript 編譯器是幫助我們編寫代碼的關鍵。比如,我們不需將 JavaScript 代碼包含到 <script> 標籤當中,只需要通過 TypeScript 編譯器傳遞它,就可以在運行程序之前得到改進程序的建議。

我們可以將這個新的步驟作為我們自己的個人「編譯階段」,這將在我們的程序抵達 JavaScript 主引擎之前,確保我們的程序是以我們預期的方式編寫的。

它與上面 Go 語言的實例類似,但是 TypeScript 編譯器只是基於我們編寫程序的方式提供提示信息,並不會將其轉換成低級的可執行文件,它只會生成純 JavaScript 代碼。

# One option for passing our source .ts file through the TypeScript
# compiler is to use the command line tool "tsc"
tsc ultimate-angular.ts

# ...this will produce a .js file of the same name
# i.e. ultimate-angular.js

官方文檔中,有許多關於將 TypeScript 編譯器以各種方式融入到你的現有工作流程中的文章。這些已經超出本文範圍。

動態類型與靜態類型

就像對比編譯程序與解釋程序一樣,動態類型與靜態類型的對比在現有的資料中也是極其模稜兩可的。

讓我們先回顧一下我們在 JavaScript 中對於類型的理解。

我們的代碼如下:

var name = &apos;James&apos;;
var sum = 1 + 2;

我們如何給別人描述這段代碼?

「我們聲明了一個變數 name,它被分配了一個 「James」 的字元串,然後我們又申請了一個變數 sum,它被分配了一個數字 1 和數字 2 的求和的數值結果。」

即使在這樣一個簡單的程序中,我們也使用了兩個 JavaScript 的基本類型:StringNumber

就像上面我們講編譯一樣,我們不會陷入編程語言類型的學術細節當中,關鍵是要理解在 JavaScript 中類型表示的是什麼,並擴展到 TypeScript 的類型的理解上。

從每夜拜讀的最新 ECMAScript 規範中我們可以學到(LOL, JK - 「wat』s an ECMA?」),它大量引用了 JavaScript 的類型及其用法。

直接引自官方規範:

ECMAScript 語言的類型取決於使用 ECMAScript 語言的 ECMAScript 程序員所直接操作的值。

ECMAScript 語言的類型有 Undefined、Null、Boolean、String、Symbol、Number 和 Object。

我們可以看到,JavaScript 語言有 7 種正式類型,其中我們在我們現在程序中使用了 6 種(Symbol 首次在 ES2015 中引入,也就是 ES6)。

現在我們來深入一點看上面的 JavaScript 代碼中的 「name 和 sum」。

我們可以把我們當前被分配了字元串「James」的變數 name 重新賦值為我們的第二個變數 sum 的當前值,目前是數字 3。

var name = &apos;James&apos;;
var sum = 1 + 2;

name = sum;

name 變數開始「存有」一個字元串,但現在它「存有」一個數字。這凸顯了 JavaScript 中變數和類型的基本特性:

「James」 值一直是字元串類型,而 name 變數可以分配任何類型的值。和 sum 賦值的情況相同,1 是一個數字類型,sum 變數可以分配任何可能的值。

在 JavaScript 中,值是具有類型的,而變數是可以隨時保存任何類型的值。

這也恰好是一個「動態類型語言」的定義。

相比之下,我們可以將「靜態類型語言」視為我們可以(也必須)將類型信息與特定變數相關聯的語言:

var name: string = 『James』;

在這段代碼中,我們能夠更好地顯式聲明我們對變數 name 的意圖,我們希望它總是用作一個字元串。

你猜怎麼著?我們剛剛看到我們的第一個 TypeScript 程序。

當我們 反思reflect我們自己的代碼(非編程方面的雙關語「反射」)時,我們可以得出的結論,即使我們使用動態語言(如 JavaScript),在幾乎所有的情況下,當我們初次定義變數和函數參數時,我們應該有非常明確的使用意圖。如果這些變數和參數被重新賦值為與我們原先賦值不同類型的值,那麼有可能某些東西並不是我們預期的那樣工作的。

作為 JavaScript 開發者,TypeScript 的靜態類型注釋給我們的一個巨大的幫助,它能夠清楚地表達我們對變數的意圖。

這種改進不僅有益於 TypeScript 編譯器,還可以讓我們的同事和將來的自己明白我們的代碼。代碼是用來讀的。

TypeScript 在我們的 JavaScript 工作流程中的作用

我們已經開始看到「為什麼經常說 TypeScript 只是 JavaScript + 靜態類型」的說法了。: string 對於我們的 name 變數就是我們所謂的「類型注釋」,在編譯時被使用(換句話說,當我們讓代碼通過 TypeScript 編譯器時),以確保其餘的代碼符合我們原來的意圖。

我們再來看看我們的程序,並添加顯式注釋,這次是我們的 sum 變數:

var name: string = &apos;James&apos;;
var sum: number = 1 + 2;

name = sum;

如果我們使用 TypeScript 編譯器編譯這個代碼,我們現在就會收到一個在 name = sum 這行的錯誤: Type &apos;number&apos; is not assignable to type &apos;string&apos;,我們的這種「偷渡」被警告,我們執行的代碼可能有問題。

重要的是,如果我們想要繼續執行,我們可以選擇忽略 TypeScript 編譯器的錯誤,因為它只是在將 JavaScript 代碼發送給我們的用戶之前給我們反饋的工具。

TypeScript 編譯器為我們輸出的最終 JavaScript 代碼將與上述原始源代碼完全相同:

var name = &apos;James&apos;;
var sum = 1 + 2;

name = sum;

類型注釋全部為我們自動刪除了,現在我們可以運行我們的代碼了。

注意:在此示例中,即使我們沒有提供顯式類型注釋的 : string: number ,TypeScript 編譯器也可以為我們提供完全相同的錯誤 。

TypeScript 通常能夠從我們使用它的方式推斷變數的類型!

我們的源文件是我們的文檔,TypeScript 是我們的拼寫檢查

對於 TypeScript 與我們的源代碼的關係來說,一個很好的類比,就是拼寫檢查與我們在 Microsoft Word 中寫的文檔的關係。

這兩個例子有三個關鍵的共同點:

  1. 它能告訴我們寫的東西的客觀的、直接的錯誤:
    • 拼寫檢查:「我們已經寫了字典中不存在的字」
    • TypeScript:「我們引用了一個符號(例如一個變數),它沒有在我們的程序中聲明」
  2. 它可以提醒我們寫的可能是錯誤的:
    • 拼寫檢查:「該工具無法完全推斷特定語句的含義,並建議重寫」
    • TypeScript:「該工具不能完全推斷特定變數的類型,並警告不要這樣使用它」
  3. 我們的來源可以用於其原始目的,無論工具是否存在錯誤:
    • 拼寫檢查:「即使您的文檔有很多拼寫錯誤,您仍然可以列印出來,並把它當成文檔使用」
    • TypeScript:「即使您的源代碼具有 TypeScript 錯誤,它仍然會生成您可以執行的 JavaScript 代碼」

TypeScript 是一種可以啟用其它工具的工具

TypeScript 編譯器由幾個不同的部分或階段組成。我們將通過查看這些部分之一 The Parser(語法分析程序)來結束這篇文章,除了 TypeScript 已經為我們做的以外,它為我們提供了在其上構建其它開發工具的機會。

編譯過程的「解析器步驟」的結果是所謂的抽象語法樹,簡稱為 AST。

什麼是抽象語法樹(AST)?

我們以普通文本形式編寫我們的程序,因為這是我們人類與計算機交互的最好方式,讓它們能夠做我們想要的東西。我們並不是很擅長於手工編寫複雜的數據結構!

然而,不管在哪種情況下,普通文本在編譯器裡面實際上是一個非常棘手的事情。它可能包含程序運作不必要的東西,例如空格,或者可能存在有歧義的部分。

因此,我們希望將我們的程序轉換成數據結構,將數據結構全部映射為我們所使用的所謂「標記」,並將其插入到我們的程序中。

這個數據結構正是 AST!

AST 可以通過多種不同的方式表示,我使用 JSON 來看一看。

我們從這個極其簡單的基本源代碼來看:

var a = 1;

TypeScript 編譯器的 Parser(語法分析程序)階段的(簡化後的)輸出將是以下 AST:

{
  "pos": 0,
  "end": 10,
  "kind": 256,
  "text": "var a = 1;",
  "statements": [
    {
      "pos": 0,
      "end": 10,
      "kind": 200,
      "declarationList": {
        "pos": 0,
        "end": 9,
        "kind": 219,
        "declarations": [
          {
            "pos": 3,
            "end": 9,
            "kind": 218,
            "name": {
              "pos": 3,
              "end": 5,
              "text": "a"
            },
            "initializer": {
              "pos": 7,
              "end": 9,
              "kind": 8,
              "text": "1"
            }
          }
        ]
      }
    }
  ]
}

我們的 AST 中的對象稱為節點。

示例:在 VS Code 中重命名符號

在內部,TypeScript 編譯器將使用 Parser 生成的 AST 來提供一些非常重要的事情,例如,發生在編譯程序時的類型檢查。

但它不止於此!

我們可以使用 AST 在 TypeScript 之上開發自己的工具,如代碼美化工具、代碼格式化工具和分析工具。

建立在這個 AST 代碼之上的工具的一個很好的例子是: 語言伺服器 Language Server

深入了解語言伺服器的工作原理超出了本文的範圍,但是當我們編寫程序時,它能為我們提供一個絕對重量級別功能,就是「重命名符號」。

假設我們有以下源代碼:

// The name of the author is James
var first_name = &apos;James&apos;;
console.log(first_name);

經過代碼審查和對完美的適當追求,我們決定應該改換我們的變數命名慣例;使用駝峰式命名方式,而不是我們當前正在使用這種蛇式命名。

在我們的代碼編輯器中,我們一直以來可以選擇多個相同的文本,並使用多個游標來一次更改它們。

Manually select matches

當我們把程序也視作文本這樣繼續操作時,我們已經陷入了一個典型的陷阱中。

那個注釋中我們不想修改的「name」單詞,在我們的手動匹配中卻被誤選中了。我們可以看到在現實世界的應用程序中這樣更改代碼是有多危險。

正如我們在上面學到的那樣,像 TypeScript 這樣的東西在幕後生成一個 AST 的時候,與我們的程序不再像普通文本那樣可以交互,每個標記在 AST 中都有自己的位置,而且它有很清晰的映射關係。

當我們右鍵單擊我們的 first_name 變數時,我們可以在 VS Code 中直接「重命名符號」(TypeScript 語言伺服器插件也可用於其他編輯器)。

Rename Symbol Example

非常好!現在我們的 first_name 變數是唯一需要改變的東西,如果需要的話,這個改變甚至會發生在我們項目中的多個文件中(與導出和導入的值一樣)!

總結

哦,我們在這篇文章中已經講了很多的內容。

我們把有關學術方面的規避開,圍繞編譯器和類型還有很多專業術語給出了通俗的定義。

我們對比了編譯語言與解釋語言、運行階段與編譯階段、動態類型與靜態類型,以及抽象語法樹(AST)如何為我們的程序構建工具提供了更為優化的方法。

重要的是,我們提供了 TypeScript 作為我們 JavaScript 開發工具的一種思路,以及如何在其上構建更棒的工具,比如說作為重構代碼的一種方式的重命名符號。

快來 UltimateAngular 平台上學習從初學者到 TypeScript 高手的課程吧,開啟你的學習之旅!

via: https://toddmotto.com/typescript-the-missing-introduction

作者:James Henry 譯者:MonkeyDEcho 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國