Wyliczenia w Javascript z ES6

136

Odbudowuję stary projekt Java w Javascript i zdałem sobie sprawę, że nie ma dobrego sposobu na wykonanie wyliczeń w JS.

Najlepsze, co mogę wymyślić, to:

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

constUtrzymuje Colorsprzed przeniesiony i zamrażania zapobiega mutacji klucze i wartości. Używam symboli, więc Colors.REDto nie jest równe 0ani nic poza sobą.

Czy jest jakiś problem z tym preparatem? Czy jest lepszy sposób?


(Wiem, że to pytanie jest trochę powtórzone, ale wszystkie poprzednie pytania i odpowiedzi są dość stare, a ES6 daje nam nowe możliwości.)


EDYTOWAĆ:

Inne rozwiązanie, które rozwiązuje problem serializacji, ale moim zdaniem nadal ma problemy z dziedziną:

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

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

Używając odniesień do obiektów jako wartości, uzyskuje się takie samo unikanie kolizji, jak w przypadku symboli.

Eric the Red
źródło
2
byłoby to idealne podejście w es6. Nie musisz go zamrażać
Nirus
2
@Nirus robisz, jeśli nie chcesz, aby był modyfikowany.
zerkms
2
Czy zauważyłeś tę odpowiedź ?
Bergi
3
Jeden problem, który przychodzi mi do głowy: nie można użyć tego wyliczenia z JSON.stringify(). Nie można serializować / deserializować Symbol.
le_m
1
@ErictheRed Od lat używam stałych wyliczania ciągów bez żadnych kłopotów, ponieważ używanie Flow (lub TypeScript) gwarantuje o wiele większe bezpieczeństwo typów niż martwienie się o unikanie kolizji
Andy

Odpowiedzi:

131

Czy jest jakiś problem z tym preparatem?

Nie widzę żadnego.

Czy jest lepszy sposób?

Złożyłbym te dwa stwierdzenia w jedno:

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

Jeśli nie podoba ci się standardowy szablon, na przykład powtarzające się Symbolwywołania, możesz oczywiście napisać funkcję pomocniczą, makeEnumktóra utworzy to samo z listy nazwisk.

Bergi
źródło
3
Czy nie ma tutaj problemów z rzeczywistością?
2
@torazaburo Masz na myśli, że gdy kod zostanie załadowany dwukrotnie, wygeneruje różne symbole, co nie byłoby problemem w przypadku ciągów? Tak, słuszna uwaga, niech to będzie odpowiedź :-)
Bergi
2
@ErictheRed Nie, Symbol.fornie nie mają problemów cross-realm, jednak to ma zwykle problemu kolizji z nazw prawdziwie globalnym .
Bergi
1
@ErictheRed To rzeczywiście gwarantuje utworzenie dokładnie tego samego symbolu niezależnie od tego, kiedy i gdzie (z jakiego obszaru / ramki / karty / procesu) jest on wywoływany
Bergi
1
@jamesemanon Możesz uzyskać opis, jeśli chcesz , ale użyłbym go głównie do debugowania. Zamiast tego używaj niestandardowej funkcji konwersji wyliczenia na ciąg, jak zwykle (coś wzdłuż linii enum => ({[Colors.RED]: "bright red", [Colors.BLUE]: "deep blue", [Colors.GREEN]: "grass green"}[enum])).
Bergi
18

Chociaż użycie Symbolwartości wyliczenia jako wartości wyliczeniowej działa dobrze w prostych przypadkach użycia, może być przydatne nadanie właściwości wyliczeniom. Można to zrobić, używając Objectjako wartości wyliczenia zawierającej właściwości.

Na przykład możemy podać Colorsnazwę i wartość szesnastkową:

