ECMAScript 5 中的类
ECMAScript 5 给属性特性新增了方法支持(getter、setter、可枚举性、可写性和可配置性),并且增加了对象可扩展性的限制。
让属性不可枚举
我们在前面定义集合类 Set
的时候,为对象和数组元素设置过对象id属性(|**objectid**|
),如果在 for/in 循环中对这个对象做遍历,这个属性也会遍历到,ECMAScript 5 中可以通过设置属性为「不可枚举」让属性不会被遍历,下面我们就通过 Object.defineProperty()
来实现这个功能:
// 定义不可枚举的属性
(function () {
Object.defineProperty(Object.prototype, "objectid", {
get: idGetter, // 取值器
enumerable: false, // 不可枚举
configurable: false // 不可删除
});
function idGetter() {
if (!(idprop in this)) {
if (Object.isExtensible(this))
throw Error("Can't instantiate id for non extensible objects");
Object.defineProperty(this, idprop, {
value: nextid++,
writable: false,
enumerable: false,
configurable: false
});
}
return this[idprop];
}
// idGetter() 用到了这些变量,这些都属于私有变量
var idprop = "|**objectid**|"; // 假设这个属性没有用到
var nextid = 1; // 初始值
}()); // 立即执行这个匿名函数
测试代码如下:
定义不可变的类
除了设置属性为不可枚举,还可以设置属性为只读属性。下面我们将通过 Object.defineProperties()
和 Object.create()
方法定义不可变的 Range 类:
// 这个方法既是构造函数,也是工厂函数
function Range(from, to) {
// 通过属性描述符设置 from 和 to 为只读属性
var props = {
from: {value: from, enumerable: true, writable: false, configurable: false},
to: {value: to, enumerable: true, writable: false, configurable: false}
};
if (this instanceof Range) // 作为构造函数
Object.defineProperties(this, props);
else // 作为工厂函数
return Object.create(Range.prototype, props);
}
// 使用同样的方法为 Range 原型对象方法设置属性,默认都设置为false
Object.defineProperties(Range.prototype, {
includes: {
value: function (x) {
return this.from <= x && x <= this.to;
}
},
foreach: {
value: function (f) {
for (var x = Math.ceil(this.from); x <= this.to; x++) {
f(x);
}
}
},
toString: {
value: function () {
return "(" + this.from + "," + this.to + ")";
}
}
});
测试代码如下:
Object.defineProperty()
和 Object.defineProperties()
可以用来创建新属性,也可以用来修改已有属性的特性,当用它们来创建新属性时,默认的属性特性的值都是 false
,但用它们来修改已有属性时,默认的属性特性值保持不变,比如我们可以将上面代码中的属性描述符对象创建属性调整为修改已有属性的工具函数:
/**
* 将对象o指定名字(或所有)的属性特性值设置为不可写和不可配置的
* @param o
* @returns {*}
*/
function freezeProps(o) {
// 如果只有一个参数,则使用所有属性,否则使用传入指定对象名的属性
var props = arguments.length == 1 ? Object.getOwnPropertyNames(o) : Array.prototype.splice.call(arguments, 1);
// 将对象属性设置为只读的和不可配置的
props.forEach(function (n) {
// 忽略不可配置的属性
if (!Object.getOwnPropertyDescriptor(o, n).configurable)
return;
Object.defineProperty(o, n, {
writable: false,
configurable: false
})
});
return o;
}
/**
* 将对象o指定名字(或所有)的属性特性值设置为不可枚举和可配置的
* @param o
* @returns {*}
*/
function hideProps(o) {
var props = arguments.length == 1 ? Object.getOwnPropertyNames(o) : Array.prototype.splice.call(arguments, 1);
props.forEach(function (n) {
if (!Object.getOwnPropertyDescriptor(o, n).configurable)
return;
Object.defineProperty(o, n, {
enumerable: false
});
});
return o;
}
这样的写法会让代码的可读性和可维护性更好。我们还可以通过上面两个工具函数在 ECMAScript 5 中实现一个不可变的类:
// 实现一个简单的不可变类
function Range(from, to) {
this.from = from;
this.to = to;
freezeProps(this); // 将属性设置为不可变的
}
Range.prototype = hideProps({ // 将原型方法设置为不可枚举的
constructor: Range,
includes: function (x) {
return this.from <= x && x <= this.to;
},
foreach: function (f) {
for (var x = Math.ceil(this.from); x <= this.to; x++)
f(x);
},
toString: function () {
return "(" + this.from + ", " + this.to + ")";
}
});
封装对象状态
之前我们已经编写过封装私有状态的代码,但是该方法在 ECMAScript 3 中有一个缺点就是访问这些私有状态的存取器是可以替换的,在 ECMAScript 5 中可以通过定义属性 getter
和 setter
方法将状态变量更健壮地封装起来:
/**
* ECMAScript 5 版本的 Range 构造函数
* @param from
* @param to
* @constructor
*/
function Range(from, to) {
if (from > to)
throw new Error("Range: from must be <= to");
// 定义存取器方法
function getFrom() {
return from;
}
function getTo() {
return to;
}
function setFrom(f) {
if (f <= to)
from = f;
else
throw new Error("Range: from must be <= to");
}
function setTo(t) {
if (t >= from)
to = t;
else
throw new Error("Range: to must be >= from");
}
// 将存取器的属性值设置为可枚举,不可配置的
Object.defineProperties(this, {
from: { get: getFrom, set: setFrom, enumerable: true, configurable: false },
to: { get: getTo, set: setTo, enumerable: true, configurable: false }
});
}
// 原型对象并没有做任何修改
Range.prototype = hideProps({
constructor: Range,
includes: function (x) {
return this.from <= x && x <= this.to;
},
foreach: function (f) {
for (var x = Math.ceil(this.from); x <= this.to; x++)
f(x);
},
toString: function () {
return "(" + this.from + "," + this.to + ")";
}
});
下面时测试代码:
防止类的扩展
在 JavaScript 中,通过给原型对象添加方法就可以动态地对类进行扩展,ECMAScript 5 可以根据需要对此特性加以限制:通过 Object.preventExtensions()
可以将对象设置为不可扩展,也就是不能添加任何新属性;Object.seal()
则更强大,除了能阻止对象添加新属性外,还能将当前已有属性设置为不可配置的,这样就不能删除这些属性了。比如我们通过这样一段简单的代码就可以阻止对 Object.prototype
的扩展:
Object.seal(Object.prototype);
JavaScript 的另一个特性是「对象的方法可以随时替换」(monkey-patch),可以通过将实例方法设置为只读来防止这类修改,比如我们上面定义的 freezeProps()
工具函数就可以实现,此外还可以使用内置的 Object.freeze()
方法,将所有属性设置为只读的和不可配置的。
理解类的只读属性至关重要,如果对象 o 继承了只读属性 p,那么给 o.p 赋值操作将会失败,如果想要重写一个继承来的只读属性,必须使用 Object.defineProperty()
、Object.defineProperties()
或 Object.create()
来创建这个新属性。
子类和 ECMAScript 5
下面我们用 ECMAScript 5 的新特性来实现子类,我们用到了 Object.create()
来定义 AbstractWritableSet
的子类 StringSet
:
function StringSet() {
this.set = Object.create(null);
this.n = 0;
this.add.apply(this, arguments);
}
// 使用 Object.create() 可以继承父类的原型
StringSet.prototype = Object.create(AbstractWritableSet.prototype, {
constructor: {
value: StringSet
},
contains: {
value: function (x) {
return x in this.set;
}
},
size: {
value: function () {
return this.n;
}
},
foreach: {
value: function (f, c) {
Object.keys(this.set).forEach(f, c);
}
},
add: {
value: function () {
for (var i = 0; i < arguments.length; i++) {
if (!(arguments[i] in this.set)) {
this.set[arguments[i]] = true;
this.n++;
}
}
return this;
}
},
remove: {
value: function () {
for (var i = 0; i < arguments.length; i++) {
if (arguments[i] in this.set) {
delete this.set[arguments[i]];
this.n--;
}
}
return this;
}
}
});
以下是测试代码:
属性描述符
本节给出一个例子,用来讲述如何基于 ECMAScript 5 对属性进行各种操作:
/*
* 给 Object.prototype 定义 properties() 方法
* 这个方法返回一个表示调用它的对象上的属性名列表的对象
* 返回的对象上定义了4个方法:toString()、descriptors()、hide() 和 freeze()
*/
(function namespace() { // 将所有逻辑闭包在一个私有函数作用域中
// 这个函数成为所有对象的方法
function properties() {
var names; // 属性名数组
if (arguments.length === 0) { // 所有自有属性
names = Object.getOwnPropertyNames(this);
} else if (arguments.length === 1 && Array.isArray(arguments[0])) {
names = arguments[0]; // 属性名数组
} else {
names = Array.prototype.splice.call(arguments, 0); // 参数列表指定属性名
}
// 返回一个新的 Property 对象用以表示属性名
return new Properties(this, names);
}
// 这个构造函数是由上面的properties()函数所调用的
// Properties 类表示一个对象的属性集合
function Properties(o, names) {
this.o = o; // 所属对象
this.names = names; // 属性名
}
// 将 properties() 设置为 Object 的不可枚举的新属性
Object.defineProperty(Object.prototype, "properties", {
value: properties,
enumerable: false,
writable: true,
configurable: true
});
// 将这些对象属性置为不可枚举的
Properties.prototype.hide = function () {
var o = this.o, hidden = { enumerable: false };
this.names.forEach(function (n) {
if (o.hasOwnProperty(n))
Object.defineProperty(o, n, hidden);
});
return this;
};
// 将这些对象属性设置为只读的和不可配置的
Properties.prototype.freeze = function () {
var o = this.o, frozen = { writable: false, configurable: false };
this.names.forEach(function (n) {
if (o.hasOwnProperty(n))
Object.defineProperty(o, n, frozen);
});
};
// 返回属性名到属性描述符映射关系的对象
// 使用它来复制属性,会连同属性特性一起复制
// Object.defineProperties(dest, src.properties().descriptors());
Properties.prototype.descriptors = function () {
var o = this.o, desc = {};
this.names.forEach(function (n) {
if (!o.hasOwnProperty(n))
return;
desc[n] = Object.getOwnPropertyDescriptor(o, n);
});
return desc;
};
// 返回一个格式化良好的属性列表
Properties.prototype.toString = function () {
var o = this.o;
var lines = this.names.map(nameToString);
return "{\n" + lines.join(",\n ") + "\n}";
function nameToString(n) {
var s = "", desc = Object.getOwnPropertyDescriptor(o, n);
if (!desc) {
return "non existent " + n + ": undefined";
}
if (!desc.configurable) {
s += "permanent "; // 不可配置
}
if ((desc.get && !desc.set) || !desc.writable) {
s += "readonly "; // 只读
}
if (!desc.enumerable) {
s += "hidden "; // 不可枚举
}
if (desc.get || desc.set) {
s += "accessor " + n; // 可访问
} else {
s += n + ": " + (typeof desc.value === "function" ? "function" : desc.value);
}
return s;
}
};
// 最后,将原型对象中的实例方法设置为不可枚举的
Properties.prototype.properties().hide();
}()); // 立即执行这个匿名函数
测试代码如下:
No Comments