Singleton: initialize()
対象の Singleton 派生クラスを初期化します。
引数として関数を与えることで、追加の初期化処理を記述できます。
構文
singleton.initialize(initializer) => Singleton
引数
-
initializer:function|undefined対象の
Singleton派生クラスを初期化する関数です。 引数initializerとして指定された関数は、引数を持たず、また返値も利用されません。引数
initializerとして関数が指定された場合、それは対象のSingleton派生クラスが未初期化である場合にのみ呼び出されます。この引数の指定は任意です。指定がない場合、対象の
Singleton派生クラスは初期化済みとして扱われますが、初期化処理としては何も行われません。初期化関数にはアロー関数を渡しましょう-
初期化関数内では、初期化したい
Singleton派生クラスのインスタンスをthisを介して操作することになります。 JavaScript の言語仕様上、function式では明示的にthisを指定して呼び出さない限り、function式を書いたスコープにおけるthisは利用されません(クラス定義の中ではthisの値がundefinedになります)。代わりに、常にアロー関数式を使うべきです(アロー関数式は記述位置でのthisを取り込むため、期待通り初期化したいSingleton派生クラスのインスタンスを参照します)。// 誤った例 class Misused extends Singleton { constructor() { super(); return this.initialize(function() { // ここでの this は Misused のインスタンスではなく undefined を指します // 以下は undefined.x の参照になるためエラーを引き起こします this.x = 42; }); } } // 好ましい例 class Preferable extends Singleton { constructor() { super(); // アロー関数式の代わりに `(function() { ... }).bind(this)` でもよいですが、 // アロー関数の方が簡潔かつ bind() の書き忘れなどのおそれもないので、 // bind() を使う動機はすくないはずです。 return this.initialize(() => { // ここでの this は期待通り Preferable のインスタンスになります this.x = 42; }); } }
非同期処理にご注意ください- 引数
initializerとしてasync関数やPromiseを返す関数を与えないでください。initialize()関数は返値はinitializerを値を返さない同期関数であるものとして扱い、Promiseが返されたとしても何も行いません。Promiseのthen内(async関数内でのawait以降)の処理はマイクロタスクとして実行されますが、マイクロタスクの実行はイベントループに積まれたタスクの実行後に処理されます(詳細はMDNの『JavaScript で queueMicrotask() によるマイクロタスクの使用』 を参照)。
-
返値: Singleton
対象の Singleton 派生クラスのインスタンスです。
対象の Singleton 派生クラスが未初期化であれば、対象のインスタンスそのものが返ります。
初期化済みであれば代わりに、対象のインスタンスではなく、対象の Singleton 派生クラスの初期化済みのインスタンスが返ります。
この値は、対象の Singleton 派生クラスのコンストラクタの返値を上書きするために用います。
- JavaScriptにおいて、コンストラクタは暗黙に
thisを返しますが、一方で、明示的に値を返すこと(つまりreturn文を書くこと)もできます。明示的に値を返すことを「返値の上書き (return overriding)」と呼びます。 - 返値の上書きをすることで、
new演算子の結果やsuper()呼び出し後に参照されるthisの値を変えることができます。Singleton派生クラスにおける利用では、新たに生成された未初期化のインスタンスの代わりに、初期化済みのインスタンスを返すために使います。 - 返値の上書きによって
thisと異なるオブジェクトを返した場合、それ以前に行ったthisへの操作は反映されません。returnされたオブジェクトとそれ以前にthisで参照されていたオブジェクトが別のオブジェクトであるためです。- 通常のプログラムで上記のような混乱が生じることはありませんが、クラスのフィールドの定義などを行っている場合には注意が必要です。フィールドの定義は、
super()の呼び出し後、その時点でのthisに対して行われます。そのため、フィールド定義と返値の上書きを組み合わせると奇妙な振る舞いをすることがあります。
- 通常のプログラムで上記のような混乱が生じることはありませんが、クラスのフィールドの定義などを行っている場合には注意が必要です。フィールドの定義は、
例
以下ではファイル SharedData.js の中でシングルトンパターンを実装する派生クラスとして、SharedData クラスを定義しています。
class SharedData extends Singleton {
#data;
constructor() {
super();
// initialize() の結果で SharedData クラスのコンストラクタの返値を上書きします
return this.initialize(() => {
// initialize() の引数の中で初期化処理が行われます。
// この初期化関数は SharedData クラスの最初のインスタンス生成時にのみ
// 実行され、2回目以降のコンストラクタ呼び出しでは実行されません。
// 例示のため固定のデータ構造を初期化時に与えています。
this.#data = {
x: 42,
y: "foo"
};
});
}
// 値の取得用の関数
// データオブジェクトの変更不可能なコピーを取得し、
// 引数として与えた関数 block() で何らかの処理を行います。
// block の this および第一引数にはデータオブジェクトのコピーが与えられます。
async get(block) {
await this.#acquire();
// 例示のためネストしたオブジェクトは考慮していません。
const it = Object.freeze({ ...this.#data });
try {
const result = block.call(it, it);
if (result instanceof Promise) {
await result;
}
} catch(error) {
this.#release();
throw error;
}
this.#release();
}
// 値の更新用の関数
// データオブジェクトの変更可能なコピーを取得し、
// 引数として与えた関数 block() で何らかの処理を行います。
// block の this および第一引数にはデータオブジェクトのコピーが与えられます。
// コピーに対して加えた変更は元のデータオブジェクトに反映されます。
async update(block) {
await this.#acquire();
// 例示のためネストしたオブジェクトは考慮していません。
const it = ({ ...this.#data });
try {
const result = block.call(it, it);
if (result instanceof Promise) {
await result;
}
} catch(error) {
this.#release();
throw error;
}
for (const k of Object.keys(it)) {
if (!Object.hasOwn(this.#data, k)) { continue; }
const old_value = this.#data[k];
const new_value = it[k];
if (old_value !== new_value) {
this.#data[k] = new_value;
}
}
this.#release();
}
async #acquire() {
await Promise.race([this.#lock, this.#timer]);
const { promise: lock, resolve: release } = Promise.withResolvers();
lock.release = release;
const { promise: timer, resolve: timeout } = Promise.withResolvers();
timer.timeout = timeout;
setTimeout(() => {
timeout();
this.#timer = null;
}, 1000);
this.#lock = lock;
this.#timer = timer;
}
#release() {
this.#lock?.release();
this.#lock = null;
}
}
解説
対象の Singleton 派生クラスを初期化します。
引数として関数を与えることで、追加の初期化処理を記述できます。
この関数は、対象の Singleton 派生クラスが未初期化であれば、対象のインスタンスそのものを返します。
初期化済みであれば代わりに、対象のインスタンスではなく、対象の Singleton 派生クラスの初期化済みのインスタンスを返します。
この関数は必ず constructor() の中で呼び出してください。また、この関数の返値は呼び出し元の constructor() の返値の上書きのために使用してください(Singleton 派生クラスのコンストラクタの使用についてはSingleton # コンストラクタも参照してください)。
Singleton 派生クラスでこの関数を呼び出さなかった場合、そのクラスは Singleton クラスではありますが、シングルトンパターンを実装するクラスにはなりません。つまり、そのクラスのインスタンスはアプリケーション上に複数作ることができます。