TypeScript最大のハマりポイント:this
TypeScriptは、全体としては整然として、素直に動作します。しかし、たまに意外な落とし穴があります(自分もはまったのですが、その時のコードが残ってませんでした。すみません)。
「なぜか型がundefinedになっている」などの問題が起きるようなら、次のTypeScript公式の文書を参考にするといいでしょう。
重要なのは「メソッドを参照だけして呼び出しが後、という形式はヤバい」ということです。それをアロー関数などで回避すれば、意味不明のトラブルに見舞われる頻度は下がると思います。
なお、参考として、以下に2016年12月1日版の原文の簡易版の和訳を示します。
(※注意)訳質が怪しい部分があります。誤りに気付いた場合、コメントなどいただければ幸いです。
TypeScriptにおける「this」
導入
this
キーワードは、JavaScript(および、その帰結としてTypeScript)では、他の多くの言語の場合とは動作が異なります。このことは、特にthis
の動作について確たる直観をもつ言語の利用者からみると、とても奇妙に思えるかもしれません。
このページでは、TypeScriptにおけるthis
にまつわる問題について、いかに理解、原因解明するかを解説しようと思います。また、この問題についての解決法と、それにまつわるトレードオフについても記述します。
典型的な症状とリスク要因
this
のコンテクストが失われた場合の典型的な症状には、次のものが含まれます。
- クラス・フィールド(
this.foo
)が(他の何らかの値のはずが)undefined
になっている this
の参照先が(何らかのクラス・インスタンスのはずが)グローバルなwindow
オブジェクトである(非strictモードの場合)this
の参照先が(何らかのクラス・インスタンスのはずが)undefined
である(strictモードの場合)- クラス・メソッド(
this.doBar()
)の呼び出しが、次のようなメッセージのエラーになっている:"TypeError: undefined is not a function"、"Object doesn't support property or method 'doBar'"、"this.doBar is not a function"
これらの症状は、しばしば特定のコーディングパターンで発生します。
- イベント・リスナ 例:
window.addEventListener('click', myClass.doThing);
- Promiseの解決 例:
myPromise.then(myClass.theNextThing);
- ライブラリのイベント・コールバック 例:
$(document).ready(myClass.start);
- 関数のコールバック 例:
someArray.map(myClass.convert)
- ViewModelタイプのライブラリのクラス 例:
<div data-bind="click: myClass.doSomething">
- "option bags"中の関数 例:
$.ajax(url, { success: myClass.handleData })
JavaScriptのthis
とは?
多数の記事が、JavaScriptにおけるthis
の害について書かれてきました。それについては「thisキーワード(英語)」や「JavaScriptの"this"を明確に理解し、マスターしよう(英語)」、「thisの全て(英語・リンク切れ)」あたりを参照してください。
JavaScriptで関数が呼び出される時、次の手順で追跡すれば、this
が何になるか決定できます(以下は優先度が高いものから並べてあります)。
- 関数が
function#bind
の結果として呼び出される場合、this
はbind
の引数 - 関数が
foo.func()
といった形式で呼び出される場合、this
はfoo
- strictモードなら、
this
はundefined
- さもなくば、
this
はグローバル・オブジェクト(ブラウザならwindow
)
これらのルールの結果は、時に直観に反する挙動となります。例えば:
class Foo { x = 3; print() { console.log('x is ' + this.x); } } var f = new Foo(); f.print(); // 出力は 'x is 3'(想定通り) // オブジェクト・リテラル中でクラス・メソッドを使う場合 var z = { x: 10, p: f.print }; z.p(); // 出力は 'x is 10' var p = z.p; p(); // 出力は 'x is undefined'
this
にまつわる危険信号
最大の危険信号として頭に入れておいてほしいのは、「クラス・メソッドを使っておいて、即座に呼び出さないこと」です。もしメソッドを参照するだけで、同一の式の中で呼び出していない場合、this
の参照先は不適切になっている可能性があります。
例。
var x = new MyObject(); x.printThing(); // 安全。メソッド呼び出しが参照箇所で行われている。 var y = x.printThing; // 危険。y()の呼び出し時、'this'は想定外の可能性あり。 window.addEventListener('click', x.printThing, 10); // 危険。参照箇所と呼び出し箇所が異なる。 window.addEventListener('click', () => x.printThing(), 10); // 安全。メソッド呼び出しは同じ式の中。
修正方法
this
のコンテキストを適切に維持する方法は、いくつかあります。
インスタンス関数を使う
プロトタイプ・メソッド(JavaScriptのメソッドの扱いのデフォルト)の代わりに、インスタンス・アロー関数をクラス・メンバの定義に利用できます。
class MyClass { private status = "blah"; public run = () => { // <-- この文法に注意 alert(this.status); } } var x = new MyClass(); $(document).ready(x.run); // 安全。'run'中の'this'は常に想定通り。
- (長点または欠点)この方法は、新たなクロージャを、クラスのインスタンスのメソッドごとに生成します。通常のメソッド呼び出しにはオーバーキルな手法です。しかし、コールバックでこの方法を多用する場合、クラス・インスタンスは1回特定の
this
コンテキストをキャプチャするだけで済み、呼び出しごとにクロージャを生成する必要がなくなるからです。 - (長点)外部で呼び出す側にとっては、
this
コンテキストの喪失がありえなくなります。 - (長点)TypeScriptで使うと、タイプ・セーフなコードとなります。
- (長点)関数にパラメータが必要な場合でも、特別な措置は不要です。
- (欠点)派生クラスの場合、
super
を使っても基底クラスのメソッドが呼び出せません。 - (欠点)厳格な意味論での「事前束縛済み(pre-bound)」メソッドと、そうでないメソッドが生じることで、タイプ・セーフでない契約関係が、クラスとconsumer間に加わってしまいます([訳注]この欠点の訳質は他より低いと思います)。
ローカル・ファット・アロー
TypeScript向け(下のコードは説明用にダミーのパラメータを含みます)。
var x = new SomeClass(); someCallback((n, m) => x.doSomething(n, m));
- (長点または欠点)メモリとパフォーマンスのトレードオフ関係については、前項目(インスタンス・アロー関数)の逆になります。
- (長点)TypeScriptでは、この方法は100%タイプ・セーフです。
- (長点)ECMAScript 3でも利用できます。
- (長点)インスタンスの名前は1回入力するだけで済みます。
- (欠点)パラメータの名前は2回入力する必要があります。
- (欠点)可変長引数は使えません。
Function.bind
var x = new SomeClass(); // 安全。function.bindで関数を呼び出す場合、常に'this'を維持します。 window.setTimeout(x.someMethod.bind(x), 100);
- (長点または欠点)メモリとパフォーマンスのトレードオフ関係については、前項目(インスタンス・アロー関数)の逆になります。
- (長点)関数にパラメータが必要な場合でも、特別な措置は不要です。
- (欠点)TypeScriptの場合、現状では、型安全性は確保されません。
- (欠点)ECMAScript 5以降でしか使えません。
- (欠点)インスタンス名を2回入力する必要があります。