クラスコンポーネントでイベントハンドラを設定する際に必要になる、constructor
メソッドでのバインド。
そもそもこれはなぜ必要なのか。どのような動作をすることで、どのような問題を解決しているのか。
Hooks が登場したことで今後はクラスコンポーネントを書く機会は減っていくと思うが、これは React 固有の話ではなく JavaScript の言語仕様の話なので、理解しておくに越したことはない。
React のコードは React のv16.10.2
で、それ以外のコードは Node.js のv12.12.0
で動作確認している。
問題のおさらい
bind
しないと何が起こるのか。
以下は、ボタンを押すたびにオンオフが切り替わるコンポーネント。
import React from 'react'; class App extends React.Component { constructor() { super(); this.state = {isOn: true}; this.toggle = this.toggle.bind(this); // この行をコメントアウトすると...? } toggle() { this.setState(state => ({ isOn: !state.isOn, })); } render() { const {state, toggle} = this; return ( <> <h1>{state.isOn ? 'ON' : 'OFF'}</h1> <button type="button" onClick={toggle}> click me </button> </> ); } } export default App;
constructor
内のthis.toggle = this.toggle.bind(this);
をコメントアウトした状態でボタンを押すと、以下のエラーが出る。
Cannot read property 'setState' of undefined at toggle
toggle
内のthis
がundefined
になってしまい、undefined.setState
にアクセスしたことでエラーになっている。
つまり、イベントハンドラに設定したメソッドのなかで記述したthis
が、意図したものとは違うものを指してしまっている。bind
を使うことでそれを回避している。
この現象を理解するために、まずクラスについて見ていく。
次に、this
を見失ってしまう問題と、それを解決するbind
やアロー関数について説明する。
クラスにおける this と プロトタイプチェーン
クラス定義のなかで書かれたthis
は、そのクラスのインスタンスを指す。
なのでthis.x
と書けば、そのインスタンスが持っているプロパティx
にアクセスできる。
class MyClass { constructor() { this.x = { value: 1, }; } sum(arg) { return this.x.value + arg; } } const instanceFoo = new MyClass(); console.log(instanceFoo.x); // { value: 1 } console.log(instanceFoo.sum(3)); // 4
constructor
で定義したプロパティはインスタンスのプロパティである。
一方、メソッドについては通常、プロトタイプメソッドとして定義する。MyClass
の場合はx
はインスタンス毎に保持し、sum
はプロトタイプに格納され、全てのインスタンスがそれを参照する。
class MyClass { constructor() { this.x = { value: 1, }; } sum(arg) { return this.x.value + arg; } } const instanceFoo = new MyClass(); const instanceBar = new MyClass(); console.log(instanceFoo.x !== instanceBar.x); // true console.log(instanceFoo.sum === instanceBar.sum); // true console.log(Object.getOwnPropertyNames(instanceFoo)); // [ 'x' ] console.log(Object.getOwnPropertyNames(MyClass.prototype)); // [ 'constructor', 'sum' ]
このことはつまり、instanceFoo.sum
を呼び出したときに、プロトタイプチェーンが発生していることを意味する。
そのため、constructor
にthis.sum
を定義することでオーバーライドすることが可能である。
class MyClass { constructor() { this.x = { value: 1, }; this.sum = () => 'override'; } sum(arg) { return this.x.value + arg; } } const instanceFoo = new MyClass(); console.log(instanceFoo.sum(3)); // "override" console.log(Object.getOwnPropertyNames(instanceFoo)); // [ 'x', 'sum' ]
instanceFoo
自身がsum
を持っているためそれが実行され、MyClass.prototype.sum
は実行されない。
関数を入れ子にすると this を見失ってしまう問題とその解決法
JavaScript のthis
の闇の深さはよく知られているが、この記事を理解する上で重要なのは「関数を入れ子にするとthis
の値が変わってしまう」ということである。
// 入れ子の this が何を指すかは実行環境による // 以下は Node.js v12.12.0 の場合 global.x = 9; const obj = { x: 1, func() { const nestFunc = function() { console.log(this.x); }; console.log(this.x); // 1 nestFunc(); // 9 }, }; obj.func();
this
が何に変わってしまうかは実行環境によって異なるのだが、重要なのは、this
が変わってしまうということ。これによって様々な問題が発生しうることは容易に想像がつく。
この問題を解決するための代表的な手段として、bind
とアロー関数がある。
bind
はFunction.prototype
のメソッドで、そのため全ての関数が使うことが出来る。
console.log(Function.prototype.hasOwnProperty('bind')); // true
関数を返す関数であり、第一引数に渡した値でthis
を固定した関数を、返してくれる。
function showThis() { console.log(this); } showThis.bind(1)(); // [Number: 1] showThis.bind({value: 5})(); // { value: 5 }
先程の入れ子のケースでは、入れ子になっていない(this
が本来の値を指している)場所でbind(this)
とすることで、this
の値が変わってしまうのを防ぐことが出来る。
global.x = 9; const obj = { x: 1, func() { const nestFunc = function() { console.log(this.x); }; const bindedFunc = nestFunc.bind(this); // bind(this) の this は入れ子になっていないので obj を指す console.log(this.x); // 1 nestFunc(); // 9 bindedFunc(); // 1 const nestObj = { x: 2, nestObjFunc() { console.log(this.x); }, bindedFunc, }; nestObj.nestObjFunc(); // 2 nestObj.bindedFunc(); // 1 }, }; obj.func();
アロー関数を使うことでも、this
を固定することが出来る。アロー関数のなかのthis
は、定義したときの場所におけるthis
で固定され、変化しない。
global.x = 9; const obj = { x: 1, func() { const nestFunc = () => { console.log(this.x); }; console.log(this.x); // 1 nestFunc(); // 1 const nestObj = { x: 2, nestObjFunc() { console.log(this.x); }, nestFunc, }; nestObj.nestObjFunc(); // 2 nestObj.nestFunc(); // 1 }, }; obj.func();
クラス定義での this の取り扱い
ここまでの内容を踏まえて、クラスにおけるthis
の問題を見ていく。
既述の通りクラス定義のなかのthis
はインスタンスを指すが、関数を入れ子にするとやはり、this
の値は変わってしまう。
先程の例ではグローバルオブジェクトに変化していたが、クラス定義ではundefined
に変化する。
class MyClass { constructor() { this.x = { value: 1, }; } methodA(callback) { console.log(this); console.log(this instanceof MyClass); callback(); } methodB() { console.log(this); } } const instanceFoo = new MyClass(); // MyClass { x: { value: 1 } } // true // undefined instanceFoo.methodA(instanceFoo.methodB);
イベントハンドラに設定したメソッドのなかでthis
がundefined
になってしまうクラスコンポーネントの問題も、同じ原因で発生している。
簡易な疑似コンポーネントのようなコードでこの問題を表現すると、以下のようになる。
class ButtonClass { setState() { console.log('called setState'); } click(callback) { // click イベントに共通の処理をここで行う // それ以外に、コールバックで指定された任意の処理を行う callback(); } toggle() { console.log(this); try { this.setState(); } catch (e) { console.log(e.message); } } render() { return { onClick: () => { this.click(this.toggle); }, }; } } const buttonInstance = new ButtonClass(); const button = buttonInstance.render(); // undefined // "Cannot read property 'setState' of undefined" button.onClick();
render
でオブジェクトを返すのだが、そのときにonClick
メソッドに対してクリックイベントと、コールバックで実行したいメソッド(this.toggle
)を設定している。
そのため、render
によって生成されたbutton
のonClick
を実行すると、toggle
メソッドが実行される。
だが、click
のなかでtoggle
をコールバックで実行する(click
もtoggle
もアロー関数ではない)、という入れ子構造になってしまっているため、this
がundefined
になってしまい、setState
へのアクセスに失敗してしまう。
そこで、bind
とオーバーライドの出番となる。
constructor
直下のthis
はインスタンスを指しているので、そこでbind(this)
を行えば、バインドされた関数のなかのthis
はインスタンス変数を指すことになる。
constructor() { function showThis() { console.log(this); } showThis(); // undefined console.log(this); // ButtonClass {} const bindedFunc = showThis.bind(this); bindedFunc(); // ButtonClass {} }
そして、バインドした関数でプロトタイプメソッドをオーバーライドしてしまえば、そのメソッドを使うときは必ずthis
がインスタンス変数を指すようになる。
class ButtonClass { constructor() { const bindedFunc = this.toggle.bind(this); this.toggle = bindedFunc; } setState() { console.log('called setState'); } click(callback) { // click イベントに共通の処理をここで行う // それ以外に、コールバックで指定された任意の処理を行う callback(); } toggle() { console.log(this); try { this.setState(); } catch (e) { console.log(e.message); } } render() { return { onClick: () => { this.click(this.toggle); }, }; } } const buttonInstance = new ButtonClass(); const button = buttonInstance.render(); // ButtonClass { toggle: [Function: bound toggle] } // "called setState" button.onClick();
バインドされた関数によるオーバーライド、を使わずとも、コールバック関数をアロー関数で渡すことでも解決できる。
ButtonClass
の場合はrender
メソッドを以下のように書き換えればよい。
既に述べたように、アロー関数のなかのthis
はその場所におけるthis
で固定される。
render() { return { onClick: () => { this.click(() => { this.toggle(); }); }, }; }
しかし実際に React のコンポーネントでこの手法を使うと、パフォーマンスに影響を与える可能性がある。
例えば冒頭のApp
コンポーネントのrender
を以下のようにすれば、this
を見失う問題は解決し正しく動くが、App
がレンダリングされる度にコールバック関数が新しく作られてしまう。
この例では問題にならないが、このコールバック関数が子コンポーネントのprops
として渡されている場合、不要な処理が行われてしまう可能性がある。
render() { const {state} = this; return ( <> <h1>{state.isOn ? 'ON' : 'OFF'}</h1> <button type="button" onClick={() => { this.toggle(); }} > click me </button> </> ); }
パブリッククラスフィールド
React の公式ドキュメントでは、アロー関数とbind
による解決の他に、パブリッククラスフィールドを用いた方法も紹介している。
イベント処理 – React
これは、現在策定中の新しい構文を利用したもの。
2019年11月現在でstage3
なので、まだ正式に採用されているわけではない。
tc39/proposal-class-fields: Orthogonally-informed combination of public and private fields proposals
この構文を使うと、constructor
を使わずにインスタンスのプロパティを定義できる。
class MyClass { x = { value: 1, }; } const instanceFoo = new MyClass(); console.log(instanceFoo.x); // { value: 1 } console.log(Object.getOwnPropertyNames(instanceFoo)); // [ 'x' ]
関数も定義できる。その場合は当然、インスタンスのメソッドになる。
class MyClass { x = { value: 1, }; sum = function(arg) { return this.x.value + arg; }; } const instanceFoo = new MyClass(); console.log(instanceFoo.sum(3)); // 4 console.log(Object.getOwnPropertyNames(instanceFoo)); // [ 'x', 'sum' ] console.log(Object.getOwnPropertyNames(MyClass.prototype)); // [ 'constructor' ]
アロー関数を割り当てることも出来る。何度も繰り返しているように、アロー関数でthis
を使った場合は定義時にその中身が決まるため、入れ子になっても値は変わらない。
class MyClass { x = { value: 1, }; methodA(callback) { console.log(this); console.log(this instanceof MyClass); callback(); } methodB = () => { console.log(this); }; } const instanceFoo = new MyClass(); // MyClass { x: { value: 1 }, methodB: [Function: methodB] } // true // MyClass { x: { value: 1 }, methodB: [Function: methodB] } instanceFoo.methodA(instanceFoo.methodB);
以下はこの構文を使ったクラスコンポーネント。
toggle
をアロー関数で定義している。
import React from 'react'; class App extends React.Component { constructor() { super(); this.state = {isOn: true}; } toggle = () => { this.setState(state => ({ isOn: !state.isOn, })); }; render() { const {state, toggle} = this; return ( <> <h1>{state.isOn ? 'ON' : 'OFF'}</h1> <button type="button" onClick={toggle}> click me </button> </> ); } } export default App;
まだ策定中の構文のためトランスパイルしないと使えないはずなので、注意。