当变量的值来自一组有限的预定义常量时,此乃枚举的用武之地。枚举使我们规避“魔术数字”和“魔术字符串”等反模式。
大多数编程语言都原生支持枚举数据类型。虽然目前 JS 自己并不支持object.freeze(),但好在 TS 内置了枚举。
有趣的是,当我们将 TS 编译为 JS 之后,就会发现 TS 的枚举其实也是用原生 JS 来模拟的。
本文共享的是,在 JS 中创建枚举的若干方案及其利弊:
基于普通对象的枚举
枚举是一种定义一组有限命名常量的数据结构。每个常量都可以通过其名称读写。
让我们考虑一下猫猫的体积:Small、 和 Large。
在 JS 中创建枚举的一种简单方法(尽管不是最佳实践),是使用普通 JS 对象。
const Sizes = {
Small: '薛定谔',
Medium: '盯裆猫',
Large: '龙猫'
}
const mySize = Sizes.Medium
console.log(mySize === Sizes.Medium) // true
Sizes 是一个基于普通 JS 对象的枚举,它有 3 个命名常量:
Sizes 也是一个字符串枚举,因为命名常量的值是字符串:
要读写命名常量的值,请使用对象属性操作符。举个栗子,Sizes. 的值是 '盯裆猫'。
枚举更具可读性、更直观,且消除了“魔术字符串”或“魔术数字”的滥用。
利弊
普通对象枚举之所以有吸引力,是因为它十分简单:只需定义一个键值对象,枚举就欧了。
但在大型代码库中,我们可能会意外修改枚举对象,这会影响 App 的运行时。
const size1 = Sizes.Medium
const size2 = (Sizes.Medium = '柴郡猫') // 意外修改!
console.log(size1 === Sizes.Medium) // false
Sizes. 枚举值被意外修改。
size1 使用 Sizes. 初始化时,不再等于 Sizes.!
基于普通对象的枚举无法规避此类意外修改。
让我们瞄一下字符串和 枚举,以及如何冻结枚举对象,从而规避意外修改。
枚举值类型
除了字符串类型之外,枚举的值还可以是数字:
const Sizes = {
Small: 0,
Medium: 1,
Large: 2
}
const mySize = Sizes.Medium
console.log(mySize === Sizes.Medium) // true
上述示例中的 Sizes 枚举是数字枚举,因为值为数字:0、1、2。
我们还可以创建 枚举:
const Sizes = {
Small: Symbol('薛定谔'),
Medium: Symbol('盯裆猫'),
Large: Symbol('龙猫')
}
const mySize = Sizes.Medium
console.log(mySize === Sizes.Medium) // true
使用 的福利在于, 都独一无二。这意味着,我们必须使用枚举本身来比较枚举:
const mySize = Sizes.Medium
console.log(mySize === Sizes.Medium) // true
console.log(mySize === Symbol('盯裆猫')) // false
使用 枚举的短板在于,JSON.() 会将 序列化为 null、,或者跳过 值的属性:
const str1 = JSON.stringify(Sizes.Small)
console.log(str1) // undefined
const str2 = JSON.stringify([Sizes.Small])
console.log(str2) // '[null]'
const str3 = JSON.stringify({ size: Sizes.Small })
console.log(str3) // '{}'
下述示例中,我会使用字符串枚举。但大家可以按需使用任意值类型。
如果大家不受限于枚举值类型,那么优先选择字符串即可。字符串比数字和 更易调试。
基于 .() 的枚举
保护枚举对象免遭修改的优秀方案之一是冻结它。当对象被冻结时,您无法修改该对象,或者向该对象添加新属性。换而言之,该对象变为只读对象。
在 JS 中,.() 工具函数可以冻结对象。让我们冻结 Sizes 枚举:
const Sizes = Object.freeze({
Small: '薛定谔',
Medium: '盯裆猫',
Large: '龙猫'
})
const mySize = Sizes.Medium
console.log(mySize === Sizes.Medium) // true
const Sizes = .({ ... }) 创建一个冻结对象。即使对象被冻结,我们也可以自由读写枚举值:const = Sizes.。
利弊
如果枚举属性被意外修改,那么 JS 会报错(严格模式下):
const size1 = Sizes.Medium
const size2 = (Sizes.Medium = 'foo') // 报错
语句 const size2 = Sizes. = 'foo' 对 Sizes. 属性意外赋值。
因为 Sizes 是一个冻结对象,JS(严格模式下)会报错:
TypeError: Cannot assign to read only property 'Medium' of object
冻结对象枚举可以规避意外修改。
不过,还有一个问题。如果我们不小心拼错了枚举常量,那么结果会变成 :
console.log(Sizes.Med1um) // undefined
是 的错误拼写,Sizes. 表达式结果为 ,而不是抛出有关不存在的枚举常量的错误。
让我们瞄一下基于 Proxy 的枚举如何解决此问题。
基于 Proxy 的枚举
一个有趣的、也是我最爱的实现是基于 Proxy 的枚举。
Proxy 是一种特殊对象,它包装一个对象,修改对原始对象的操作行为。Proxy 不会改变原始对象的结构。
枚举代理拦截枚举对象上的读写操作,并且:
下面是一个工厂函数的实现,它接受一个普通的枚举对象,并返回一个 Proxy 对象:
// enum.js
export function Enum(baseEnum) {
return new Proxy(baseEnum, {
get(target, name) {
if (!baseEnum.hasOwnProperty(name)) {
throw new Error(`此枚举中不存在 ${name} 枚举值`)
}
return baseEnum[name]
},
set(target, name, value) {
throw new Error('无法向此枚举中添加新的枚举值')
}
})
}
Proxy 的 get() 方法会拦截读取操作,如果属性不存在就会报错。
set() 方法拦截存写操作,且只是为了报错。它旨在保护枚举对象规避存写操作的影响。
让我们将 Sizes 对象枚举包装到 Proxy 中:
import { Enum } from './enum'
const Sizes = Enum({
Small: '薛定谔',
Medium: '盯裆猫',
Large: '龙猫'
})
const mySize = Sizes.Medium
console.log(mySize === Sizes.Medium) // true
代理枚举的工作方式与普通对象枚举一毛一样。
利弊
虽然但是object.freeze(),代理枚举不会被意外重写,或读写不存在的枚举常量:
const size1 = Sizes.Med1um // 报错:常量不存在
const size2 = (Sizes.Medium = '柴郡猫') // 报错:只读枚举
Sizes. 会报错,因为枚举中不存在 常量名。
Sizes. = '柴郡猫' 会报错,因为枚举属性被修改。
代理枚举的短板在于,我们必须导入 Enum 工厂函数,并将枚举对象包装进去。
基于类的枚举
创建枚举的另一种有趣方法是使用 class。
基于类的枚举包含一组静态字段,其中每个静态字段代表一个常量名枚举。每个枚举常量的值本身就是该类的一个实例。
我们使用 Sizes 类来实现枚举:
class Sizes {
static Small = new Sizes('薛定谔')
static Medium = new Sizes('盯裆猫')
static Large = new Sizes('龙猫')
#value
constructor(value) {
this.#value = value
}
toString() {
return this.#value
}
}
const mySize = Sizes.Small
console.log(mySize === Sizes.Small) // true
console.log(mySize instanceof Sizes) // true
Sizes 是代表枚举的类。枚举常量是类中的静态字段,比如 Small = new Sizes('薛定谔')。
Sizes 类的每个实例还有一个私有字段 #value,它表示枚举的原始值。
基于类的枚举的福利之一在于,能够在运行时使用 操作符确定该值是否为枚举。举个栗子, Sizes 的计算结果为 true,因为 是一个枚举值。
基于类的枚举的比较是基于实例的(普通枚举、冻结枚举或代理枚举则是原始比较):
const mySize = Sizes.Small
console.log(mySize === new Sizes('small')) // false
Sizes.Small 不等于 new Sizes('薛定谔')。
Sizes.Small 和 new Sizes('薛定谔') 即使具有相同的 #value,也是不同的对象实例。
利弊
基于类的枚举无法规避重写,或读写不存在的常量命名枚举。
const size1 = Sizes.medium // 允许读写不存在的枚举值
const size2 = (Sizes.Medium = 'foo') // 枚举值允许意外重写
但我们可以控制新实例的创建,举个栗子,通过计算构造函数内创建的实例数量。如果创建的实例超过 3 个就报错。
当然了,尽量简化枚举的实现。枚举是简单的数据结构。
总结
JS 中创建枚举有 4 种方案。
最简单的方法是使用普通 JS 对象:
const MyEnum = {
Option1: 'option1',
Option2: 'option2',
Option3: 'option3'
}
普通对象枚举适合小型项目或快速演示。
如果想保护枚举对象规避意外重写,第二个选项是冻结普通对象:
const MyEnum = Object.freeze({
Option1: 'option1',
Option2: 'option2',
Option3: 'option3'
})
冻结对象枚举适用于希望确保枚举不会意外修改的中大型项目。
第三种选择是 Proxy:
// enum.js
export function Enum(baseEnum) {
return new Proxy(baseEnum, {
get(target, name) {
if (!baseEnum.hasOwnProperty(name)) {
throw new Error(`"${name}" value does not exist in the enum`)
}
return baseEnum[name]
},
set(target, name, value) {
throw new Error('Cannot add a new value to the enum')
}
})
}
// index.js
import { Enum } from './enum'
const MyEnum = Enum({
Option1: 'option1',
Option2: 'option2',
Option3: 'option3'
})
代理枚举适用于中大型项目,更好地保护枚举规避重写,或者读写不存在的命名常量。
代理枚举是我的个人偏好。
第四个选项是使用基于类的枚举,其中每个命名常量都是该类的一个实例,并存储为该类的静态属性:
class MyEnum {
static Option1 = new MyEnum('option1')
static Option2 = new MyEnum('option2')
static Option3 = new MyEnum('option3')
#value
constructor(value) {
this.#value = value
}
toString() {
return this.#value
}
}
如果您喜欢类,基于类的枚举也能奏效。虽然但是,基于类的枚举的鲁棒性低于冻结或代理枚举。