Define Static Constant Properties In JS Objects

在 JavaScript 定义静态常量属性

Saturday, February 13, 2016

看到了下面这段 Java 代码,突然有种冲动想要用 JavaScript 来实现:

public class Movie {
  public static final int REGULAR = 0;
  public static final int NEW_RELEASE = 1;
  public static final int CHILREN = 2;
  ...
}

JavaScript 虽然是面向对象的编程语言,但是本身并没有类的概念。在 ES5 中,我们一般借助 Function 对象来模拟类即所谓的『伪类模式』,在 ES6 中,则可以新的语法糖 class 关键词实现:

// ES5
var Movie = function() {};

// ES6
class Movie {
}

静态常量

接下来实现 REGULARNEW_RELEASECHILDREN 这三个静态常量。static 意味着值这三个属性可以被直接访问而不需要实例化对象,而 final 则意味着它们都是常量,其值一旦被定义就无法被修改。

首先我们先编写测试用例表达我们的意图。

测试用例

由于静态属性可以被直接访问,所以(1)Movie 这个『类』(实质是一个对象)应该拥有这三个属性。我们把这个测试要求编写为一个 expect 函数:

var expectOwnProperty = function(property) {
  expect(Movie).to.have.ownProperty(property);
  expect(Movie.prototype).not.to.have.ownProperty(property);
};

接下来我们继续编写一个检查属性是否为常量的测试代码。按照常量的定义,我们可以确定(2)如果对象的某个属性是一个常量,那么这个属性的两个特征值 writableconfigurable 均为 false

在 JavaScript 中,调用 Object.getOwnPropertyDescriptor 方法来获取属性的特性信息,为此我们编写另一个 expect 函数:

var expectConstant = function(property) {
  var propDescriptor = Object.getOwnPropertyDescriptor(Movie, property);
  expect(propDescriptor).to.have.property('writable', false);
  expect(propDescriptor).to.have.property('configurable', false);
};

利用上述两个 expect 函数,我们就可以编写完整的测试用例了:

var MovieTypes = ['REGULAR', 'NEW_RELEASE', 'CHILDREN'];
...

it('should define three static properties', function() {
  MovieTypes.forEach(expectOwnProperty);
});

it('should define three constants', function() {
  MovieTypes.forEach(expectConstant);
});

运行测试用例,正如预期,得到了两个 failing,现在我们开始定义这三个静态属性。

Test Cases

定义静态属性

因为静态属性无需实例化对象就能被访问,所以无需把属性委托到原型上,直接作为 Movie 这个『类』的属性存在即可:

// ES5
Movie.REGULAR     = 0;
Movie.NEW_RELEASE = 1;
Movie.CHILDREN    = 2;

// ES6
class Movie {
  static REGULAR     = 0;
  static NEW_RELEASE = 1;
  static CHILDREN    = 2;
}

看上去很简单,我们运行一下测试用例。Oops…… 一个成功,一个失败了。

Define Static Properties

另一个失败的原因是:have a property 'writable' of false, but got true,也就是这些属性虽然都是静态的,但是它们并非常量,因为它们的值可以被任意修改的。

庆幸的是,ES6 提供了全新的解决方案:const 关键词。

定义常量

The const declaration creates a read-only reference to a value. It does not mean the value it holds is immutable, solely that the variable identifier can not be reassigned.

Mozilla Developer Network

ES6 标准提供了一个关键词 const。顾名思义,就是定义常量。


常量无法保证不可变性

需要注意的是 const 关键词其实用于保护常量名与对应值之间的引用,并不意味着值也是不可变化的,这点与 Java 的 final 关键词作用一致,这是因为 constant 并不等于 immutable,相当于低安全级别的 immutable。对于原始数据类型,const 关键词能够保证值的不可变性,但对于复合数据类型,只能保证引用的不可变性。因此,如果常量的值正好是一个对象,那么这个对象的属性并不会受到 const 的限制。Mozilla Developer Network 给出了下面的代码示例:

// const also works on objects
const MY_OBJECT = {"key": "value"};

// Overwriting the object fails as above (in Firefox and Chrome but not in Safari)
MY_OBJECT = {"OTHER_KEY": "value"};

// However, object attributes are not protected,
// so the following statement is executed without problems
MY_OBJECT.key = "otherValue";

但是目前 ES6 并不支持在 Class 内声明常量,所以这条路行不通。不过天无绝人之路!既然 ES6 救不了,那么回到 ES5!我们其实可以通过 Object.defineProperty 方法定义一个『类』的属性特征,让属性的行为与常量行为保持一致就可以了。由于我们一次需要定义多个常量属性,所以这里调用的是 Object.defineProperties 方法。

Object.defineProperties(Movie, {
  'REGULAR': {
    value: 0,
    writable: false,
    configurable: false
  },
  'NEW_RELEASE': {
    value: 1,
    writable: false,
    configurable: false
  },
  'CHILDREN': {
    value: 2,
    writable: false,
    configurable: false
  }
});

这里需要注意的是,默认情况下,Object.defineProperty 方法所定义的属性特征 enumerablefalse,这个要根据实际情况进行调整,由于我不打算枚举这三个属性,所以沿用缺省状态了。


到这里引发了另外一个问题,究竟是在 Movie 还是 Movie.prototypedefineProperties

Define the Properties on Object.prototype or Directly on Object?

定义在 Object.prototype 上的属性是其所有子对象所共享的属性,所以,如果是定义对象内的常量属性,应该在对象的原型 Object.prototype 上定义属性的特征。

// the constant properties shared between all instances
Object.defineProperty(Movie.prototype, 'PROP', {
  value: 'value',
  configurable: false,
  writable: false,
  ...
});

> var movie = new Movie();
> movie.PROP === 'value';

如果是定义一个静态常量属性,则直接在『类』即 Object 上定义属性特征即可。

// the static constant properties 
Object.defineProperty(Movie, 'PROP', {
  value: 'value',
  configurable: false,
  writable: false,
  ...
});

> Movie.PROP === 'value';

Is It Impossible to Define a Static Constant Property in ES6 Class?

答案是:虽然目前并不支持,但是可以间接地实现这种属性定义,如下:

class Movie {
  static get PROP() {
    return 'value';
  }
}

> Movie.PROP === 'value';
> Movie.PROP = 'anotherValue'; // try to alter the value
> Movie.PROP !== 'anotherValue'; // failed
> Movie.PROP === 'value'; // looks like a constant

static get PROPERTY_NAME() {} 在 Class 上面通过 getter 直接定义一个静态『常量』属性。之所以这个属性表现出*不可变性*,是因为其 getter 总是返回一个定值 value,所以 Movie.PROP 恒等于 value,但是这个属性依然是可以配置的 configurable: true,所以并非真正意义上的『常量』。


重新运行测试,如愿以偿地通过了测试:

Successful Test Cases

总结

这只是一个简单的代码转换,从中可以明显地看出,同为面向对象的编程语言,Java 在表述静态常量的意图方面显得更加简洁、明了,而 JavaScript 则显得冗长、累赘,庆幸的是 ECMAScript 正在朝着更开放和友好的方向发展。好了,废话太多,直接总结:

  • 定义静态属性:定义为『类』的直接属性即可 Class.staticProperty = x 或者使用 static 关键词
  • 定义对象的常量属性 / 定义类的常量静态属性:通过 Object.defineProperty 或者 Object.defineProperties 设置属性的特征 writableconfigurablefalse;如果是定义对象的常量属性,则在 Object.prototype 上定义,如果是定义类的静态常量,则在 Object 上定义。或者在 Class 内通过 static getter 定义静态『常量』属性。

参考资料