Aufzählungen in Javascript mit ES6


136

Ich habe ein altes Java-Projekt in Javascript neu erstellt und festgestellt, dass es in JS keine gute Möglichkeit gibt, Aufzählungen durchzuführen.

Das Beste, was ich mir einfallen lassen kann, ist:

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

Das consthält Colorsvon neu zugewiesen wird, und das Einfrieren es verhindert , dass die Schlüssel und Werte mutiert. Ich benutze Symbole, damit das Colors.REDnicht gleich ist 0, oder irgendetwas anderes außer sich selbst.

Gibt es ein Problem mit dieser Formulierung? Gibt es einen besseren Weg?


(Ich weiß, dass diese Frage ein bisschen wiederholt wird, aber alle vorherigen Fragen und Antworten sind ziemlich alt, und ES6 bietet uns einige neue Funktionen.)


BEARBEITEN:

Eine andere Lösung, die sich mit dem Serialisierungsproblem befasst, aber meiner Meinung nach immer noch Probleme mit dem Bereich hat:

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

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

Durch die Verwendung von Objektreferenzen als Werte erhalten Sie die gleiche Kollisionsvermeidung wie bei Symbolen.


2
Dies wäre ein perfekter Ansatz in es6. Sie müssen es nicht einfrieren
Nirus

2
@Nirus tun Sie, wenn Sie nicht möchten, dass es geändert wird.
Zerkms

2
Haben Sie diese Antwort bemerkt ?
Bergi

3
Ein Problem, an das ich denken kann: Ich kann diese Aufzählung nicht verwenden JSON.stringify(). Kann nicht serialisieren / deserialisieren Symbol.
le_m

1
@ErictheRed Ich verwende seit Jahren ohne Probleme konstante Werte für die String-Enumeration, da die Verwendung von Flow (oder TypeScript) weitaus mehr Typensicherheit garantiert, als es die Angst vor Kollisionsvermeidung jemals tun wird
Andy

Antworten:


131

Gibt es ein Problem mit dieser Formulierung?

Ich sehe keine.

Gibt es einen besseren Weg?

Ich würde die beiden Aussagen zu einer zusammenfassen:

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

Wenn Ihnen das Boilerplate nicht gefällt, wie die wiederholten SymbolAufrufe, können Sie natürlich auch eine Hilfsfunktion schreiben makeEnum, die dasselbe aus einer Liste von Namen erstellt.


3
Gibt es hier keine Reichsprobleme?

2
@torazaburo Du meinst, wenn der Code zweimal geladen wird, werden verschiedene Symbole generiert, was bei Zeichenfolgen kein Problem wäre? Ja, guter Punkt, machen Sie eine Antwort :-)
Bergi

2
@ErictheRed Nein, es Symbol.forgibt keine bereichsübergreifenden Probleme, es gibt jedoch das übliche Kollisionsproblem mit einem wirklich globalen Namespace .
Bergi

1
@ErictheRed Es garantiert in der Tat, genau das gleiche Symbol zu erstellen, unabhängig davon, wann und wo (von welchem ​​Bereich / Rahmen / Tab / Prozess) es aufgerufen wird
Bergi

1
@jamesemanon Sie können die Beschreibung erhalten, wenn Sie möchten , aber ich würde sie hauptsächlich zum Debuggen verwenden. Verwenden Sie lieber wie gewohnt eine benutzerdefinierte Konvertierungsfunktion von Enum zu String (etwas in der Art enum => ({[Colors.RED]: "bright red", [Colors.BLUE]: "deep blue", [Colors.GREEN]: "grass green"}[enum])).
Bergi

18

Während die Verwendung Symbolals Aufzählungswert für einfache Anwendungsfälle gut funktioniert, kann es nützlich sein, Aufzählungen Eigenschaften zuzuweisen. Dies kann durch Verwendung von Objectals Aufzählungswert erfolgen, der die Eigenschaften enthält.

Zum Beispiel können wir jedem Colorseinen Namen und einen Hex-Wert geben:

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

Durch das Einfügen von Eigenschaften in die Aufzählung wird vermieden, dass switchAnweisungen geschrieben werden müssen (und möglicherweise werden neue Fälle für die switch-Anweisungen vergessen, wenn eine Aufzählung erweitert wird). Das Beispiel zeigt auch die Enum-Eigenschaften und -Typen, die mit der JSDoc-Enum-Annotation dokumentiert sind .

Gleichheit funktioniert wie erwartet mit Colors.RED === Colors.REDSein trueund Colors.RED === Colors.BLUESein false.


9

Wie oben erwähnt, können Sie auch eine makeEnum()Hilfsfunktion schreiben :

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

