自主的20%るぅる

各々が自主的に好き勝手書くゆるふわ会社ブログ

やっぱり JavaScript (TypeScript) でも アノテーション (Decorator) 使いたい!

JavaScript でもアノテーションしたい!

こんにちは、江嵜です。

JavaScript バリバリ書いてますか?

JavaScript をガッツリ書いていると、

  • やっぱり型が欲しくなったり
  • Enum 欲しくなったり
  • Interface 欲しくなったり

みたいなことあって、TypeScript 使っちゃうんですね。
やっぱりあった方が便利ですからね。

で、次はアノテーション欲しいんですか?
TypeScript にもありますよ!良かったですね!

(もう Java 書いてればいいんじゃないかって気がしなくもない)

実際に書いてみよう!

先ほどはアノテーションという名前を使いましたが、これは Java での呼び方ですね。
JavaScript ではデコレーターという名前になっています。

大体出来ることは同じなので、呼び方が違うんだなという程度の考え方で OK です。

デコレーターは、あるクラスやメソッド、パラメーターに対して、その動きを追加したり変更したりと、
デコレーション出来る機能なんですね!

今回は、一番分かりやすいメソッドに対するデコレーターを見ていきましょう。

まずはデコレーターなしで同じことをやってみる

…と、まずはデコレーターなしで同じことを書いてみましょう。

今回は TypeScript で書いていきますね。

ちなみに、デコレーターはまだ実験段階の内容の為、
TypeScript でのコンパイル時にオプションを指定してあげる必要があります。

とりあえず

npm install -g typescript

とでもして、 TypeScript をインストールしておきましょう。

サンプルはこんな感じ。

test1.ts

class Test {
  main() {
    console.log(’exec main!’);
  };
}

const test = new Test();
test.main();

Test クラスからオブジェクトを作成して、 main メソッドを呼び出すだけですね。

コンパイルして実行してみます

tsc --target ES5 test1.ts
node test1.js
exec main!

ここまではいいですよね。

さて、ここで(定番ですが)、main 実行前にログを出力するするようにしたいとしましょう。

function log(fn: Function) {
  console.log(’exec log!’);
  return fn();
};

class Test {
  main() {
    console.log(’exec main!’);
  }
}

const test = new Test();
log(test.main);

こんな感じですかね。

JavaScript では引数に関数を渡せますから、
log 関数の引数に main の関数を渡して、
ログを出力後に渡された関数を実行する、という流れでいきました。

ここでやったのは、log という関数を別で定義して、main 関数を渡すことで、
main の動作時に log の動作を付け加えた、ということになります。

デコレーターを使ってみる

さて、ではここでデコレーターを使ってみましょう。

先ほどと同じような内容で、デコレーターを使ってみるとこんな感じ。

function log() {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {

    // デコレーターが設定された関数を origin に保存
    const origin = descriptor.value;

    // 関数を書き換える
    descriptor.value = function(...args) {
      console.log(’decorate exec!’);

      // 元の関数を呼ぶ
      origin.apply(this, args);
    };
  }
};

class Test {
  @log()
  main() {
    console.log(’exec main!’);
  }
}

const test = new Test();
test.main();

なんか突然難しそうな感じになりましたね?
実際の内容はそうでもないので、一つずつみていきましょう。

function log() {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {

    // デコレーターが設定された関数を origin に保存
    const origin = descriptor.value;

    // 関数を書き換える
    descriptor.value = function(...args) {
      console.log(’decorate exec!’);

      // 元の関数を呼ぶ
      origin.apply(this, args);
    };
  }
};

この部分がデコレーターの設定ですね。
デコレーターもその実、単純に関数なので、 function で定義します。

return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {

次に、 function を返り値にしていますが、
これは実際にデコレーターが関数にくっつけて使用されたときに実行される関数になります。
(デコレーターを付けた関数が呼び出されたときではなく、使用された時点ですね。)

この時、引数には

  • target: クラスのプロトタイプ
  • name: 呼ばれたメソッドの名前 (今回は test.main なので、 main になります)
  • descriptor: 呼ばれたメソッドの定義

ここで、 descriptor が少し難しいところなのですが、 Object.defineProperty()
3 つめの引数に設定するものと同じですね。
とりあえず、 descriptor.value に元々のメソッド、
今回の場合は console.log(’exec main!’); をする関数が入っていることを覚えておいてください。

ここで、呼ばれたメソッドの定義が渡ってきているので、
これを編集すると、定義を書き換えることが出来る、ということになります。

ということで、

const origin = descriptor.value;

ここで一度、元々の関数の定義を退避しまして、

descriptor.value = function(...args) {

で、新しい関数の定義を代入しましょう。

引数の

...args

は、引数を配列ですべて受け取る書き方ですよ。
(例えば function hoge(…args) {} に対して、 hoge(1, ‘fuga’, true) と呼び出すと、
args は [1, ‘fuga’, true] の配列になります)

今回は元の関数の実行前にログを出力したかったので、
まず

console.log(’decorate exec!’);

でログの出力。

それに続けて、

origin.apply(this, args);

で、先ほど退避させておいた関数を、引数を渡して実行しています。
(apply は第一引数に実行時の this を指定、第二引数に実行時の引数を配列の形で渡すことで、
関数を実行してくれる命令ですね)

ここまでできればデコレーターの設定は終わりです。

最後に、

  @log()
  main() {

のように、デコレーターを適用したいメソッドに対して、
@関数名() として頭にくっつけてあげれば、デコレーターを使用してメソッドを呼び出せるという寸法ですね。

まとめ

まずは、というところで、デコレーターの書き方を学んでみました。
いかがでしょうか。
もしかしたら、まだ何がどう便利なのか、というところがいまいちピンと来ていないかもしれないですね。

ここで注目したいのは、デコレーターを使えば
middleware のように前後に処理を追加することもできますが、
関数の挙動そのものも変えることが出来てしまう、ということです。
(今回は apply で元の関数をそのまま呼び出しましたが、そこを工夫すると色々できそうですよね)

例えば、 Express を使う際に普通であればルーターを使用して

app.get(’/hoge’, function(req, res) {
  //処理
});

みたいに書いて、 /hoge に Get でアクセスされたときの挙動を書いたりしますが、
デコレーターを使えば

@Get(’/hoge’)

みたいなデコレーターを使って定義が書けるようになったりしそうですよね?

皆さんも、JavaScript でデコレーターというものを使う選択肢で、
ラクをしてみてくださいね。
(2019/03 現在では、まだ実験段階の為、仕様が変わる可能性にはお気をつけて!)

Let’s share this article!

{ 関連記事 }

{ この記事を書いた人 }

takato_ezaki
記事一覧