Enum dalam Javascript dengan ES6

136

Saya membangun kembali proyek Java lama di Javascript, dan menyadari bahwa tidak ada cara yang baik untuk melakukan enum di JS.

Yang terbaik yang bisa saya dapatkan adalah:

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

The constterus Colorsdari yang ditugaskan kembali, dan pembekuan mencegah bermutasi kunci dan nilai-nilai. Saya menggunakan Simbol sehingga Colors.REDtidak sama dengan 0, atau apa pun selain itu sendiri.

Apakah ada masalah dengan formulasi ini? Apakah ada cara yang lebih baik?


(Saya tahu pertanyaan ini sedikit berulang, tetapi semua T / As sebelumnya cukup lama, dan ES6 memberi kita beberapa kemampuan baru.)


EDIT:

Solusi lain, yang berhubungan dengan masalah serialisasi, tapi saya percaya masih memiliki masalah ranah:

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

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

Dengan menggunakan referensi objek sebagai nilainya, Anda mendapatkan tabrakan-penghindaran yang sama dengan Simbol.

Eric si Merah
sumber
2
ini akan menjadi pendekatan yang sempurna dalam es6. Anda tidak perlu membekukannya
Nirus
2
@Nirus Anda lakukan, jika Anda tidak ingin itu dimodifikasi.
zerkms
2
Apakah Anda memperhatikan jawaban ini ?
Bergi
3
Satu masalah yang bisa saya pikirkan: Tidak dapat menggunakan enum ini dengan JSON.stringify(). Tidak dapat membuat serial / deserialize Symbol.
le_m
1
@ErictheRed Saya telah menggunakan nilai konstan string enum selama bertahun-tahun tanpa kerepotan, karena menggunakan Flow (atau TypeScript) menjamin keamanan jenis yang jauh lebih banyak daripada mengkhawatirkan tentang penghindaran tabrakan yang pernah
Andy

Jawaban:

131

Apakah ada masalah dengan formulasi ini?

Saya tidak melihat apa pun.

Apakah ada cara yang lebih baik?

Saya akan menciutkan dua pernyataan menjadi satu:

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

Jika Anda tidak menyukai boilerplate, seperti Symbolpanggilan berulang , tentu saja Anda juga dapat menulis fungsi pembantu makeEnumyang menciptakan hal yang sama dari daftar nama.

Bergi
sumber
3
Apakah tidak ada masalah ranah di sini?
2
@torazaburo Maksud Anda, ketika kode dimuat dua kali akan menghasilkan simbol yang berbeda, yang tidak akan menjadi masalah dengan string? Ya, poin bagus, buatlah jawabannya :-)
Bergi
2
@ErictheRed Tidak, Symbol.fortidak tidak memiliki isu-isu lintas bidang, namun memang memiliki masalah tabrakan biasa dengan namespace benar-benar global .
Bergi
1
@ErictheRed Memang tidak menjamin untuk membuat simbol yang sama persis terlepas dari kapan dan di mana (dari mana ranah / frame / tab / proses) itu disebut
Bergi
1
@jamesemanon Anda bisa mendapatkan deskripsi jika Anda mau , tetapi saya akan menggunakannya terutama untuk debugging saja. Alih-alih memiliki fungsi konversi enum-to-string khusus seperti biasa (sesuatu di sepanjang baris enum => ({[Colors.RED]: "bright red", [Colors.BLUE]: "deep blue", [Colors.GREEN]: "grass green"}[enum])).
Bergi
18

Sementara menggunakan Symbolkarena nilai enum berfungsi dengan baik untuk kasus penggunaan sederhana, akan berguna untuk memberikan properti ke enum. Ini dapat dilakukan dengan menggunakan Objectsebagai nilai enum yang mengandung properti.

Sebagai contoh, kita dapat memberikan masing-masing Colorsnama dan nilai hex:

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

Termasuk properti dalam enum menghindari keharusan menulis switchpernyataan (dan mungkin melupakan kasus baru dengan pernyataan beralih ketika enum diperpanjang). Contoh ini juga memperlihatkan properti enum dan tipe yang didokumentasikan dengan anotasi enum JSDoc .

Kesetaraan bekerja seperti yang diharapkan dengan Colors.RED === Colors.REDkeberadaan true, dan Colors.RED === Colors.BLUEkeberadaan false.

Justin Emery
sumber
9

Seperti disebutkan di atas, Anda juga bisa menulis makeEnum()fungsi pembantu:

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

Gunakan seperti ini:

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
sumber
2
Sebagai one-liner: const makeEnum = (...lst) => Object.freeze(Object.assign({}, ...lst.map(k => ({[k]: Symbol(k)})))); Kemudian gunakan sebagai const colors = makeEnum("Red", "Green", "Blue")
Manuel Ebert
9

