关于javascript:JavaScript中的四种枚举方式

2次阅读

共计 6573 个字符,预计需要花费 17 分钟才能阅读完成。

字符串和数字具备无数个值,而其余类型如布尔值则是无限的汇合。

一周的日子(星期一,星期二,…,星期日),一年的节令(夏季,秋季,冬季,秋季)和根本方向(北,东,南,西)都是具备无限值汇合的例子。

当一个变量有一个来自无限的预约义常量的值时,应用枚举是很不便的。枚举使你不用应用魔法数字和字符串(这被认为是一种反模式)。

让咱们看看在 JavaScript 中创立枚举的四种好办法(及其优缺点)。

基于对象的枚举

枚举是一种数据结构,它定义了一个无限的具名常量集。每个常量都能够通过其名称来拜访。

让咱们来思考一件 T 恤衫的尺寸:SmallMedium,和Large

在 JavaScript 中创立枚举的一个简略办法(尽管不是最现实的)是应用一个一般的 JavaScript 对象。

const Sizes = {
  Small: 'small',
  Medium: 'medium',
  Large: 'large',
}

const mySize = Sizes.Medium

console.log(mySize === Sizes.Medium) // logs true

Sizes是一个基于 JavaScript 对象的枚举,它有三个具名常量:Sizes.SmallSizes.Medium以及Sizes.Large

Sizes也是一个字符串枚举,因为具名常量的值是字符串:'small''medium',以及 'large'

要拜访具名常量值,请应用属性拜访器。例如,Sizes.Medium的值是'medium'

枚举的可读性更强,更明确,并打消了对魔法字符串或数字的应用。

优缺点

一般的对象枚举之所以吸引人,是因为它很简略:只有定义一个带有键和值的对象,枚举就能够了。

然而在一个大的代码库中,有人可能会意外地批改枚举对象,这将影响应用程序的运行。

const Sizes = {
  Small: 'small',
  Medium: 'medium',
  Large: 'large',
}

const size1 = Sizes.Medium
const size2 = Sizes.Medium = 'foo' // Changed!

console.log(size1 === Sizes.Medium) // logs false

Sizes.Medium 枚举值被意外地扭转。

size1,尽管被初始化为Sizes.Medium,但不再等同于Sizes.Medium

一般对象的实现没有受到爱护,因而无奈防止这种意外的扭转。

让咱们认真看看字符串和 symbol 枚举。以及如何解冻枚举对象以防止意外扭转的问题。

枚举值类型

除了字符串类型,枚举值能够是一个数字:

const Sizes = {
  Small: 0,
  Medium: 1,
  Large: 2
}

const mySize = Sizes.Medium

console.log(mySize === Sizes.Medium) // logs true

上述例子中,Sizes枚举是数值枚举,因为值都是数字:0,1,2。

你也能够创立 symbol 枚举:

const Sizes = {Small: Symbol('small'),
  Medium: Symbol('medium'),
  Large: Symbol('large')
}

const mySize = Sizes.Medium

console.log(mySize === Sizes.Medium) // logs true

应用 symbol 的益处是,每个 symbol 都是惟一的。这意味着,你总是要通过应用枚举自身来比拟枚举:

const Sizes = {Small: Symbol('small'),
  Medium: Symbol('medium'),
  Large: Symbol('large')
}

const mySize = Sizes.Medium

console.log(mySize === Sizes.Medium)     // logs true
console.log(mySize === Symbol('medium')) // logs false

应用 symbol 枚举的毛病是 JSON.stringify()symbol字符串化为 nullundefined,或者跳过有symbol 作为值的属性:

const Sizes = {Small: Symbol('small'),
  Medium: Symbol('medium'),
  Large: Symbol('large')
}

const str1 = JSON.stringify(Sizes.Small)
console.log(str1) // logs undefined

const str2 = JSON.stringify([Sizes.Small])
console.log(str2) // logs '[null]'

const str3 = JSON.stringify({size: Sizes.Small})
console.log(str3) // logs '{}'

在上面的例子中,我将应用字符串枚举。然而你能够自在地应用你须要的任何值类型。

如果你能够自由选择枚举值类型,就用字符串吧。字符串比数字和 symbol 更容易进行调试。

