「这是我参与2022首次更文挑战的第23天,活动详情查看:2022首次更文挑战」。
枚举
在任何项目开发中,我们都会遇到定义常量的情况,常量就是指不会被改变的值。
TS 中我们使用 const
来声明常量,但是有些取值是在一定范围内的一系列常量,比如一周有七天,比如方向分为上下左右四个方向。
这时就可以使用枚举(Enum)来定义。
基本使用
enum Direction {
Up,
Down,
Left,
Right
}
这样就定义了一个数字枚举,他有两个特点:
- 数字递增
- 反向映射
枚举成员会被赋值为从 0
开始递增的数字,
console.log(Direction.Up) // 0
console.log(Direction.Down) // 1
console.log(Direction.Left) // 2
console.log(Direction.Right) // 3
枚举会对枚举值到枚举名进行反向映射,
console.log(Direction[0]) // Up
console.log(Direction[1]) // Down
console.log(Direction[2]) // Left
console.log(Direction[3]) // Right
如果枚举第一个元素赋有初始值,就会从初始值开始递增,
enum Direction {
Up = 6,
Down,
Left,
Right
}
console.log(Direction.Up) // 6
console.log(Direction.Down) // 7
console.log(Direction.Left) // 8
console.log(Direction.Right) // 9
反向映射的原理
枚举是如何做到反向映射的呢,我们不妨来看一下被编译后的代码,
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 6] = "Up";
Direction[Direction["Down"] = 7] = "Down";
Direction[Direction["Left"] = 8] = "Left";
Direction[Direction["Right"] = 9] = "Right";
})(Direction || (Direction = {}));
主体代码是被包裹在一个自执行函数里,封装了自己独特的作用域。
Direction["Up"] = 6
会将 Direction 这个对象的 Up 属性赋值为 6,JS 的赋值运算符返回的值是被赋予的值。
Direction["Up"] = 6 返回 6
执行 Direction[Direction["Up"] = 6] = "Up";
相当于执行
Direction["Up"] = 6
Direction[6] = "Up"
这样就实现了枚举的反向映射。
手动赋值
定义一个枚举来管理外卖状态,分别有已下单,配送中,已接收三个状态。
可以这么写,
enum ItemStatus {
Buy = 1,
Send,
Receive
}
console.log(ItemStatus['Buy']) // 1
console.log(ItemStatus['Send']) // 2
console.log(ItemStatus['Receive']) // 3
但有时候后端给你返回的数据状态是乱的,就需要我们手动赋值。
比如后端说 Buy 是 100,Send 是 20,Receive 是 1,就可以这么写,
enum ItemStatus {
Buy = 100,
Send = 20,
Receive = 1
}
console.log(ItemStatus['Buy']) // 100
console.log(ItemStatus['Send']) // 20
console.log(ItemStatus['Receive']) // 1
别问为什么,实际开发中经常会有这种情况发生。
计算成员
枚举中的成员可以被计算,比如经典的使用位运算合并权限,可以这么写,
enum FileAccess {
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
}
console.log(FileAccess.Read) // 2 -> 010
console.log(FileAccess.Write) // 4 -> 100
console.log(FileAccess.ReadWrite) // 6 -> 110
看个实例吧,Vue3 源码中的 patchFlags,用于标识节点更新的属性。
// packages/shared/src/patchFlags.ts
export const enum PatchFlags {
TEXT = 1, // 动态文本节点
CLASS = 1 << 1, // 动态 class
STYLE = 1 << 2, // 动态 style
PROPS = 1 << 3, // 动态属性
FULL_PROPS = 1 << 4, // 具有动态 key 属性,当 key 改变时,需要进行完整的 diff 比较
HYDRATE_EVENTS = 1 << 5, // 具有监听事件的节点
STABLE_FRAGMENT = 1 << 6, // 子节点顺序不会被改变的 fragment
KEYED_FRAGMENT = 1 << 7, // 带有 key 属或部分子节点有 key 的 fragment
UNKEYED_FRAGMENT = 1 << 8, // 子节点没有 key 的 fragment
NEED_PATCH = 1 << 9, // 非 props 的比较,比如 ref 或指令
DYNAMIC_SLOTS = 1 << 10, // 动态插槽
DEV_ROOT_FRAGMENT = 1 << 11, // 仅供开发时使用,表示将注释放在模板根级别的片段
HOISTED = -1, // 静态节点
BAIL = -2 // diff 算法要退出优化模式
}
字符串枚举
字符串枚举的意义在于,提供有具体语义的字符串,可以更容易地理解代码和调试。
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
const value = 'UP'
if (value === Direction.Up) {
// do something
}
常量枚举
上文的例子,使用 const 来定义一个常量枚举
const enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
const value = 'UP'
if (value === Direction.Up) {
// do something
}
编译出来的 JS 代码会简洁很多,提高了性能。
const value = 'UP';
if (value === 'UP' /* Up */) {
// do something
}
不写 const 编译出来是这样的,
var Direction;
(function (Direction) {
Direction["Up"] = "UP";
Direction["Down"] = "DOWN";
Direction["Left"] = "LEFT";
Direction["Right"] = "RIGHT";
})(Direction || (Direction = {}));
const value = 'UP';
if (value === Direction.Up) {
// do something
}
这一堆定义枚举的逻辑会在编译阶段会被删除,常量枚举成员在使用的地方被内联进去。
很显然,常量枚举不允许包含计算成员,不然怎么叫常量呢?
const enum Test {
A = "lin".length
}
这么写直接报错:
总结一下,常量枚举可以避免在额外生成的代码上的开销和额外的非直接的对枚举成员的访问。
小结
枚举的意义在于,可以定义一些带名字的常量集合,清晰地表达意图和语义,更容易地理解代码和调试。
常用于和后端联调时,区分后端返回的一些代表状态语义的数字或字符串,降低阅读代码时的心智负担。
往期