本文译者为 360 奇舞团前端开发工程师
原文标题:4 Ways to Create an Enum in JavaScript
原文作者:Dmitri Pavlutin
原文地址:https://dmitripavlutin.com/javascript-enum/
使用枚举(enum)可以方便地表示一个变量,从一个有限的预定义常量集合中获取值,枚举可以避免使用魔法数字和字符串(这被认为是一种反模式)。
让我们看看在 JavaScript 中创建枚举的四种方法(以及它们的优缺点)。
1. 基于普通对象的枚举
枚举是一种数据结构,它定义了一组有限的命名常量。每个常量可以通过其名称访问。
定义一个T-shirt
的size
为:Small
、Medium
、Large
。
在JavaScript
中创建枚举的一种简单的方式,是使用普通JavaScript对象
const Sizes = {
Small: 'small',
Medium: 'medium',
Large: 'large',
}
const mySize = Sizes.Medium
console.log(mySize === Sizes.Medium) // logs true
Sizes
是一个基于普通JavaScript
对象的枚举,它有3个命名的常量: Sizes.Small
、 Sizes.Medium
、Sizes.Large
。
Sizes
同时也是一个字符串枚举,命名的常量的值是字符串: 'small'
, 'medium'
、'large'
。
要访问命名的常量值,请使用属性访问器。例如: Sizes.Medium
的值是'medium'
。
枚举的可读性更强,更明确,并消除了对魔法字符串或数字的使用。
优点和缺点
这种使用普通对象的枚举方法非常简单明了,只要定义一个带有键和值的对象,就可以创建一个枚举。
在大型项目中,有人可能会无意间修改枚举对象,这将影响应用程序的运行时。由于枚举对象是可变的,因此开发人员无法将其设置为不可变。这是普通对象枚举的主要缺点。
const Sizes = {
Small: 'small',
Medium: 'medium',
Large: 'large',
}
const size1 = Sizes.Medium
const size2 = Sizes.Medium = 'foo' // Changed!
console.log(size1 === Sizes.Medium) // logs false
在上述代码中Sizes.Medium
枚举值被意外的修改了,size1
在初始化时为Sizes.Medium
, 不再和Sizes.Medium
相等!普通对象的实现无法防止这种意外更改。
下面让我们看看字符串和符号枚举。以及如何冻结枚举对象以避免意外更改的问题。
2.枚举值类型
除了字符串类型外,枚举的值也可以是number
类型:
const Sizes = {
Small: 0,
Medium: 1,
Large: 2
}
const mySize = Sizes.Medium
console.log(mySize === Sizes.Medium) // logs true
上例中的Sizes
枚举是一个数字枚举,因为其值是数字: 0
、1
、2
。
同时也可以创建一个符号(symbol
)枚举:
const Sizes = {
Small: Symbol('small'),
Medium: Symbol('medium'),
Large: Symbol('large')
}
const mySize = Sizes.Medium
console.log(mySize === Sizes.Medium) // logs true
使用符号的好处是,每一个符号都是唯一的,这意味着您必须始终使用枚举本身来比较枚举:
const Sizes = {
Small: Symbol('small'),
Medium: Symbol('medium'),
Large: Symbol('large')
}
const mySize = Sizes.Medium
console.log(mySize === Sizes.Medium) // logs true
console.log(mySize === Symbol('medium')) // logs false
使用符号枚举的缺点是 JSON.stringify()
将符号序列化为 null
、undefined
,或者跳过包含符号值的属性,这将导致在需要将枚举转换为字符串形式的情况下产生问题。因此,符号枚举的使用可能需要在其他位置进行修改以支持 JSON.stringify()
。
const Sizes = {
Small: Symbol('small'),
Medium: Symbol('medium'),
Large: Symbol('large')
}
const str1 = JSON.stringify(Sizes.Small)
console.log(str1) // logs undefined
const str2 = JSON.stringify([Sizes.Small])
console.log(str2) // logs '[null]'
const str3 = JSON.stringify({ size: Sizes.Small })
console.log(str3) // logs '{}'
如果你可以自由选择枚举值的类型,就选择字符串吧。字符串比数字和符号更容易进行调试。
3.基于Object.freeze()的枚举
可以保护枚举对象免受修改的一种好方法是将其冻结。当对象被冻结时,您无法修改或添加新属性到该对象中。换句话说,该对象变为只读。
在JavaScript中,Object.freeze() 函数冻结一个对象。让我们冻结Sizes
枚举:
const Sizes = Object.freeze({
Small: 'small',
Medium: 'medium',
Large: 'large',
})
const mySize = Sizes.Medium
console.log(mySize === Sizes.Medium) // logs true
const Sizes = Object.freeze({ ... })
创建一个被冻结的对象. 即使被冻结,也可以自由的访问这些枚举值:const mySize = Sizes.Medium
。
优点和缺点
如果一个枚举属性被意外的修改,JavaScript会抛出一个错误(在严格模式下)
const Sizes = Object.freeze({
Small: 'Small',
Medium: 'Medium',
Large: 'Large',
})
const size1 = Sizes.Medium
const size2 = Sizes.Medium = 'foo' // throws TypeError
语句 const size2 = Sizes.Medium = 'foo'
对 Sizes.Medium
属性进行了意外的赋值操作。因为Sizes
是一个被冻结的对象,JavaScript(在严格模式下)抛出一个错误:
TypeError: Cannot assign to read only property 'Medium' of object <Object>
冻结的对象枚举被保护起来,这可以防止无意中修改枚举对象。
还有一个问题。如果无意间拼写错了枚举常量,结果会变成undefined
:
const Sizes = Object.freeze({
Small: 'small',
Medium: 'medium',
Large: 'large',
})
console.log(Sizes.Med1um) // logs undefined
在上面的示例中Sizes.Med1um
表达式(Med1um
是Medium
的错误拼写)的结果将会是undefined
。
下面让我们看看如何使用代理实现的枚举可以解决这个问题。
4. 基于代理的枚举
代理枚举是一种有趣的实现方式,也是我最喜欢的一种方式。一个代理对象是一个特殊的对象,它包装另一个对象并修改对原始对象的操作的行为。代理不会改变原始对象的结构。
枚举代理拦截枚举对象的读取和写入操作,并且:
当访问不存在的枚举值时抛出错误
当更改枚举对象属性时抛出错误
下面是一个工厂函数的实现,该函数接受一个普通的枚举对象,并返回一个代理对象:
// 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')
}
})
}
代理的get()
方法拦截读操作,并在属性名称不存在时抛出错误。set()
方法拦截写操作并抛出错误。它旨在保护枚举对象免受写操作的影响。
将Sizes
对象枚举包装到代理中:
import { Enum } from './enum'
const Sizes = Enum({
Small: 'small',
Medium: 'medium',
Large: 'large',
})
const mySize = Sizes.Medium
console.log(mySize === Sizes.Medium) // logs true
代理枚举的使用方式和普通对象枚举完全相同,
优点和缺点
使用基于代理的枚举,可以获得一种既灵活又安全的枚举实现方式。代理枚举不仅可以捕获枚举属性的更改,还可以防止访问不存在的枚举常量。
import { Enum } from './enum'
const Sizes = Enum({
Small: 'small',
Medium: 'medium',
Large: 'large',
})
const size1 = Sizes.Med1um // throws Error: non-existing constant
const size2 = Sizes.Medium = 'foo' // throws Error: changing the enum
因为枚举中没有定义名为'Med1um'
的常量,所以 Sizes.Med1um
会引发错误。由于枚举属性已被更改,因此 Sizes.Medium = 'foo'
会引发错误。
代理枚举的缺点是始终需要导入 Enum
工厂函数并将枚举对象包装在其中。与使用其他实现方式相比,代理枚举可能会带来一些性能损失。代理枚举涉及使用 JavaScript 的代理特性,这可能会使枚举对象的访问速度稍慢一些。因此,在实现枚举时,应该考虑的代码的性能需求。
5. 基于类的枚举
使用 JavaScript 类创建枚举, 基于类的枚举包含一组 静态字段,每个静态字段都代表一个命名的枚举常量。每个枚举常量的值本身就是该类的一个实例。
使用 Sizes
类实现sizes
枚举:
class Sizes {
static Small = new Sizes('small')
static Medium = new Sizes('medium')
static Large = new Sizes('large')
#value
constructor(value) {
this.#value = value
}
toString() {
return this.#value
}
}
const mySize = Sizes.Small
console.log(mySize === Sizes.Small) // logs true
console.log(mySize instanceof Sizes) // logs true
Sizes
是代表枚举的类。枚举常量是类上的静态字段,例如 static Small = new Season('small')
。 Sizes
类的每个实例都有一个私有字段 #value
,它代表枚举的原始值。由于 Sizes
类的构造函数是私有的,所以不能创建新的枚举实例。这就确保了枚举值的不可变性。
基于类的枚举的一个很好的优点是,在运行时能够使用instanceof
操作来确定值是否为枚举。例如,mySize instanceof Sizes
评估为true
,因为mySize
是一个枚举值。
在基于类的枚举中,比较是基于实例的(相对于普通、冻结或代理枚举而言,基于实例的比较更为原始):
class Sizes {
static Small = new Sizes('small')
static Medium = new Sizes('medium')
static Large = new Sizes('large')
#value
constructor(value) {
this.#value = value
}
toString() {
return this.#value
}
}
const mySize = Sizes.Small
console.log(mySize === new Sizes('small')) // logs false
mySize
(它的值为 Sizes.Small
)并不等于 new Sizes('small')
。即使 Sizes.Small
和 new Sizes('small')
拥有相同的 #value
值,但是它们是不同的对象实例。
优点和缺点
基于类的枚举无法防止覆盖或访问不存在的枚举命名常量。
class Sizes {
static Small = new Season('small')
static Medium = new Season('medium')
static Large = new Season('large')
#value
constructor(value) {
this.#value = value
}
toString() {
return this.#value
}
}
const size1 = Sizes.medium // a non-existing enum value can be accessed
const size2 = Sizes.Medium = 'foo' // enum value can be overwritten accidentally
但是,可以通过构造函数内部已创建的实例数来控制新实例的创建。如果创建的实例数超过3个,则抛出一个错误。当然,最好将枚举的实现尽可能简单。枚举旨在成为普通的数据结构。
6. 结论
在 JavaScript 中创建枚举的方法有 4 种。
最简单的方法是使用一个普通的 JavaScript 对象:
const MyEnum = {
Option1: 'option1',
Option2: 'option2',
Option3: 'option3'
}
普通对象枚举最适合小项目或演示demo。
如果想保护枚举对象不被意外重写,则可以使用被冻结的纯对象:
const MyEnum = Object.freeze({
Option1: 'option1',
Option2: 'option2',
Option3: 'option3'
})
冻结的对象枚举在中大型项目中非常适用,可以确保枚举不会被意外修改。
3.代理方法:
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')
}
})
}
import { Enum } from './enum'
const MyEnum = Enum({
Option1: 'option1',
Option2: 'option2',
Option3: 'option3'
})
代理枚举适用于中型或大型项目,可以更好地保护枚举不被覆盖或访问不存在的命名常量。代理枚举是我的个人比较喜欢的。
是使用基于类的枚举,其中每个命名常量都是该类的实例,并存储为该类的静态属性:
class MyEnum {
static Option1 = new Season('option1')
static Option2 = new Season('option2')
static Option3 = new Season('option3')
#value
constructor(value) {
this.#value = value
}
toString() {
return this.#value
}
}
如果你喜欢使用类,那么基于类的枚举就可以发挥作用。然而,与冻结或代理枚举相比,基于类的枚举受到的保护较少。
你还知道 JavaScript 中创建枚举的其他方式吗?
- END -
关于奇舞团
奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。