一、枚举

1. 什么是枚举

枚举(Enumeration)是一种特殊的数据类型,允许变量具有预定义的用户指定的值。在编程语言中,枚举类型用于表示变量可以仅赋值为一组预先定义的值的数据类型,即一组相关的常量。

枚举的用途主要包括:

(1) 提高代码清晰度:当一个变量只有几个特定的可能值时,使用枚举可以使代码更清晰,易读,易维护。例如,表示一周的七天,可以使用枚举类型来明确表示(如 Monday,Tuesday等)。

(2) 类型安全:如果使用枚举,编译器将检查赋给枚举变量的值是否在枚举的范围内,从而提供了类型安全。

(3) 方便比较:枚举值可以方便地用在switch语句或者if...else语句中。

(4) 更好的性能:枚举通常是整数类型,因此在处理上会比字符串等类型更加高效。

2. 传统方法模拟枚举的弊端

在 JavaScript 中并没有内建的枚举类型,我们可以用对象或者声明多个常量来模拟枚举。

(1) 使用对象模拟枚举

const Days = {
    MONDAY: 0,
    TUESDAY: 1,
    WEDNESDAY: 2,
    THURSDAY: 3,
    FRIDAY: 4,
    SATURDAY: 5,
    SUNDAY: 6
}

console.log(Days.MONDAY);  // 0

创建一个对象,该对象的属性代表枚举的值。这样做确实可以模拟出枚举的功能,但对象本身不具备安全性。枚举是一组相关的常量,使用对象的方式模拟枚举对象内的属性值可以被修改。虽然可以设置对象的属性不可写入,但实现过程比较繁琐。

(2) 使用多个常量模拟枚举

const MONDAY = 0;
const TUESDAY = 1;
const WEDNESDAY = 2;
const THURSDAY = 3;
const FRIDAY = 4;
const SATURDAY = 5;
const SUNDAY = 6;

console.log(MONDAY);  // 0

声明多个常量来模拟枚举,也可以实现枚举的功能。但这样做并没有体现出这些常量之间的关联性,只能我们人为定义它们相关联。如一个枚举作为一个 js 文件、使用注释标明等。

使用传统方法模拟枚举还存在一个语义不明确的问题,如果其他人使用这个枚举,看到枚举值为 4,如果不了解这个枚举的具体细节,他并不知道 4 代表的是哪个常量。

TypeScript 提供了真正的枚举类型,可以解决上述的弊端。

二、TypeScript 枚举

1. 数字枚举

// 枚举
enum DAYS {
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
}

console.log(DAYS.MONDAY); // 0
console.log(DAYS.WEDNESDAY); // 2

默认情况下,枚举的第一个值(在这个例子中是 MONDAY)被赋予0,然后每个后续的值被赋予比前一个值大1的值。所以在这个例子中,MONDAY 的值是0,TUESDA Y的值是1,等等。

2. 手动设置枚举值

// 枚举
enum DAYS {
    MONDAY = 1,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
}

console.log(DAYS.MONDAY); // 1
console.log(DAYS.WEDNESDAY); // 3

在这个例子中,我们设置了 MONDAY 的值为1,因为后续的值被赋予比前一个值大1的值,所以 WEDNESDAY 的值成了3.

3. 数字枚举实现的原理——反向映射

当我们在控制台直接打印枚举,会得到一个反向映射后的对象。

// 枚举
enum DAYS {
    MONDAY = 1,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
}

console.log(DAYS);
/* {
  "1": "MONDAY",
  "2": "TUESDAY",
  "3": "WEDNESDAY",
  "4": "THURSDAY",
  "5": "FRIDAY",
  "6": "SATURDAY",
  "7": "SUNDAY",
  "MONDAY": 1,
  "TUESDAY": 2,
  "WEDNESDAY": 3,
  "THURSDAY": 4,
  "FRIDAY": 5,
  "SATURDAY": 6,
  "SUNDAY": 7
} */