基于 Object.freeze()枚举

爱护枚举对象不被批改的一个好办法是解冻它。当一个对象被解冻时,你不能批改或向该对象增加新的属性。换句话说,这个对象变成了只读。

在 JavaScript 中,Object.freeze()工具函数能够解冻一个对象。让咱们来解冻 Sizes 枚举:

const Sizes = Object.freeze({
  Small: 'small',
  Medium: 'medium',
  Large: 'large',
})

const mySize = Sizes.Medium

console.log(mySize === Sizes.Medium) // logs true

const Sizes = Object.freeze({...}) 创立一个解冻的对象。即便被解冻,你也能够自在地拜访枚举值:const mySize = Sizes.Medium

优缺点

如果一个枚举属性被意外地扭转了,JavaScript 会抛出一个谬误(在严格模式下):

const Sizes = Object.freeze({
  Small: 'Small',
  Medium: 'Medium',
  Large: 'Large',
})

const size1 = Sizes.Medium
const size2 = Sizes.Medium = 'foo' // throws TypeError

语句const size2 = Sizes.Medium = 'foo'Sizes.Medium 属性进行了意外的赋值。

因为 Sizes 是一个解冻的对象,JavaScript(在严格模式下)会抛出谬误:

TypeError: Cannot assign to read only property 'Medium' of object <Object>

解冻的对象枚举被爱护起来,不会被意外地扭转。

不过,还有一个问题。如果你不小心把枚举常量拼错了,那么后果将是未undefined

const Sizes = Object.freeze({
  Small: 'small',
  Medium: 'medium',
  Large: 'large',
})

console.log(Sizes.Med1um) // logs undefined

Sizes.Med1um表达式(Med1umMedium 的谬误拼写版本)后果为未定义,而不是抛出一个对于不存在的枚举常量的谬误。

让咱们看看基于代理的枚举如何解决这个问题。

基于 proxy 枚举

一个乏味的,也是我最喜爱的实现,是基于代理的枚举。

代理是一个非凡的对象,它包裹着一个对象,以批改对原始对象的操作行为。代理并不扭转原始对象的构造。

枚举代理拦挡对枚举对象的读和写操作,并且:

  • 当拜访一个不存在的枚举值时,会抛出一个谬误。
  • 当一个枚举对象的属性被扭转时抛出一个谬误

上面是一个工厂函数的实现,它承受一个一般枚举对象,并返回一个代理对象:

// enum.js
export function Enum(baseEnum) {  
  return new Proxy(baseEnum, {get(target, name) {if (!baseEnum.hasOwnProperty(name)) {throw new Error(`"${name}" value does not exist in the enum`)
      }
      return baseEnum[name]
    },
    set(target, name, value) {throw new Error('Cannot add a new value to the enum')
    }
  })
}

代理的 get() 办法拦挡读取操作,如果属性名称不存在,则抛出一个谬误。

set()办法拦挡写操作,但只是抛出一个谬误。这是为爱护枚举对象不被写入操作而设计的。

让咱们把 sizes 对象枚举包装成一个代理:

import {Enum} from './enum'

const Sizes = Enum({
  Small: 'small',
  Medium: 'medium',
  Large: 'large',
})

const mySize = Sizes.Medium

console.log(mySize === Sizes.Medium) // logs true

代理枚举的工作形式与一般对象枚举齐全一样。

优缺点

然而,代理枚举受到爱护,以避免意外笼罩或拜访不存在的枚举常量:

import {Enum} from './enum'

const Sizes = Enum({
  Small: 'small',
  Medium: 'medium',
  Large: 'large',
})

const size1 = Sizes.Med1um         // throws Error: non-existing constant
const size2 = Sizes.Medium = 'foo' // throws Error: changing the enum

Sizes.Med1um抛出一个谬误,因为 Med1um 常量名称在枚举中不存在。

Sizes.Medium = 'foo' 抛出一个谬误,因为枚举属性已被扭转。

代理枚举的毛病是,你总是要导入枚举工厂函数,并将你的枚举对象包裹在其中。

基于类的枚举

另一种乏味的创立枚举的办法是应用一个 JavaScript 类。

一个基于类的枚举蕴含一组动态字段,其中每个动态字段代表一个枚举的常量。每个枚举常量的值自身就是该类的一个实例。

