字串和數字具有無數個值,而其他型別如布林值則是有限的集合。
一週的日子(星期一,星期二,...,星期日),一年的季節(冬季,春季,夏季,秋季)和基本方向(北,東,南,西)都是具有有限值集合的例子。
當一個變數有一個來自有限的預定義常量的值時,使用列舉是很方便的。列舉使你不必使用魔法數字和字串(這被認為是一種反模式)。
讓我們看看在JavaScript中建立列舉的四種好方法(及其優缺點)。
基於物件的列舉
列舉是一種資料結構,它定義了一個有限的具名常量集。每個常量都可以透過其名稱來訪問。
讓我們來考慮一件T恤衫的尺寸:Small
,Medium
,和Large
。
在JavaScript中建立列舉的一個簡單方法(雖然不是最理想的)是使用一個普通的JavaScript物件。
const Sizes = {
Small: 'small',
Medium: 'medium',
Large: 'large',
}
const mySize = Sizes.Medium
console.log(mySize === Sizes.Medium) // logs true
Sizes
是一個基於JavaScript物件的列舉,它有三個具名常量:Sizes.Small
、Sizes.Medium
以及Sizes.Large
。
Sizes
也是一個字串列舉,因為具名常量的值是字串:'small'
,'medium'
,以及 'large'
。
要訪問具名常量值,請使用屬性訪問器。例如,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
!
普通物件的實現沒有受到保護,因此無法避免這種意外的改變。
讓我們仔細看看字串和symbol
列舉。以及如何凍結列舉物件以避免意外改變的問題。
列舉值型別
除了字串型別,列舉值可以是一個數字:
const Sizes = {
Small: 0,
Medium: 1,
Large: 2
}
const mySize = Sizes.Medium
console.log(mySize === Sizes.Medium) // logs true
上述例子中,Sizes
列舉是數值列舉,因為值都是數字:0,1,2。
你也可以建立symbol
列舉:
const Sizes = {
Small: Symbol('small'),
Medium: Symbol('medium'),
Large: Symbol('large')
}
const mySize = Sizes.Medium
console.log(mySize === Sizes.Medium) // logs true
使用symbol
的好處是,每個symbol
都是唯一的。這意味著,你總是要透過使用列舉本身來比較列舉:
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
使用symbol
列舉的缺點是JSON.stringify()
將symbol
字串化為null
、undefined
,或者跳過有symbol
作為值的屬性:
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 '{}'
在下面的例子中,我將使用字串列舉。但是你可以自由地使用你需要的任何值型別。
如果你可以自由選擇列舉值型別,就用字串吧。字串比數字和symbol
更容易進行除錯。
基於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
表示式(Med1um
是Medium
的錯誤拼寫版本)結果為未定義,而不是丟擲一個關於不存在的列舉常量的錯誤。
讓我們看看基於代理的列舉如何解決這個問題。
基於proxy列舉
一個有趣的,也是我最喜歡的實現,是基於代理的列舉。
代理是一個特殊的物件,它包裹著一個物件,以修改對原始物件的操作行為。代理並不改變原始物件的結構。
列舉代理攔截對列舉物件的讀和寫操作,並且:
- 當訪問一個不存在的列舉值時,會丟擲一個錯誤。
- 當一個列舉物件的屬性被改變時丟擲一個錯誤
下面是一個工廠函式的實現,它接受一個普通列舉物件,並返回一個代理物件:
// 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
Sizes.Med1um
丟擲一個錯誤,因為Med1um
常量名稱在列舉中不存在。
Sizes.Medium = 'foo'
丟擲一個錯誤,因為列舉屬性已被改變。
代理列舉的缺點是,你總是要匯入列舉工廠函式,並將你的列舉物件包裹在其中。
基於類的列舉
另一種有趣的建立列舉的方法是使用一個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 Sizes('small')
。
Sizes
類的每個例項也有一個私有欄位#value
,它代表列舉的原始值。
基於類的列舉的一個很好的優點是能夠在執行時使用instanceof
操作來確定值是否是列舉。例如,mySize instanceof Sizes
結果為真,因為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.Small
和new Sizes('small')
,即使具有相同的#value
,也是不同的物件例項。
優缺點
基於類的列舉不能受到保護,以防止覆蓋或訪問不存在的列舉具名常量。
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
但你可以控制新例項的建立,例如,透過計算在建構函式內建立了多少個例項。然後在建立超過3個例項時丟擲一個錯誤。
當然,最好讓你的列舉實現儘可能的簡單。列舉的目的是為了成為普通的資料結構。
總結
在JavaScript中,有4種建立列舉的好方法。
最簡單的方法是使用一個普通的JavaScript物件:
const MyEnum = {
Option1: 'option1',
Option2: 'option2',
Option3: 'option3'
}
普通的物件列舉適合小型專案或快速演示。
第二種選擇,如果你想保護列舉物件不被意外覆蓋,則可以使用凍結的物件:
const MyEnum = Object.freeze({
Option1: 'option1',
Option2: 'option2',
Option3: 'option3'
})
凍結的物件列舉適合於中型或大型專案,你要確保列舉不會被意外地改變。
第三種選擇是代理方法:
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'
})
代理列舉適用於中型或大型專案,以更好地保護你的列舉不被覆蓋或訪問不存在的命名常量。
代理的列舉是我個人的偏好。
第四種選擇是使用基於類的列舉,其中每個命名的常量都是類的例項,並作為類的靜態屬性被儲存:
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
}
}
如果你喜歡類的話,基於類的列舉是可行的。然而,基於類的列舉比凍結的或代理的列舉保護得更少。
你還知道哪些在JavaScript中建立列舉的方法?
本文譯自:https://dmitripavlutin.com/javascript-enum/
以上就是本文的全部內容,如果對你有所幫助,歡迎點贊、收藏、轉發~