JS 没有枚举,但 JS 可以创建枚举

大家好,这里是大家的林语冰。欢迎持续关注"前端俱乐部"

每周的星期(周一、周二二、...、周日)、每年的季节(春夏秋冬)和基本方位(东西南北)等等,都是有限值的集合。

当变量的值来自一组有限的预定义常量时,此乃枚举的用武之地。枚举使我们规避"魔术数字"和"魔术字符串"等反模式。

大多数编程语言都原生支持枚举数据类型。虽然目前 JS 自己并不支持,但好在 TS 内置了枚举。

有趣的是,当我们将 TS 编译为 JS 之后,就会发现 TS 的枚举其实也是用原生 JS 来模拟的。

本文共享的是,在 JS 中创建枚举的若干方案及其利弊:

  • 基于普通对象的枚举
  • 枚举值类型
  • 基于 Object.freeze() 的枚举
  • 基于 Proxy 的枚举
  • 基于类的枚举

免责声明

本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请传送 4 Ways to Create an Enum in JavaScript

基于普通对象的枚举

枚举是一种定义一组有限命名常量的数据结构。每个常量都可以通过其名称读写。

让我们考虑一下猫猫的体积:Small、Medium 和 Large。

在 JS 中创建枚举的一种简单方法(尽管不是最佳实践),是使用普通 JS 对象。

js 复制代码
const Sizes = {
  Small: '薛定谔',
  Medium: '盯裆猫',
  Large: '龙猫'
}
const mySize = Sizes.Medium

console.log(mySize === Sizes.Medium) // true

Sizes 是一个基于普通 JS 对象的枚举,它有 3 个命名常量:

  • Sizes.Small
  • Sizes.Medium
  • Sizes.Large

Sizes 也是一个字符串枚举,因为命名常量的值是字符串:

  • '薛定谔'
  • '盯裆猫'
  • '龙猫'

要读写命名常量的值,请使用对象属性操作符。举个栗子,Sizes.Medium 的值是 '盯裆猫'

枚举更具可读性、更直观,且消除了"魔术字符串"或"魔术数字"的滥用。

利弊

普通对象枚举之所以有吸引力,是因为它十分简单:只需定义一个键值对象,枚举就欧了。

但在大型代码库中,我们可能会意外修改枚举对象,这会影响 App 的运行时。

js 复制代码
const size1 = Sizes.Medium
const size2 = (Sizes.Medium = '柴郡猫') // 意外修改!

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

Sizes.Medium 枚举值被意外修改。

size1 使用 Sizes.Medium 初始化时,不再等于 Sizes.Medium

基于普通对象的枚举无法规避此类意外修改。

让我们瞄一下字符串和 Symbol 枚举,以及如何冻结枚举对象,从而规避意外修改。

枚举值类型

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

js 复制代码
const Sizes = {
  Small: 0,
  Medium: 1,
  Large: 2
}
const mySize = Sizes.Medium

console.log(mySize === Sizes.Medium) // true

上述示例中的 Sizes 枚举是数字枚举,因为值为数字:012

我们还可以创建 Symbol 枚举:

js 复制代码
const Sizes = {
  Small: Symbol('薛定谔'),
  Medium: Symbol('盯裆猫'),
  Large: Symbol('龙猫')
}
const mySize = Sizes.Medium

console.log(mySize === Sizes.Medium) // true

使用 Symbol 的福利在于,Symbol 都独一无二。这意味着,我们必须使用枚举本身来比较枚举:

js 复制代码
const mySize = Sizes.Medium

console.log(mySize === Sizes.Medium) // true
console.log(mySize === Symbol('盯裆猫')) // false

使用 Symbol 枚举的短板在于,JSON.stringify() 会将 Symbol 序列化为 nullundefined,或者跳过 Symbol 值的属性:

js 复制代码
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) // '{}'

下述示例中,我会使用字符串枚举。但大家可以按需使用任意值类型。

如果大家不受限于枚举值类型,那么优先选择字符串即可。字符串比数字和 Symbol 更易调试。

基于 Object.freeze() 的枚举

保护枚举对象免遭修改的优秀方案之一是冻结它。当对象被冻结时,您无法修改该对象,或者向该对象添加新属性。换而言之,该对象变为只读对象。

在 JS 中,Object.freeze() 工具函数可以冻结对象。让我们冻结 Sizes 枚举:

js 复制代码
const Sizes = Object.freeze({
  Small: '薛定谔',
  Medium: '盯裆猫',
  Large: '龙猫'
})
const mySize = Sizes.Medium

console.log(mySize === Sizes.Medium) // true

const Sizes = Object.freeze({ ... }) 创建一个冻结对象。即使对象被冻结,我们也可以自由读写枚举值:const mySize = Sizes.Medium

利弊

如果枚举属性被意外修改,那么 JS 会报错(严格模式下):

js 复制代码
const size1 = Sizes.Medium
const size2 = (Sizes.Medium = 'foo') // 报错

语句 const size2 = Sizes.Medium = 'foo'Sizes.Medium 属性意外赋值。

因为 Sizes 是一个冻结对象,JS(严格模式下)会报错:

md 复制代码
TypeError: Cannot assign to read only property 'Medium' of object <Object>

冻结对象枚举可以规避意外修改。

不过,还有一个问题。如果我们不小心拼错了枚举常量,那么结果会变成 undefined

js 复制代码
console.log(Sizes.Med1um) // undefined

