JavaScript
RPGツクールMV

JavaScriptのフックパターンの楽な書き方

More than 1 year has passed since last update.

JavaScriptでコアスクリプトのファイルを直接いじらずに
別ファイルから既存の関数に機能追加するのがフックパターン。
プラグインを作る際に頻出ですね。

従来の(苦しい)書き方

var _Class_method = Class.prototype.method;
Class.prototype.method = function() {
    _Class_method.apply(this, arguments);   //元の関数
    console.log('プラグインで処理挿入~');   //追加処理
};

よし、ちょっと面倒だけど仕方ないな。
この調子でもういっこ…コピペして改変するか。

var _Class_method2 = Class.prototype.method;    //←あー!!!!!111
Class.prototype.method2 = function() {
    _Class_method2.apply(this, arguments);  //元の関数
    console.log('別の関数に別の処理挿入~');    //追加処理
};

…一箇所書き換え忘れてしまったようですね。
しかもこれエラー出ないから見つけづらい。

新しい(ナイスな)書き方

4回もclass_methodほにゃらら~みたいな呪文を唱える書き方が悪いのです。
フックパターンそのものを関数化しましょう。

function hook(baseClass, target, addition) {
    if (baseClass.prototype[target]) baseClass = baseClass.prototype;
    else if (!baseClass[target]) throw new Error('フック先が無いんですけど!');
    var origin = baseClass[target];
    baseClass[target] = function() {
        arguments[arguments.length] = origin;
        arguments.length++;
        return addition.apply(this, arguments);
    };
}

仕組みは追って説明しますが、とりあえず使い方は以下。

hook(Class, 'method', function() {
    var origin = arguments[arguments.length - 1];
    origin.apply(this, arguments);  //元の関数
    console.log('プラグインで処理挿入~');   //追加処理
});

バ…バカな… か…簡単すぎる… あっけなさすぎる…

4回もあった面倒くさい呪文が1回ぽっきりに減りました。
しかも万が一メソッド名を間違えた時は即座にエラーを吐いてくれる!
(var originとかいう謎の呪文が増えてますがそれは目をつぶってねお願いです)

解説

まずhook関数を解説していきます。
引数はbaseClassがフックしたいメソッドの存在するクラス、
targetがフックしたいメソッドの名前、additionが新たな関数です。

if (baseClass.prototype[target]) baseClass = baseClass.prototype;
else if (!baseClass[target]) throw new Error('フック先が無いんですけど!');

一行目ではまずtargetという名前のメソッドがプロトタイプにあるか調べ、
存在すればプロトタイプをフックの対象に設定しています。
よくわからなければ「静的メソッドと動的メソッドの両方に対応している」とご理解ください。
特にRPGツクールMVではXxxManager系の静的クラスに
フックしたいこともよくあるので、この記述は私的には必要ですね。

二行目は静的メソッドと動的メソッドのどちらも存在しなかった時にエラーを吐きます。

var origin = baseClass[target];
baseClass[target] = function() {
    arguments[arguments.length] = origin;
    arguments.length++;
    return addition.apply(this, arguments);
};

フックの本体。よく見ると最初に出てきた例と似たような記述であることがわかりますね。

ただ目新しいのがargumentsに関する記述。
これは何をしているかというと引数リストの末尾に、元の関数を加えているのです!
arguments.push(origin)としたかったのですが、argumentsにはpushメソッドが生えていないようです)
他にreturnという記述がさりげなく増えてますが、戻り値が存在する関数に対応したまでです。

これにより、新しく作るaddition関数から元の関数を好きなタイミングで呼び出すことが出来ます!
先フックだろうが後フックだろうが引数があろうが戻り値があろうが自由自在ですね!

hook(Class, 'method', function() {
    var origin = arguments[arguments.length - 1];
    origin.apply(this, arguments);  //元の関数
    console.log('プラグインで処理挿入~');   //追加処理
});

再掲ですがフック側。ここで問題なのはvar originです。
こんな回りくどい書き方をしなくても引数の末尾のoriginを直接受け取ればいいじゃん、
と思われるかもしれませんが、それではダメなパターンがあります。
そう、同じメソッドへの多重フックです。

何回もフックしていくと引数の末尾にorigin1,origin2とどんどん関数が増えていくので、
それぞれの関数のその場で最後尾の関数を正しく選び出さないと無限ループを起こします。
そのためのおまじないがvar originの一行なわけです。

「結局おまじないかよ!」と思うかもしれませんが、
元々の4箇所書き換えないと失敗、しかもエラーは出ないと比べると
1箇所書き換え、おまじないは一行そのままコピペ、しくじったらエラーが出るでは
全然使用感が違うと思います。ってか違います。

・呼び出し側で引数が省略されたらどうするの?
…ただし引数の末尾に元の関数を加えていくという荒業なので
呼び出し側が引数を省略しだしたりすると破綻がちらついてきます。
ちなみにRPGツクールMVではGame_Party.prototype.hasItem
Window_Command.prototype.addCommandの2つの関数で引数省略が前提の処理があります。
この2つではhook関数を作ったフックはやめておきましょう。

…というわけでJavaScriptのフックパターンを楽で失敗なく
書けるようにする関数を考えついたので紹介してみました。
これでプラグインが作りやすくなると思うので、みんなもツクール買ってプラグイン作ろう!