TypeScript魔法堂:枚舉的超實用手冊
- 2020 年 10 月 29 日
- 筆記
- javascript
前言
也許前端的同學會問JavaScript從誕生至今都沒有枚舉類型,我們不是都活得挺好的嗎?為什麼TypeScript需要引入枚舉類型呢?
也許被迫寫前端的後端同學會問,TypeScript的枚舉類型是和Java/.NET的一樣嗎?
下面我們來一起探討和嘗試解答吧!
前端一直都需要枚舉
我敢保證,前端的同學都會萬分肯定地告訴大家:我們從來沒有寫過枚舉。那是因為雖然ECMAScript將enum
作為保留字,但至ES2020為止還沒有提出枚舉的實現規範。語言沒有提供規範和語言實現,不代表思想活躍勇於造輪子的程式設計師們不會自己擼一個。
如果語言沒有提供,還有那麼毅然決然要自己造一個,那枚舉到底能解決我們什麼問題呢?
枚舉真的有點用
首先,枚舉字面上的意思就遍歷一個存在若干個的值有窮集合的所有成員。核心有兩點:
- 有窮集合;
- 遍歷。
也就是說,只要我們需要表示某個變數的值必須為某個有窮集合的成員時,我們是怎麼也繞不開枚舉的。
寫個JavaScript版本的枚舉
下面是剛好滿足大部分業務需求的枚舉實現:
class Color {
// tricky:自增枚舉成員值
static counter = null
// 枚舉成員
static Red = new Color('Red')
static Green = new Color('Green')
// 反向映射
static valueOf(value) {
for (var name in Color) {
if (!(name in Color.prototype) && Color[name].value === value) {
return Color[name]
}
}
}
constructor(name, value){
if ('counter' in Color);else return
this.name = name
if (value == null) {
if (Color.counter === null) {
this.value = Color.counter = 0
}
else {
this.value = ++Color.counter
}
}
else {
this.value = Color.counter = value
}
}
toString() {
return `Color.${this.name}`
}
}
delete Color.counter
Object.freeze(Color) // tricky:禁止在定義之外的位置修改枚舉成員
其實我們只想表達某些變數將以含有Red、Green兩個成員的Color有窮集合作為值域而已,卻要寫這麼多語義無關的程式碼(嚴格遵循「能寫hi絕對不寫hello」原則)。而且在一般規模的項目當中,往往不止一個枚舉類型,複製粘貼確實可以解決問題,但真心不優雅。
而TypeScript內置枚舉的語言實現恰恰能解決這個問題。
TypeScript的枚舉和後端的真不一樣
後端的同學對枚舉絕對是不會陌生的(除非是Pyton/Nodejs後端的同學啦),雖然TypeScript是JavaScript的超集,但最終需要編譯為JavaScript程式碼,並且要兼容現有JavaScript庫,所以確實無法和後端的枚舉類型一模一樣。
所以我還是建議大家運用空杯心理,重頭理解TypeScript的枚舉類型,將過去的知識作為助燃劑,而不是圍欄更適宜。
數字枚舉類型和字元串枚舉類型
TypeScript官網教程已經對枚舉類型進行了詳細的講解說明,我認為最核心是理解清楚其分為兩大類:
- 數字枚舉類型
enum Response {
No = 0, // 手動設置初始化器
Yes = // 附加默認的支援自動增長的初始化器,因此Yes的值為1
}
特性為:
1.1. 枚舉成員附帶默認的初始化器;
1.2. 初始化器支援自動增長;
1.3. 支援反向映射。(注意:這裡是反向映射,而不是通過值轉換為枚舉成員)
- 字元串枚舉類型
enum Color {
Red = 'Red',
Green = 'Green',
}
特性為:
1.1. 必須為枚舉成員設置初始化器;
1.2. 初始化器不支援自動增長;
1.3. 不支援反向映射。
而計算和常量成員其實就是上述兩種枚舉類型中初始化器的細分特性罷了。
enum
讓數字枚舉類型反向映射成為可能
上一節介紹到數字枚舉類型支援反向映射,但前提是通過enum
定義的數字枚舉類型才支援。那是因為enum Respose {No,Yes,}
最終會被編譯為如下JavaScript程式碼:
var Response;
(function (Response) {
Response[Response["No"] = 0] = "No";
Response[Response["Yes"] = 1] = "Yes";
})(Response || (Response = {}));
那麼我們就可以通過Response[0]反向映射得到“No”。
但對於字元串枚舉類型就沒有這種好事了,看看enum Color {Red='Red',Green='Green',}
編譯出來的程式碼吧
var Color;
(function (Color) {
Color["Red"] = "Red";
Color["Green"] = "Green";
})(Color || (Color = {}));
只能說非常樸實無華,就這樣沒啥好說的,大家在使用時注意差異就好了。
const enum
高效的編譯時內聯
官方文檔明確寫出「大多數情況下,枚舉是十分有效的方案。 然而在某些情況下需求很嚴格。 為了避免在額外生成的程式碼上的開銷和額外的非直接的對枚舉成員的訪問,我們可以使用 const枚舉」,那是為什麼呢?
那是因為通過const enum
定義的編譯時枚舉類型,效果和通過C/C++的#define
定義常量沒實質區別。說白了就是假如僅僅通過通過const enum
定義了枚舉類型而沒有其它地方調用,這段程式碼將在編譯時被直接抹掉。
當其它地方調用該枚舉類型時,將直接把枚舉類型成員的值內聯到使用處,如下:
const enum Response {
No,
Yes,
}
console.log(Response.NO, Response.Yes)
編譯後成為console.log(0, 1)
,運行效果自然只能比enum
定義的好。
什麼時候用enum
?又在什麼場景下用const enum
呢?
先說說結論:
- 使用
enum
的場景:
1.1. 需要使用反向映射時;
1.2. 需要編譯後的JavaScript程式碼保留對象.屬性
或對象[屬性]
形式時。 - 使用
const enum
的場景:能不用enum
時就用const enum
(哈哈!)
使用enum
的場景中的第一條還很好理解,但第二條是啥回事呢?我這裡有個真實發生的示例,可以讓大家更好的理解:
背景:為Photoshop的ExtendScript編寫類型聲明。
需求:DialogModes.NO
在ExtendScript中返回值為DialogModes.No
本身,編譯後的JavaScript中必須保留DialogModes.NO
的程式碼形式。
那麼又為何鼓勵大家能用const enum
時就用const enum
呢?
這是TypeScript為大家特意準備的編譯時優化方式,好東西為啥不用呢?編譯時優化難道不香嗎?
外部枚舉declare enum
的作用?
所謂外部枚舉,即使我們為了在TypeScript開發環境下,更好地使用某些已採用JavaScript編寫的庫,而被迫為其編寫的枚舉類型聲明。
如ExtendScript標準庫存在枚舉DialogModes.NO
,DialogModes.YES
,DialogModes.ALL
。於是在.d.ts
文件中編寫如下外部枚舉類型聲明
declare enum DialogModes {
NO,
YES,
ALL,
}
總結
對於日常開發中我們繞不過枚舉類型,TypeScript為我們提供語言實現和編譯時優化,除了保護了我們為如何優化實現枚舉類型而日思夜想導致日漸稀疏的頭髮外,還大大降低了因複製粘貼帶來的程式碼庫體積徒增的風險。
寫更少的程式碼,做正確的事,早點下班豈不更快哉_
轉載請註明來自://www.cnblogs.com/fsjohnhuang/p/13893641.html —— _肥仔John