77
58

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

JavaScript の デコレータ の使い方

Last updated at Posted at 2018-03-16

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'
		};
	}
};

取り出した関数内の thisthis として使えなくなるため。

参考「束縛された関数を生成する - 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'
	}

}
prototype と this の違い
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;
		};
	}
};
77
58
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
77
58

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?