Enums ใน Javascript กับ ES6


147

ฉันกำลังสร้างโปรเจ็กต์ Java เก่าใน Javascript และตระหนักว่าไม่มีวิธีที่ดีในการทำ enums ใน JS

สิ่งที่ดีที่สุดที่ฉันสามารถทำได้คือ:

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

การconstเก็บรักษาไม่ให้Colorsถูกกำหนดใหม่และการแช่แข็งจะป้องกันไม่ให้คีย์และค่ากลายพันธุ์ ฉันใช้ Symbols Colors.REDไม่เท่ากับ0หรืออย่างอื่นนอกจากตัวมันเอง

สูตรนี้มีปัญหาหรือไม่? มีวิธีที่ดีกว่า?


(ฉันรู้ว่าคำถามนี้ค่อนข้างซ้ำ แต่Q / As ก่อนหน้านี้ทั้งหมดค่อนข้างเก่าและ 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
@Nirus คุณทำถ้าคุณไม่ต้องการให้แก้ไข
zerkms

2
คุณสังเกตเห็นคำตอบนี้หรือไม่?
Bergi

3
ประเด็นหนึ่งที่ฉันสามารถคิด: ไม่สามารถใช้ enum JSON.stringify()นี้กับ ไม่สามารถเป็นอันดับ / Symboldeserialize
le_m

1
@ErictheRed ฉันใช้ค่าคงที่สตริง enum มาหลายปีแล้วโดยไม่ต้องยุ่งยากใด ๆ เพราะการใช้ Flow (หรือ TypeScript) รับประกันความปลอดภัยมากกว่าการกังวลเกี่ยวกับการหลีกเลี่ยงการชนกัน
Andy

คำตอบ:


141

สูตรนี้มีปัญหาหรือไม่?

ไม่เห็นมีเลย

มีวิธีที่ดีกว่า?

ฉันจะยุบสองประโยคเป็นหนึ่ง:

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

หากคุณไม่ชอบหนังสือต้นแบบเช่นการSymbolโทรซ้ำ ๆคุณสามารถเขียนฟังก์ชันตัวช่วยmakeEnumที่สร้างสิ่งเดียวกันจากรายการชื่อได้


3
ที่นี่ไม่มีปัญหาเรื่องขอบเขตเหรอ?

2
@torazaburo คุณหมายถึงเมื่อโหลดโค้ดสองครั้งมันจะสร้างสัญลักษณ์ที่แตกต่างกันซึ่งจะไม่เป็นปัญหากับสตริง? ใช่ประเด็นที่ดีให้คำตอบ :-)
Bergi

2
@ErictheRed ไม่มีSymbol.forไม่ได้มีปัญหาข้ามดินแดน แต่มันจะมีปัญหาการปะทะกันตามปกติกับnamespace ระดับโลกอย่างแท้จริง
Bergi

1
@ErictheRed ไม่รับประกันว่าจะสร้างสัญลักษณ์เดียวกันอย่างแน่นอนไม่ว่าจะเรียกว่าเมื่อไรและที่ไหน (จากขอบเขต / เฟรม / แท็บ / กระบวนการใด)
Bergi

1
@jamesemanon คุณสามารถรับคำอธิบายได้หากต้องการแต่ฉันจะใช้เป็นหลักในการดีบักเท่านั้น แต่จะมีฟังก์ชันการแปลง enum-to-string ที่กำหนดเองตามปกติ (บางอย่างตามเส้นenum => ({[Colors.RED]: "bright red", [Colors.BLUE]: "deep blue", [Colors.GREEN]: "grass green"}[enum]))
Bergi

22

ในขณะที่การใช้Symbolเป็นค่า enum ทำงานได้ดีสำหรับกรณีการใช้งานที่เรียบง่าย แต่ก็มีประโยชน์ในการให้คุณสมบัติแก่ enum ซึ่งสามารถทำได้โดยใช้Objectเป็นค่า enum ที่มีคุณสมบัติ

ตัวอย่างเช่นเราสามารถกำหนด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" }
});

การรวมคุณสมบัติใน enum หลีกเลี่ยงการเขียนswitchคำสั่ง (และอาจลืมกรณีใหม่ไปยังคำสั่ง switch เมื่อขยาย enum) ตัวอย่างยังแสดงให้เห็นคุณสมบัติ enum และชนิดเอกสารที่มีคำอธิบายประกอบ JSDoc enum

ความเท่าเทียมกันทำงานตามที่คาดหวังกับColors.RED === Colors.REDการเป็นtrueและColors.RED === Colors.BLUEความเป็นfalseอยู่


11

นี่เป็นแนวทางส่วนตัวของฉัน

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 โดยไม่ต้องตรวจสอบด้วยตนเอง
Domino

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")
Manuel Ebert

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;. ฉันได้ยินมาตลอดว่าการใช้การมอบหมายงานเป็นสำนวนนั้นไม่ดี นอกจากนี้คุณจะได้รับประโยชน์อะไรจากงานที่ทำมิเรอร์
Eric the Red

1
ต่อไปนี้เป็นลิงค์ไปยังวิธีรวบรวม enum mapping ไปยัง es5 ในเอกสารของพวกเขา typescriptlang.org/docs/handbook/enums.html#reverse-mappingsMAP[MAP[1] = 'A'] = 1;ฉันสามารถภาพมันจะเพียงแค่จะกระชับได้ง่ายขึ้นและมากขึ้นเพื่อรวบรวมให้เช่นบรรทัดเดียว
Givehug

