JavaScript中实现枚举的4种方案:从对象到Proxy与Class

作者:代码拾荒时间:2026-10-26 14:14:13浏览:162

本文深入解析JavaScript中创建枚举的四种主流方式:普通对象、冻结对象、Proxy代理以及基于Class的实现。通过对比代码示例与适用场景,帮助开发者在小型项目、中型项目及大型项目中选择最合适的枚举方案,兼顾安全性与开发效率。

字符串和数字拥有无限可能的取值,而布尔值等类型则属于有限的集合。

诸如一周的日期(周一至周日)、四季(冬春夏秋)以及基本方位(北东南西)等,都是取值有限的典型例子。

当变量需要从有限的预定义常量中取值时,枚举显得尤为便利。它避免了使用魔法数字或字符串,从而规避了这种反模式带来的潜在风险。

接下来将探讨JavaScript中创建枚举的四种常用方法,并分析各自的优缺点。

基于对象的枚举

枚举是一种数据结构,用于定义一组有限的具名常量,每个常量均可通过其名称进行访问。

以T恤尺寸为例:Small,Medium,以及Large。

在JavaScript中,创建枚举最简单(尽管并非最优)的方式是使用普通对象。

const Sizes = {
  Small: 'small',
  Medium: 'medium',
  Large: 'large',
}
const mySize = Sizes.Medium
console.log(mySize === Sizes.Medium) // logs true

Sizes是一个基于JavaScript对象的枚举,包含三个具名常量: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!

普通对象的实现缺乏保护机制,无法避免这种意外的修改。

接下来我们将深入探讨字符串和symbol枚举,并介绍如何冻结枚举对象以解决意外修改的问题。

枚举值类型

除了字符串类型,枚举值还可以是数字:

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

使用symbol的优势在于,每个symbol都是唯一的。这意味着,你始终需要通过枚举本身来比较枚举值:

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

使用symbol枚举的缺点在于,JSON.stringify()会将symbol字符串化为null、undefined,或者跳过以symbol作为值的属性:

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 '{}'

在后续示例中,我将使用字符串枚举。当然,你也可以根据需求自由选择其他值类型。

如果拥有枚举值类型的选择权,建议使用字符串。因为字符串比数字和symbol更易于调试。

基于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的错误拼写版本)结果为未定义,而非抛出不存在枚举常量的错误。

接下来,我们将看看基于代理的枚举如何解决这一问题。

基于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')
    }
  })
}

代理的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

Sizes.Med1um抛出错误,因为Med1um常量名称在枚举中不存在。

Sizes.Medium = 'foo' 抛出错误,因为枚举属性已被改变。

代理枚举的缺点在于,你必须始终导入枚举工厂函数,并将枚举对象包裹其中。

基于类的枚举

另一种有趣的创建枚举的方法是使用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 Sizes('small')。

Sizes类的每个实例还包含一个私有字段#value,用于表示枚举的原始值。

基于类的枚举的一个显著优势是,可以在运行时使用instanceof操作来确定值是否属于枚举。例如,mySize instanceof Sizes结果为真,因为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 Sizes('small')
  static Medium = new Sizes('medium')
  static Large = new Sizes('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个实例,则抛出错误。

当然,最好让枚举实现尽可能简单。枚举的目的是作为普通的数据结构存在。

总结

在JavaScript中,有4种创建枚举的有效方法。

最简单的方法是使用普通JavaScript对象:

const MyEnum = {
  Option1: 'option1',
  Option2: 'option2',
  Option3: 'option3'
}

普通对象枚举适用于小型项目或快速演示。

第二种选择,若希望保护枚举对象不被意外覆盖,可使用冻结对象:

const MyEnum = Object.freeze({
  Option1: 'option1',
  Option2: 'option2',
  Option3: 'option3'
})

冻结对象枚举适用于中型或大型项目,确保枚举不会被意外修改。

第三种选择是代理方法:

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 MyEnum('option1')
  static Option2 = new MyEnum('option2')
  static Option3 = new MyEnum('option3')
  #value
  constructor(value) {
    this.#value = value
  }
  toString() {
    return this.#value
  }
}

如果你喜欢类的使用方式,基于类的枚举是可行的。然而,其保护程度不如冻结或代理枚举。

你还知道哪些在JavaScript中创建枚举的方法?

到此这篇关于JavaScript中的四种枚举方式的文章就介绍到这了,更多相关js枚举方式内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!

作者最新文章