Перечисления в Javascript с ES6


136

Я перестраиваю старый Java-проект в Javascript и понял, что в JS нет хорошего способа сделать перечисления.

Лучшее, что я могу придумать, это:

const Colors = {
    RED: Symbol("red"),
    BLUE: Symbol("blue"),
    GREEN: Symbol("green")
};
Object.freeze(Colors);

constХранит Colorsот быть переназначены, и замораживание он предотвращает мутирует ключи и значения. Я использую символы, которые Colors.REDне равны 0, или что-то еще, кроме самого себя.

Есть ли проблема с этой формулировкой? Есть ли способ лучше?


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


РЕДАКТИРОВАТЬ:

Другое решение, которое касается проблемы сериализации, но я считаю, что все еще есть проблемы с областями:

const enumValue = (name) => Object.freeze({toString: () => name});

const Colors = Object.freeze({
    RED: enumValue("Colors.RED"),
    BLUE: enumValue("Colors.BLUE"),
    GREEN: enumValue("Colors.GREEN")
});

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


2
это был бы идеальный подход в es6. Вы не должны замораживать это
Nirus

2
@ Вирус, если вы не хотите, чтобы он был изменен.
зеркмс

2
Вы заметили этот ответ ?
Берги

3
Одна проблема, о которой я могу подумать: не могу использовать это перечисление JSON.stringify(). Невозможно сериализовать / десериализовать Symbol.
le_m

1
@ErictheRed Я годами использую постоянные значения строкового перечисления без каких-либо хлопот, потому что использование Flow (или TypeScript) гарантирует гораздо большую безопасность типов, чем когда-либо беспокоятся об избежании столкновений
Энди

Ответы:


131

Есть ли проблема с этой формулировкой?

Я не вижу никого.

Есть ли способ лучше?

Я бы свел два утверждения в одно:

const Colors = Object.freeze({
    RED:   Symbol("red"),
    BLUE:  Symbol("blue"),
    GREEN: Symbol("green")
});

Если вам не нравится шаблон, как повторяющиеся Symbolвызовы, вы, конечно же, можете написать вспомогательную функцию, makeEnumкоторая создает то же самое из списка имен.


3
Здесь нет проблем с царством?

2
@torazaburo Вы имеете в виду, что когда код загружается дважды, он генерирует разные символы, что не будет проблемой для строк? Да, хорошая мысль, сделайте ответ :-)
Берги

2
@ErictheRed Нет, Symbol.forэто не имеет проблем кросс-Realm, однако он имеет обычную проблему столкновения с поистине глобальным пространством имен .
Берги

1
@ErictheRed Это действительно гарантирует создание одного и того же символа независимо от того, когда и где (из какой области / фрейма / табуляции / процесса) он называется
Bergi

1
@jamesemanon Вы можете получить описание, если хотите , но я бы использовал его в основном только для отладки. Скорее обычная функция преобразования перечисления в строку (что-то вроде строки enum => ({[Colors.RED]: "bright red", [Colors.BLUE]: "deep blue", [Colors.GREEN]: "grass green"}[enum])).
Берги

18

Хотя использование Symbolв качестве значения перечисления отлично подходит для простых случаев использования, может быть удобно присвоить свойства перечислениям. Это можно сделать, используя в Objectкачестве значения перечисления, содержащего свойства.

Например, мы можем дать каждому из Colorsимя и шестнадцатеричное значение:

/**
 * Enum for common colors.
 * @readonly
 * @enum {{name: string, hex: string}}
 */
const Colors = Object.freeze({
  RED:   { name: "red", hex: "#f00" },
  BLUE:  { name: "blue", hex: "#00f" },
  GREEN: { name: "green", hex: "#0f0" }
});

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

Равенство, как и ожидалось, работает с Colors.RED === Colors.REDбытием trueи Colors.RED === Colors.BLUEбытием false.


9

Как упоминалось выше, вы также можете написать makeEnum()вспомогательную функцию:

function makeEnum(arr){
    let obj = {};
    for (let val of arr){
        obj[val] = Symbol(val);
    }
    return Object.freeze(obj);
}

Используйте это так:

const Colors = makeEnum(["red","green","blue"]);
let startColor = Colors.red; 
console.log(startColor); // Symbol(red)

if(startColor == Colors.red){
    console.log("Do red things");
}else{
    console.log("Do non-red things");
}

2
Как однострочник: const makeEnum = (...lst) => Object.freeze(Object.assign({}, ...lst.map(k => ({[k]: Symbol(k)})))); Тогда используйте это как const colors = makeEnum("Red", "Green", "Blue")
Мануэль Эберт

9

Это мой личный подход.

class ColorType {
    static get RED () {
        return "red";
    }

    static get GREEN () {
        return "green";
    }

    static get BLUE () {
        return "blue";
    }
}

// Use case.
const color = Color.create(ColorType.RED);

