JavaScript中的四种枚举方式
<p>字符串和数字具有无数个值,而其他类型如布尔值则是有限的集合。</p><p>一周的日子(星期一,星期二,...,星期日),一年的季节(冬季,春季,夏季,秋季)和基本方向(北,东,南,西)都是具有有限值集合的例子。</p>
<p>当一个变量有一个来自有限的预定义常量的值时,使用枚举是很方便的。枚举使你不必使用魔法数字和字符串(这被认为是一种反模式)。</p>
<p>让我们看看在JavaScript中创建枚举的四种好方法(及其优缺点)。</p>
<h2 id="基于对象的枚举">基于对象的枚举</h2>
<p>枚举是一种数据结构,它定义了一个有限的具名常量集。每个常量都可以通过其名称来访问。</p>
<p>让我们来考虑一件T恤衫的尺寸:<code>Small</code>,<code>Medium</code>,和<code>Large</code>。</p>
<p>在JavaScript中创建枚举的一个简单方法(虽然不是最理想的)是使用一个普通的JavaScript对象。</p>
<pre><code>const Sizes = {
Small: 'small',
Medium: 'medium',
Large: 'large',
}
const mySize = Sizes.Medium
console.log(mySize === Sizes.Medium) // logs true
</code></pre>
<p><code>Sizes</code>是一个基于JavaScript对象的枚举,它有三个具名常量:<code>Sizes.Small</code>、<code>Sizes.Medium</code>以及<code>Sizes.Large</code>。</p>
<p><code>Sizes</code>也是一个字符串枚举,因为具名常量的值是字符串:<code>'small'</code> ,<code>'medium'</code>,以及 <code>'large'</code>。</p>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6e8f2fa862434890a4b9eb178fee1fcd~tplv-k3u1fbpfcp-watermark.image?"></p>
<p>要访问具名常量值,请使用属性访问器。例如,<code>Sizes.Medium</code>的值是<code>'medium'</code>。</p>
<p>枚举的可读性更强,更明确,并消除了对魔法字符串或数字的使用。</p>
<h3 id="优缺点">优缺点</h3>
<p>普通的对象枚举之所以吸引人,是因为它很简单:只要定义一个带有键和值的对象,枚举就可以了。</p>
<p>但是在一个大的代码库中,有人可能会意外地修改枚举对象,这将影响应用程序的运行。</p>
<pre><code>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
</code></pre>
<p><code>Sizes.Medium</code> 枚举值被意外地改变。</p>
<p><code>size1</code>,虽然被初始化为<code>Sizes.Medium</code>,但不再等同于<code>Sizes.Medium</code>!</p>
<p>普通对象的实现没有受到保护,因此无法避免这种意外的改变。</p>
<p>让我们仔细看看字符串和<code>symbol</code>枚举。以及如何冻结枚举对象以避免意外改变的问题。</p>
<h2 id="枚举值类型">枚举值类型</h2>
<p>除了字符串类型,枚举值可以是一个数字:</p>
<pre><code>const Sizes = {
Small: 0,
Medium: 1,
Large: 2
}
const mySize = Sizes.Medium
console.log(mySize === Sizes.Medium) // logs true
</code></pre>
<p>上述例子中,<code>Sizes</code>枚举是数值枚举,因为值都是数字:0,1,2。</p>
<p>你也可以创建<code>symbol</code>枚举:</p>
<pre><code>const Sizes = {
Small: Symbol('small'),
Medium: Symbol('medium'),
Large: Symbol('large')
}
const mySize = Sizes.Medium
console.log(mySize === Sizes.Medium) // logs true
</code></pre>
<p>使用<code>symbol</code>的好处是,每个<code>symbol</code>都是唯一的。这意味着,你总是要通过使用枚举本身来比较枚举:</p>
<pre><code>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
</code></pre>
<p>使用<code>symbol</code>枚举的缺点是<code>JSON.stringify()</code>将<code>symbol</code>字符串化为<code>null</code>、<code>undefined</code>,或者跳过有<code>symbol</code>作为值的属性:</p>
<pre><code>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()
console.log(str2) // logs ''
const str3 = JSON.stringify({ size: Sizes.Small })
console.log(str3) // logs '{}'
</code></pre>
<p>在下面的例子中,我将使用字符串枚举。但是你可以自由地使用你需要的任何值类型。</p>
<p>如果你可以自由选择枚举值类型,就用字符串吧。字符串比数字和<code>symbol</code>更容易进行调试。</p>
<h2 id="基于objectfreeze枚举">基于Object.freeze()枚举</h2>
<p>保护枚举对象不被修改的一个好方法是冻结它。当一个对象被冻结时,你不能修改或向该对象添加新的属性。换句话说,这个对象变成了只读。</p>
<p>在JavaScript中,<code>Object.freeze()</code>工具函数可以冻结一个对象。让我们来冻结<code>Sizes</code>枚举:</p>
<pre><code>const Sizes = Object.freeze({
Small: 'small',
Medium: 'medium',
Large: 'large',
})
const mySize = Sizes.Medium
console.log(mySize === Sizes.Medium) // logs true
</code></pre>
<p><code>const Sizes = Object.freeze({ ... })</code> 创建一个冻结的对象。即使被冻结,你也可以自由地访问枚举值: <code>const mySize = Sizes.Medium</code>。</p>
<h3 id="优缺点-1">优缺点</h3>
<p>如果一个枚举属性被意外地改变了,JavaScript会抛出一个错误(在严格模式下):</p>
<pre><code>const Sizes = Object.freeze({
Small: 'Small',
Medium: 'Medium',
Large: 'Large',
})
const size1 = Sizes.Medium
const size2 = Sizes.Medium = 'foo' // throws TypeError
</code></pre>
<p>语句<code>const size2 = Sizes.Medium = 'foo'</code> 对 <code>Sizes.Medium</code> 属性进行了意外的赋值。</p>
<p>因为<code>Sizes</code>是一个冻结的对象,JavaScript(在严格模式下)会抛出错误:</p>
<pre><code>TypeError: Cannot assign to read only property 'Medium' of object <Object>
</code></pre>
<p>冻结的对象枚举被保护起来,不会被意外地改变。</p>
<p>不过,还有一个问题。如果你不小心把枚举常量拼错了,那么结果将是未<code>undefined</code>:</p>
<pre><code>const Sizes = Object.freeze({
Small: 'small',
Medium: 'medium',
Large: 'large',
})
console.log(Sizes.Med1um) // logs undefined
</code></pre>
<p><code>Sizes.Med1um</code>表达式(<code>Med1um</code>是<code>Medium</code>的错误拼写版本)结果为未定义,而不是抛出一个关于不存在的枚举常量的错误。</p>
<p>让我们看看基于代理的枚举如何解决这个问题。</p>
<h2 id="基于proxy枚举">基于proxy枚举</h2>
<p>一个有趣的,也是我最喜欢的实现,是基于代理的枚举。</p>
<p>代理是一个特殊的对象,它包裹着一个对象,以修改对原始对象的操作行为。代理并不改变原始对象的结构。</p>
<p>枚举代理拦截对枚举对象的读和写操作,并且:</p>
<ul>
<li>当访问一个不存在的枚举值时,会抛出一个错误。</li>
<li>当一个枚举对象的属性被改变时抛出一个错误</li>
</ul>
<p>下面是一个工厂函数的实现,它接受一个普通枚举对象,并返回一个代理对象:</p>
<pre><code>// 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
},
set(target, name, value) {
throw new Error('Cannot add a new value to the enum')
}
})
}
</code></pre>
<p>代理的<code>get()</code>方法拦截读取操作,如果属性名称不存在,则抛出一个错误。</p>
<p><code>set()</code>方法拦截写操作,但只是抛出一个错误。这是为保护枚举对象不被写入操作而设计的。</p>
<p>让我们把<code>sizes</code>对象枚举包装成一个代理:</p>
<pre><code>import { Enum } from './enum'
const Sizes = Enum({
Small: 'small',
Medium: 'medium',
Large: 'large',
})
const mySize = Sizes.Medium
console.log(mySize === Sizes.Medium) // logs true
</code></pre>
<p>代理枚举的工作方式与普通对象枚举完全一样。</p>
<h3 id="优缺点-2">优缺点</h3>
<p>然而,代理枚举受到保护,以防止意外覆盖或访问不存在的枚举常量:</p>
<pre><code>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
</code></pre>
<p><code>Sizes.Med1um</code>抛出一个错误,因为<code>Med1um</code>常量名称在枚举中不存在。</p>
<p><code>Sizes.Medium = 'foo'</code> 抛出一个错误,因为枚举属性已被改变。</p>
<p>代理枚举的缺点是,你总是要导入枚举工厂函数,并将你的枚举对象包裹在其中。</p>
<h2 id="基于类的枚举">基于类的枚举</h2>
<p>另一种有趣的创建枚举的方法是使用一个JavaScript类。</p>
<p>一个基于类的枚举包含一组静态字段,其中每个静态字段代表一个枚举的常量。每个枚举常量的值本身就是该类的一个实例。</p>
<p>让我们用一个<code>Sizes</code>类来实现<code>sizes</code>枚举:</p>
<pre><code>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
</code></pre>
<p><code>Sizes</code>是一个代表枚举的类。枚举常量是该类的静态字段,例如,<code>static Small = new Sizes('small')</code>。</p>
<p><code>Sizes</code>类的每个实例也有一个私有字段<code>#value</code>,它代表枚举的原始值。</p>
<p>基于类的枚举的一个很好的优点是能够在运行时使用<code>instanceof</code>操作来确定值是否是枚举。例如,<code>mySize instanceof Sizes</code>结果为真,因为<code>mySize</code>是一个枚举值。</p>
<p>基于类的枚举比较是基于实例的(而不是在普通、冻结或代理枚举的情况下的原始比较):</p>
<pre><code>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
</code></pre>
<p><code>mySize</code>(即<code>Sizes.Small</code>)不等于<code>new Sizes('small')</code>。</p>
<p><code>Sizes.Small</code>和<code>new Sizes('small')</code>,即使具有相同的<code>#value</code>,也是不同的对象实例。</p>
<h3 id="优缺点-3">优缺点</h3>
<p>基于类的枚举不能受到保护,以防止覆盖或访问不存在的枚举具名常量。</p>
<pre><code>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
</code></pre>
<p>但你可以控制新实例的创建,例如,通过计算在构造函数内创建了多少个实例。然后在创建超过3个实例时抛出一个错误。</p>
<p>当然,最好让你的枚举实现尽可能的简单。枚举的目的是为了成为普通的数据结构。</p>
<h2 id="总结">总结</h2>
<p>在JavaScript中,有4种创建枚举的好方法。</p>
<p>最简单的方法是使用一个普通的JavaScript对象:</p>
<pre><code>const MyEnum = {
Option1: 'option1',
Option2: 'option2',
Option3: 'option3'
}
</code></pre>
<p>普通的对象枚举适合小型项目或快速演示。</p>
<p>第二种选择,如果你想保护枚举对象不被意外覆盖,则可以使用冻结的对象:</p>
<pre><code>const MyEnum = Object.freeze({
Option1: 'option1',
Option2: 'option2',
Option3: 'option3'
})
</code></pre>
<p>冻结的对象枚举适合于中型或大型项目,你要确保枚举不会被意外地改变。</p>
<p>第三种选择是代理方法:</p>
<pre><code>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
},
set(target, name, value) {
throw new Error('Cannot add a new value to the enum')
}
})
}
</code></pre>
<pre><code>import { Enum } from './enum'
const MyEnum = Enum({
Option1: 'option1',
Option2: 'option2',
Option3: 'option3'
})
</code></pre>
<p>代理枚举适用于中型或大型项目,以更好地保护你的枚举不被覆盖或访问不存在的命名常量。</p>
<p>代理的枚举是我个人的偏好。</p>
<p>第四种选择是使用基于类的枚举,其中每个命名的常量都是类的实例,并作为类的静态属性被存储:</p>
<pre><code>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
}
}
</code></pre>
<p>如果你喜欢类的话,基于类的枚举是可行的。然而,基于类的枚举比冻结的或代理的枚举保护得更少。</p>
<p>你还知道哪些在JavaScript中创建枚举的方法?</p>
<p>以上就是本文的全部内容,如果对你有所帮助,欢迎点赞、收藏、转发~</p><br><br>
来源:https://www.cnblogs.com/chuckQu/p/17380596.html
頁:
[1]