typescript 枚举

枚举的基础
  • 就像官网介绍的,枚举是对 js 标准数据类型的补充,声明一组带名字的常量
// 利用 const 关键词也可以声明一组常量,例如,声明十二生肖的排位
const rat: number = 1
const cattle: number = 2
const tiger: number = 3
const rabbit: number = 4
const dragon: number = 5
// 上述只声明了 5 个,如果声明全排位,需要声明 12 个变量,并且注明
// 类型,但是却多了很多重复性工作,利用数字枚举,我们可以轻松声明同样的一组常量
enum ChineseZodiac {
  rat,
  cattle,
  tiger,
  rabbit,
  dragon
}
// 如果想访问 dragon 生肖,只需要像 js 中对象使用点语法或者中括号访问对象成员即可
ChineseZodiac.dragon === ChineseZodiac['dragon'] => true
  • 枚举按照枚举成员的类型可归为两大类:数字枚举类型和字符串枚举类型;
// 枚举使用 enum 关键词来声明一个枚举,数字枚举和字符串枚举的区别
// 就是枚举成员是常数还是字符串;还是以十二生肖为例:
// 01-数字枚举,声明了一个枚举成员均不带初始化器的枚举,
// 该枚举的成员值从 0 开始,依次递增
enum NumChineseZodiac {
  rat,
  cattle,
  tiger,
  rabbit,
  dragon
}
// 02-字符串枚举
enum StrChineseZodiac {
  rat = 'rat',
  cattle = 'cattle',
  tiger = 'tiger',
  rabbit = 'rabbit',
  dragon = 'dragon'
}
// 关于两者之间细微的区别,下面会说到
  • 枚举被编译之后,本质为键值对形式的对象,但是这个行为会因为枚举声明的方式以及枚举成员类型的不同,编译之后的结果也不同,下面也会详细对比这个不同点。
枚举的基本分类
数字枚举
  • 枚举的声明
// 数字枚举的声明可以分为两大类,带有初始化器和不带初始化器
// 01-不带初始化器,枚举成员默认从 0 开始,依次递增;
enum NumEnum1 { one, two }
NumEnum1.one => 0
NumEnum1.two => 1
// 02-带有初始化器,这种又可以分为两种:
// 02-01-使用初始化器并指定初始化的常数,
// 未使用初始化器的成员取值是在上一个成员的基础上 +1;
enum NumEnum2 {
  one = 10,
  two,
  three = 20,
  four
}
NumEnum2.two => 11
NumEnum2.four => 21
// 02-02-使用初始化器并且初始化值是对已经声明的枚举的枚举成员的引用
enum NumEnum3 {
  one = NumEnum2.four,
  two
}
NumEnum3.one => 21
NumEnum3.two => 22
  • 枚举的特点
    数字枚举经过编译之后会生成 lookup table (反向映射表),即除了生成键值对的集合,还会生成值键对的集合。
enum NumEnum { one, two }
NumEnum.one => 0
NumEnum[1] => 'two'
// 其实这个不难理解,编译的过程就像官网举的栗子:
var NumEnum;
(function (NumEnum) {
    NumEnum[NumEnum["one"] = 0] = "one";
    NumEnum[NumEnum["two"] = 1] = "two";
})(NumEnum || (NumEnum = {}));
// 对象支持以 number 类型的数据作为键
// 原因就是 number 类型会被转为 string 类型,但是有些却不适合,往下看;
// 枚举成员不是变量,而是常数,因此枚举成员又称为枚举常量
// 因为是常量,所以不能对枚举成员进行赋值,以下声明的枚举成员均为常量
// 01-枚举成员不使用任何形式的初始化器
enum NumEnum1 { one, two }

// 02-枚举成员不带初始化器,但是它前一个枚举成员使用了常数来初始化:
// two 未使用初始化器,但是它之前的成员 one 使用了常数赋值,
// 所以,two、three 均为常量,并且以 10 为基础递增;
enum NumEnum2 { one = 10, two, three }
// 你可能想到下面的方式来初始化,按逻辑来理解,没有问题,但是发现编译器报错了
// 是因为枚举成员初始化的时候使用的并非是常量枚举表达式,导致枚举声明错误
// 也就是说,枚举成员要么不使用初始器,要么使用常数初始器,要么使用常量枚举表达式,否则均会失败
const first = 10
enum NumEnum2 { one = first, two, three }

// 03-使用常量枚举表达式
// 03-01-字面量方式,就像前面不带初始化器声明的枚举
enum NumEnum301 { one = 1 }

// 03-02-对之前定义的常量枚举成员的引用
// 下面的枚举第一个成员 one 是对上一个枚举成员的引用,第二个成员 two 是对当前枚举成员的的引用
enum NumEnum302 { one = NumEnum301.one, two = 2 * one }