Я бы не рекомендовал использовать это, так как он не позволяет перебирать все возможные значения и проверять, является ли значение ColorType, не проверяя вручную каждое из них.
Домино

7

Проверьте, как это делает TypeScript . В основном они делают следующее:

const MAP = {};

MAP[MAP[1] = 'A'] = 1;
MAP[MAP[2] = 'B'] = 2;

MAP['A'] // 1
MAP[1] // A

Используйте символы, заморозьте объект, что хотите.


Я не понимаю, почему он использует MAP[MAP[1] = 'A'] = 1;вместо MAP[1] = 'A'; MAP['A'] = 1;. Я всегда слышал, что использование присваивания в качестве выражения - плохой стиль. Кроме того, какую выгоду вы получаете от зеркальных заданий?
Эрик Рыжий,

1
Вот ссылка на то, как enum mapping компилируется в es5 в их документах. typescriptlang.org/docs/handbook/enums.html#reverse-mappings Я могу представить, что было бы проще и лаконичнее скомпилировать его в одну строку, например MAP[MAP[1] = 'A'] = 1;.
Givehug

Да. Таким образом, похоже, что зеркальное отображение просто упрощает переключение между строковым и числовым / символьным представлениями каждого значения и проверяет, является ли некоторая строка или число / символ xдопустимым значением Enum Enum[Enum[x]] === x. Это не решает ни одну из моих первоначальных проблем, но может быть полезно и ничего не ломает.
Эрик Красный

1
Имейте в виду, что TypeScript добавляет слой надежности, который теряется после компиляции кода TS. Если все ваше приложение написано на TS, это прекрасно, но если вы хотите, чтобы код JS был надежным, замороженная карта символов звучит как более безопасный образец.
Домино


4

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


1

Может быть, это решение? :)

function createEnum (array) {
  return Object.freeze(array
    .reduce((obj, item) => {
      if (typeof item === 'string') {
        obj[item.toUpperCase()] = Symbol(item)
      }
      return obj
    }, {}))
}

Пример:

createEnum(['red', 'green', 'blue']);

> {RED: Symbol(red), GREEN: Symbol(green), BLUE: Symbol(blue)}

пример использования был бы очень признателен :-)
Абдеррахман ТАХРИ ДЖОТИ

0

Я предпочитаю подход @ tonethar с небольшим количеством улучшений и копаний, чтобы лучше понять основы экосистемы ES6 / Node.js. На фоне серверной части забора я предпочитаю подход функционального стиля вокруг примитивов платформы, это минимизирует раздувание кода, скользкий уклон в долину управления государством в тени смерти из-за введения новых типов и увеличения удобочитаемость - делает более ясным намерение решения и алгоритм.

Решение с TDD , ES6 , Node.js , Lodash , Jest , Babel , ESLint

// ./utils.js
import _ from 'lodash';

const enumOf = (...args) =>
  Object.freeze( Array.from( Object.assign(args) )
    .filter( (item) => _.isString(item))
    .map((item) => Object.freeze(Symbol.for(item))));

const sum = (a, b) => a + b;

export {enumOf, sum};
// ./utils.js

// ./kittens.js
import {enumOf} from "./utils";

const kittens = (()=> {
  const Kittens = enumOf(null, undefined, 'max', 'joe', 13, -13, 'tabby', new 
    Date(), 'tom');
  return () => Kittens;
})();

export default kittens();
// ./kittens.js 

// ./utils.test.js
import _ from 'lodash';
import kittens from './kittens';

test('enum works as expected', () => {
  kittens.forEach((kitten) => {
    // in a typed world, do your type checks...
    expect(_.isSymbol(kitten));

    // no extraction of the wrapped string here ...
    // toString is bound to the receiver's type
    expect(kitten.toString().startsWith('Symbol(')).not.toBe(false);
    expect(String(kitten).startsWith('Symbol(')).not.toBe(false);
    expect(_.isFunction(Object.valueOf(kitten))).not.toBe(false);

    const petGift = 0 === Math.random() % 2 ? kitten.description : 
      Symbol.keyFor(kitten);
    expect(petGift.startsWith('Symbol(')).not.toBe(true);
    console.log(`Unwrapped Christmas kitten pet gift '${petGift}', yeee :) 
    !!!`);
    expect(()=> {kitten.description = 'fff';}).toThrow();
  });
});
// ./utils.test.js

Array.from(Object.assign(args))абсолютно ничего не делает. Вы можете просто использовать ...argsнапрямую.
Домино

0

Вот мой подход, включая некоторые вспомогательные методы

export default class Enum {

    constructor(name){
        this.name = name;
    }

    static get values(){
        return Object.values(this);
    }

    static forName(name){
        for(var enumValue of this.values){
            if(enumValue.name === name){
                return enumValue;
            }
        }
        throw new Error('Unknown value "' + name + '"');
    }

