4 способа создания перечислений в JavaScript

Изучите 4 различных способа создания перечислений в JavaScript, включая использование ключевого слова enum, простого объекта или символа. Эта статья поможет вам выбрать лучший способ создания перечислений для вашего проекта.

Строки и числа имеют бесконечный набор значений, в то время как другие типы, такие как логические значения, ограничены конечным набором.

Дни недели (понедельник, вторник, ..., воскресенье), времена года (зима, весна, лето, осень) и стороны света (север, восток, юг, запад) являются примерами множеств с конечными значениями. .

Использование перечисления удобно, когда переменная имеет значение из конечного набора предопределенных констант. Перечисление избавляет вас от использования магических чисел и строк (что считается антишаблоном ) .

Давайте рассмотрим 4 хороших способа создания перечислений в JavaScript (с их плюсами и минусами).

Оглавление

  • 1. Перечисление на основе простого объекта
  • 2. Типы значений перечисления
  • 3. Перечисление на основе Object.freeze()
  • 4. Перечисление на основе прокси
  • 5. Перечисление на основе класса
  • 6. Заключение

1. Перечисление на основе простого объекта

Перечисление — это структура данных, определяющая конечный набор именованных констант. К каждой константе можно получить доступ по ее имени.

Рассмотрим размеры футболки: 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!

Реализация простого объекта не защищена от таких случайных изменений.

Давайте подробнее рассмотрим перечисления строк и символов. И затем, как заморозить объект перечисления, чтобы избежать проблемы случайного изменения.

2. Типы значений перечисления

Помимо строкового типа, значением перечисления может быть число:

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 '{}'

Откройте демо.

В следующих примерах я буду использовать перечисления строк. Но вы можете использовать любой тип значения, который вам нужен.

Если вы можете свободно выбирать тип значения перечисления, просто используйте строки. Строки легче отлаживать, чем числа и символы.

3. Перечисление на основе Object.freeze()

Хороший способ защитить объект перечисления от изменений — заморозить его. Когда объект заморожен, вы не можете изменять или добавлять к нему новые свойства. Другими словами, объект становится доступным только для чтения.

В 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а не выдает ошибку о несуществующей перечислимой константе.

Давайте посмотрим, как перечисление на основе прокси может решить даже эту проблему.

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()метод перехватывает операции записи и просто выдает ошибку. Он предназначен для защиты объекта перечисления от операций записи.

Давайте обернем перечисление объекта размеров в прокси:

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фабричную функцию и помещать в нее объекты перечисления.

5. Перечисление на основе класса

Еще один интересный способ создания перечисления — использование класса 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 экземпляров.

Конечно, лучше сделать реализацию перечисления максимально простой. Перечисления представляют собой простые структуры данных.

6. Заключение

Есть 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

1.30 GEEK