让咱们用一个 Sizes 类来实现 sizes 枚举:

class Sizes {static Small = new Sizes('small')
  static Medium = new Sizes('medium')
  static Large = new Sizes('large')
  #value

  constructor(value) {this.#value = value}

  toString() {return this.#value}
}

const mySize = Sizes.Small

console.log(mySize === Sizes.Small)  // logs true
console.log(mySize instanceof Sizes) // logs true

Sizes是一个代表枚举的类。枚举常量是该类的动态字段,例如,static Small = new Sizes('small')

Sizes类的每个实例也有一个公有字段#value,它代表枚举的原始值。

基于类的枚举的一个很好的长处是可能在运行时应用 instanceof 操作来确定值是否是枚举。例如,mySize instanceof Sizes后果为真,因为 mySize 是一个枚举值。

基于类的枚举比拟是基于实例的(而不是在一般、解冻或代理枚举的状况下的原始比拟):

class Sizes {static Small = new Sizes('small')
  static Medium = new Sizes('medium')
  static Large = new Sizes('large')
  #value

  constructor(value) {this.#value = value}

  toString() {return this.#value}
}

const mySize = Sizes.Small

console.log(mySize === new Sizes('small')) // logs false

mySize(即Sizes.Small)不等于new Sizes('small')

Sizes.Smallnew Sizes('small'),即便具备雷同的#value,也是不同的对象实例。

优缺点

基于类的枚举不能受到爱护,以避免笼罩或拜访不存在的枚举具名常量。

class Sizes {static Small = new Sizes('small')
  static Medium = new Sizes('medium')
  static Large = new Sizes('large')
  #value

  constructor(value) {this.#value = value}

  toString() {return this.#value}
}

const size1 = Sizes.medium         // a non-existing enum value can be accessed
const size2 = Sizes.Medium = 'foo' // enum value can be overwritten accidentally

但你能够管制新实例的创立,例如,通过计算在构造函数内创立了多少个实例。而后在创立超过 3 个实例时抛出一个谬误。

当然,最好让你的枚举实现尽可能的简略。枚举的目标是为了成为一般的数据结构。

总结

在 JavaScript 中,有 4 种创立枚举的好办法。

最简略的办法是应用一个一般的 JavaScript 对象:

const MyEnum = {
  Option1: 'option1',
  Option2: 'option2',
  Option3: 'option3'
}

一般的对象枚举适宜小型我的项目或疾速演示。

第二种抉择,如果你想爱护枚举对象不被意外笼罩,则能够应用解冻的对象:

const MyEnum = Object.freeze({
  Option1: 'option1',
  Option2: 'option2',
  Option3: 'option3'
})

解冻的对象枚举适宜于中型或大型项目,你要确保枚举不会被意外地扭转。

第三种抉择是代理办法:

export function Enum(baseEnum) {  
  return new Proxy(baseEnum, {get(target, name) {if (!baseEnum.hasOwnProperty(name)) {throw new Error(`"${name}" value does not exist in the enum`)
      }
      return baseEnum[name]
    },
    set(target, name, value) {throw new Error('Cannot add a new value to the enum')
    }
  })
}
import {Enum} from './enum'

const MyEnum = Enum({
  Option1: 'option1',
  Option2: 'option2',
  Option3: 'option3'
})

代理枚举实用于中型或大型项目,以更好地爱护你的枚举不被笼罩或拜访不存在的命名常量。

代理的枚举是我集体的偏好。

第四种抉择是应用基于类的枚举,其中每个命名的常量都是类的实例,并作为类的动态属性被存储:

class MyEnum {static Option1 = new MyEnum('option1')
  static Option2 = new MyEnum('option2')
  static Option3 = new MyEnum('option3')
  #value

  constructor(value) {this.#value = value}

  toString() {return this.#value}
}

如果你喜爱类的话,基于类的枚举是可行的。然而,基于类的枚举比解冻的或代理的枚举爱护得更少。

你还晓得哪些在 JavaScript 中创立枚举的办法?

本文译自:https://dmitripavlutin.com/javascript-enum/

以上就是本文的全部内容,如果对你有所帮忙,欢送点赞、珍藏、转发~

正文完
 0