JavaScriptの this ってオブジェクト指向と相性が悪い・・・
最近JSでクラス(class)を使っていて、そう感じる場面がありました。
そこでJavaScriptでクラスを使う場合、
コールバック内でクラス(class)のthisを参照する方法 をまとめておきます。
これは気を付けてないと重大なバグの温床になるかもしれません(怖)
このページの目次
問題に遭遇したのはクラスを作っていた時・・・
昔のJavaScriptだと クラス なんて高尚なものは使えなかったです。
せいぜい prototype で疑似的なクラスを再現する程度みたいな感じ
でもES2016から class が正式にサポートされてます。これは大きな進展
▼ あと各ブラウザでの対応状況
モダンブラウザでは確実に使えるうえ、モバイルでも対応してるブラウザは多いです。
それで困ったのはクラス内でコールバックを使った時
なんとコールバック内で this を呼び出すと エラー になってしまいました。
▼ その時のコードを簡略化したもの
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Hoge{ constructor(){ window.addEventListener('load', function(){ /// この中でクラス関数呼び出す this.doAfterLoaded(); }); } doAfterLoaded(){ alert('Page is loaded'); } } var hoge = new Hoge(); |
▼ そうするとこんなエラーが・・・
1 |
example.html:58 Uncaught TypeError: this.doAfterLoaded is not a function at... |
this.doAfterLoaded is not a function、、、
ちゃんと this を付けて呼び出してるし、ましてや未定義なんてこともありません。
実を言うと、僕はこんな思い込みしてしまってました。
「クラス内なら this は無条件でクラスのものになるはず」
残念ながらそうならないのが JavaScript の厄介さ
他のオブジェクト指向言語になれるとこんな風に思いがちなんですが、JavaScriptの this はオブジェクト思考専用のキーワードじゃなかったんです。
JavaScriptの this は文脈で全然別物になる
注意しないといけないのは次の点
JavaScriptでの this はクラス専用のキーワードじゃないこと
まあJSに慣れてる人ならこれは当然の認識かもしれないですけどね。
コンテクスト(文脈)によって全く別のオブジェクトになります。当然クラス内で呼び出したからと言って、無条件にクラス自身のインスタンスを指すことにはなりません。
たとえば addEventListener のコールバック内の場合・・・
その中での this は要素への参照を指すことになるって説明されてます。
▼ リファレンスでの解説
addEventListener() を使って要素にハンドラー関数を設定したとき、ハンドラーの中の this の値は要素への参照となります。これはハンドラーに渡された event 引数の currentTarget プロパティの値と同じです。
引用元 : https://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListener
この説明の "ハンドラー" とは "コールバック" のこと
この説明の通り、 addEventListener 内での this はイベントの起きた要素を指すらしいです。
あと
addEventListener を使ってない場合、
this が指し示すコンテキストは
window を指すという点にも注意が必要ですね。
▼ どういうことかと言うと、こういうこと
1 2 3 4 5 6 7 8 9 10 |
this.hoge = 2; this.fuga = 4; this.piyo = 8; console.log('this.hoge : ', window.hoge); /// => this.hoge : 2 console.log('this.fuga : ', window.fuga); /// => this.fuga : 4 console.log('this.piyo : ', window.piyo); /// => this.piyo : 8 |
こういう風にJavaScriptでは this がクラス専用のキーワードという "常識" は通用しません。
クラス内コールバックで class の this を参照する方法
ではどうすれば class の this を参照できるのか・・・
手軽な解決策としては次の2つ
- コールバックに bindメソッド で指定方法
- コールバックに アロー関数 を渡す方法
それぞれをコード例で示すなら、次のようにして参照可能です。
1.コールバックにbindメソッドで明示的に指定する方法
まず1つめの方法は・・・
bindメソッドから this のコンテクストを明示指定すること
▼ MDNでのbindメソッドについての解説
bind() メソッドは新しい関数を生成し、その関数が呼び出された時の一連の引数の前に、提供された値が設定された this キーワードが追加されて呼び出されます。
コールバックは関数なので、直接 bind にクラスの this を渡せば解決という訳です。
実際にクラスの this をコールバック内で使うコード例は次の通り
▼ こんなコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Hoge{ constructor(){ window.addEventListener('load', function(){ /// この中でクラス関数呼び出す this.doAfterLoaded(); }.bind(this)); } doAfterLoaded(){ alert('Page is loaded'); } } var hoge = new Hoge(); |
ハイライトした6行目に注目!
ここでコールバックに対して bindメソッドから this を指定してます。
▼ 今度はちゃんとアラートが表示された
コールバックに直接 渡せるので、コード的にもこっちの方が分かりやすいかも
他の情報源だと apply とか call も使えるらしいですが、自分はコチラの方法を使ってます。
2.コールバックとしてアロー関数を渡す方法
2つめはコールバックにアロー関数を使う方法
▼ アロー関数とは何なのかを3行で
アロー関数式は、より短く記述できる、通常の function 式の代替構文です。
また、this, arguments, super, new.target を束縛しません。
アロー関数式は、メソッドでない関数に最適で、コンストラクタとして使うことはできません。引用元 : アロー関数 - JavaScript | MDN
この説明だと メソッドでない関数 = コールバック で使うのが最適と読み取れます。
もちろん bindメソッド でも似たことはできるんですが、
アロー関数の方がもっともっとスマートにコードが書けるのが利点です。
たとえば addEventListener の場合、次のように書けばOK
▼ こんなコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Hoge{ constructor(){ window.addEventListener('load', (e) => { /// この中でクラス関数呼び出す this.doAfterLoaded(); console.log('event type : ', e.type); /// event type : load }); } doAfterLoaded(){ alert('Page is loaded'); } } var hoge = new Hoge(); |
先ほどの説明の通りアロー関数は this を束縛しません。
だから this を渡さなくても、そのままクラスの this が取得できるってことです。
もちろん同じことは jQuery の onメソッド でも可能
ちなみにアロー関数の対応状況は Can i use を参照のこと
▼ アロー関数の各ブラウザの対応状況
残念ながら(?)IEでは一切使えない模様・・・
でもモダンブラウザや主要なモバイルブラウザなら使えるので問題なし。
(でも "なぜか" 日本だと IEのシェア率は 10%以上 あるのが厄介。もう今2019年だよ)
(もういい加減 IEシェアは 1% 切ってくれた方がいい気がする・・・)
ここまでのまとめ
コールバックでクラス this を参照する方法をまとめておくと・・・
どちらを使っても同じですが、古いブラウザを気にしないなら2番目ですね。
重大なバグを残さないように明示指定するように気を付けたいです。ではではまた