深入 JavaScript
请支持本书:购买捐赠
(广告,请不要屏蔽。)

13 实例化类的技术



在本章中,我们将研究几种创建类实例的方法:构造函数、工厂函数等。我们将通过多次解决一个具体问题来做到这一点。本章的重点是类,这就是为什么忽略类的替代方案。

13.1 问题:异步初始化属性

以下容器类应该异步接收其属性 .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 的实例时,它还没有完全初始化。

13.2 解决方案:基于 Promise 的构造函数

如果我们延迟对 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 行)。此代码中可能有两种错误来源

在这两种情况下,错误都会成为从构造函数返回的 Promise 的拒绝。

优缺点

13.2.1 使用立即调用的异步箭头函数

除了直接使用 Promise API 来创建从构造函数返回的 Promise 之外,我们还可以使用我们立即调用的异步箭头函数

constructor() {
  return (async () => {
    this.#data = await Promise.resolve('downloaded');
    return this;
  })();
}

13.3 解决方案:静态工厂方法

C静态工厂方法创建 C 的实例,并且是使用 new C() 的替代方法。JavaScript 中静态工厂方法的常用名称

在以下示例中,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() 中,这使得类的其余部分可以完全同步,因此更简单。

优缺点

13.3.1 改进:通过密钥令牌实现私有构造函数

如果我们想确保实例始终设置正确,我们必须确保只有 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'));

如果 secretTokenDataContainer 位于同一个模块中,并且只导出后者,那么外部方就无法访问 secretToken,因此无法创建 DataContainer 的实例。

优缺点

13.3.2 改进:构造函数抛出异常,工厂方法借用类原型

我们解决方案的以下变体禁用了 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 行中起作用。

优缺点

13.3.3 改进:实例默认不活动,由工厂方法激活

另一个更冗长的变体是,默认情况下,实例通过标志 .#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() 的风险。

13.3.4 变体:单独的工厂函数

为了完整起见,我将展示另一个变体:除了使用静态方法作为工厂之外,您还可以使用单独的独立函数。

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'));

独立函数作为工厂偶尔很有用,但在这种情况下,我更喜欢静态方法

13.4 子类化基于 Promise 的构造函数(可选)

一般来说,子类化应该谨慎使用。

使用单独的工厂函数,扩展 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 没有任何私有字段,则此方法仍然有效。

13.5 结论

对于本章中研究的场景,我更喜欢基于 Promise 的构造函数或静态工厂方法以及通过密钥令牌实现的私有构造函数。

但是,这里介绍的其他技术在其他情况下仍然有用。

13.6 延伸阅读