ฮะ. ดังนั้นดูเหมือนว่ามิเรอร์เพียงแค่ทำให้ง่ายต่อการสลับไปมาระหว่างสตริงและการแสดงเลขหมาย / สัญลักษณ์ของแต่ละค่าและตรวจสอบว่าสตริงบางส่วนหรือเลขหมาย / สัญลักษณ์xเป็นค่า Enum Enum[Enum[x]] === xที่ถูกต้องด้วยการทำ มันไม่ได้แก้ปัญหาเดิมของฉัน แต่อาจมีประโยชน์และไม่ผิดอะไร
Eric the Red

1
โปรดทราบว่า TypeScript จะเพิ่มเลเยอร์ความทนทานซึ่งจะหายไปเมื่อคอมไพล์โค้ด TS หากทั้งแอปของคุณเขียนด้วย TS จะดีมาก แต่ถ้าคุณต้องการให้โค้ด JS มีประสิทธิภาพแผนที่สัญลักษณ์ที่หยุดนิ่งจะดูเหมือนเป็นรูปแบบที่ปลอดภัยกว่า
Domino



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

ตัวอย่างการใช้งานจะได้รับการชื่นชมจริงๆ :-)
Abderrahmane TAHRI JOUTI

1

คุณยังสามารถใช้แพ็คเกจ 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
ตัวอย่างใดด้านล่าง
Alexander

หากคุณทำตัวอย่างผู้คนจะโหวตให้คำตอบของคุณ
Artem Fedotov

1

นี่คือการใช้งานการแจงนับ 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>


1

อัปเดต 11.05.2020:
แก้ไขเพื่อรวมฟิลด์แบบคงที่และวิธีการเพื่อจำลองพฤติกรรม enum "จริง"

หากคุณกำลังวางแผนที่จะอัปเดตฉันขอแนะนำให้ลองใช้สิ่งที่ฉันเรียกว่า "Enum Class" (ยกเว้นเบราว์เซอร์หรือข้อ จำกัด ของรันไทม์ env ที่คุณไม่สามารถยอมรับได้) มันง่ายมากคลาสที่และสะอาดซึ่งใช้ฟิลด์ส่วนตัวและตัวเข้าถึงที่ จำกัด เพื่อจำลองพฤติกรรมของ enum นี่เป็นบางครั้งที่ฉันทำใน C # เมื่อฉันต้องการสร้างฟังก์ชันเพิ่มเติมใน enum

ฉันตระหนักดีว่าเขตข้อมูลชั้นเรียนส่วนตัวยังคงทดลองอยู่ ณ จุดนี้ แต่ดูเหมือนว่าจะใช้งานได้เพื่อจุดประสงค์ในการสร้างชั้นเรียนที่มีเขตข้อมูล / คุณสมบัติที่ไม่เปลี่ยนรูป การรองรับเบราว์เซอร์ก็ดีเช่นกัน เบราว์เซอร์ "หลัก" เพียงตัวเดียวที่ไม่รองรับคือ Firefox (ซึ่งฉันมั่นใจว่าจะเร็ว ๆ นี้) และ IE (ใครสนใจ)

การปฏิเสธความรับผิด :
ฉันไม่ใช่ผู้พัฒนา ฉันเพิ่งรวบรวมสิ่งนี้เข้าด้วยกันเพื่อแก้ข้อ จำกัด ของ enums ที่ไม่มีอยู่ใน JS เมื่อฉันทำงานในโครงการส่วนตัว

คลาสตัวอย่าง

class Colors {
    // Private Fields
    static #_RED = 0;
    static #_GREEN = 1;
    static #_BLUE = 2;

    // Accessors for "get" functions only (no "set" functions)
    static get RED() { return this.#_RED; }
    static get GREEN() { return this.#_GREEN; }
    static get BLUE() { return this.#_BLUE; }
}

ตอนนี้คุณควรจะโทรหา enums ของคุณโดยตรงได้แล้ว

Colors.RED; // 0
Colors.GREEN; // 1
Colors.BLUE; // 2

การรวมกันของการใช้ฟิลด์ส่วนตัวและตัวเข้าถึงที่ จำกัด หมายความว่าค่า enum ที่มีอยู่ได้รับการปกป้องอย่างดี (โดยพื้นฐานแล้วจะเป็นค่าคงที่ในตอนนี้)

Colors.RED = 10 // Colors.RED is still 0
Colors._RED = 10 // Colors.RED is still 0
Colors.#_RED = 10 // Colors.RED is still 0

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โดยตรง
Domino

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

-3

คุณสามารถใช้ ES6 Map

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

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

IMHO เป็นทางออกที่ไม่ดีเนื่องจากมีความซับซ้อน (ควรเรียก accessor method ทุกครั้ง) และการปฏิเสธลักษณะ enum (สามารถเรียกวิธี mutator และเปลี่ยนค่าของคีย์ใดก็ได้) ... ดังนั้นใช้const x = Object.freeze({key: 'value'})แทนเพื่อให้ได้สิ่งที่ดูและ ทำตัวเหมือน enum ใน ES6
Yurii Rabeshko

คุณต้องส่งสตริงเพื่อรับค่าเช่นเดียวกับที่คุณทำ colors.get ('RED') ซึ่งเป็นข้อผิดพลาดได้ง่าย
adrian oviedo
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.