2019/03/17 現在はまだ Stage 2 (Draft) の段階ですが、面白そうなのでメモ。
自分なりに分かりやすくまとめてみようと思います。
参考「Decorators proposal」
参考「GitHub - tc39/proposal-decorators: Decorators for ES6 classes」
1. デコレータとは何か
クラスやメソッドに機能を追加するもの。
(Java でいうアノテーション)
2. 基本的な使い方
デコレータは関数で定義します。
(ここではアロー関数を使っていますが、普通の function でも大丈夫です)
2.1. クラス
const F = target => {
// target: クラス
// return target; // なくても良い
};
@F // ★
class Foo {
}
class Foo {
}
Foo = F(Foo) || Foo; // ★
2.2. メソッド
const F = (target, name, descriptor) => {
// target: クラスの prototype (※ this の代わりに使うとハマることがある)
// name: メソッド名
// descriptor: メソッドのディスクリプタ
// descriptor.value: メソッドそのもの
// descriptor.writable: false なら const
// return descriptor; // なくても良い
};
class Foo {
@F // ★
bar() {}
}
class Foo {
bar() {}
}
// ★
(() => {
let bar = Object.getOwnPropertyDescriptor(Foo.prototype, 'bar');
bar = F(Foo.prototype, 'bar', bar) || bar;
if (bar) Object.defineProperty(Foo.prototype, 'bar', bar);
})();
※ Object.getOwnPropertyDescriptor()
, Object.defineProperty()
はディスクリプタを読み書きする関数。
※ディスクリプタの enumerable
, configurable
は普段使わないと思うので省略
2.3. アクセサ
ほぼメソッドと同じ (ディスクリプタの内容が異なる) 。
const F = (target, name, descriptor) => {
// target: this
// name: メソッド名
// descriptor: メソッドのディスクリプタ
// descriptor.get: getter そのもの
// descriptor.set: setter そのもの
// return descriptor; // なくても良い
};
class Foo {
@F // ★
get bar() {}
set bar(value) {}
}
class Foo {
get bar() {}
set bar(value) {}
}
// ★ (メソッドと同じ)
(() => {
let bar = Object.getOwnPropertyDescriptor(Foo.prototype, 'bar');
bar = F(Foo.prototype, 'bar', bar) || bar;
if (bar) Object.defineProperty(Foo.prototype, 'bar', bar);
})();
※ Object.getOwnPropertyDescriptor()
, Object.defineProperty()
はディスクリプタを読み書きする関数。
※ディスクリプタの enumerable
, configurable
は普段使わないと思うので省略
2.4. オブジェクトのメソッド・アクセサ
オブジェクト定義の中ののメソッド・アクセサでも同様に使用することができます。
const Foo = {
@F // ★
bar() {},
@F // ★
get baz() {},
set baz(value) {}
};
3. もう少し進んだ使い方
3.1. デコレータに引数を持たせる
デコレータを返す関数を定義する。
const F = param => (target, name, descriptor) => {
// ...
};
class Foo {
@F('Param') // ★ F('Param') == (target, name, descriptor) => {/* ... */}
bar() {}
}
3.2. 複数のデコレータを同時に適用する
適用したいクラス・メソッド・アクセサの宣言に近いものから順に適用されます。
class Foo {
// ★ F → G → H
@H
@G
@F
bar() {}
}
4. 具体的な使用例
4.1. @readonly
どの場面でも使えそうだったので具体例として挙げてみました。
// 定義
const readonly = (target, name, descriptor) => {
descriptor.writable = false;
};
// 適用
class Foo {
bar() {}
@readonly // ★
baz() {}
}
//
const foo = new Foo();
foo.bar = () => {};
foo.baz = () => {}; // ★エラーが発生する
4.2. メソッドの前後に何かする
※これはハマりポイントがいくつかあったので注意が必要です。
4.2.1. 正しく動作する例
// 定義
const F = (target, name, descriptor) => {
if ( 'value' in descriptor ) {
const old = descriptor.value;
descriptor.value = function(...args) {
console.log(name + '(): begin'); // 何か
const result = old.apply(this, args);
console.log(name + '(): end'); // 何か
return result;
};
}
};
// 適用
class Foo {
constructor() {
this.baz = 'Baz';
}
@F // ★
bar() {
console.log(this.baz);
}
}
//
const foo = new Foo();
foo.bar();
bar(): begin
Baz
bar(): end
4.2.2. descriptor.value
をそのまま使えない問題
// 定義
const F = (target, name, descriptor) => {
if ( 'value' in descriptor ) {
const old = descriptor.value; // 取り出す
descriptor.value = (...args) => {
return old(...args); // typeof this === 'undefined'
};
}
};
取り出した関数内の this
は this
として使えなくなるため。
参考「束縛された関数を生成する - Function.prototype.bind() - JavaScript | MDN」
4.2.3. bind(target)
しても正しく動作しない問題
target
== this
ではありません。
// 定義
const F = (target, name, descriptor) => {
if ( 'value' in descriptor ) {
const old = descriptor.value.bind(target); // bind(target)
descriptor.value = (...args) => {
const result = old(...args); // this === target === Foo.prototype
return result;
};
}
};
// 適用
class Foo {
constructor() {
this.baz = 'Baz';
}
@F // ★
bar() {
console.log(this.baz); // typeof this.baz === 'undefined'
}
}
class Foo {
constructor() {
this.baz = 'Baz';
}
bar() {}
}
//
const foo = new Foo();
console.log(foo); // this == {baz: "Baz", __proto__: Foo.prototype}
console.log(Foo.prototype); // prototype == {constructor: f, bar: f, __proto__: Object.prototype}
prototype
の仕組みに関しては「プロトタイプチェーン」で調べてみてください。
4.2.4. アロー関数では呼び出し元の this
が分からない問題
多くの場合は、this
を束縛しないアロー関数の方が function より便利ですが、今回ばかりは function を使わざるを得ません。
デコレータが実行される段階ではクラスはインスタンス化されていないので、this
を知るすべがありません。
Object
のプロパティやメソッドに this
を知るための機能もないので、デコレータ内で定義する関数内で this
を調べることになりますが、それはアロー関数ではなく function でないとできません。
// 定義
const F = (target, name, descriptor) => {
if ( 'value' in descriptor ) {
const old = descriptor.value; // prototype からメソッド取得
descriptor.value = function(...args) { // アロー関数ではなく function
const result = old.apply(this, args); // new 後に呼ばれたときに this が決定される
return result;
};
}
};