列舉型別分享 第五節

yxl87發表於2020-11-27

在 JS 語言裡面並不存在語言層面的列舉型別,而 TS 將列舉型別新增到了語言的型別系統裡面,這樣做的優勢:

  1. 開發者更容易清晰的窮盡某個 case 的各種可能;
  2. 更容易以文件的形式列出程式邏輯,增加可讀性;

一、整型列舉

//數字型列舉更貼近其他語言中設計的列舉型別
enum Direction {
  Up = 1,
  Down,
  Left,
  Right,
}

上述列舉型別的定義中,我們給 Up 賦值了 1,所有剩下的成員會採用自增長的方式被賦值,比如 Down 就為 2,Left=3, Right=4。
如果不給列舉變數賦值,則數字型列舉採用 0 開頭的索引,並自增長賦值。

列舉的使用也很簡單,如下

enum UserRespongse {
  No=0,
  Yes=1,
}
function respond(redcipient: string, message: UserResponse): void{
  //...
}
respond("Princess Caroline", UserResponse.Yes);

數字型列舉能與計算型別或者常量型別的列舉變數混用。不過沒有初始化器(initializer)的成員要麼放在列舉宣告的第一位,要麼跟在一個被明確賦值的數字列舉變數後面,下面這種情況會報錯:

enum E{
  A = getSomeValue(),
  B, //Enum member must have initializer.
}

二、字串型列舉

字串型別列舉跟上述數字型列舉差不多,但是在執行時稍微有些不易察覺的區別,下面我們展開說一下。

在字串型列舉裡面各個變數都必須被字串字面量初始化或者被其他字串型列舉成員初始化,如下:

enum Direction{
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}

字串型別的列舉沒有預設的自增長機制(這是肯定的),但是也有其優勢,就是程式碼再執行時,當我們檢視執行時程式碼的時候,字串型列舉每個列舉變數的值都是清晰可閱讀的,反觀數字型列舉,基本得不到什麼有用的資訊。

三、異構型列舉

示例如下:

enum BooleanLikeHeterogeneousEnum{
  No = 0,
  Yes = "YES",
}

單純的從技術實現上來講,字串型和整型列舉可以混合使用,但是這並不是一種比較明智的coding 方式,除非你真的想利用 JS 的執行時特點,否則不建議應用這種方式到日常程式碼開發中。

四、計算型和常量型別列舉變數

每個列舉變數都會被賦值,這個變數值要麼是常量,要麼是計算後的值。一般下面的情況我們認為列舉成員的值是常量:

  • 列舉成員是在一個位置,並且沒有被顯示初始化,實際上是預設賦值為 0
// E.X is constant
enum E{
  X,
}
  • 列舉成員沒有被顯示初始化,並且其前面的列舉成員是整型,這種情況下,當前的列舉成員被賦的值是前面列舉成員值加 1
// All enum members in E1 and E2 are constant
enum E1{
  X,
  Y,
  Z,
}
enum E2{
  A = 1,
  B,
  C,
}
  • 列舉成員被一個常量列舉表示式賦值。一個常量列舉表示式是在編譯階段能被完全計算出值的 TS 表示式的一個子集。如果滿足如下條件就可以認定為常量列舉表示式:
  1. 一個字面量列舉表示式(其實就是一個字串字面量或者數字字面量)
  2. 一個對之前宣告的常量列舉成員的引用
  3. 括號括起來的常量列舉表示式
  4. +,-,~一元操作符 表示式
  5. +,-,*,/,%,<<,>>,>>>,&,|,^合法的二元操作表示式
  6. 如果一個常量表示式最後運算結果是NaN或者Infinity,TS 將會報告編譯時錯誤

除此之外其他所有情況都可視為計算型列舉

enum FileAccess {
  // constant members
  None,
  Read = 1 << 1,
  Write = 1 << 2,
  ReadWrite = Read | Write,
  // computed member
  G = "123".length,
}

五、聯合列舉和列舉成員型別