Verwenden Sie es so:

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
Als const makeEnum = (...lst) => Object.freeze(Object.assign({}, ...lst.map(k => ({[k]: Symbol(k)}))));const colors = makeEnum("Red", "Green", "Blue")
Manuel Ebert

9

Dies ist mein persönlicher Ansatz.

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);

Ich würde nicht empfehlen, dies zu verwenden, da es keine Möglichkeit bietet, alle möglichen Werte zu durchlaufen und nicht zu überprüfen, ob ein Wert ein ColorType ist, ohne manuell nach jedem zu suchen.
Domino

7

Überprüfen Sie, wie TypeScript das macht . Grundsätzlich machen sie Folgendes:

const MAP = {};

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

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

Verwenden Sie Symbole, frieren Sie Objekte ein, was immer Sie wollen.


Ich folge nicht , warum es verwendet MAP[MAP[1] = 'A'] = 1;statt MAP[1] = 'A'; MAP['A'] = 1;. Ich habe immer gehört, dass die Verwendung einer Aufgabe als Ausdruck ein schlechter Stil ist. Welchen Nutzen ziehen Sie aus den gespiegelten Aufgaben?
Eric der Rote

1
Hier ist ein Link dazu, wie die Enum-Zuordnung in ihren Dokumenten zu es5 kompiliert wird. typescriptlang.org/docs/handbook/enums.html#reverse-mappings Ich kann mir vorstellen, dass es einfach einfacher und prägnanter wäre, es in eine einzelne Zeile zu kompilieren, z MAP[MAP[1] = 'A'] = 1;.
Givehug

Huh. Es sieht also so aus, als ob die Spiegelung es einfach macht, zwischen den Zeichenfolgen- und Zahlen- / Symboldarstellungen jedes Werts zu wechseln und zu überprüfen, ob eine Zeichenfolge oder eine Zahl / ein Symbol xein gültiger Enum-Wert ist Enum[Enum[x]] === x. Es löst keines meiner ursprünglichen Probleme, könnte aber nützlich sein und nichts kaputt machen.
Eric der Rote

1
Beachten Sie, dass TypeScript eine Robustheitsebene hinzufügt, die beim Kompilieren des TS-Codes verloren geht. Wenn Ihre gesamte App in TS geschrieben ist, ist das großartig, aber wenn Sie möchten, dass JS-Code robust ist, klingt die eingefrorene Karte der Symbole wie ein sichereres Muster.
Domino



1

Vielleicht diese Lösung? :) :)

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

Beispiel:

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

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

Ein Anwendungsbeispiel wäre sehr dankbar :-)
Abderrahmane TAHRI JOUTI

0

Ich bevorzuge den Ansatz von @ tonethar, mit ein paar Verbesserungen und Ausgrabungen, um die Grundlagen des ES6 / Node.js-Ökosystems besser zu verstehen. Mit einem Hintergrund auf der Serverseite des Zauns bevorzuge ich den Ansatz des funktionalen Stils um die Grundelemente der Plattform herum. Dies minimiert das Aufblähen des Codes, den rutschigen Abhang in das Managementtal des Staates im Schatten des Todes aufgrund der Einführung neuer Typen und Erhöhungen die Lesbarkeit - macht die Absicht der Lösung und des Algorithmus klarer.

Lösung mit 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))macht absolut nichts. Sie könnten einfach ...argsdirekt verwenden.
Domino

0

Hier ist mein Ansatz, einschließlich einiger Hilfsmethoden

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

Sie können auch das es6-enum-Paket verwenden ( https://www.npmjs.com/package/es6-enum ). Es ist sehr einfach zu bedienen. Siehe das folgende Beispiel:

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

10
Welches Beispiel unten?
Alexander

Wenn Sie ein Beispiel geben, werden die Leute für Ihre Antwort stimmen.
Artem Fedotov

0

Hier ist meine Implementierung einer Java-Aufzählung in JavaScript.

Ich habe auch Unit-Tests eingeschlossen.

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

Sie können ES6 Map verwenden

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

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

IMHO ist es eine schlechte Lösung wegen seiner Komplexität (sollte die Accessor-Methode jedes Mal aufrufen) und eines Widerspruchs der Aufzählungsart (kann die Mutator-Methode aufrufen und einen Wert eines beliebigen Schlüssels ändern) ... also verwenden Sie const x = Object.freeze({key: 'value'})stattdessen, um etwas zu erhalten, das aussieht und aussieht verhält sich wie Enum in ES6
Yurii Rabeshko

Sie müssen eine Zeichenfolge übergeben, um den Wert zu erhalten, wie Sie es bei farben.get ('ROT') getan haben. Welches ist fehleranfällig.
Adrian Oviedo
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.