デコレータ関数の正しい作り方
ここまででデコレータ関数の作り方のおおよその枠組みを紹介できたと思いますが、実はまだ問題があります。それは、originalMethodの実行です。本節では、そこを修正していきます。
フィールドを利用するクラスの場合
ここまでのサンプルとして紹介してきたデコレータを付与するメソッドであるshow()は、クラスのメソッドとはいえ、クラスフィールドの値を利用しない、いわば関数的なメソッドでした。一方、リスト5のようなshow()メソッドの場合、リスト4のデコレータ関数では、実行時にエラーとなります。
class SelfIntro { #name: String; constructor(name: String) { this.#name = name; } @logging show(age: number) { console.log(`私は${this.#name}で${age}歳です。`); // (1) } }
リスト5の(1)のshow()メソッド内のコードでは、メソッドの引数だけでなく、クラスのフィールド#nameの値を利用しています。もちろんこの値は、あらかじめクラスがnewされる際にコンストラクタによって渡された値です。ところが、デコレータ関数の引数として渡されるoriginalMethodを、これまでのコードサンプルのように関数実行した場合、#nameの値が存在しないことになるからです。
replacementMethod()関数の第1引数thisの利用
そこで登場するのが、replacementMethod()関数の第1引数として渡されるthisです。このthisには、originalMethod、すなわち、デコレータが付与されたメソッドが含まれるオブジェクトそのものが渡ってきます。そのため、このthisの中には、フィールドの値も含まれていることになります。このthisの環境下でoriginalMethodを実行する必要があります。そのためのメソッドとして、call()がJavaScriptには用意されており、これを利用します。
ここまでの内容を踏まえて、デコレータ関数logging()を正しく実装したコードは、リスト6のようになります。(2)のように、直接originalMethodを実行するのではなく、call()メソッドを利用してoriginalMethodを実行します。その際、replacementMethod()関数の引数をそのまま渡します。すなわち、第1引数にthisを、第2引数に...argsを渡します。
function logging(originalMethod: any, context: any) { function replacementMethod(this: any, ...args: any[]) { console.log("メソッドが実行されました。"); // (1) const originalReturn = originalMethod.call(this, ...args); // (2) return originalReturn; } return replacementMethod; }
リスト6の形、すなわち、デコレータ関数を定義し、その中でreplacementMethod()関数を定義してリターンする。そのreplacementMethod()内でcall()メソッドを使ってoriginalMethodを実行する、というコードパターンが、デコレータ関数のコードパターンの基本といえます。そして、デコレータが付与されたメソッドがフィールドを利用している、いないに関わらず、このcall()によるoriginalMethodの実行を基本とします。
call()の代わりにapply()も使える
JavaScriptのメソッドcall()は、関数やメソッドを第1引数で渡されたオブジェクトに割り当てて呼び出すことができるメソッドです。そして、同じようなメソッドにapply()があります。そのため、リスト6の(2)は次のようにapply()を利用することも可能です。違いは、第2引数のargsをスプレット演算子で展開して渡すか、そのまま渡すかです。
originalMethod.apply(this, args)
デコレータの引数
デコレータには、引数を設定することもできます。例えば、リスト7のようなコードです。(1)を見ればわかるように、通常の関数のように()を記述して、その中に渡したいデータを記載します。
class SelfIntro { : @logging("Nakata") // (1) show(age: number) { console.log(`私は${this.#name}で${age}歳です。`) } }
このような引数をデコレータ関数で受け取る場合、リスト6のようなデコレータ関数を、さらに外側の関数でラップします。結果、例えば、リスト8のようなコードとなります。
function logging(userName: String = "NoName") { // (1) return (originalMethod: any, context: any) => { // (2) function replacementMethod(this: any, ...args: any[]) { console.log(`メソッドが${userName}の名のもと実行されました。`); const originalReturn = originalMethod.call(this, ...args); return originalReturn; } return replacementMethod; } }
まず、(1)のように、デコレータ名の関数を定義するところは、これまでも同じです。ただし、この関数の引数が、デコレータを付与する際に受け取りたい値とします。(1)では文字列のuserNameとしています。そのため、リスト7の(1)でデコレータを付与するコードの()内には文字列を記述することができるようになります。
このデコレータ名の関数が、これまでのデコレータの定義関数をラップした関数となります。リスト8では、その内部で、(2)のように、アロー関数をリターンしており、このアロー関数の引数を見ればわかるように、これまでデコレータとして定義していた関数そのものです。なお、このラップした関数内でリターンするデコレータ関数は、(2)のように、アロー関数でも問題ありません。