Ini adalah pendekatan pribadi saya.

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
sumber
Saya tidak akan merekomendasikan menggunakan ini karena tidak ada cara untuk mengulangi semua nilai yang mungkin, dan tidak ada cara untuk memeriksa apakah suatu nilai adalah ColorType tanpa secara manual memeriksa masing-masing.
Domino
7

Periksa bagaimana TypeScript melakukannya . Pada dasarnya mereka melakukan hal berikut:

const MAP = {};

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

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

Gunakan simbol, bekukan objek, apa pun yang Anda inginkan.

beri pelukan
sumber
Saya tidak mengikuti mengapa menggunakan MAP[MAP[1] = 'A'] = 1;bukan MAP[1] = 'A'; MAP['A'] = 1;. Saya selalu mendengar bahwa menggunakan tugas sebagai ekspresi adalah gaya yang buruk. Juga, manfaat apa yang Anda dapatkan dari tugas cermin?
Eric the Red
1
Berikut adalah tautan ke bagaimana pemetaan enum dikompilasi ke es5 dalam dokumen mereka. typescriptlang.org/docs/handbook/enums.html#reverse-mappings Saya bisa membayangkannya akan lebih mudah dan lebih ringkas untuk mengkompilasinya menjadi satu baris misalnya MAP[MAP[1] = 'A'] = 1;.
berikan
Hah. Jadi sepertinya mirroring hanya membuatnya mudah untuk beralih antara representasi string dan angka / simbol dari setiap nilai, dan memeriksa apakah beberapa string atau angka / simbol xadalah nilai Enum yang valid dengan melakukan Enum[Enum[x]] === x. Itu tidak memecahkan masalah asli saya, tetapi bisa bermanfaat dan tidak merusak apa pun.
Eric the Red
1
Perlu diingat bahwa TypeScript menambahkan lapisan kekokohan yang hilang begitu kode TS dikompilasi. Jika seluruh aplikasi Anda ditulis dalam TS itu bagus, tetapi jika Anda ingin kode JS menjadi kuat, peta simbol yang beku terdengar seperti pola yang lebih aman.
Domino
1

Mungkin solusi ini? :)

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

Contoh:

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

> {RED: Symbol(red), GREEN: Symbol(green), BLUE: Symbol(blue)}
Mateusz Stefański
sumber
contoh penggunaan akan sangat dihargai :-)
Abderrahmane TAHRI JOUTI
0

Saya lebih suka pendekatan @ tonethar, dengan sedikit peningkatan dan menggali untuk kepentingan memahami dengan lebih baik dasar-dasar ekosistem ES6 / Node.js. Dengan latar belakang di sisi server pagar, saya lebih suka pendekatan gaya fungsional di sekitar platform primitif, ini meminimalkan kode mengasapi, lereng licin ke lembah manajemen negara bayangan bayang-bayang karena pengenalan jenis baru dan peningkatan keterbacaan - memperjelas maksud solusi dan algoritma.

Solusi dengan 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
sumber
Array.from(Object.assign(args))sama sekali tidak melakukan apa pun. Anda bisa ...argslangsung menggunakannya .
Domino
0

Inilah pendekatan saya, termasuk beberapa metode pembantu

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
sumber
0

Anda juga dapat menggunakan paket es6-enum ( https://www.npmjs.com/package/es6-enum ). Ini sangat mudah digunakan. Lihat contoh di bawah ini:

import Enum from "es6-enum";
const Colors = Enum("red", "blue", "green");
Colors.red; // Symbol(red)
Fawaz
sumber
10
contoh mana di bawah ini?
Alexander
jika Anda memberi contoh, orang-orang akan memilih jawaban Anda.
Artem Fedotov
0

Berikut ini adalah implementasi enumerasi Java saya dalam JavaScript.

Saya juga menyertakan unit test.

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>

Tuan Polywhirl
sumber
-3

Anda bisa menggunakan Peta ES6

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

console.log(colors.get('RED'));
Valentin Micu
sumber
IMHO itu solusi yang buruk karena kerumitannya (harus memanggil metode accessor setiap waktu) dan kontradiksi dari sifat enum (dapat memanggil metode mutator dan mengubah nilai kunci apa saja) ... jadi gunakan const x = Object.freeze({key: 'value'})sebagai gantinya untuk mendapatkan sesuatu yang terlihat dan berperilaku seperti enum di ES6
Yurii Rabeshko
Anda harus melewati string untuk mendapatkan nilai, seperti yang Anda lakukan pada colors.get ('RED'). Yang rawan kesalahan.
adrian oviedo