Appearance
深入理解枚举类型
深入理解枚举类型
枚举类型是很多语言都拥有的类型,它用于声明一组命名的常数,当一个变量有几种可能的取值时,可以将它定义为枚举类型。
数字枚举
当我们声明一个枚举类型是,虽然没有给它们赋值,但是它们的值其实是默认的数字类型,而且默认从0开始依次累加:
enum Direction {
Up,
Down,
Left,
Right
}
console.log(Direction.Up === 0); // true
console.log(Direction.Down === 1); // true
console.log(Direction.Left === 2); // true
console.log(Direction.Right === 3); // true
因此当我们把第一个值赋值后,后面也会根据第一个值进行累加:
enum Direction {
Up = 10,
Down,
Left,
Right
}
console.log(Direction.Up, Direction.Down, Direction.Left, Direction.Right); // 10 11 12 13
字符串枚举
枚举类型的值其实也可以是字符串类型:
enum Direction {
Up = 'Up',
Down = 'Down',
Left = 'Left',
Right = 'Right'
}
console.log(Direction['Right'], Direction.Up); // Right Up
异构枚举
既然我们已经有了字符串枚举和数字枚举,那么这两个枚举是不是可以混合使用呢?
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
}
是的,这样也是没问题的,通常情况下我们很少会这样使用枚举,但是从技术的角度来说,它是可行的。
反向映射
我们看一个例子:
enum Direction {
Up,
Down,
Left,
Right
}
console.log(Direction.Up === 0); // true
console.log(Direction.Down === 1); // true
console.log(Direction.Left === 2); // true
console.log(Direction.Right === 3); // true
这就是我们数字枚举那一部分的例子,我们可以通过枚举名字获取枚举值,这当然看起来没问题,那么能不能通过枚举值获取枚举名字呢?
是可以的:
enum Direction {
Up,
Down,
Left,
Right
}
console.log(Direction[0]); // Up
这就很奇怪了,我们印象中一个 JavaScript 对象一般都是正向映射的,即 name => value
,为什么在枚举中是可以正反向同时映射的?即 name <=> value
。
我们往下看,通过了解枚举的本质,我们就可以理解这种正反向同时映射的特性了。
枚举的本质
以上面的 Direction
枚举类型为例,我们不妨看一下枚举类型被编译为 JavaScript 后是什么样子:
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 10] = "Up";
Direction[Direction["Down"] = 11] = "Down";
Direction[Direction["Left"] = 12] = "Left";
Direction[Direction["Right"] = 13] = "Right";
})(Direction || (Direction = {}));
这个编译后的代码可能看起来比较复杂,不过我们可以把Direction
看成一个对象,比如我们在 TypeScript 中做几个小实验:
enum Direction {
Up = 10,
Down,
Left,
Right
}
console.log(Direction[10], Direction['Right']); // Up 13
原因就在编译后的 JavaScript 中体现出来了,因为 Direction[Direction["Up"] = 10] = "Up"
也就是 Direction[10] = "Up"
,所以我们可以把枚举类型看成一个JavaScript对象,而由于其特殊的构造,导致其拥有正反向同时映射的特性。
常量枚举
枚举其实可以被 const
声明为常量的,这样有什么好处?我们看以下例子:
const enum Direction {
Up = 'Up',
Down = 'Down',
Left = 'Left',
Right = 'Right'
}
const a = Direction.Up;
大家猜一下它被编译为 JavaScript 后是怎样的?
var a = "Up";
我们在上面看到枚举类型会被编译为 JavaScript 对象,怎么这里没有了?
这就是常量枚举的作用,因为下面的变量 a
已经使用过了枚举类型,之后就没有用了,也没有必要存在与 JavaScript 中了, TypeScript 在这一步就把 Direction
去掉了,我们直接使用 Direction
的值即可,这是性能提升的一个方案。
如果你非要 TypeScript 保留对象 Direction ,那么可以添加编译选项
--preserveConstEnums
联合枚举与枚举成员的类型
我们假设枚举的所有成员都是字面量类型的值,那么枚举的每个成员和枚举值本身都可以作为类型来使用,
- 任何字符串字面量,如:
const enum Direction {
Up = 'Up',
Down = 'Down',
Left = 'Left',
Right = 'Right'
}
- 任何数字字面量,如:
enum Direction {
Up,
Down,
Left,
Right
}
- 应用了一元
-
符号的数字字面量,如:
enum Direction {
Up = -1,
Down = -2,
Left = -3,
Right = -4,
}
枚举成员类型
当所有枚举成员都拥有字面量枚举值时,它就带有了一种特殊的语义,即枚举成员成为了类型。
比如我们声明一个数字类型:
enum Direction {
Up,
Down,
Left,
Right
}
const a = 0
console.log(a === Direction.Up) // true
我们把成员当做值使用,看来是没问题的,因为成员值本身就是0,那么我们再加几行代码:
type c = 0
declare let b: c
b = 1 // 不能将类型“1”分配给类型“0”
b = Direction.Up // ok
我们看到,上面的结果显示这个枚举的成员居然也可以被当做类型使用,这就是枚举成员当做类型使用的情况。
联合枚举类型
由于联合联合枚举,类型系统可以知道枚举里的值的集合。
联合类型会在后面的章节提到
enum Direction {
Up,
Down,
Left,
Right
}
declare let a: Direction
enum Animal {
Dog,
Cat
}
a = Direction.Up // ok
a = Animal.Dog // 不能将类型“Animal.Dog”分配给类型“Direction”
我们把 a
声明为 Direction
类型,可以看成我们声明了一个联合类型 Direction.Up | Direction.Down | Direction.Left | Direction.Right
,只有这四个类型其中的成员才符合要求。
枚举合并
我们可以分开声明枚举,他们会自动合并
enum Direction {
Up = 'Up',
Down = 'Down',
Left = 'Left',
Right = 'Right'
}
enum Direction {
Center = 1
}
编译为 JavaScript 后的代码如下:
var Direction;
(function (Direction) {
Direction["Up"] = "Up";
Direction["Down"] = "Down";
Direction["Left"] = "Left";
Direction["Right"] = "Right";
})(Direction || (Direction = {}));
(function (Direction) {
Direction[Direction["Center"] = 1] = "Center";
})(Direction || (Direction = {}));
因此上面的代码并不冲突。
为枚举添加静态方法
借助 namespace
命名空间,我们甚至可以给枚举添加静态方法。
我们举个简单的例子,假设有十二个月份:
enum Month {
January,
February,
March,
April,
May,
June,
July,
August,
September,
October,
November,
December,
}
我们要编写一个静态方法,这个方法可以帮助我们把夏天的月份找出来:
function isSummer(month: Month) {
switch (month) {
case Month.June:
case Month.July:
case Month.August:
return true;
default:
return false
}
}
「命名空间」在第 30 小节,读者可以暂时跳过下面内容
想要把两者结合就需要借助命名空间的力量了:
namespace Month {
export function isSummer(month: Month) {
switch (month) {
case Month.June:
case Month.July:
case Month.August:
return true;
default:
return false
}
}
}
console.log(Month.isSummer(Month.January)) // false
小结
我们本节深入理解了枚举类型,通过编译后的 JavaScript 了解到其实本质上是 JavaScript 对象.