thisとアロー関数の関係

f:id:matsuihopen8:20201225202937p:plain

Video BRAINのフロントエンドを担当している松井です。もうすっかり寒くなりました、在宅勤務になってから初めての冬ですが、朝晩の寒い中、外に出て電車に乗らなくていいのはすごい楽ですね。

さて今回の話は、フロントエンド開発をしていて、Componentにコールバック関数を渡すときにアロー関数とFunction関数を使い分ける理由が何となくフワーっとした理解のままで気持ち悪かったので、はっきりさせようと色々調べたり、教わった事をまとめてみました。

まずこの2つのClassを見てください。

どちらもClick Meというボタンを描画するだけのシンプルなClassで、handleClickという名前のコールバック関数が渡されてます。(*便宜上Constructor等は省いてます)

1つ目はhandleClickがアロー関数
class Button {
  handleClick = () => {
    this.setState({ hoge: xxxx })
  }
  render() {
    return (
      <button onClick={this.handleClick}>
        Click me
      </button>
    );
  }
}
2つ目はhandleClickがFunction関数
class Button {
  handleClick() {
    console.log('hogehoge');
  }
  render() {
    return (
      <button onClick={this.handleClick}>
        Click me
      </button>
    );
  }
}

全く同じ構造のClassでなぜ1つ目はアロー関数で書かないといけなくて、2つ目はFunction関数でいいのか?

その答えの鍵は「this」にあったのです。

thisは奥が深いので完璧に理解することは厳しいと言われてますし、完璧に理解したからコードがすごい書けるようになる訳でもないと言われています。英語で言うところの「the」を完璧に理解したから凄い英語力が上がる訳ではないのと似てる気がしますね。

でも今回、アロー関数を理解する上ではthisは重要な鍵を握っていたので改めてthisを見直してみました。

そもそもthisとは?

thisは、読み取り専用のグローバル変数のようなものでどこからでも参照可能。 基本的にはthisを使うと、参照先はその呼び出し元のオブジェクトになりますが、問題は、thisの参照先(評価結果)が呼び出すタイミングや場所によって変わってしまうという事です。。(変数なので)

thisの参照先は主に次の条件によって変化します。

・ 実行コンテキストにおけるthis
・ コンストラクタにおけるthis
・ 関数とメソッドにおけるthis
・ Arrow Functionにおけるthis

JavaScript Primerから引用。

ちょっと言葉だけではピンとこないですね。サンプルコードを見ていきましょう。

class App {
  constructor() {
    super();
    this.state = {
      title: ""
    };
  }
  handleChangeTitle(e) {
    const title = e.target.value;
    this.setState({
      title
    });
  }
  render() {
    return (
      <input
        type="text"
        onChange={this.handleChangeTitle}
      />
    );
  }
}

テキスト入力するinputタグがあり、入力が発生するたびにhandleChangeTitleというコールバックが呼ばれ、ローカルStateに内容を保存するという単純なClassです。

一見問題なさそうですが、入力をするとUncaught TypeError: Cannot read property 'setState' of undefinedというエラーが出てしまいます。

なぜこれでエラーになるのか??

まず、inputタグにコールバック関数を渡す際に、Reactでは裏側で以下のコードと同じ事が行われています。

target.addEventListener('change', function(e){
  this.handleChangeTitle(e);
});

addEventListener関数は、イベント発生時、第2引数の無名関数を呼ぶ時に、その中で使われるthisをイベント発生元の要素(この場合input)に束縛してしまう特性がある為です。

(注)consoleで確認しようとしてもReactの仕様上、undefinedとしか出ませんが、以下のようなPlain JSだと確認できます。

const inputBox = document.createElement('input')
inputBox.addEventListener("change", function(){
  console.log(this)
  // 出力結果は <input type="text" id="inputBox" />
})

thisが参照しているのはInputである為、InputにはsetStateなんてないのでエラーになってしまうのです。

f:id:matsuihopen8:20201225202624p:plain

こうしたthisの挙動を解決するため、アロー関数が導入された。

以下、MDNから引用。

2つの理由から、アロー関数が導入されました、

1つ目の理由は関数を短く書きたいということ、

2つ目の理由は this を束縛したくないということです。

アロー関数自身は this を持ちません。 レキシカルスコープの this 値を使います。つまり、アロー関数内の this 値は通常の変数検索ルールに従います。 このためスコープに this 値がない場合、その一つ外側のスコープで this 値を探します。

すなわち、アロー関数は、関数が定義された時の環境のthisで確定させて変更させない、とも解釈できます。

ただ関数を短く書けるというだけはなかったのです!

先ほどのPlain JSをアロー関数に変えてみると、出力結果はInputではなく、一つ外側のWindowオブジェクトになります。

const inputBox = document.createElement('input')
inputBox.addEventListener("change", () => {
  console.log(this)
  // 出力結果は、Windowオブジェクト
})

ちなみに、アロー関数以前は、bindを当てるという方法がありました。

先ほどのPlain JSにbind関数を当てると、こちらもthisの参照は一つ外側のWindowオブジェクトになります。

const inputBox = document.createElement('input')
inputBox.addEventListener("change", function(){
  console.log(this)
  // 出力結果は、Windowオブジェクト
}.bind(this))

という訳で、最初のサンプルコードもアロー関数にすると、thisの参照が呼び出し元のオブジェクトであるAppインスタンスになり、handleChangeTitleでsetStateがないぞ、というエラーも出ずに正常に動作します。 f:id:matsuihopen8:20201225202642p:plain

以上の事を踏まえて、アロー関数とFunction関数を使い分ける理由って何なのか?

大きな理由としては関数の処理の中で、thisを束縛したいかどうか?にあると思います。

冒頭でお見せした2つのコードの違い、アロー関数の方はthisを使うので、束縛しないといけない、Fucntion関数の方はそうでない。

  handleClick = () => {
    this.setState({ hoge: xxxx })
  }
  handleClick() {
    console.log('hogehoge');
  }

パフォーマンスへの影響

コールバック内でアロー関数を使っても問題なく動作はできるのですが、React公式ドキュメントにはこう書かれてます。

コールバック内でアロー関数を使ってしまうと、レンダーされるたびに異なるコールバック関数が毎回作成されて、パフォーマンスに影響するので避けるべき。 参考:React公式ドキュメント

つまり、何気なくこんな感じでコールバックを書いてしまいがちだったのですが、、

class Button {
  handleClick() {
    this.setState({ hoge: xxxx })
  }
  render() {
    return (
      <button onClick={() => this.handleClick()}>
        Click me
      </button>
    );
  }
}

//もしくは外に処理を出さず、JSXの中に直接処理を書いたり。
class Button {
  render() {
    return (
      <button onClick={() => this.setState({ hoge: xxxx })}>
        Click me
      </button>
    );
  }
}

大抵のケースでは問題ないのですが、このコールバックが props の一部として下層のコンポーネントに渡される場合、それら下層コンポーネントが余分に再描画されることになります。

なので、この様にクラスフィールドを利用して書くのが一番良いパターンと言う事ですね。

class Button {
  handleClick = () => {
    this.setState({ hoge: xxxx })
  }
  render() {
    return (
      <button onClick={this.handleClick}>
        Click me
      </button>
    );
  }
}

まとめ

アロー関数って関数を短くシンプルに書けるだけと思ってましたが、thisにも絡んでいたのは大きな学びになりました。 完璧に理解したとは言い難いですが、苦手意識はなくなりました。

また今回みたいにフロント開発で学んだJavaScriptの小ネタを題材に書いてみようと思います。