常量列舉成員有一個不需要計算的特殊的子集:字面量列舉成員。一個字面量列舉成員是一個不需要初始化值的常量列舉成員或者值是被以下情況初始化的:

  • 任意的字串字面量(e.g."foo","bar,"baz"
  • 任意的數字字面量(e.g.1,100)
  • 任意數字字面量的一元操作表示式(e.g.-1,-100)

當一個列舉型別裡面所有的成員都被賦值了字面量值,就會出現一些特殊的程式設計模式。

首先:列舉成員也能變成型別。舉個例子,我們可以看到如下某個成員變數僅僅擁有一個列舉變數型別:

enum ShapeKind {
  Circle,
  Square,
}
interface Circle {
  kind: ShapeKind.Circle;
  radius: number;
}
interface Square {
  kind: ShapeKind.Square;
  sideLength: number;
}
let c: Circle = {
  kind: ShapeKind.Square,
//Type 'ShapeKind.Square' is not assignable to type'ShapeKind.Circle'.
  radius: 100,
};

其次,列舉類本身能變成每個列舉成員的聯合型別。利用列舉成員組成的聯合型別,型別檢測系統能清楚的知道列舉本身內部存在的每個成員,TS 從而可以發現 bug,當我們對比錯誤的變數的時候。示例如下:

enum E {
  Foo,
  Bar,
}
function f(x: E) {
  if (x !== E.Foo || x !== E.Bar) {
//上述 if 判斷總是會返回 true,因為 E.Foo 和 E.Bar並沒有交集
// 首先檢查 x 是否不等於 E.Foo,如果檢查通過,根據邏輯運算的短路原則最終表示式判
// 定為 true,如果檢查不通過,那麼 x 的型別只能為 E.Foo,那麼 x !== E.Bar通過,
// 仍然返回 true
  }
}

六、 執行時(runtime)的列舉

列舉是存在在執行時(runtime)中的真實物件,例如:

enum E {
  X,
  Y,
  Z,
}
//上面的列舉型別能作為引數傳遞進函式
function f(obj: { X: number }) {
  return obj.X;
}
// Works, since 'E' has a property named 'X' which is a number.
f(E);

七、編譯時(compile-time)的列舉

儘管列舉在 js 執行時裡面是真實存在的物件,但是用 keyof 關鍵字處理列舉得到的結果可能與標準的物件有所區別。事實上,用 keyof typeof 來獲取列舉型別的時候,發現所有的列舉的鍵都是 string 型別。

enum LogLevel {
  ERROR,
  WARN,
  INFO,
  DEBUG,
}
/**
 * This is equivalent to:
 * type LogLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
 */
type LogLevelStrings = keyof typeof LogLevel;
function printImportant(key: LogLevelStrings, message: string) {
  const num = LogLevel[key];
  if (num <= LogLevel.WARN) {
    console.log("Log level key is:", key);
    console.log("Log level value is:", num);
    console.log("Log level message is:", message);
  }
}
printImportant("ERROR", "This is a message");

八、反向對映

整型列舉也可以反向對映,從列舉成員的值對映為列舉成員的變數名,舉個例子:

enum Enum {
  A,
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"

TS 將上述邏輯轉譯為如下 js 邏輯:

"use strict";
var Enum;
(function (Enum) {
    Enum[Enum["A"] = 0] = "A";
})(Enum || (Enum = {}));
let a = Enum.A;
let nameOfA = Enum[a]; // "A"

注意:字串型列舉沒有反向對映

九、常量列舉型別

大多數情況下,列舉型別是十分有效的解決方案。但是在個別情況下,為了避免轉譯程式碼帶來的額外的開銷以及訪問列舉型別帶來的間接性,於是就出現了 const 關鍵字宣告的列舉型別,如下:

const enum Enum {
  A = 1,
  B = A * 2,
}

常量列舉型別只能使用常量列舉表示式,同時也不想普通的列舉型別,常量列舉型別不會存在於執行時,會在編譯時被替換。常量列舉型別的成員會在編譯時直接替換為其列舉成員的值。

const enum Direction {
  Up,
  Down,
  Left,
  Right,
}
let directions = [
  Direction.Up,
  Direction.Down,
  Direction.Left,
  Direction.Right,
];
// 上述列舉型別轉譯後程式碼如下
"use strict";
let directions = [
    0 /* Up */,
    1 /* Down */,
    2 /* Left */,
    3 /* Right */,
];

相關文章