在本章中,我们将研究几种创建类实例的方法:构造函数、工厂函数等。我们将通过多次解决一个具体问题来做到这一点。本章的重点是类,这就是为什么忽略类的替代方案。
以下容器类应该异步接收其属性 .data
的内容。这是我们的第一次尝试
class DataContainer {
#data; // (A)
constructor() {
Promise.resolve('downloaded')
.then(data => this.#data = data); // (B)
}
getData() {
return 'DATA: '+this.#data; // (C)
}
}
此代码的关键问题:属性 .data
最初是 undefined
。
const dc = new DataContainer();
assert.equal(dc.getData(), 'DATA: undefined');
setTimeout(() => assert.equal(
dc.getData(), 'DATA: downloaded'), 0);
在 A 行,我们声明了私有字段 .#data
,我们在 B 行和 C 行中使用它。
DataContainer
构造函数中的 Promise 是异步解析的,这就是为什么我们只有在完成当前任务并通过 setTimeout()
启动新任务后才能看到 .data
的最终值。换句话说,当我们第一次看到 DataContainer
的实例时,它还没有完全初始化。
如果我们延迟对 DataContainer
实例的访问,直到它完全初始化,会怎么样?我们可以通过从构造函数返回 Promise 来实现这一点。默认情况下,构造函数返回它所属类的新实例。如果我们显式返回一个对象,我们可以覆盖它
class DataContainer {
#data;
constructor() {
return Promise.resolve('downloaded')
.then(data => {
this.#data = data;
return this; // (A)
});
}
getData() {
return 'DATA: '+this.#data;
}
}
new DataContainer()
.then(dc => assert.equal( // (B)
dc.getData(), 'DATA: downloaded'));
现在我们必须等到可以访问我们的实例(B 行)。在“下载”数据后,它会被传递给我们(A 行)。此代码中可能有两种错误来源
.then()
回调的主体中可能会抛出异常。在这两种情况下,错误都会成为从构造函数返回的 Promise 的拒绝。
优缺点
DataContainer
的实例。除了直接使用 Promise API 来创建从构造函数返回的 Promise 之外,我们还可以使用我们立即调用的异步箭头函数
constructor() {
return (async () => {
this.#data = await Promise.resolve('downloaded');
return this;
})();
}
类 C
的静态工厂方法创建 C
的实例,并且是使用 new C()
的替代方法。JavaScript 中静态工厂方法的常用名称
.create()
:创建一个新实例。示例:Object.create()
.from()
:通过复制和/或转换不同的对象来创建基于该对象的新实例。示例:Array.from()
.of()
:通过组合通过参数指定的值来创建新实例。示例:Array.of()
在以下示例中,DataContainer.create()
是一个静态工厂方法。它返回 DataContainer
实例的 Promise
class DataContainer {
#data;
static async create() {
const data = await Promise.resolve('downloaded');
return new this(data);
}
constructor(data) {
this.#data = data;
}
getData() {
return 'DATA: '+this.#data;
}
}
DataContainer.create()
.then(dc => assert.equal(
dc.getData(), 'DATA: downloaded'));
这一次,所有异步功能都包含在 .create()
中,这使得类的其余部分可以完全同步,因此更简单。
优缺点
new DataContainer()
创建设置不正确的实例。如果我们想确保实例始终设置正确,我们必须确保只有 DataContainer.create()
可以调用 DataContainer
的构造函数。我们可以通过密钥令牌来实现这一点
const secretToken = Symbol('secretToken');
class DataContainer {
#data;
static async create() {
const data = await Promise.resolve('downloaded');
return new this(secretToken, data);
}
constructor(token, data) {
if (token !== secretToken) {
throw new Error('Constructor is private');
}
this.#data = data;
}
getData() {
return 'DATA: '+this.#data;
}
}
DataContainer.create()
.then(dc => assert.equal(
dc.getData(), 'DATA: downloaded'));
如果 secretToken
和 DataContainer
位于同一个模块中,并且只导出后者,那么外部方就无法访问 secretToken
,因此无法创建 DataContainer
的实例。
优缺点
我们解决方案的以下变体禁用了 DataContainer
的构造函数,并使用了一种技巧来以另一种方式创建它的实例(A 行)
class DataContainer {
static async create() {
const data = await Promise.resolve('downloaded');
return Object.create(this.prototype)._init(data); // (A)
}
constructor() {
throw new Error('Constructor is private');
}
_init(data) {
this._data = data;
return this;
}
getData() {
return 'DATA: '+this._data;
}
}
DataContainer.create()
.then(dc => {
assert.equal(dc instanceof DataContainer, true); // (B)
assert.equal(
dc.getData(), 'DATA: downloaded');
});
在内部,DataContainer
的实例是任何原型为 DataContainer.prototype
的对象。这就是为什么我们可以通过 Object.create()
创建实例(A 行),以及为什么 instanceof
在 B 行中起作用。
优缺点
instanceof
有效。另一个更冗长的变体是,默认情况下,实例通过标志 .#active
关闭。初始化方法 .#init()
可以打开它们,但不能从外部访问,但 Data.container()
可以调用它
class DataContainer {
#data;
static async create() {
const data = await Promise.resolve('downloaded');
return new this().#init(data);
}
#active = false;
constructor() {
}
#init(data) {
this.#active = true;
this.#data = data;
return this;
}
getData() {
this.#check();
return 'DATA: '+this.#data;
}
#check() {
if (!this.#active) {
throw new Error('Not created by factory');
}
}
}
DataContainer.create()
.then(dc => assert.equal(
dc.getData(), 'DATA: downloaded'));
标志 .#active
通过私有方法 .#check()
强制执行,该方法必须在每个方法开始时调用。
此解决方案的主要缺点是冗长。还存在忘记在每个方法中调用 .#check()
的风险。
为了完整起见,我将展示另一个变体:除了使用静态方法作为工厂之外,您还可以使用单独的独立函数。
const secretToken = Symbol('secretToken');
class DataContainer {
#data;
constructor(token, data) {
if (token !== secretToken) {
throw new Error('Constructor is private');
}
this.#data = data;
}
getData() {
return 'DATA: '+this.#data;
}
}
async function createDataContainer() {
const data = await Promise.resolve('downloaded');
return new DataContainer(secretToken, data);
}
createDataContainer()
.then(dc => assert.equal(
dc.getData(), 'DATA: downloaded'));
独立函数作为工厂偶尔很有用,但在这种情况下,我更喜欢静态方法
DataContainer
的私有成员。DataContainer.create()
的外观。一般来说,子类化应该谨慎使用。
使用单独的工厂函数,扩展 DataContainer
相对容易。
唉,使用基于 Promise 的构造函数扩展类会导致严重的限制。在以下示例中,我们对 DataContainer
进行子类化。子类 SubDataContainer
有自己的私有字段 .#moreData
,它通过挂钩到其超类的构造函数返回的 Promise 来异步初始化。
class DataContainer {
#data;
constructor() {
return Promise.resolve('downloaded')
.then(data => {
this.#data = data;
return this; // (A)
});
}
getData() {
return 'DATA: '+this.#data;
}
}
class SubDataContainer extends DataContainer {
#moreData;
constructor() {
super();
const promise = this;
return promise
.then(_this => {
return Promise.resolve('more')
.then(moreData => {
_this.#moreData = moreData;
return _this;
});
});
}
getData() {
return super.getData() + ', ' + this.#moreData;
}
}
唉,我们无法实例化此类
assert.rejects(
() => new SubDataContainer(),
{
name: 'TypeError',
message: 'Cannot write private member #moreData ' +
'to an object whose class did not declare it',
}
);
为什么失败?构造函数总是将其私有字段添加到其 this
中。但是,在这里,子构造函数中的 this
是由超构造函数返回的 Promise(而不是通过 Promise 传递的 SubDataContainer
实例)。
但是,如果 SubDataContainer
没有任何私有字段,则此方法仍然有效。
对于本章中研究的场景,我更喜欢基于 Promise 的构造函数或静态工厂方法以及通过密钥令牌实现的私有构造函数。
但是,这里介绍的其他技术在其他情况下仍然有用。