Enums trong Javascript với ES6


136

Tôi đang xây dựng lại một dự án Java cũ trong Javascript và nhận ra rằng không có cách nào tốt để thực hiện enum trong JS.

Điều tốt nhất tôi có thể đưa ra là:

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

Việc constgiữ Colorskhông bị gán lại và đóng băng nó sẽ ngăn chặn các khóa và giá trị. Tôi đang sử dụng các Biểu tượng để nó Colors.REDkhông bằng 0hoặc bất cứ thứ gì khác ngoài chính nó.

Có một vấn đề với công thức này? Có cách nào tốt hơn?


(Tôi biết câu hỏi này có một chút lặp lại, nhưng tất cả các Câu hỏi trước đó đều khá cũ và ES6 cung cấp cho chúng tôi một số khả năng mới.)


BIÊN TẬP:

Một giải pháp khác, liên quan đến vấn đề tuần tự hóa, nhưng tôi tin rằng vẫn có vấn đề về địa hạt:

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

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

Bằng cách sử dụng các tham chiếu đối tượng làm các giá trị, bạn sẽ có được sự tránh va chạm giống như các Biểu tượng.


2
đây sẽ là một cách tiếp cận hoàn hảo trong es6. Bạn không cần phải đóng băng nó
Nirus

2
@Nirus bạn làm, nếu bạn không muốn nó được sửa đổi.
zerkms

2
Bạn có nhận thấy câu trả lời này ?
Bergi

3
Một vấn đề tôi có thể nghĩ đến: Không thể sử dụng enum này với JSON.stringify(). Không thể tuần tự hóa / giải trừ Symbol.
le_m

1
@ErictheRed Tôi đã sử dụng các giá trị hằng số của chuỗi enum trong nhiều năm mà không gặp rắc rối nào, bởi vì sử dụng Flow (hoặc TypeScript) đảm bảo an toàn cho loại nhiều hơn so với việc băn khoăn về việc tránh va chạm sẽ xảy ra
Andy

Câu trả lời:


131

Có một vấn đề với công thức này?

Tôi không thấy cái nào cả.

Có cách nào tốt hơn?

Tôi sẽ thu gọn hai tuyên bố thành một:

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

Nếu bạn không thích bản tóm tắt, như các Symbolcuộc gọi lặp đi lặp lại , tất nhiên bạn cũng có thể viết một hàm trợ giúp makeEnumtạo ra điều tương tự từ danh sách các tên.


3
Không có vấn đề cảnh giới ở đây?

2
@torazaburo Ý bạn là, khi mã được tải hai lần, nó sẽ tạo ra các ký hiệu khác nhau, điều đó sẽ không thành vấn đề với chuỗi? Vâng, điểm tốt, làm cho nó một câu trả lời :-)
Bergi

2
@ErictheRed Không, Symbol.forkhông không có vấn đề xuyên lĩnh vực, tuy nhiên nó không có vấn đề va chạm thông thường với một không gian tên toàn cầu thực sự .
Bergi

1
@ErictheRed Thực sự đảm bảo tạo ra cùng một biểu tượng bất kể khi nào và ở đâu (từ đó cõi / khung / tab / quy trình) được gọi là
Bergi

1
@jamesemanon Bạn có thể lấy mô tả nếu bạn muốn , nhưng tôi sẽ chỉ sử dụng nó để gỡ lỗi. Thay vào đó có một hàm chuyển đổi enum-to-string tùy chỉnh như bình thường (một cái gì đó dọc theo dòng enum => ({[Colors.RED]: "bright red", [Colors.BLUE]: "deep blue", [Colors.GREEN]: "grass green"}[enum])).
Bergi

18

Trong khi sử dụng Symbollàm giá trị enum hoạt động tốt cho các trường hợp sử dụng đơn giản, nó có thể hữu ích để cung cấp các thuộc tính cho enum. Điều này có thể được thực hiện bằng cách sử dụng một Objectgiá trị enum chứa các thuộc tính.

Ví dụ: chúng ta có thể cung cấp cho mỗi Colorstên và giá trị 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" }
});

Bao gồm các thuộc tính trong enum tránh việc phải viết các switchcâu lệnh (và có thể quên các trường hợp mới cho các câu lệnh chuyển đổi khi một enum được mở rộng). Ví dụ này cũng cho thấy các thuộc tính và kiểu enum được ghi lại bằng chú thích enum của JSDoc .

Bình đẳng công trình như mong đợi với Colors.RED === Colors.REDviệc true, và Colors.RED === Colors.BLUEfalse.


9

Như đã đề cập ở trên, bạn cũng có thể viết một makeEnum()hàm trợ giúp:

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

