大家好,这里是大家的林语冰。欢迎持续关注"前端俱乐部"。
每周的星期(周一、周二二、...、周日)、每年的季节(春夏秋冬)和基本方位(东西南北)等等,都是有限值的集合。
当变量的值来自一组有限的预定义常量时,此乃枚举的用武之地。枚举使我们规避"魔术数字"和"魔术字符串"等反模式。
大多数编程语言都原生支持枚举数据类型。虽然目前 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
枚举是数字枚举,因为值为数字:0
、1
、2
。
我们还可以创建 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
序列化为 null
、undefined
,或者跳过 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
Med1um
是 Medium
的错误拼写,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('无法向此枚举中添加新的枚举值')
}
})
}
Proxy
的 get()
方法会拦截读取操作,如果属性不存在就会报错。
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.Small
和 new 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
}
}
如果您喜欢类,基于类的枚举也能奏效。虽然但是,基于类的枚举的鲁棒性低于冻结或代理枚举。
欢迎持续关注"前端俱乐部"!坚持阅读,自律打卡,每天一次,进步一点。
谢谢大家的点赞,掰掰~