Изучите 4 различных способа создания перечислений в JavaScript, включая использование ключевого слова enum, простого объекта или символа. Эта статья поможет вам выбрать лучший способ создания перечислений для вашего проекта.
Строки и числа имеют бесконечный набор значений, в то время как другие типы, такие как логические значения, ограничены конечным набором.
Дни недели (понедельник, вторник, ..., воскресенье), времена года (зима, весна, лето, осень) и стороны света (север, восток, юг, запад) являются примерами множеств с конечными значениями. .
Использование перечисления удобно, когда переменная имеет значение из конечного набора предопределенных констант. Перечисление избавляет вас от использования магических чисел и строк (что считается антишаблоном ) .
Давайте рассмотрим 4 хороших способа создания перечислений в JavaScript (с их плюсами и минусами).
Перечисление — это структура данных, определяющая конечный набор именованных констант. К каждой константе можно получить доступ по ее имени.
Рассмотрим размеры футболки: 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!
Реализация простого объекта не защищена от таких случайных изменений.
Давайте подробнее рассмотрим перечисления строк и символов. И затем, как заморозить объект перечисления, чтобы избежать проблемы случайного изменения.
Помимо строкового типа, значением перечисления может быть число:
const Sizes = {
Small: 0,
Medium: 1,
Large: 2
}
const mySize = Sizes.Medium
console.log(mySize === Sizes.Medium) // logs true
Sizesenum в приведенном выше примере является числовым перечислением, поскольку значения являются числами: 0, 1, 2.
Вы также можете создать перечисление символов:
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()символы преобразуются в строки null, undefinedили пропускаются свойства, имеющие символ в качестве значения:
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 '{}'
В следующих примерах я буду использовать перечисления строк. Но вы можете использовать любой тип значения, который вам нужен.
Если вы можете свободно выбирать тип значения перечисления, просто используйте строки. Строки легче отлаживать, чем числа и символы.
Хороший способ защитить объект перечисления от изменений — заморозить его. Когда объект заморожен, вы не можете изменять или добавлять к нему новые свойства. Другими словами, объект становится доступным только для чтения.
В JavaScript служебная функция Object.freeze() замораживает объект. Давайте заморозим перечисление размеров:
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) оценивается, undefinedа не выдает ошибку о несуществующей перечислимой константе.
Давайте посмотрим, как перечисление на основе прокси может решить даже эту проблему.
Интересная и моя любимая реализация — перечисления на основе прокси .
Прокси — это специальный объект, который обертывает объект для изменения поведения операций над исходным объектом. Прокси не меняет структуру исходного объекта.
Прокси-сервер перечисления перехватывает операции чтения и записи объекта перечисления и:
Вот реализация фабричной функции, которая принимает простой объект перечисления и возвращает прокси-объект:
// 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()метод перехватывает операции записи и просто выдает ошибку. Он предназначен для защиты объекта перечисления от операций записи.
Давайте обернем перечисление объекта размеров в прокси:
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'выдает ошибку, поскольку свойство перечисления было изменено.
Недостаток прокси-перечисления заключается в том, что вам всегда придется импортировать Enumфабричную функцию и помещать в нее объекты перечисления.
Еще один интересный способ создания перечисления — использование класса JavaScript.
Перечисление на основе классов содержит набор статических полей, где каждое статическое поле представляет перечисление с именем константа. Значение каждой перечислимой константы само по себе является экземпляром класса.
Давайте реализуем перечисление размеров с помощью класса 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оценивается как 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.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 экземпляров.
Конечно, лучше сделать реализацию перечисления максимально простой. Перечисления представляют собой простые структуры данных.
Есть 4 хороших способа создания перечислений в JavaScript.
Самый простой способ — использовать простой объект 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