TypeScript・JavaScriptのデコレータは、クラス宣言やそのメソッドなどにアタッチできる特別な宣言です。
公式ドキュメント: https://www.typescriptlang.org/docs/handbook/decorators.html
使い方としては@hoge
のようなものを追加するだけという魔法のような機能なのですが、いまいち動き方のイメージがしづらくないでしょうか?
今回はメソッドデコレータを例に、ES5へのトランスパイル後のソースを見つつ、「内部的にどう処理されているのか?」にフォーカスして紹介していきます。
デコレータそのものについては説明していないので、「そもそもデコレータとは?」という方には下記の記事をおすすめします。
TypeScriptによるデコレータの基礎と実践
ClassからPrototypeへの変換を見てみる
デコレータの挙動を知るためには、前提としてJavaScriptのPrototypeについて理解しておく必要があります。
ということで、まずは「Class構文をES5へトランスパイルしたときに、どんな感じのPrototypeの構文に書き換えられるか」を追っていきましょう。
TypeScriptのソース
class Person {
name: string = 'Some Name';
sayHello() {
console.log('Hello.');
}
sayBye() {
console.log('Bye.');
}
}
トランスパイル後のJavaScript
"use strict";
var Person = /** @class */ (function () {
function Person() {
this.name = 'Some Name';
}
Person.prototype.sayHello = function () {
console.log('Hello.');
};
Person.prototype.sayBye = function () {
console.log('Bye.');
};
return Person;
}());
グローバルに定義されているPerson関数がクロージャとして内部で別のPerson関数を返しています。
そして、Person関数のオブジェクトは、prototypeを通じてsayHello
とsayBye
を実行できるような作りになっています。
ここで理解しておく必要があるのは、
-
name
はPersonオブジェクトのプロパティであるが、 -
sayHello
とsayBye
はPersonオブジェクトのメソッドではなく、Person.prototypeオブジェクトのメソッドである
ということです。
メソッドデコレータの基本的な動きを解析する
それを踏まえた上で、Personクラスのメソッドにデコレータを追加して、デコレータが内部的にどんな動きをしているのかを確認してみましょう。
TypeScriptのソース
下記は、メソッドデコレータを先ほどのPersonクラスに追加したものです。
デコレータはログ収集に使われることが多いみたいなので、便宜的にloggerという名前にしています。
class Person {
name: string = 'Some Name';
@logger // メソッドデコレータを追加
sayHello() {
console.log('Hello.');
}
sayBye() {
console.log('Bye.');
}
}
// デコレータ
function logger(target: any, key: string) {
console.log(target);
console.log(key);
}
メソッドデコレータは、第一引数にtarget
、第二引数にkey
、第三引数にdescriptor
(今回は省略)を受け取ります。
実行結果
このファイルを実行してみると、
Person { greet: [Function], sayBye: [Function] }
sayHello
と出力されます。
第一引数のtarget
にはTypeScript上でのPersonクラスが持っているメソッド、
第二引数のkey
にはデコレータの対象としている関数名(sayHello
)が入ってきます。
(第三引数は今回省略していますが、Object.getOwnPropertyDescriptor
によって取得できるオブジェクトが受け取れます。)
ここで注目すべきは、target
にはname
プロパティは含まれないことです。
この理由は、target
として渡されるものが、「トランスパイル後のPerson関数のPrototype」だからです。
解説
この挙動に関しては、デコレータを追加したTypeScriptのトランスパイル後コードを見てみれば分かります。
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var Person = /** @class */ (function () {
function Person() {
this.name = 'Some Name';
}
Person.prototype.sayHello = function () {
console.log('Hello.');
};
Person.prototype.sayBye = function () {
console.log('Bye.');
};
// ↓↓↓ここに注目↓↓↓
__decorate([
logger
], Person.prototype, "sayHello", null);
return Person;
}());
function logger(target, key) {
console.log(target); // Person { greet: [Function], sayBye: [Function] }
console.log(key); // sayHello
}
__decorate
関数が定義され、Person関数内で実行されていますね。
そして、Person.prototype
がその第二引数に入っています。
__decorate
関数自体は、ごちゃごちゃといろいろ書いてありますが、今回の理解に必要な部分だけ抜き出すと、
var __decorate = function (decorators, target, key, desc) {
for (var i = decorators.length - 1; i >= 0; i--) {
decorators[i](target, key);
}
}
こんな感じになります。
今回の例で言えば、下記の箇所で行われていることは、
__decorate([
logger
], Person.prototype, "sayHello", null);
要するにこれと同じということになります。
logger(Person.prototype, "sayHello");
だから、第一引数のtarget
にはPersonクラスのメソッドのみを持ったオブジェクト(つまりトランスパイル後のPerson.prototype
)が入り、第二引数のkey
にはメソッド名が入ってくるということですね。
ちなみに今回は省略した第三引数のdescriptor
についても、__decorate
関数内で実行されているObject.getOwnPropertyDescriptor(target, key)
の値がそれに当たる部分だということが分かります。
なんだ、メソッドデコレータって結局それだけか。という感じですね。
メソッドデコレータの評価順を解析する
デコレータを使うときは評価の順番について理解しておくことが重要ですが、__decorate
関数の中で何が行われているかだけ把握しておけばそこまで混乱することはありません。
- メソッドデコレータを二つに増やした場合
- Decorator Factoryのパターンを使用した場合
について見ていきましょう。
デコレータを二つに増やした場合
TypeScriptのソース
class Person {
name: string = 'Some Name';
@logger1
@logger2 // これを追加
sayHello() {
console.log('Hello.');
}
sayBye() {
console.log('Bye.');
}
}
function logger1(target: any, key: string, desc: PropertyDescriptor) {
console.log('This is logger1');
}
// これを追加
function logger2(target: any, key: string, desc: PropertyDescriptor) {
console.log('This is logger2');
}
実行結果
メソッドデコレータが二つの場合、後に書いたデコレータから評価されるようです。
This is logger2
This is logger1
解説
例によってES5へのトランスパイル後のソースを見てみましょう。
トランスパイル後は、下記のように、デコレータの関数が記述順に配列に入れられ、それが__decorate
関数の引数となります。
// var __decorate = ...
var Person = /** @class */ (function () {
// function Person() {...
__decorate([
// ↓↓↓記述順に配列に入れられる↓↓↓
logger1,
logger2
], Person.prototype, "sayHello", null);
return Person;
}());
// function logger1(target, key, desc) {...
// function logger2(target, key, desc) {...
そして、前述したように、メソッドデコレータは本質的には下記のような処理に変換されています。
// 再掲
var __decorate = function (decorators, target, key, desc) {
for (var i = decorators.length - 1; i >= 0; i--) {
decorators[i](target, key);
}
}
見れば分かる通り、decorators
配列に入っているものを後ろから実行するような処理になっているので、logger2
、logger1
の順に出力されたということですね。
Decorator Factoryのパターンを使用した場合
デコレータを返す関数をDecorator Factoryと呼びます。
デコレータに引数を渡したい場合なんかに使われるのですが、このパターンを使用した場合、ややトリッキーな動きをするので注意が必要です。
TypeScriptのソース
class Person {
name: string = 'Some Name';
@logger1() // ()を追加
@logger2() // ()を追加
sayHello() {
console.log('Hello.');
}
sayBye() {
console.log('Bye.');
}
}
// Decorator Factoryに変更
function logger1() {
console.log('This is logger1 factory');
return function(target: any, key: string, desc: PropertyDescriptor) {
console.log('This is logger1');
}
}
// Decorator Factoryに変更
function logger2() {
console.log('This is logger2 factory');
return function(target: any, key: string, desc: PropertyDescriptor) {
console.log('This is logger2');
}
}
実行結果
評価順は、まずDecorator Factoryを上から、そしてデコレータを下から、という感じになります。
This is logger1 factory
This is logger2 factory
This is logger2
This is logger1
解説
デコレータが下から評価されるのは、前述の通りですね。
では、Decorator Factoryが上から評価されるのはなぜでしょうか?
これは、トランスパイル後のソースを見れば分かる通り、__decorate
関数への引数として渡すときに評価されてしまうからです。
// var __decorate = ...
var Person = /** @class */ (function () {
// function Person() {...
__decorate([
logger1(), // ここでlogger1のDecorator Factoryが評価される
logger2() // ここでlogger2のDecorator Factoryが評価される
], Person.prototype, "sayHello", null);
return Person;
}());
// function logger1() {...
// function logger2() {...
つまり、__decorate
関数への引数として渡す際にまずDecorator Factoryが記述順に評価され、__decorate
関数内でデコレータが記述の逆順に評価される、ということです。
トランスパイル後のソースを見れば、評価順に関しても、「なんだ、そういうことだったのか」という感じですね。
今回はメソッドデコレータに絞って解析しましたが、クラスデコレータやプロパティデコレータなども、__decorate
関数を見れば中でどんな処理がされているかが分かるはずです。
予定をドタキャンされていきなり暇になってしまった時などに見てみてください。