JavaScript实现枚举的几种方案总结

在前端开发中,我们可能经常需要用到枚举 ,比如一周中的日子:周一、周二、周三、...、周日或者是业务中的订单状态:已完成、进行中等等。使用枚举的好处大家肯定也非常清楚:让代码的可读性更强,避免直接使用数字或未知的字符串。在TypeScript中已经有了实现枚举的方式,但是在JavaScript中,则需要自己实现一个枚举功能,那么大家能想到多少种实现枚举的方法呢?每种方法又有什么优势和劣势呢?接下来,我将介绍几种实现枚举的好方法。

基于普通对象

我们来考虑一个场景:T恤的尺寸:Small、Medium、Large三种类型,那么我们使用普通对象的方式来实现,代码如下:

javascript 复制代码
const Sizes = {
  Small: 'small',
  Medium: 'medium',
  Large: 'large',
}

基于这种方式实现一个枚举是非常简单的,而且也足够的清晰,此时Sizes就是一个基于JavaScript普通对象的枚举,它有3个命名常量:Size.SmallSize.MediumSize.Large,我们可以通过Size.small来获取对应的枚举值。

优点

  • 简单:使用这种方式实现枚举是非常简单,只需要定义一个带有键和值的对象即可

缺点

  • 容易被外部修改

当我们在维护一个大型的代码仓库时,枚举值可能会被意外更改,代码如下:

javascript 复制代码
const size1 = Sizes.Medium
const size2 = Sizes.Medium = 'foo' // Changed!

console.log(size1 === Sizes.Medium) // logs false

如代码中所示,当枚举被修改后,之后的逻辑将会出现问题,这也就是说,我们实现枚举的时候需要考虑对象的值不能被修改

基于Symbol

我们也可以这样实现一个枚举:

javascript 复制代码
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或直接跳过,代码如下:
javascript 复制代码
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可以使一个对象被冻结:被冻结的对象不能再被更改:不能添加新的属性,不能移除现有的属性,不能更改它们的可枚举性、可配置性、可写性或值,对象的原型也不能被重新指定。我们使用这种实现枚举,代码如下:

javascript 复制代码
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并不会改变原始对象的结构,而且我们可以实现如下两个需求:

  • 访问不存在枚举时,抛出错误
  • 修改枚举对象属性时,抛出错误

这样,就可以同时满足我们前面几种方案遇到的问题了,接下来,我们封装一个函数,代码如下:

javascript 复制代码
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,代码如下:

javascript 复制代码
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类实现的,这个类中包含一组静态的字段,而每一个对应的值本身又是这个实例,代码如下:

javascript 复制代码
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, 用来表示枚举的原始值。我们举几个例子来看下使用这种方式实现的具有哪些特性:

javascript 复制代码
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方式,防止枚举值被意外修改;
  • 如果您遇到的情况相对简单,使用基于普通对象的方式;

总之,我们最终的实现要尽量的简单,不要过度设计,按照具体情况选择合适的方式。

感谢阅读🙏

参考文章: # 4 Ways to Create an Enum in JavaScript

相关推荐
wearegogog1231 天前
基于 MATLAB 的卡尔曼滤波器实现,用于消除噪声并估算信号
前端·算法·matlab
Drawing stars1 天前
JAVA后端 前端 大模型应用 学习路线
java·前端·学习
品克缤1 天前
Element UI MessageBox 增加第三个按钮(DOM Hack 方案)
前端·javascript·vue.js
小二·1 天前
Python Web 开发进阶实战:性能压测与调优 —— Locust + Prometheus + Grafana 构建高并发可观测系统
前端·python·prometheus
小沐°1 天前
vue-设置不同环境的打包和运行
前端·javascript·vue.js
qq_419854051 天前
CSS动效
前端·javascript·css
烛阴1 天前
3D字体TextGeometry
前端·webgl·three.js
桜吹雪1 天前
markstream-vue实战踩坑笔记
前端
南村群童欺我老无力.1 天前
Flutter应用鸿蒙迁移实战:性能优化与渐进式迁移指南
javascript·flutter·ci/cd·华为·性能优化·typescript·harmonyos
C_心欲无痕1 天前
nginx - 实现域名跳转的几种方式
运维·前端·nginx