副題【prototypeベースでの継承とprototypeチェーンをきちんと理解してから年を越そうぜ】
【目次】
- 前置き
- prototypeによるウインドウクラスの継承
- 実例:Window_Scrollableの「おまじない」
- おまじないの全体像
- まずは定義-prototypeを親から引き継ぐ
- インスタンス生成-
newしてみよう - 最後にinitializeが仕事をしてインスタンスの出来上がり
- 「おまじない」のフローまとめ
- prototypeチェーンについて
- 終わりに
前置き
JavaScript初級者がツクールMV、MZのプラグインを書いていると
「毎回なんとなく書いてはいるけれど、具体的にこれが何なのかはきちんと説明できない」
という**「おまじない」**のような記述が幾つか出てくるかと思います。
典型的なものとしては「プラグインコマンド関連処理」「プラグインパラメータ関連処理」、そして「新規シーンクラスや新規ウインドウクラスの定義」などです。
確かにこれら「おまじない」を「おまじない」のまま見様見真似で書いても動いてはくれますが、きちんと仕組みを理解しておくことで応用できる範囲が大きく広がり、また、自身でコアスクリプトを読み解く力が格段に上がります。
プラグインコマンドについては前回記事にしましたので、今回は新規ウインドウ作成を例としたprototypeベースの継承について書いてみます。
なお、本記事は基本情報としてmdnの
「Object のプロトタイプ」
「継承とプロトタイプチェーン」
を下地として記述しています。
prototypeによるウインドウクラスの継承
ES20XX(以前はES6などとも呼ばれていた)以降はJavaScriptにもclass構文が導入されましたが、基本的な仕組みはprototypeベースのままです。
…というようなprototype概論は既にあちこちで記事にされていますし、なによりmdnに詳細な説明ページが用意されていますので、その辺の詳しい説明はそちらに譲ります。
代えて、本記事ではツクールMZのコアスクリプトから例を取り、そこで何が行われているのかを順に追うことによってprototypeへの理解を深めることを試みます。
実例:Window_Scrollableの「おまじない」
おまじないの全体像
ツクールMZのウインドウは、元をたどれば全てWindow_Baseを継承して作られています。
今回はこのWindow_Baseを継承しているウインドウクラスの中から「Window_Scrollable」を例として追ってみましょう。
該当コードは以下です。
function Window_Scrollable() {
this.initialize(...arguments);
}
Window_Scrollable.prototype = Object.create(Window_Base.prototype);
Window_Scrollable.prototype.constructor = Window_Scrollable;
Window_Scrollable.prototype.initialize = function(rect) {
Window_Base.prototype.initialize.call(this, rect);
this._scrollX = 0;
this._scrollY = 0;
this._scrollBaseX = 0;
this._scrollBaseY = 0;
this.clearScrollStatus();
};
以下の4つの要素から構成されていることがわかりますね。
(1)functionによる関数の宣言
(2)prototypeへの代入
(3)prototype.constructorへの代入
(4)initializeの定義
まずは定義-prototypeを親から引き継ぐ
まず(1)でWindow_Scrollable関数が宣言されています。この関数が後々new演算子によってインスタンスを作り出す元になります。
このようにnew演算子によってインスタンスを作り出す関数を「コンストラクター関数」と呼びます。
さて、ここでは(1)の中身にまでは踏み込まず、(2)および(3)から見ていきます。
いきなりですが、(2)がprototypeベースによる継承の肝となる部分です。
Window_Scrollable.prototype = Object.create(Window_Base.prototype);
Object.create()で、Window_Base.prototypeを[[prototype]]として持つ新しいオブジェクトを生成し、Window_Scrollableのprototypeプロパティに代入しています。
prototypeおよび[[prototype]]という紛らわしい名前が出てきましたが、ここでは
・prototype…「子に引き継がせたいメソッドの情報を詰め込んだオブジェクト」
・[[prototype]]…「親から引き継いだメソッドを使用するために親へとたどるための参照」
くらいに留めておきます。
この辺について詳しくは「prototypeチェーンについて」で後述します。
ひとまず、これにより親クラスの持つメソッドを子クラスでも利用可能となったと認識してください。
さて、続けて(3)についてもここで理解しておきましょう。
Window_Scrollable.prototype.constructor = Window_Scrollable;
prototypeの中には「そのインスタンスを生成した際のコンストラクター関数への参照」がプロパティとして保持されています。それがprototype.constructorプロパティです。
(参考リンク:mdn「Object.prototype.constructor」)
通常は自動的にコンストラクター関数そのものへの参照が設定されるため、手動で書き換える必要はありません。ただし、クラスを継承する場合は(2)の処理によってprototypeが丸ごと親クラス(Window_Base)の情報に上書きされてしまうため、このままの状態でnew Window_Scrollable()を行うと、実際のコンストラクター関数はWindow_Scrollableなのにprototype.constructorはWindow_Baseになるという齟齬が発生してしまいます。
そのため、上記齟齬が起こらないようにprototype.constructorを改めてWindow_Scrollableに紐付けしなおしておきます。
実際にはprototype.constructorに齟齬が発生していても問題ない場合も多いのですが、「念のため」みたいな処理です。
この辺についてより詳しく知りたい場合はmdn「Object.prototype.constructor:オブジェクトのコンストラクターの変更」をご参照ください。
(余談:このように、prototypeを作って渡して、それをまた本来のコンストラクタに書き換えて、というような不要に捏ね繰り回す辺りが、prototypeベースの記述はイケてないと言われる大きな要因な気がします。class構文が使える場面ならclass構文を使いたいですね)
インスタンス生成-newしてみよう
定義が終わったので、次にWindow_Scrollableの新規インスタンス作成時、つまりはnew Window_Scrollable();が行われた際に実行される処理を追っていきます。
new演算子は新しいインスタンスを生成するおなじみの演算子ですが、もう一歩だけ中に踏み込んで見てみましょう。
new演算子を実行すると、処理の裏側では空っぽの新規オブジェクトが生成され、それに対しコンストラクターのprototypeを[[prototype]]に設定、thisの設定、コンストラクター関数の実行、などを行ってくれています。
(参考リンク:mdn「new演算子」)
よって、new Window_Scrollable();を行うと、まずまっさらな新規オブジェクト"{}"を作成し、そのオブジェクトの[[prototype]]にWindow_Scrollable.prototypeへの参照をセットします。これによりWindow_Scrollableの持つメソッドをインスタンスから利用できるようになるわけです。(これについても後の「prototypeチェーンについて」で詳述)
そしてnewで指定したコンストラクター関数Window_Scrollableが適用され、インスタンスの情報が作られていきます。
さて、これでようやく(1)の中身に踏み込むタイミングが来ました。
function Window_Scrollable() {
this.initialize(...arguments);
}
とは言え、やっていることは結局initializeメソッドへの丸投げです。
受け取った引数を展開し、自身のinitializeメソッドへ渡してWindow_Scrollable関数の仕事はおしまいです。
(※なお、argumentsやスプレッド構文(...)については本記事の主旨から外れるためここでは説明しませんが、頻出する記述なので馴染の薄い方はここで押さえておくことをお勧めします。)
最後にinitializeが仕事をしてインスタンスの出来上がり
Window_Scrollable関数からinitializeメソッドが呼ばれました。
(4)の部分です。
Window_Scrollable.prototype.initialize = function(rect) {
Window_Base.prototype.initialize.call(this, rect);
this._scrollX = 0;
this._scrollY = 0;
this._scrollBaseX = 0;
this._scrollBaseY = 0;
this.clearScrollStatus();
};
後はこのWindow_Scrollableクラスでインスタンス生成時に行っておきたい処理を記述するだけですね。
通常は親クラスの初期化処理(initialize)を呼び出した上で独自のプロパティ追加やメソッド呼び出しを行う場合が多いです。
今回のWindow_Scrollableでも、まずはWindow_Base側の初期化処理(initialize)を行い、加えて自身で追加したいプロパティを追加、処理しておきたいメソッドなどを記述しています。
これでWindow_Scrollableクラスのインスタンスが作成されました。
「おまじない」の実行内容まとめ
・まずは一連のコードが読み込まれ、定義される。
この際、(1)で宣言された「子クラス」のコンストラクター関数に対し、(2)によって
「親クラス」のprototypeが引き継がれる。
ただしprototype.constructorは親クラスのコンストラクター関数でなく子クラスの
コンストラクター関数でないと齟齬が起こるので、(3)でprototype.constructorだけは
子クラスのコンストラクター関数に繋ぎ変える。
・実処理。コード内のどこかで「子クラス」のインスタンスがnew演算子によって生成される。
この時、new演算子の働きにより新規オブジェクトが生成され、その新規オブジェクトに対し
コンストラクター関数(1)が実行される。更に新規オブジェクトの[[prototype]]に
コンストラクター関数のprototypeへの参照が設定される。
>(1)の関数オブジェクトは(4)のinitializeメソッドを呼び出すだけ。
(4)によって「子クラス」のインスタンスが初期化処理を行われてインスタンスの出来上がり。
こうして一連の流れを追ってみると、我々がクラスと呼んでいる物の正体はとどのつまり少し手の込んだFunctionオブジェクトに過ぎないことが見えて来ますね。
prototypeチェーンについて
ここまでprototypeによるクラスの定義とインスタンス生成時の処理の流れを追ってきましたが、prototypeについて語る上では外せない要素がもう一つ残っています。
それはメソッドのprototypeチェーンです。
正確かつ詳細な説明についてはmdn:「継承とプロトタイプチェーン」を参照してもらうのが確実なのですが、ここではツクールMV/MZに即した説明を試みます。
まず登場人物を整理します。
prototype・・・子(インスタンス)で使用させたいメソッドやプロパティが詰め込まれている。
[[prototype]]・・・自身の親(コンストラクター)のprototypeへの参照が代入されている。
内部プロパティであり表面上は見えない(※)ようになっている。
(※余談だが、getPrototypeOfで間接的にアクセスすることは可能)
名前が紛らわしいですが、この二つは明確に異なる物なのでしっかり区別しましょう。
さて、今回のWindow_Scrollableを例として、メソッドは何でもいいですが、仮にウインドウの行高さを得るlineHeightメソッドを使用したとします。
このメソッドはWindow_Base.prototype.lineHeightとして、Window_Baseのprototypeプロパティに設定されたメソッドです。
この時、何が起こるのかを順に追ってみましょう。
const testWindow = new Window_Scrollable(rect); //rectは事前に設定済とする
const lineHeight = testWindow.lineHeight();
この場合、それぞれのprototypeおよび[[prototype]]は以下のようになります。
| 自身が直接所有するプロパティ | prototype | [[prototype]] | |
|---|---|---|---|
| Window_Base (親クラスのコンストラクター) |
lineHeightを持っていない |
lineHeightを持っている(ここで定義されている) |
Functionオブジェクトのprototype(今回の主題とはあまり関係なし) |
| Window_Scrollable (子クラスのコンストラクター) |
lineHeightを持っていない |
lineHeightを持っていない |
Functionオブジェクトのprototype(今回の主題とはあまり関係なし) |
| testWindow (子クラスのインスタンス) |
lineHeightを持っていない |
undefined | Window_Scrollable.prototype |
(1) まずは自身が該当のプロパティ(メソッド)を持っていないかチェックする
testWindowに対しlineHeightメソッドを呼び出すと、まず最初にtestWindowがlineHeightメソッドを持っていないか、つまりtestWindow.lineHeightが存在するかどうかをチェックします。
ここでの「持っている/いない」というのは、[[prototype]]を経由せずに、そのインスタンス(testWindow)自身に対し直接lineHeightというプロパティ(メソッド)が定義されているかどうか、です。
チェックした結果、testWindowはlineHeightを直接には持っていませんので次に進みます。
(2)次に[[prototype]]の中をチェックする
インスタンス(testWindow)が直接的に該当プロパティを持っていなければ、今度は[[prototype]]にlineHeightメソッドがないかを探します。
インスタンスの[[prototype]]にはWindow_Scrollable.prototypeへの参照が入っていましたね。
インスタンスの[[prototype]](その参照先はWindow_Scrollable.prototype)にはlineHeightメソッドはまだ見つかりません。
次へと進みます。
(3)[[prototype]]になければ、その[[prototype]]、そこにもなければ更にその…
[[prototype]]に該当プロパティが見つからなければ、次は[[prototype]]の[[prototype]]、つまりtestWindow.[[prototype]].[[prototype]]をチェックします。
以下同様に、該当のメソッドが見つかるまで(あるいは見つからずにnullに辿り着くまで)延々と[[prototype]]を遡って行くことになります。
今回の場合、Window_Scrollable.prototypeは(2)の処理によってObject.create(Window_Base.prototype)で新規生成されたオブジェクトが代入されています。
そのためWindow_Scrollable.prototypeの[[prototype]]はWindow_Base.prototypeであり、つまりtestWindow.[[prototype]].[[prototype]]とはWindow_Base.prototypeを示しているわけです。
結果、testWindow.lineHeight()を行うと、[[prototype]]を頼りにWindow_Scrollable、次にWindow_Baseと順にprototype内を遡って行き、Window_Base.prototype.lineHeightを見つけることができました。
また、このように、遡って行くのはあくまでもprototypeの中なので、仮にWindow_Scrollable.hogeMethodのようにprototypeではなくクラスに対し直接定義されていたメソッドは、インスタンスからは参照されません。
static(静的)メソッドがインスタンスから使用できない理由がこれです。
以上、ツクールMZのWindow_Selectableを例に流れを追ってみましたが、「余計にわかりにくいわー!」と感じた方は元ネタである「継承とプロトタイプチェーン」の方を読んでみてください。
シンプルなオブジェクトを例にしてあるので、返ってそっちの方が分かりやすいかもしれません。
終わりに
以上、JavaScriptのprototypeによる継承とprototypeチェーンについてでした。
これらを理解しているかいないかで、コアスクリプトのコードを読み解く速さと精度が大きく変わることかと思います。
脱・初級のためには早めに理解しておきたいポイントではないでしょうか。
では、今回もこの言葉で締めとさせていただきます。
忌憚ないツッコミ待ちだぁ! щ(゚Д゚щ) バッチコーイ