Javascriptは関数型がベースにあり、Webで広く利用されるようになってクライアントサイドでもサーバーサイドでもよく利用されるようになりました。
その後、おそらくは関数型概念だけでは他の言語から入ってきた開発者等からは直感的ではない等の理由から、世の中の要望に応えるためにclass等のオブジェクト指向型言語で馴染みのある記述法をECMAScript 2015(ES6)より言語仕様として取り入れてきました。
しかしながら、実はJSのclassはシンタックスシュガーであり、ES2015以前から利用されていたfunctionによるクラス風構文の置き換えにすぎず、クラスもメソッドも依然としてfunctionである事は頭の片隅に置いておかないと怪奇現象に病まされることになりかねません。
そこで今回はclassメソッドをハンドラとして利用する場合にありがちな記述方法の間違いとオススメの書き方を今回の書き方に関係するthisとbindとarrow関数を簡単におさらいしながら理解を深めていきます。
なお、サンプルコードはReactNativeのコンポーネントでボタンが押されたときのイベントハンドラをメソッドとして記述したことを例に取ります。
目次
スポンサーリンク
ES2015以降オススメの書き方
理由とかどうでも良いから書き方教えてという方の為に、まずは結論を先に書いておきます。
class MyComponent extends Component {
state = {
isShow = false;
}
onPress = () => {
this.setState({ isShow: true });
}
render() {
return (
<View>
<TouchableOpacity onPress={this.onPress}>
<Text>ボタン</Text>
</TouchableOpacity>
<Text>{this.state.isShow ? '押した' : '押してない'}</Text>
</View>
);
}
}
クラスメソッドの定義としてarrow関数を使って記述すると良いというのがこの記事の提案です。
以下、どうしてそうなったのかを考察していきます。
やってはいけない書き方
以下はバグが混入しているサンプルです:
class MyComponent extends Component {
state = {
isShow = false;
}
onPress() {
this.setState({ isShow: true });
}
render() {
return (
<View>
<TouchableOpacity onPress={this.onPress}>
<Text>ボタン</Text>
</TouchableOpacity>
<Text>{this.state.isShow ? '押した' : '押してない'}</Text>
</View>
);
}
}
普通にonPress()でメソッド定義され、onPressに関数ハンドラそのメソッドthis.onPress
を登録しています。
一見、なんの問題も無さそうに見えますが、setState()
呼び出し時に例外が発生したり、this.state.isShow
が状態変化しなかったりします。
ここでJSのclassはシンタックスシュガーであることを思い出して下さい。this.onPress
はJSではインスタンスメソッドでは無くfunctionです。
onPressで呼びされたハンドラ内、つまり、onPress()
で記述されたthis.setState()
のthis
はイベント呼び出し元の関数スコープにおけるthis
となってMyComponentのインスタンスではないということです。
正しく挙動する書き方案1
ここからはよく見かけるハンドラ記述案を見ていきます。先ずはbind
を利用した方法です:
class MyComponent extends Component {
state = {
isShow = false;
}
onPress() {
this.setState({ isShow: true });
}
render() {
return (
<View>
<TouchableOpacity onPress={this.onPress.bind(this)}>
<Text>ボタン</Text>
</TouchableOpacity>
<Text>{this.state.isShow ? '押した' : '押してない'}</Text>
</View>
);
}
}
func.bind(obj)
は関数funcにobjを結びつけた関数を生成して返します。
サンプルコードのthis.onPress.bind(this)
はMyComponent生成時のインスタンスthis
に結びつけた関数をonPress
イベントハンドラとして登録しているので、イベント呼び出された先のthis
はMyComponentインスタンスのものとなり正しく動作します。
巷のドキュメントサンプルコード等でもこの記述方法が書かれていたりするのでこれでも問題はありません。
しかしながら、親コンポーネントからイベントハンドラ登録するときなどに子コンポーネントがbind(this)
で記述されているかを確認しなければならない等デメリットがあります。
正しく挙動する書き方案2
次はアロー関数を使った記述案です。
class MyComponent extends Component {
state = {
isShow = false;
}
onPress() {
this.setState({ isShow: true });
}
render() {
return (
<View>
<TouchableOpacity onPress={() => { this.onPress(); }}>
<Text>ボタン</Text>
</TouchableOpacity>
<Text>{this.state.isShow ? '押した' : '押してない'}</Text>
</View>
);
}
}
アロー関数(Arrow function)はES2015(ES6)で追加された言語仕様で、無名関数(function () { ... }
)を置き換えることが出来ます。但し、無名関数と違って関数内のthisを置き換えないで定義側の関数スコープで動作します。
従って、アロー関数内で呼び出されたthis.onPress()
のthis
はMyComponentインスタンスで関数が呼び出されているのでonPress()
メソッド内のthis
もMyComponentインスタンスとなって正しく動作します。
こちらの書き方も動作上問題ありませんが、呼び出し側が呼び出し元の記述方法を気にする必要があります。
正しく挙動する書き方案3(ES2015以降オススメ)
最初に書いた結論のコードをもう一度見てみます。
class MyComponent extends Component {
state = {
isShow = false;
}
onPress = () => {
this.setState({ isShow: true });
}
render() {
return (
<View>
<TouchableOpacity onPress={this.onPress}>
<Text>ボタン</Text>
</TouchableOpacity>
<Text>{this.state.isShow ? '押した' : '押してない'}</Text>
</View>
);
}
}
インスタンスメソッド側をアロー関数で定義してイベントハンドラを登録しています。
こうすることによって、イベントハンドラ呼び出し側がメソッド登録、bind付き登録、アロー関数呼び出し登録といったどのような書き方をしていても影響を受けずにMyComponentインスタンスでonPressメソッドが呼び出されて正しく動作します。
まとめ
いかがだったでしょうか。
このようなことを考えると個人的にはES2015でのclassメソッドのシンタックスシュガーは最初からアロー関数の置き換え、つまり、class内で利用するメソッドのthisはクラスインスタンスとすれば良かったのでは無いかと思いますが、JSは関数型言語なので譲れなかった部分もあるかもしれません。
なんにせよ、ES2015でクラスやアロー関数が記述出来る様になったのはとても便利なので、JS特有の性質も理解しながら上手く付き合って行きたいものですね 😉