JavaScript の デコレータ の使い方

2019/03/17 現在はまだ Stage 2 (Draft) の段階ですが、面白そうなのでメモ。
自分なりに分かりやすくまとめてみようと思います。

参考「[Decorators proposal](https://tc39.github.io/proposal-decorators/)」
参考「[GitHub - tc39/proposal-decorators: Decorators for ES6 classes](https://github.com/tc39/proposal-decorators)」

# 1. デコレータとは何か

クラスやメソッドに機能を追加するもの。
(Java でいうアノテーション)

# 2. 基本的な使い方

デコレータは関数で定義します。
(ここではアロー関数を使っていますが、普通の function でも大丈夫です)

## 2.1. クラス

```js:定義
const F = target => {
    // target: クラス
    // return target; // なくても良い
};
```

```js:適用
@F // ★
class Foo {

}
```

```js:やっていること
class Foo {

}

Foo = F(Foo) || Foo; // ★
```

## 2.2. メソッド

```js:定義
const F = (target, name, descriptor) => {
    // target: クラスの prototype (※ this の代わりに使うとハマることがある)
    // name: メソッド名
    // descriptor: メソッドのディスクリプタ
    //     descriptor.value: メソッドそのもの
    //     descriptor.writable: false なら const
    // return descriptor; // なくても良い
};
```

```js:適用
class Foo {
    @F // ★
    bar() {}
}
```

```js:やっていること
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. アクセサ

ほぼメソッドと同じ (ディスクリプタの内容が異なる) 。

```js:定義
const F = (target, name, descriptor) => {
    // target: this
    // name: メソッド名
    // descriptor: メソッドのディスクリプタ
    //     descriptor.get: getter そのもの
    //     descriptor.set: setter そのもの
    // return descriptor; // なくても良い
};
```

```js:適用
class Foo {
    @F // ★
    get bar() {}
    set bar(value) {}
}
```

```js:やっていること
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. オブジェクトのメソッド・アクセサ

オブジェクト定義の中ののメソッド・アクセサでも同様に使用することができます。

```js:適用
const Foo = {

    @F // ★
    bar() {},

    @F // ★
    get baz() {},
    set baz(value) {}

};
```

# 3. もう少し進んだ使い方

## 3.1. デコレータに引数を持たせる

デコレータを返す関数を定義する。

```js:定義
const F = param => (target, name, descriptor) => {
    // ...
};
```

```js:適用
class Foo {
    @F('Param') // ★ F('Param') == (target, name, descriptor) => {/* ... */}
    bar() {}
}
```

## 3.2. 複数のデコレータを同時に適用する

適用したいクラス・メソッド・アクセサの宣言に近いものから順に適用されます。

```js:適用
class Foo {

    // ★ F → G → H
    @H
    @G
    @F
    bar() {}

}
```

# 4. 具体的な使用例

## 4.1. `@readonly`

どの場面でも使えそうだったので具体例として挙げてみました。

```js
// 定義
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. 正しく動作する例

```js
// 定義
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();
```

```shell-session:コンソール
bar(): begin
Baz
bar(): end
```

### 4.2.2. `descriptor.value` をそのまま使えない問題

```js
// 定義
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](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Function/bind#%E6%9D%9F%E7%B8%9B%E3%81%95%E3%82%8C%E3%81%9F%E9%96%A2%E6%95%B0%E3%82%92%E7%94%9F%E6%88%90%E3%81%99%E3%82%8B)」

### 4.2.3. `bind(target)` しても正しく動作しない問題

**`target` == `this` ではありません。**

```js
// 定義
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'
	}

}
```

```js:prototype&nbsp;と&nbsp;this&nbsp;の違い
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 でないとできません。

```js:「正しく動作する例」再掲&nbsp;(引用)
// 定義
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;
		};
	}
};
```