// 03-03-带括号的常量枚举表达式:
// 关于对这块的个人的理解,即可以是函数的调用,也可以是计算表达式的求值
function returnNumber (x: number): number {
  return x
}
enum NumEnum303 {
  one = (function () { return 1 })(),
  two = returnNumber(10),
  three = (NumEnum2.one + NumEnum2.two) % NumEnum2.three
}

// 03-04-一元运算符 +, -, ~ 其中之一应用在了常量枚举表达式
enum NumEnum304 {
  // 取反运算
  one = ~NumEnum2.one,
  two = +NumEnum2.two,
  three = -NumEnum2.three,
  four = void 0
}

// 03-05-二元运算符 +、-、*、/、%、
// 位运算符: <<(左移运算符)、>>(有符号右移运算符)、>>>(无符号右移运算符)、&, |, ^ 应用在了常量枚举表达式中;
enum NumEnum305 {
  // 左移运算
  one = 2 << 5,
  // 右移运算
  two = 64 >> 5,
  three = 64 >>> 5,
  // 或 运算合并 乘性运算
  four = (one | two) * three
}

// 03-06-枚举成员也可以是经过计算得来的
// 这个计算并非是运算符或者函数的处理结果,类似计算属性
enum StrEnum {
  one = 'one'
}
enum NumEnum306 {
  one = StrEnum.one.length,
  three = 'three'.length
}

// 03-07-枚举表达式求值结果为 NaN 或者 Infinity,在编译阶段会抛错,下面详解原因
  • 常量枚举表达式求值结果为什么不能是 NaN 或者 Infinity
// 异构枚举
enum MixinsEnum {
  a = 1,
  b = 'b'
}
// 数字枚举
enum NumEnum {
  // 会发现编辑器提示错误,因为运算符的右侧并非期望的 number 或者 enum 类型
  a = MixinsEnum.a * MixinsEnum.b
}

上面我们说到了,数字枚举成员不是变量,是一个常数,可以理解成为别名,并且数字枚举会生成反向映射,值键对形式中,keynumber,虽然会被转为 string 类型;
NaN: not a number,即不是一个数字,在W3C关于NaN的介绍中提到,NaN 不是常量,虽然它的 typenumber,所以在 ts 的数字枚举系统中不可作为枚举成员的值,同样的,Infinity 表示的是正无穷大的数值,并非一个常数,所以枚举表达式求值结果返回 Infinity 也会报错

字符串枚举
  • 字符串枚举需使用字符串字面量或者之前定义的字符串枚举成员来初始化
// 全部使用字符串字面量来初始化
enum StrEnum1 {
  one = 'one',
  two = 'two'
}

// 全部使用其他枚举成员的字面量初始化,
// 当然不仅限于 StrEnum1 枚举,也可以是其他字符串枚举
enum StrEnum2 {
  one = StrEnum.one,
  two = StrEnum.two
}

// 但是不可将这两种方式初始化方式混写
enum StrEnum3 {
  // 编辑器会报错
  one = 'first',
  // 采用计算属性的枚举表达式,ts 会认为你在初始化一个数字枚举,
  // 但是如果当前枚举含有字符串枚举成员,这回形成矛盾,所以会报错;
  two = StrEnum.two
  // 当然你可以手动指定常数枚举成员,从而避开这种校验;
  // ok
  three = 3
}
// 你可能会对上述第二种初始化枚举有疑问,为什么均使用其他枚举引用不会有问题
// 这是因为 ts 不会校验引用枚举成员
// 如果都为常数,那么就是数字枚举,如果都为字符串,就是字符串枚举,否则就是异构枚举。
  • 字符串枚举不会生成反向映射
// 看这个栗子
enum StrEnum {
  one = 'one',
  two = 'two'
}
// 会发现编译之后的并没有像数字枚举生成方向映射
var StrEnum;
(function (StrEnum) {
  StrEnum["one"] = "one";
  StrEnum["two"] = "two";
})(StrEnum || (StrEnum = {}));
// 上述栗子比较极端,但很容易明白,如果生成值键对,将毫无意义
// 因为会覆盖掉,归根结底还是因为字符串赋值的不确定性。
运行时的枚举
  • 由于枚举类型最终的编译结果是一个对象,所以你可以访问里面数据
// 一个十二生肖中前五个的排位
enum ChineseZodiac { rat = 1, cattle, tiger, rabbit, dragon }

// 读取某一生肖的排位或者根据某一排位查找生肖名称
function getChineseZodiac(zodiac: { tiger: number }): void {
  console.log(zodiac.tiger) // => tiger 是第三位
  console.log(zodiac[5])    // => 第五位的是 dragon
}
getChineseZodiac(ChineseZodiac)
枚举的进阶

