Promiseの「正体」を見極める:Thenable地獄からの脱却とアーキテクチャの防衛術
JavaScriptの非同期処理において、`instanceof Promise` を信じ切っている諸君は、一度立ち止まって考えてみてほしい。我々が扱うのは、ただの自作Promiseだけではない。ライブラリが返す謎のオブジェクト、ブラウザのネーティブ実装、さらには別ウィンドウのコンテキストで生成されたPromiseまで、多種多様な「非同期の亡霊」たちが、君たちの堅牢なアーキテクチャの隙間を虎視眈々と狙っている。
今日は、表面的な `typeof` の話ではなく、プロダクション環境で「本当に信頼できる」Promiseの判定ロジックについて、V8エンジンの裏側を覗き見るような話をしよう。
—
なぜ `instanceof Promise` が「裏切り」を生むのか
まず、基本の確認だ。`instanceof` はプロトタイプチェーンを辿る。しかし、現代のWebアプリケーションにおいて、これは致命的な欠陥を抱えている。
1. クロスコンテキスト(iframe等)の壁: 別ウィンドウやiframe内で定義された `Promise` コンストラクタは、親ウィンドウの `Promise` とは別物だ。`instanceof` はプロトタイプチェーンを比較するため、ここであっさりと `false` を返す。
2. ポリフィルの罠: 古い環境や特定のライブラリが独自に組み込んだPromiseポリフィルは、標準の `Promise` クラスを継承していない場合がある。
これらを無視して「俺の書いたコードはES6環境だから大丈夫」と高を括るのは、戦場で銃の整備を怠るのと同じだ。
Thenable:非同期の「なりすまし」をどう裁くか
Promiseの厳密な判定において避けて通れないのが「Thenable(ゼナブル)」だ。`then` メソッドを持つオブジェクトは、Promiseのフリをして非同期処理のパイプラインに紛れ込む。
/
- 厳密なThenable判定のアーキテクチャ
- 単に “then” というプロパティがあるか確認するだけでは不十分。
- それが「関数」であり、呼び出し可能であることを保証しなければならない。
/
function isThenable(value) {
// null/undefinedを除外し、オブジェクトまたは関数であることを確認
if (value !== null && (typeof value === ‘object’ || typeof value === ‘function’)) {
// thenが関数であるかを確認。これはPromise/A+仕様の根幹
return typeof value.then === ‘function’;
}
return false;
}
この判定は非常に安価だ。メモリを確保せず、プロトタイプチェーンを走査せず、単なるプロパティアクセスのみで完了する。レンダリングループや高頻度で実行されるイベントリスナー内でも、この実装ならパフォーマンスへの影響は皆無といっていい。
—
プロダクションレベルの「Promise」判定関数
では、真に信頼できる判定ロジックを実装しよう。我々が知りたいのは「このオブジェクトは `Promise.resolve()` に渡しても安全か」という一点に尽きる。
/
- 堅牢なPromise判定の実装
- @param {any} value – 判定対象のデータ
- @returns {boolean} – 本当にPromiseとして扱えるか
/
function isPromise(value) {
// 1. 基本的なNullチェック
if (value == null) return false;
// 2. 厳密な型チェック
// 多くの環境で、Promiseはオブジェクトまたは関数として振る舞う
if (typeof value !== ‘object’ && typeof value !== ‘function’) return false;
// 3. thenableであるかを確認(これが最も重要)
// Promise/A+準拠の全てのPromiseは、thenメソッドを持つ
return typeof value.then === ‘function’;
}
なぜ「Symbol.toStringTag」に頼ってはいけないのか?
一部のエンジニアは `Object.prototype.toString.call(value) === ‘[object Promise]’` を使う。これは確かに便利だが、改竄が可能だ。`Symbol.toStringTag` を上書きすれば、ただのプレーンなオブジェクトを `Promise` と偽ることができてしまう。
セキュリティを重視するライブラリ開発や、外部からの入力を扱うゲートウェイ部分では、こうした「メタデータ」に依存する判定は避けるべきだ。あくまで「振る舞い(Behavior)」、すなわち `then` メソッドの存在こそが、唯一の真実である。
—
メモリとパフォーマンス:非同期の競合を回避する設計
Promiseの判定を間違えると、最悪の場合「無限ループ」や「メモリリーク」を引き起こす。例えば、`then` メソッド内で自身を解決するような循環参照を持つThenableを、安易に `await` し続ければどうなるか?
- 非同期の競合: 判定処理自体が重いと、マイクロタスクキューが溢れ、ブラウザのメインスレッドをブロックする。
- メモリ効率: 過剰な判定ロジックでクロージャを多用すれば、GC(ガベージコレクション)の頻度が高まり、UIのジャンクを引き起こす。
結論として、判定は「最小コストで、振る舞いのみを問う」。
君たちが作るWebアプリケーションが、複雑な状態遷移を抱えるものならば、型判定は「守り」のための静的なルールではなく、システムを動かし続けるための「動的なインターフェース」であるべきだ。
次に `if (promise instanceof Promise)` を書くとき、一瞬だけ立ち止まってほしい。そのPromiseは、君のアプリケーションという戦場を生き残れるほど、信頼に値するものだろうか?
さあ、コードに戻ろう。最適化の余地はまだ、そこら中に転がっているはずだ。

コメント