在JavaScript中4种创建枚举方式

news2024/12/23 0:43:40

本文译者为 360 奇舞团前端开发工程师

原文标题:4 Ways to Create an Enum in JavaScript
原文作者:Dmitri Pavlutin
原文地址:https://dmitripavlutin.com/javascript-enum/

使用枚举(enum)可以方便地表示一个变量,从一个有限的预定义常量集合中获取值,枚举可以避免使用魔法数字和字符串(这被认为是一种反模式)。

让我们看看在 JavaScript 中创建枚举的四种方法(以及它们的优缺点)。

1. 基于普通对象的枚举

枚举是一种数据结构,它定义了一组有限的命名常量。每个常量可以通过其名称访问。

定义一个T-shirtsize为:SmallMediumLarge

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.SmallSizes.MediumSizes.Large

Sizes 同时也是一个字符串枚举,命名的常量的值是字符串: 'small', 'medium''large'

63f986feab7c7c48586712e7422e2a25.png
T_shirt_size

要访问命名的常量值,请使用属性访问器。例如: 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枚举是一个数字枚举,因为其值是数字: 012

同时也可以创建一个符号(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() 将符号序列化为 nullundefined,或者跳过包含符号值的属性,这将导致在需要将枚举转换为字符串形式的情况下产生问题。因此,符号枚举的使用可能需要在其他位置进行修改以支持 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表达式(Med1umMedium的错误拼写)的结果将会是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.Smallnew 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 种。

  1. 最简单的方法是使用一个普通的 JavaScript 对象:

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

普通对象枚举最适合小项目或演示demo。

  1. 如果想保护枚举对象不被意外重写,则可以使用被冻结的纯对象:

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

代理枚举适用于中型或大型项目,可以更好地保护枚举不被覆盖或访问不存在的命名常量。代理枚举是我的个人比较喜欢的。

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

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 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。

9133359ca1707e01bc82b52364f095c0.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/531154.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一个通用的多相机视觉SLAM框架的设计和评估

文章&#xff1a;Design and Evaluation of a Generic Visual SLAM Framework for Multi-Camera Systems 作者&#xff1a;Pushyami Kaveti, Arvind Thamil Chelvan Hanumant Singh 编辑&#xff1a;点云PCL 来源&#xff1a;https://arxiv.org/pdf/2210.07315.pdf 代码&#x…

针对基于智能卡进行认证的活动目录的攻击

最近&#xff0c;我参与了一项攻击基于智能卡的活动目录的工作。实际上&#xff0c;你根本不需要使用物理智能卡来验证登录这个活动目录。证书的属性决定了它是否可以用于基于智能卡进行登录。因此&#xff0c;如果你能获得相应的私钥&#xff0c;那么就可以绕过智能卡的验证实…

借助ChatGPT自动生成PPT

借助ChatGPT自动生成PPT 首先让GPT生成一段markdown格式的PPT内容&#xff0c;尽量描述全面&#xff0c;以什么语言&#xff0c;什么格式&#xff0c;排版等等。 打开mindshow网址&#xff0c;点击import and create&#xff0c;选择以markdown方式创建&#xff0c;再次点击弹…

LayerNorm 在 Transformers 中对注意力的作用研究

LayerNorm 一直是 Transformer 架构的重要组成部分。如果问大多人为什么要 LayerNorm&#xff0c;一般的回答是&#xff1a;使用 LayerNorm 来归一化前向传播的激活和反向传播的梯度。 其实这只是部分正确&#xff1a;Brody、Alon 和 Yahav 的一篇题为“On the Expressivity Ro…

LabVIEWCompactRIO 开发指南20 应用程序接口

应用程序接口 STMAPI如图4.24所示。对于基本操作&#xff0c;它由一个读VI和一个写VI组成。它还具有两个补充VI&#xff0c;以帮助传输元数据&#xff0c;但它们的使用不是强制性的。每个主要的VI都是多态&#xff0c;这意味着可以将它们与不同的传输层一起使用。本文档讨论基…

WMS 窗口添加流程

WMS 系统窗口添加流程 文章目录 WMS 系统窗口添加流程一. addView二. addView代码分析2.1 应用端调用WindowManager的addView2.2 WindowManager的实现类是WindowManagerImpl2.3 WindowManagerGlobal2.4 setView2.4 addToDisplayAsUser&#xff08;Session.java&#xff09;2.5 …

韦东山Linux驱动入门实验班(2)hello驱动---驱动层与应用层通讯,以及自动产生设备节点

前言 &#xff08;1&#xff09;学习韦东山老师的Linux&#xff0c;因为他讲的很精简&#xff0c;以至于很多人听不懂。接下来我讲介绍韦东山老师的驱动实验班的第二个Hello程序。 &#xff08;2&#xff09;注意&#xff0c;请先学习完视频再来看这个教程&#xff01;本文仅供…

Oracle数据库实现limit功能

Oracle数据库不支持mysql中limit功能&#xff0c;但可以通过rownum来限制返回的结果集的行数&#xff0c;rownum并不是用户添加的字段&#xff0c;而是oracle系统自动添加的。 #1、使查询结果最多返回前100行&#xff1a; SELECT * FROM TESTSDK WHERE rownum<10; #2、查询结…

跟庄买股票得新技巧(2023.05.16)(绝密资料,只发一次)

昨天学了一个跟庄买卖股票的得新技能 统计昨天庄家异动的情况&#xff0c;按照行业分类&#xff08;板块&#xff09;板块对涨幅进行排序&#xff0c;涨幅排名分前三的是&#xff0c;龙头一&#xff0c;龙头二&#xff0c;龙头三买卖规则&#xff1a;看龙一&#xff0c;玩龙二…

如何使用Sentinel做流量控制?此文将附代码详细介绍Sentinel几种限流模式

前言&#xff1a;大家好&#xff0c;我是小威&#xff0c;24届毕业生&#xff0c;在一家满意的公司实习。本篇文章将详细介绍Sentinel的两种限流模式&#xff0c;由于篇幅原因&#xff0c;后续文章将详细介绍Sentinel的其他三种。 如果文章有什么需要改进的地方还请大佬不吝赐教…

SCS【25】单细胞细胞间通信第一部分细胞通讯可视化(CellChat)

桓峰基因公众号推出单细胞生信分析教程并配有视频在线教程&#xff0c;目前整理出来的相关教程目录如下&#xff1a; Topic 6. 克隆进化之 Canopy Topic 7. 克隆进化之 Cardelino Topic 8. 克隆进化之 RobustClone SCS【1】今天开启单细胞之旅&#xff0c;述说单细胞测序的前世…

Servlet进阶API、监听器与过滤器

过滤器和监听器是Servlet规范里的两个高级特性&#xff0c; 过滤器的作用是通过对request、response 的修改实现特定的功能&#xff0c;例如请求数据字符编码、IP地址过滤、异常过滤、用户身份认证等。监听器的作用是用于监听Web程序中正在执行的程序&#xff0c; 根据发生的事…

罗德与施瓦茨Rohde Schwarz FSW8 2HZ-8GHZ信号分析仪FSW13收购

罗德与施瓦茨Rohde & Schwarz FSW8 2HZ-8GHZ信号分析仪 附加功能&#xff1a; 10 kHz 偏移&#xff08;1 GHz 载波&#xff09;时的低相位噪声为 –137 dBc (1 Hz) 用于 WCDMA ACLR 测量的 –88 dBc 动态范围&#xff08;带噪声消除&#xff09; 高达 2 GHz 的分析带宽 &…

Camtasia2023.0.1CS电脑录制屏幕动作工具新功能介绍

Camtasia Studio是一款专门录制屏幕动作的工具&#xff0c;它能在任何颜色模式下轻松地记录 屏幕动作&#xff0c;包括影像、音效、鼠标移动轨迹、解说声音等等&#xff0c;另外&#xff0c;它还具有即时播放和编 辑压缩的功能&#xff0c;可对视频片段进行剪接、添加转场效果。…

云原生背景下如何配置 JVM 内存

image.png 背景 前段时间业务研发反馈说是他的应用内存使用率很高&#xff0c;导致频繁的重启&#xff0c;让我排查下是怎么回事&#xff1b; 在这之前我也没怎么在意过这个问题&#xff0c;正好这次排查分析的过程做一个记录。 首先我查看了监控面板里的 Pod 监控&#xff1a;…

指令的运行原理及Linux权限解读

目录 一. 指令的运行原理 二. Linux下的用户 2.1 Linux的用户分类 2.2 用户之间的切换 三. 文件权限的概念 3.1 文件类型 3.2 文件的权限 3.3 ls -l 打印文件全部信息的解读 四. 权限的修改 五. 拥有者和所属组的修改 六. 起始权限问题和权限掩码umask 七. 目录文件…

ArcGIS之克里金插值教学

本文来自&#xff1a;GIS科研实验室 基本概念 1.什么是克里金插值&#xff1f; 克里金插值又称空间局部插值法&#xff0c;是以半变异函数理论和结构分析为基础&#xff0c;在有限区域内对区域化变量进行无偏最优估计的一种方法&#xff0c;是地统计学的主要内容之一。南非矿产…

【消息中间件】RocketMQ消息重复消费场景及解决办法

文章目录 前言那么在什么情况下会发生RocketMQ的消息重复消费呢&#xff1f;消息重复消费的场景大概可以分为生产者端重复消费和消费者端重复消费&#xff0c;那么如何来解决消息的重复消费呢&#xff1f;既然在生产者做幂等性的方案都不是特别靠谱&#xff0c;那就再在消费者端…

信创办公–基于WPS的EXCEL最佳实践系列 (宏的录制)

信创办公–基于WPS的EXCEL最佳实践系列 &#xff08;宏的录制&#xff09; 目录 应用背景操作步骤1、宏的录制启用2、宏的使用3、宏的保存4、宏的禁用 应用背景 宏是一个自动化完成重复性工作的工具&#xff0c;使用宏可以提高我们的工作效率&#xff0c;那应该怎样使用宏这一…

日志与时间戳,客户端与服务器端,打包压缩解压解包介绍,date,cal,zip,unzip,tar指令等

日志与时间戳 计算机世界里面&#xff0c;时间其实很重要的&#xff0c;首先我们需要有日志这个概念&#xff0c;这个日志其实就跟日记一样&#xff0c;那么在日记里面的话就会有时间。时间真的非常关键&#xff0c;比方在出现问题的时候去找到这个问题出现的时间点&#xff0…