枚举按照枚举成员可以分为数字、字符串、异构(混入)三大类,上面都介绍过了;枚举按照声明方式可分为四种,下面依次介绍

  • 我把它叫做普通枚举,就是上面枚举的声明方式,不做介绍了;
// 关键词 + 枚举名称
enum NumEnum { a, b c }
  • const 枚举,即使用 const 修饰符来强调当前枚举类型,并且会影响编译结果
// 修饰符 + 关键词 + 枚举名称
const enum NumEnum { a, b, c }
// 上述声明了一个常量枚举,并且内部的数据均为只读常量
// const 枚举不会生成 lookup table,并且运行时不可访问当前枚举对象,只允许访问枚举成员的值;
const OBJ = NumEnum => Error,运行时不存在
function returnNumber (obj: { a: number }): number {
  return obj.a
}
returnNumber(NumEnum) => Error,运行时不存在
const A = NumEnum['0'] => Error,没有生成反向映射,所以不存在该属性
const B = NumEnum['b'] => 0/* b */
// 基于 const 枚举的特点,如果你只是为了生成一组常量并且只需要获取某一个常量
// 从性能方面考虑,const 枚举是首选;
// 当然如果你在 tsconfig.json 中指定下面选项
"compilerOptions": {
  // 保留 const 和 enum 声明该项为 true
  "preserveConstEnums": true
}
// 或者在命令行中添加了 --preserveConstEnums 指令,均会让当前 const 枚举转变为普通枚举
// 即会生成反向映射表;在实际开发中,一般情况你可能需要禁止掉该项,除非想要用于调试;
  • 外部枚举,即使用 declear 关键词来声明一个枚举,这种声明枚举的方式比较特别,使用的时候需要非常谨慎,该枚举类型不会生成反向映射
// 声明语 + 关键词 + 枚举名称
declear enum ChineseZodiac {
  rat = 1,
  cattle,
  tiger,
  rabbit,
  dragon
}
console.log(ChineseZodiac)
console.log(ChineseZodiac.rat)
// 你会发现,无论你是访问枚举本身还是内部成员,均会报错: ChineseZodiac is not defined
// 编译之后并没有生成该枚举,也就是说,声明的外部枚举是没有被编译的,导致在 runtime 的时候就会报错
// 这就让人很头疼,既然不能访问,那为何要能做出这个声明呢。

官网对其的描述是:外部枚举用来描述已经存在的枚举类型的形状,这样听起来很晦涩,下面是对其的释义:

  1. 外部枚举是为了描述当前环境中已经存在的对象的,这个对象可以存在任意的地方,但是一定是已声明的;
    1-1- 一个 .html 后缀文件,为了引入 .ts 文件编译之后的结果,用于调试
    index.html.png

    1-2- .ts 源文件
    index.ts.png

    1-3- 编译之后的 .js 文件
    index.js.png

    1-4- 其他 .js 资源文件
    other.js.png

    1-5- 访问结果
    declear-enum-result.png

    会发现是不会报错的,但是你可能会疑问了,这个不就是访问自己声明的一个变量吗,那跟 .ts 文件中声明的枚举有什么关系?图样图森破,外部枚举类似于 ts 的类型断言,只要在开发中有这个声明,意味着在当前开发环境上下文中一定存在当前这个对象,你可以随意使用当前对象;当然也就意味着你声明外部枚举的时候慎重,我是否真的需要这样做,不然 runtime 使用的时候就出错了;
  2. 外部枚举还可以防止声明枚举的命名冲突和成员冲突
    2-1- 我在上面文件结构基础上新增了一个 enum.ts 文件,并在里面声明了一个普通枚举,但是枚举成员和外部枚举成员相同
    enum.ts.png

    2-2- 之所以会有这样的提示,是 declear 的作用,因为 ts 类型系统能够侦测到当前整个文件目录上下文中的所有 declear 声明的变量,编译器也会有语法提示;
  • 外部常量枚举, 即使用 declearconst 关键词联合声明的枚举类型,这个枚举类型和 const 枚举类型并没有什么区别,只是会提示是否有枚举命名冲突和成员冲突,该枚举类型不会生成反向映射
// 声明语 + 修饰符 + 关键词 + 枚举名称
declear const enum ChineseZodiac {
  rat = 1,
  cattle,
  tiger,
  rabbit,
  dragon
}
ChineseZodiac.dragon => 5/* dragon */
参考链接
  1. typescript中文网-枚举

2.关于不同枚举类型之间的区别

推荐阅读更多精彩内容