在本章中,我们将研究几种创建类实例的方法:构造函数、工厂函数等。我们将通过多次解决一个具体问题来做到这一点。本章的重点是类,这就是为什么忽略类的替代方案。
以下容器类应该异步接收其属性 .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 的构造函数或静态工厂方法以及通过密钥令牌实现的私有构造函数。
但是,这里介绍的其他技术在其他情况下仍然有用。