在JavaScript中4种创建枚举方式

news/2024/11/28 1:33:48/

本文译者为 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')#valueconstructor(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')#valueconstructor(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')#valueconstructor(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')#valueconstructor(value) {this.#value = value}toString() {return this.#value}
}

如果你喜欢使用类,那么基于类的枚举就可以发挥作用。然而,与冻结或代理枚举相比,基于类的枚举受到的保护较少。

你还知道 JavaScript 中创建枚举的其他方式吗?

- END -

关于奇舞团

奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。

9133359ca1707e01bc82b52364f095c0.png


http://www.ppmy.cn/news/69012.html

相关文章

浅聊一下Linuxptp

浅聊一下Linuxptp 文章目录 浅聊一下Linuxptp1.什么是Linuxptp2.安装Linuxptp3.源码解析一下1.8个带main函数的源文件1.hwstamp_ctl.c2.nsm.c3.phc2sys.84.phc_ctl.85.pmc.86.ptp4l.c7.timemaster.c8.ts2phc.c 2.clock.c文件 4.自己实践 1.什么是Linuxptp LinuxPTP&#xff08…

使用python进行图片的文字识别

使用python进行图片的文字识别 文章目录 使用python进行图片的文字识别安装 Tesseract OCR安装过程配置系统的环境变量 安装python的第三方库Pytesseract库Pillow库 运行个demo 安装 Tesseract OCR Tesseract OCR 是一款由 Google 团队开发的开源 OCR&#xff08;Optical Chara…

想发就发の日常朋友圈文案-小众碎片版

1.绝版的昨天 2.本周的潦草快乐 3.捕捉温柔 4.东拼西凑也蛮新鲜 5.过期日常一小筐 6.远古库存 7.稀里咕噜睡大觉 8.小小世界&#xff0c;开心至上 9.疯疯癫癫喜乐常常 10.天地狭小 日子紧凑 11.懒人日记 12.不普通的新鲜事 13.逃不掉的肥肉 14.多喝养乐多把快乐养…

软件 工程

目录 第十章、软件工程1、瀑布模型&#xff08;SDLC&#xff09;2、快速原型模型3、增量模型4、螺旋模型5、Ⅴ模型6、喷泉模型7、构建组装模型&#xff08;CBSD&#xff09;8、统一过程&#xff08;RUP&#xff09;9、敏捷开发方法10、信息系统开发方法11、需求开发12、结构化设…

应用架构总结

架构目标 高可用性 整体系统可用性最低99.9%&#xff0c;目标99.99%。全年故障时间整个系统不超过500分钟&#xff0c;单个系统故障不超过50分钟。 高可扩展性 系统架构简单清晰&#xff0c;应用系统间耦合低&#xff0c;容易水平扩展&#xff0c;业务功能增改方便快捷。 低成…

FL Studio21中文完整版All Plugins Edition及切换教程

说到制作电音的软件&#xff0c;coco玛奇朵一定会把FL Studio放到第一个来讲。水果是一款为了电子音乐而生的的宿主软件。水果&#xff0c;独特的节拍音序器组件和通道机架与混音台模块打造的编曲“块”的思路。是极为适合于电子音乐的编排。而且随着水果版本不断地升级&#x…

eKuiper 源码解读:从一条 SQL 到流处理任务的旅程

概述 LF Edge eKuiper 是 Golang 实现的轻量级物联网边缘分析、流式处理开源软件&#xff0c;可以运行在各类资源受限的边缘设备上。eKuiper 的主要目标是在边缘端提供一个流媒体软件框架。其规则引擎允许用户提供基于SQL 或基于图形&#xff08;类似于 Node-RED&#xff09;的…

无延迟直播/超低延迟直播快速接入的示例

简要说明 接入无延迟直播/超低延迟直播播放前&#xff0c;需确保直播间频道是无延迟频道&#xff0c;SDK中使用无延迟与常规播放无异&#xff0c;只需加入若干配置就可以快速接入。 什么是无延迟/超低延迟直播&#xff0c;可参见我的这篇文章&#xff1a; 无延时直播/超低延时…