Med1umMedium 的错误拼写,Sizes.Med1um 表达式结果为 undefined,而不是抛出有关不存在的枚举常量的错误。

让我们瞄一下基于 Proxy 的枚举如何解决此问题。

基于 Proxy 的枚举

一个有趣的、也是我最爱的实现是基于 Proxy 的枚举。

Proxy 是一种特殊对象,它包装一个对象,修改对原始对象的操作行为。Proxy 不会改变原始对象的结构。

枚举代​​理拦截枚举对象上的读写操作,并且:

  • 访问不存在的枚举值时会报错
  • 变更枚举对象属性时会报错

下面是一个工厂函数的实现,它接受一个普通的枚举对象,并返回一个 Proxy 对象:

js 复制代码
// 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('无法向此枚举中添加新的枚举值')
    }
  })
}

Proxyget() 方法会拦截读取操作,如果属性不存在就会报错。

set() 方法拦截存写操作,且只是为了报错。它旨在保护枚举对象规避存写操作的影响。

让我们将 Sizes 对象枚举包装到 Proxy 中:

js 复制代码
import { Enum } from './enum'

const Sizes = Enum({
  Small: '薛定谔',
  Medium: '盯裆猫',
  Large: '龙猫'
})
const mySize = Sizes.Medium

console.log(mySize === Sizes.Medium) // true

代理枚举的工作方式与普通对象枚举一毛一样。

利弊

虽然但是,代理枚举不会被意外重写,或读写不存在的枚举常量:

js 复制代码
const size1 = Sizes.Med1um // 报错:常量不存在
const size2 = (Sizes.Medium = '柴郡猫') // 报错:只读枚举

Sizes.Med1um 会报错,因为枚举中不存在 Med1um 常量名。

Sizes.Medium = '柴郡猫' 会报错,因为枚举属性被修改。

代理枚举的短板在于,我们必须导入 Enum 工厂函数,并将枚举对象包装进去。

基于类的枚举

创建枚举的另一种有趣方法是使用 class

基于类的枚举包含一组静态字段,其中每个静态字段代表一个常量名枚举。每个枚举常量的值本身就是该类的一个实例。

我们使用 Sizes 类来实现枚举:

js 复制代码
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 是代表枚举的类。枚举常量是类中的静态字段,比如 static Small = new Sizes('薛定谔')

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

基于类的枚举的福利之一在于,能够在运行时使用 instanceof 操作符确定该值是否为枚举。举个栗子,mySize instanceof Sizes 的计算结果为 true,因为 mySize 是一个枚举值。

基于类的枚举的比较是基于实例的(普通枚举、冻结枚举或代理枚举则是原始比较):

js 复制代码
const mySize = Sizes.Small

console.log(mySize === new Sizes('small')) // false

Sizes.Small 不等于 new Sizes('薛定谔')

Sizes.Smallnew Sizes('薛定谔') 即使具有相同的 #value,也是不同的对象实例。

利弊

基于类的枚举无法规避重写,或读写不存在的常量命名枚举。

js 复制代码
const size1 = Sizes.medium // 允许读写不存在的枚举值
const size2 = (Sizes.Medium = 'foo') // 枚举值允许意外重写

但我们可以控制新实例的创建,举个栗子,通过计算构造函数内创建的实例数量。如果创建的实例超过 3 个就报错。

当然了,尽量简化枚举的实现。枚举是简单的数据结构。

总结

JS 中创建枚举有 4 种方案。

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

js 复制代码
const MyEnum = {
  Option1: 'option1',
  Option2: 'option2',
  Option3: 'option3'
}

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

如果想保护枚举对象规避意外重写,第二个选项是冻结普通对象:

js 复制代码
const MyEnum = Object.freeze({
  Option1: 'option1',
  Option2: 'option2',
  Option3: 'option3'
})

冻结对象枚举适用于希望确保枚举不会意外修改的中大型项目。

第三种选择是 Proxy

js 复制代码
// 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'
})

代理枚举适用于中大型项目,更好地保护枚举规避重写,或者读写不存在的命名常量。

代理枚举是我的个人偏好。

第四个选项是使用基于类的枚举,其中每个命名常量都是该类的一个实例,并存储为该类的静态属性:

js 复制代码
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
  }
}

如果您喜欢类,基于类的枚举也能奏效。虽然但是,基于类的枚举的鲁棒性低于冻结或代理枚举。

欢迎持续关注"前端俱乐部"!坚持阅读,自律打卡,每天一次,进步一点。

谢谢大家的点赞,掰掰~

相关推荐
codingandsleeping8 分钟前
Express入门
javascript·后端·node.js
Vaclee11 分钟前
JavaScript-基础语法
开发语言·javascript·ecmascript
拉不动的猪33 分钟前
前端常见数组分析
前端·javascript·面试
小吕学编程1 小时前
ES练习册
java·前端·elasticsearch
Asthenia04121 小时前
Netty编解码器详解与实战
前端
袁煦丞1 小时前
每天省2小时!这个网盘神器让我告别云存储混乱(附内网穿透神操作)
前端·程序员·远程工作
一个专注写代码的程序媛2 小时前
vue组件间通信
前端·javascript·vue.js
一笑code2 小时前
美团社招一面
前端·javascript·vue.js
懒懒是个程序员3 小时前
layui时间范围
前端·javascript·layui
NoneCoder3 小时前
HTML响应式网页设计与跨平台适配
前端·html