所谓反向映射指的是把原枚举中所有元素的元素值作为 key,元素名作为 value,把这些属性再添加到枚举对象中。具体实现过程如下:

var Days;
(function (Days) {
    Days[Days["MONDAY"] = 1] = "MONDAY";
    Days[Days["TUESDAY"] = 2] = "TUESDAY";
    Days[Days["WEDNESDAY"] = 3] = "WEDNESDAY";
    Days[Days["THURSDAY"] = 4] = "THURSDAY";
    Days[Days["FRIDAY"] = 5] = "FRIDAY";
    Days[Days["SATURDAY"] = 6] = "SATURDAY";
    Days[Days["SUNDAY"] = 7] = "SUNDAY";
})(Days || (Days = {}));

4. 字符串枚举

字符串枚举的创建和数字枚举一样,区别在于字符串枚举的枚举值为字符串,且实现过程不会做反向映射。

// 字符串枚举
enum MESSAGE {
    SUCCESS = '成功',
    FAIL = '失败'
}

console.log(MESSAGE);
/* {
  "SUCCESS": "成功",
  "FAIL": "失败"
} */

具体实现如下:

var MESSAGE;
(function (MESSAGE) {
    MESSAGE["SUCCESS"] = "\u6210\u529F";
    MESSAGE["FAIL"] = "\u5931\u8D25";
})(MESSAGE || (MESSAGE = {}));

5. 异构枚举

异构枚举的元素同时包含数字枚举和字符串枚举,在实现过程中数字枚举元素会进行反向映射,字符串枚举元素保持不变。

// 异构枚举
enum MIXIN {
    Y,
    FAIL = '失败'
}

console.log(MIXIN);
/* {
  "0": "Y",
  "Y": 0,
  "FAIL": "失败"
} */

具体实现如下:

var MIXIN;
(function (MIXIN) {
    MIXIN[MIXIN["Y"] = 0] = "Y";
    MIXIN["FAIL"] = "\u5931\u8D25";
})(MIXIN || (MIXIN = {}));

6. 常量枚举

当我们不需要一个对象,只需要对象的值的时候,就可以使用常量枚举,可以节省编译环境中的代码。

在声明枚举类型时,前面加上 const 关键字,即可声明一个常量枚举。

// 常量枚举
const enum NUMBER {
    ONE,
    TWO,
    THREE
}
let count = [NUMBER.ONE, NUMBER.TWO, NUMBER.THREE];
console.log(count); // [0, 1, 2]

具体实现如下:

let count = [0 /* NUMBER.ONE */, 1 /* NUMBER.TWO */, 2 /* NUMBER.THREE */];

可以看到编译后的 js 文件中,并没有声明 NUMBER 对象。TS 允许我们用枚举的代码格式来使用常量枚举,增加代码的可读性。 

三、枚举成员的特性

1. 成员只读,无法修改

如果修改枚举成员的值,会报错。

enum MIXIN {
    Y,
    FAIL = '失败'
}

MIXIN.Y = 1;
console.log(MIXIN);

报错:Cannot assign to 'Y' because it is a read-only property.,表示不能给枚举成员赋值,因为它是一个常量。

2. 数字枚举 value 的类型

可以使用默认值、常量、常量表达式、非常量表达式和随机值多种类型。

// 枚举
enum DAYS {
    MONDAY, // 默认值
    TUESDAY = 2, // 常量
    WEDNESDAY = 2 + 1, // 常量表达式
    THURSDAY = 'hello ts'.length, // 非常量表达式
    FRIDAY = Math.random(), // 随机值
    SATURDAY,
    SUNDAY
}

console.log(DAYS);

上述语法会报错:Enum member must have initializer.,表示枚举成员必须要初始化。因为我们同时使用了多种类型的 value,编辑器无法判断最后两个元素使用哪种类型的 value。