Sử dụng nó như thế này:

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
Là một lớp lót: const makeEnum = (...lst) => Object.freeze(Object.assign({}, ...lst.map(k => ({[k]: Symbol(k)})))); Sau đó sử dụng nó như const colors = makeEnum("Red", "Green", "Blue")
Manuel Ebert

9

Đây là cách tiếp cận cá nhân của tôi.

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

Tôi không khuyên bạn nên sử dụng điều này vì nó không cung cấp cách lặp lại cho tất cả các giá trị có thể và không có cách nào để kiểm tra xem giá trị có phải là ColorType hay không mà không kiểm tra thủ công cho từng giá trị.
Domino

7

Kiểm tra cách TypeScript làm điều đó . Về cơ bản họ làm như sau:

const MAP = {};

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

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

Sử dụng các biểu tượng, đóng băng đối tượng, bất cứ điều gì bạn muốn.


Tôi không theo dõi lý do tại sao nó sử dụng MAP[MAP[1] = 'A'] = 1;thay vì MAP[1] = 'A'; MAP['A'] = 1;. Tôi đã luôn nghe nói rằng sử dụng một bài tập như một biểu thức là phong cách xấu. Ngoài ra, bạn có được lợi ích gì từ các bài tập nhân đôi?
Eric the Red

1
Đây là một liên kết đến cách ánh xạ enum được biên dịch thành es5 trong tài liệu của họ. typecriptlang.org/docs/handbook/enums.html#reverse-mappings Tôi có thể hình ảnh đơn giản sẽ dễ dàng và ngắn gọn hơn để biên dịch nó thành một dòng đơn, ví dụ MAP[MAP[1] = 'A'] = 1;.
đưa ra

Huh. Vì vậy, có vẻ như phản chiếu chỉ giúp dễ dàng chuyển đổi giữa các biểu diễn chuỗi và số / ký hiệu của từng giá trị và kiểm tra xem một số chuỗi hoặc số / ký hiệu xcó phải là giá trị Enum hợp lệ hay không Enum[Enum[x]] === x. Nó không giải quyết bất kỳ vấn đề ban đầu nào của tôi, nhưng có thể hữu ích và không phá vỡ bất cứ điều gì.
Eric the Red

1
Hãy nhớ rằng TypeScript thêm một lớp mạnh mẽ bị mất sau khi mã TS được biên dịch. Nếu toàn bộ ứng dụng của bạn được viết bằng TS thì thật tuyệt, nhưng nếu bạn muốn mã JS mạnh mẽ, bản đồ biểu tượng đóng băng có vẻ như là một mẫu an toàn hơn.
Domino



1

Có lẽ giải pháp này? :)

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

Thí dụ:

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

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

một ví dụ sử dụng sẽ thực sự được đánh giá cao :-)
Abderrahmane TAHRI JOUTI

0

Tôi thích cách tiếp cận của @ tonethar, với một chút cải tiến và đào sâu vì lợi ích của việc hiểu rõ hơn về nền tảng của hệ sinh thái ES6 / Node.js. Với một nền tảng ở phía máy chủ của hàng rào, tôi thích cách tiếp cận của phong cách chức năng xung quanh các nguyên thủy của nền tảng, điều này giảm thiểu sự phình to mã, độ dốc trơn trượt vào thung lũng quản lý của bóng tối của cái chết do sự ra đời của các loại mới và tăng khả năng đọc - làm rõ hơn ý định của giải pháp và thuật toán.

Giải pháp với 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))hoàn toàn không làm gì cả Bạn chỉ có thể sử dụng ...argstrực tiếp.
Domino

0

Đây là cách tiếp cận của tôi, bao gồm một số phương pháp trợ giúp

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

bạn cũng có thể sử dụng gói es6-enum ( https://www.npmjs.com/package/es6-enum ). Nó rất dễ sử dụng. Xem ví dụ dưới đây:

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

10
ví dụ nào dưới đây?
Alexander

nếu bạn làm một ví dụ, mọi người sẽ bỏ phiếu cho câu trả lời của bạn.
Artem Fedotov

0

Đây là cách triển khai Java của tôi trong JavaScript.

Tôi cũng bao gồm các bài kiểm tra đơn vị.

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

Bạn có thể sử dụng Bản đồ ES6

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

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

IMHO là một giải pháp tồi vì tính phức tạp của nó (nên gọi phương thức truy cập mọi lúc) và tự phân biệt bản chất enum (có thể gọi phương thức trình biến đổi và thay đổi giá trị của bất kỳ khóa nào) ... const x = Object.freeze({key: 'value'})thay vào đó hãy sử dụng để có được thứ gì đó trông và cư xử như enum trong ES6
Yurii Rabeshko

Bạn phải vượt qua một chuỗi để có được giá trị, như bạn đã làm colors.get ('ĐỎ'). Đó là lỗi dễ bị.
adrian oviedo
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.