    toString(){
        return this.name;
    }
}

-

import Enum from './enum.js';

export default class ColumnType extends Enum {  

    constructor(name, clazz){
        super(name);        
        this.associatedClass = clazz;
    }
}

ColumnType.Integer = new ColumnType('Integer', Number);
ColumnType.Double = new ColumnType('Double', Number);
ColumnType.String = new ColumnType('String', String);

0

вы также можете использовать пакет es6-enum ( https://www.npmjs.com/package/es6-enum ). Это очень просто в использовании. Смотрите пример ниже:

import Enum from "es6-enum";
const Colors = Enum("red", "blue", "green");
Colors.red; // Symbol(red)

10
какой пример ниже?
Александр

если вы приведете пример, люди будут голосовать за ваш ответ.
Артем Федотов

0

Вот моя реализация перечисления Java в JavaScript.

Я также включил модульные тесты.

const main = () => {
  mocha.setup('bdd')
  chai.should()

  describe('Test Color [From Array]', function() {
    let Color = new Enum('RED', 'BLUE', 'GREEN')
    
    it('Test: Color.values()', () => {
      Color.values().length.should.equal(3)
    })

    it('Test: Color.RED', () => {
      chai.assert.isNotNull(Color.RED)
    })

    it('Test: Color.BLUE', () => {
      chai.assert.isNotNull(Color.BLUE)
    })

    it('Test: Color.GREEN', () => {
      chai.assert.isNotNull(Color.GREEN)
    })

    it('Test: Color.YELLOW', () => {
      chai.assert.isUndefined(Color.YELLOW)
    })
  })

  describe('Test Color [From Object]', function() {
    let Color = new Enum({
      RED   : { hex: '#F00' },
      BLUE  : { hex: '#0F0' },
      GREEN : { hex: '#00F' }
    })

    it('Test: Color.values()', () => {
      Color.values().length.should.equal(3)
    })

    it('Test: Color.RED', () => {
      let red = Color.RED
      chai.assert.isNotNull(red)
      red.getHex().should.equal('#F00')
    })

    it('Test: Color.BLUE', () => {
      let blue = Color.BLUE
      chai.assert.isNotNull(blue)
      blue.getHex().should.equal('#0F0')
    })

    it('Test: Color.GREEN', () => {
      let green = Color.GREEN
      chai.assert.isNotNull(green)
      green.getHex().should.equal('#00F')
    })

    it('Test: Color.YELLOW', () => {
      let yellow = Color.YELLOW
      chai.assert.isUndefined(yellow)
    })
  })

  mocha.run()
}

class Enum {
  constructor(values) {
    this.__values = []
    let isObject = arguments.length === 1
    let args = isObject ? Object.keys(values) : [...arguments]
    args.forEach((name, index) => {
      this.__createValue(name, isObject ? values[name] : null, index)
    })
    Object.freeze(this)
  }

  values() {
    return this.__values
  }

  /* @private */
  __createValue(name, props, index) {
    let value = new Object()
    value.__defineGetter__('name', function() {
      return Symbol(name)
    })
    value.__defineGetter__('ordinal', function() {
      return index
    })
    if (props) {
      Object.keys(props).forEach(prop => {
        value.__defineGetter__(prop, function() {
          return props[prop]
        })
        value.__proto__['get' + this.__capitalize(prop)] = function() {
          return this[prop]
        }
      })
    }
    Object.defineProperty(this, name, {
      value: Object.freeze(value),
      writable: false
    })
    this.__values.push(this[name])
  }

  /* @private */
  __capitalize(str) {
    return str.charAt(0).toUpperCase() + str.slice(1)
  }
}

main()
.as-console-wrapper {
  top: 0;
  max-height: 100% !important;
}
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/2.2.5/mocha.css">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mocha/2.2.5/mocha.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.2.0/chai.js"></script>
<!--

public enum Color {
  RED("#F00"),
  BLUE("#0F0"),
  GREEN("#00F");
  
  private String hex;
  public String getHex()  { return this.hex;  }
  
  private Color(String hex) {
    this.hex = hex;
  }
}

-->
<div id="mocha"></div>


-3

Вы можете использовать ES6 Map

const colors = new Map([
  ['RED', 'red'],
  ['BLUE', 'blue'],
  ['GREEN', 'green']
]);

console.log(colors.get('RED'));

ИМХО, это плохое решение из-за его сложности (должен вызывать метод доступа каждый раз) и противоречия природы enum (может вызвать метод мутатора и изменить значение любого ключа) ... так что используйте const x = Object.freeze({key: 'value'})вместо этого, чтобы получить что-то, что выглядит и ведет себя как enum в ES6
Юрий Рабешко

Вы должны передать строку, чтобы получить значение, как вы сделали colors.get ('RED'). Что подвержено ошибкам.
Adrian Oviedo
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.