看到了下面这段 Java 代码,突然有种冲动想要用 JavaScript 来实现:
public class Movie {
public static final int REGULAR = 0;
public static final int NEW_RELEASE = 1;
public static final int CHILREN = 2;
...
}
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 {
}
// ES5
var Movie = function() {};
// ES6
class Movie {
}
静态常量
接下来实现 REGULAR
,NEW_RELEASE
,CHILDREN
这三个静态常量。static
意味着值这三个属性可以被直接访问而不需要实例化对象,而 final
则意味着它们都是常量,其值一旦被定义就无法被修改。
首先我们先编写测试用例表达我们的意图。
测试用例
由于静态属性可以被直接访问,所以(1)Movie 这个『类』(实质是一个对象)应该拥有这三个属性。我们把这个测试要求编写为一个 expect 函数:
var expectOwnProperty = function(property) {
expect(Movie).to.have.ownProperty(property);
expect(Movie.prototype).not.to.have.ownProperty(property);
};
var expectOwnProperty = function(property) {
expect(Movie).to.have.ownProperty(property);
expect(Movie.prototype).not.to.have.ownProperty(property);
};
接下来我们继续编写一个检查属性是否为常量的测试代码。按照常量的定义,我们可以确定(2)如果对象的某个属性是一个常量,那么这个属性的两个特征值 writable
和 configurable
均为 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);
};
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);
});
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,现在我们开始定义这三个静态属性。
定义静态属性
因为静态属性无需实例化对象就能被访问,所以无需把属性委托到原型上,直接作为 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;
}
// 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…… 一个成功,一个失败了。
另一个失败的原因是: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";
// 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.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
方法所定义的属性特征 enumerable
为 false
,这个要根据实际情况进行调整,由于我不打算枚举这三个属性,所以沿用缺省状态了。
到这里引发了另外一个问题,究竟是在 Movie
还是 Movie.prototype
上 defineProperties
呢
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';
// 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';
// 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
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
,所以并非真正意义上的『常量』。
重新运行测试,如愿以偿地通过了测试:
总结
这只是一个简单的代码转换,从中可以明显地看出,同为面向对象的编程语言,Java 在表述静态常量的意图方面显得更加简洁、明了,而 JavaScript 则显得冗长、累赘,庆幸的是 ECMAScript 正在朝着更开放和友好的方向发展。好了,废话太多,直接总结:
- 定义静态属性 :定义为『类』的直接属性即可
Class.staticProperty = x
或者使用static
关键词 - 定义对象的常量属性 / 定义类的常量静态属性 :通过
Object.defineProperty
或者Object.defineProperties
设置属性的特征writable
与configurable
为false
;如果是定义对象的常量属性,则在Object.prototype
上定义,如果是定义类的静态常量,则在Object
上定义。或者在 Class 内通过static getter
定义静态『常量』属性。