/**
 * 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" }
});

Uwzględnienie właściwości w wyliczeniu pozwala uniknąć konieczności pisania switchinstrukcji (i prawdopodobnie zapominania o nowych przypadkach do instrukcji przełączania, gdy wyliczenie jest rozszerzane). Przykład pokazuje również właściwości wyliczenia i typy udokumentowane za pomocą adnotacji wyliczenia JSDoc .

Równość działa zgodnie z oczekiwaniami z Colors.RED === Colors.REDbyciem truei Colors.RED === Colors.BLUEbyciem false.

Justin Emery
źródło
9

Jak wspomniano powyżej, możesz również napisać funkcję makeEnum()pomocniczą:

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

Użyj tego w ten sposób:

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");
}
tonethar
źródło
2
Jako jedno-liniowiec: const makeEnum = (...lst) => Object.freeze(Object.assign({}, ...lst.map(k => ({[k]: Symbol(k)})))); użyj go jako const colors = makeEnum("Red", "Green", "Blue")
Manuel Ebert
9

To jest moje osobiste podejście.

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);
Vasile Alexandru Peşte
źródło
Nie polecałbym używania tego, ponieważ nie zapewnia możliwości iteracji po wszystkich możliwych wartościach i nie ma możliwości sprawdzenia, czy wartość jest typem ColorType bez ręcznego sprawdzania każdej z nich.
Domino
7

Sprawdź, jak to robi TypeScript . Zasadniczo wykonują następujące czynności:

const MAP = {};

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

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

Używaj symboli, zamrażaj obiekty, cokolwiek chcesz.

Przytulić
źródło
Nie rozumiem, dlaczego używa MAP[MAP[1] = 'A'] = 1;zamiast MAP[1] = 'A'; MAP['A'] = 1;. Zawsze słyszałem, że używanie przypisania jako wyrażenia to zły styl. Jakie korzyści uzyskasz z lustrzanych zadań?
Eric the Red
1
Oto łącze do sposobu kompilowania mapowania wyliczeń do es5 w ich dokumentach. typescriptlang.org/docs/handbook/enums.html#reverse-mappings Mogę sobie wyobrazić , że byłoby po prostu łatwiej i bardziej zwięźle skompilować to do pojedynczej linii, np MAP[MAP[1] = 'A'] = 1;.
Givehug
Huh. Wygląda więc na to, że dublowanie po prostu ułatwia przełączanie między reprezentacjami ciągu i liczby / symbolu każdej wartości i sprawdza, czy jakiś ciąg lub liczba / symbol xjest prawidłową wartością Enum Enum[Enum[x]] === x. Nie rozwiązuje żadnego z moich pierwotnych problemów, ale może być przydatny i niczego nie psuje.
Eric the Red
1
Należy pamiętać, że TypeScript dodaje warstwę niezawodności, która jest tracona po skompilowaniu kodu TS. Jeśli cała aplikacja jest napisana w TS, to świetnie, ale jeśli chcesz, aby kod JS był niezawodny, zamrożona mapa symboli brzmi jak bezpieczniejszy wzór.
Domino
1

Może to rozwiązanie? :)

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

Przykład:

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

> {RED: Symbol(red), GREEN: Symbol(green), BLUE: Symbol(blue)}
Mateusz Stefański
źródło
przykład użycia byłby naprawdę mile widziany :-)
Abderrahmane TAHRI JOUTI
0

Wolę podejście @ tonethar, z niewielkimi ulepszeniami i kopaniem w celu lepszego zrozumienia podstaw ekosystemu ES6 / Node.js. Mając tło po stronie serwera, wolę podejście stylu funkcjonalnego do prymitywów platformy, co minimalizuje rozdęcie kodu, śliskie nachylenie w dolinie cienia śmierci w zarządzaniu stanem dzięki wprowadzeniu nowych typów i wzrostów czytelność - wyjaśnia intencję rozwiązania i algorytm.

Rozwiązanie z 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
Cristian Malinescu
źródło
Array.from(Object.assign(args))nic nie robi. Możesz po prostu użyć ...argsbezpośrednio.
Domino
0

Oto moje podejście, w tym kilka metod pomocniczych

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);
Stefan
źródło
0

możesz również użyć pakietu es6-enum ( https://www.npmjs.com/package/es6-enum ). Jest bardzo łatwy w użyciu. Zobacz poniższy przykład:

import Enum from "es6-enum";
const Colors = Enum("red", "blue", "green");
Colors.red; // Symbol(red)
Fawaz
źródło
10
który przykład poniżej?
Alexander
jeśli podasz przykład, ludzie będą głosować na twoją odpowiedź.
Artem Fedotov
0

Oto moja implementacja wyliczenia Java w JavaScript.

Uwzględniłem również testy jednostkowe.

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>

Panie Polywhirl
źródło
-3

Możesz użyć mapy ES6

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

console.log(colors.get('RED'));
Valentin Micu
źródło
IMHO to złe rozwiązanie ze względu na swoją złożoność (powinno wywoływać metodę akcesora za każdym razem) i sprzeczność natury wyliczenia (można wywołać metodę mutatora i zmienić wartość dowolnego klucza) ... więc użyj const x = Object.freeze({key: 'value'})zamiast tego, aby uzyskać coś, co wygląda i zachowuje się jak wyliczenie w ES6
Yurii Rabeshko
Aby uzyskać wartość, musisz przekazać łańcuch, tak jak zrobiłeś to z colors.get ('RED'). Który jest podatny na błędy.
adrian oviedo