解决方案为为最后面2个元素设置初始值:

// 枚举
enum DAYS {
    MONDAY, // 默认值
    TUESDAY = 2, // 常量
    WEDNESDAY = 2 + 1, // 常量表达式
    THURSDAY = 'hello ts'.length, // 非常量表达式
    FRIDAY = Math.random(), // 随机值
    SATURDAY = 3,
    SUNDAY = 4
}

console.log(DAYS);
/* {
  "0": "MONDAY",
  "2": "TUESDAY",
  "3": "SATURDAY",
  "4": "SUNDAY",
  "8": "THURSDAY",
  "MONDAY": 0,
  "TUESDAY": 2,
  "WEDNESDAY": 3,
  "THURSDAY": 8,
  "FRIDAY": 0.825442810801333,
  "0.825442810801333": "FRIDAY",
  "SATURDAY": 3,
  "SUNDAY": 4
} */

具体实现如下:

var DAYS;
(function (DAYS) {
    DAYS[DAYS["MONDAY"] = 0] = "MONDAY";
    DAYS[DAYS["TUESDAY"] = 2] = "TUESDAY";
    DAYS[DAYS["WEDNESDAY"] = 3] = "WEDNESDAY";
    DAYS[DAYS["THURSDAY"] = 'hello ts'.length] = "THURSDAY";
    DAYS[DAYS["FRIDAY"] = Math.random()] = "FRIDAY";
    DAYS[DAYS["SATURDAY"] = 3] = "SATURDAY";
    DAYS[DAYS["SUNDAY"] = 4] = "SUNDAY";
})(DAYS || (DAYS = {}));

可以看到,常量即常量表达式在编译阶段就会进行计算,如上例种 WEDNESDAY 的值由2+1计算得到3,而非常量表达式并没有在编译阶段计算,在运行阶段才会计算。

3. 枚举及枚举成员都可以作为单独的类型存在

看下面的例子:

enum N {
    x = 1,
    y = 2
}
let z: N = 3;

在这个例子中,我们定义了一个枚举类型 N,有2个成员。成员 x 的值为1,成员 y 的值为2。我们定义了一个变量 z,它的类型为枚举 N,值为3。

会报错:Type '3' is not assignable to type 'N'.,表示3不能赋值给枚举 N 类型的变量。因为3超出了枚举的范围,枚举 N 中并没有值为3的成员,改为1或2即可通过编译。

enum N {
    x = 1,
    y = 2
}
let z: N = 2;

四、使用枚举的优势

TS 中的枚举类型提供了一种有效的方式来定义一组命名的数值常量,以清晰地表示代码的意图。使用枚举的优势包括:

1. 提高代码的可读性

枚举通过为一组相关的数值提供有意义的名字,使得代码更易于理解和阅读。例如,使用枚举来表示星期的七天,如 enum Day {MONDAY, TUESDAY, WEDNESDAY, ...},比起使用一组无意义的整数,枚举使得代码的意图更加明显。

2. 类型安全

枚举类型是类型安全的,这意味着你不能将任何其他值赋值给一个枚举类型的变量。这在编译时提供了一个额外的错误检查层,有助于避免错误。

3. 易于维护

如果你的代码依赖于一组特定的数值,而这些数值可能会在项目的生命周期中改变,使用枚举可以使得这些改变更容易管理。当你需要修改或添加新的值时,只需要更新枚举的定义即可,而不需要搜索和替换整个代码库中的硬编码值。

4. 方便调试

在调试过程中,看到一个具有描述性名称的枚举值比看到一个可能没有任何含义的数值更有帮助。

5. 提高性能

枚举值通常在编译时解析,因此在运行时没有额外的成本,这使得它们在性能上非常有效。

以上这些优势使得枚举在编程中成为一种非常有用的工具,我们需要养成一种使用枚举的思维,在需要一组固定值,并且这些值具有明确含义的场景中,应该去使用枚举。