JavaScript实现枚举的几种方法总结
作者:liangyue
基于普通对象
我们来考虑一个场景:T恤的尺寸:Small、Medium、Large
三种类型,那么我们使用普通对象的方式来实现,代码如下:
const Sizes = { Small: 'small', Medium: 'medium', Large: 'large', }
基于这种方式实现一个枚举是非常简单的,而且也足够的清晰,此时Sizes
就是一个基于JavaScript
普通对象的枚举,它有3个命名常量:Size.Small
、Size.Medium
、Size.Large
,我们可以通过Size.small
来获取对应的枚举值。
优点
- 简单:使用这种方式实现枚举是非常简单,只需要定义一个带有键和值的对象即可
缺点
- 容易被外部修改
当我们在维护一个大型的代码仓库时,枚举值可能会被意外更改,代码如下:
const size1 = Sizes.Medium const size2 = Sizes.Medium = 'foo' // Changed! console.log(size1 === Sizes.Medium) // logs false
如代码中所示,当枚举被修改后,之后的逻辑将会出现问题,这也就是说,我们实现枚举的时候需要考虑对象的值不能被修改。
基于Symbol
我们也可以这样实现一个枚举:
const Sizes = { Small: Symbol('small'), Medium: Symbol('medium'), Large: Symbol('large'), } const mySize = Sizes.Large; console.log(mySize === Sizes.Large); // true console.log(mySize === Symbol('large')); // false
使用这种方式实现的枚举看上去和机遇普通对象的方式类似,只是把对象中的值修改成了Symbol
类型,那么这样和刚才的方式又有什么不同呢?
优点
- 必须使用枚举本身来进行比较:也就是上面代码中我们必须使用
Sizes.Large
来比较,再重新创建一个Symbol('large')
对比的话则不相等,而对比第一种方法,只要字符串相同即为相同,这种对比方式更加严格了
缺点
- 不能使用JSON.stringify,使用
JSON.stringify
会将Symbol
转为undefined、null或直接跳过,代码如下:
console.log(JSON.stringify(Sizes.Small)); // undefined console.log(JSON.stringify([Sizes.Small])); // [null] console.log(JSON.stringify({ size: Sizes.Small })) // {}
- 容易被外部修改,这点与第一种方案的情况类似,接下来,我们将介绍如何能够保证枚举值不会被修改的方案
基于Object.freeze
使用Object.freeze可以使一个对象被冻结:被冻结的对象不能再被更改:不能添加新的属性,不能移除现有的属性,不能更改它们的可枚举性、可配置性、可写性或值,对象的原型也不能被重新指定。我们使用这种实现枚举,代码如下:
const Sizes = Object.freeze({ Small: 'small', Medium: 'medium', Large: 'large', }) const mySize = Sizes.Large; Sizes.Large = '111' console.log(mySize === Sizes.Large) // true
使用这种方式,就算去修改枚举值也是无效的,如果在严格模式下,这种赋值的情况还会抛出错误。
优点
- 有效防止枚举值被修改
缺点
- 当拼写错误时,会直接返回
undefined
,比如我们直接获取Sizes.a
,此时会返回undefined
,这个问题在前面的几个方案中也是同样的,我们在开发过程中,应该是更希望抛出一个错误,这样在开发阶段更直接的发现问题所在。于是,就有了下面一种方案。
基于Proxy
使用Proxy用于创建一个对象的代理,从而实现基本操作的拦截和自定义,Proxy并不会改变原始对象的结构,而且我们可以实现如下两个需求:
- 访问不存在枚举时,抛出错误
- 修改枚举对象属性时,抛出错误
这样,就可以同时满足我们前面几种方案遇到的问题了,接下来,我们封装一个函数,代码如下:
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') } }) }
这个函数中,我们传入一个初始枚举对象,当我们访问某个属性的时候,如果没有将会抛出错误"${name}" value does not exist in the enum
,当我们修改值的时候,也会抛出一个错误Cannot add a new value to the enum
接下来,我们使用这个函数包装一下Sizes
,代码如下:
const Sizes = Enum({ Small: 'small', Medium: 'medium', Large: 'large', }) const mySize = Sizes.Large; // large Sizes.Small = '1' // 抛出错误: Cannot add a new value to the enum console.log(Sizes.a) // 抛出错误:"a" value does not exist in the enum
优点
- 枚举值防止修改
- 访问不存在的枚举时会抛出错误
缺点
- 相对复杂,必须导入
Enum
函数
基于Class
另一个方法是基于JavaScript
中的Class类实现的,这个类中包含一组静态的字段,而每一个对应的值本身又是这个实例,代码如下:
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; } }
每一个枚举值都是一个Sizes
实例,内部有个私有属性#value
, 用来表示枚举的原始值。我们举几个例子来看下使用这种方式实现的具有哪些特性:
const mySize = Sizes.Large; console.log('mySize', Sizes.Large); // Sizes {} console.log(mySize === Sizes.Large); // true console.log(mySize === new Sizes('large')) // false console.log('mySize string', Sizes.toString()) // large console.log(mySize instanceof Sizes) // true
优点
- 可以通过
instanceof
来判断是否是枚举:上面例子中我们可以判断出来mySize
是一个枚举 - 这种方式枚举的对比是基于实例的:上面例子中
mySize === new Sizes('large')
,即使是相同的#value
,也是不同的实例
缺点
- 枚举值可能会被意外修改
- 访问不存在的枚举时不会抛出错误
总结
上面我们介绍了几种在JavaScript中实现枚举的方式,每种方式都有各自的优缺点,相比之下,我认为:
- Proxy方式更为灵活,可以按照自己的需求进行更多的定制化;
- 如果枚举值用的较多,且项目较大,选择Object.freeze方式,防止枚举值被意外修改;
- 如果您遇到的情况相对简单,使用基于普通对象的方式;
总之,我们最终的实现要尽量的简单,不要过度设计,按照具体情况选择合适的方式。