JavaScript: thisと格闘する /Reactでthisの壁にぶつかったら読む記事


React触っていて突如現れる謎のキーワード: this

フロントエンジニアの皆さん,Reactは使っていますか?
Reactの勉強を初めてチュートリアルを見ていてthisの壁にぶつかったことはありませんか?
私はJavaScriptを触るのがReactで初めてだったので,基礎的な文法しか知らない状態で挑んでしまったがためにこのthisという存在に恐怖してしまいました.
コンポーネントに関数を渡す-React
↑ちょうどこの記事を読んでいて「thisわからん. JSわからん」となってしまいました.
ですが日々JSやpythonなどでオブジェクト指向プログラミングをしているうちになんとなくthisの存在を認められ、理解できるようになってきました。特にpythonではクラスを定義したときにメソッドの第一引数にJSの this にあたる self を強制的に書かされるので, pythonを勉強していたおかげでthisから逃げずに済んだと思っています。

この記事ではJavaScriptを書いていてたまに時々遭遇するthisという名のオブジェクトについてその存在やthisをバインドするってどういうことやについて解説をしたいと思います。

実行環境

この記事で示したコードは以下の環境で動作確認を行っております。

  • Node.js: 12.16.1
  • Brave: 1.3.115

コンテンツ

  1. this, お前って何者や?
  2. 書き方によってthisは別のものに変身する。
  3. アロー関数で関数は楽に書けるね。あれ? thisどこいった?

1. this, お前って何者や?

JSにおいてすべてのデータはオブジェクトであり、thisも例にもれずオブジェクトなので, オブジェクトを作ってみて, thisの正体を見てみましょう。

// オブジェクトをpersonという変数に格納します。
// オブジェクトには名前(name)を設定し、あいさつ(greet())ができるようにします。
const person = {
  name: "my_name",
  greet(){
    console.log(`My name is ${this.name}`);
//                            ^^^^ 出ました this!!
  }
}

ここでpersonオブジェクトのgreetメソッドを起動すると返り値はどうなるでしょうか?

person.greet();
// => My name is my_name

ふ~む。this.nameとしていた部分がmy_nameに置き換わったな...ということは this.name === person.name ということか?

正解です。
thisとはpersonオブジェクト本人を指しているのです。
次に実験をしてみます. greetメソッドを別の定数に割り当てるとその動作は変化するでしょうか?

const greet = person.greet;
greet():
// Browser
// => My name is
                 ^^^

// Node.js
// => My name is undefined
                 ^^^^^^^^^

???
あれ?変わってしまいました...
person.greet()が参照しているthisとgreet()が参照しているthisは違うということでしょうか??
そのとおりです。
悪名高いJavaScriptさま。実はthisは関数の宣言方法や実行環境によって変わってしまう業の深い存在なのですww

今の所thisは関数またはメソッドの中で使われていました。では次に関数という視点からthisを見てみましょう。

2. 書き方によってthisは別のものに変身する

thisと関数の関係は切っても切れないので関数のなかでthisがどのような姿をしているか調べることで, thisへの理解が深まると思います.
まず関数を宣言しましょう. ここでは単純な関数宣言によって関数を定義します。

function greet(thing){
  condole.log(`${this} says hello ${thing}`);
}

次に関数を呼び出します. 最もプリミティブな方法はFunction.prototype.callメソッドを用いることです.

Function.prototype.call ?? なにそのクソ長いメソッド怖い!!

安心してください. 関数オブジェクトが持っているメソッドのことです. JSでは何でもかんでもオブジェクトになっているので関数自体もオブジェクトなのです.
(*Functionオブジェクトの他のプロパティやメソッドは興味を持った方はこちらをご参照ください)

callメソッドは第一引数にthis値を, それ以降に通常の引数を受け取ります。
call(thisValue, arg1, arg2, ...) こんな感じですね。
出ました this(thisValueとなっていますがthisのことです)
thisは関数に引数として渡すことができるんですねー。
じゃあ早速thisを渡して関数を実行してみましょう.

callメソッド
hello.call("Solenoid", "world");
//          ^^^^^^^^
// => Solenoid says hello world
//    ^^^^^^^^

// callしなかったらどうなるん?
hello("world");
// Browserで実行した場合
// => [object Window] world
//    ^^^^^^^^^^^^^^^ ???

// Node.jsのREPLで実行した場合
// => [object global] world
//    ^^^^^^^^^^^^^^^ ???

callメソッドにthisオブジェクトを渡すことができましたね。

✅thisは関数オブジェクトのcallメソッドに渡すもの

一方、通常の呼び出し方法で関数を実行するとブラウザの場合はWindowオブジェクトが, Node.jsの場合はglobalオブジェクトが表示されていますね。(strict modeで書くとどちらの環境でもundefinedが設定されます)

これがthisの業です.
this値を設定したつもりはないのになんか勝手に設定されてますね。しかも実行環境に応じて値が変わっています。

実行環境 「おまえがthis使いたいって言ってたからつけといたわ」
わい 「え, 困る...」
実行環境 「じゃあちゃんと渡してよ」

まあ, 実際引数に直接thisを使いたいときなんてそうそうないのであんまり問題にならないんですけどねー。
うそうそ、Reactとかはthisめっちゃ出てくるから。なのでthisに何が渡されているかにしっかり注目しましょうねー。

むしろcallメソッドの場合, 第一引数は必ずthis値を設定するので, 最低でも2つ渡さないとおかしな挙動になってしまいます.
だからこそ通常の関数実行はthis値の設定を無視できるので, コードが短く、挙動も正常になりやすいですね。

おれはthis値を設定したくないんや
function hello(thing){
  console.log(`Hello ${thing}!`);
}

hello("world");
// => Hello world!

オブジェクトのメソッドの中のthis

先程thisはあまり見ないと書きましたが正確には誤りで, thisのみで見ることはあまりありませんがthisオブジェクトのプロパティにアクセスすることはJavaScriptにおいて頻繁にあります。
そもそも一番最初のコードに出てきたgreetメソッドはthis.name使ってましたよね。
そんなわけで次はメソッドの中に現れるthisです。
最初のコードをもう一度示します.

const person = {
  name: "my_name",
  greet(){
    console.log(`My name is ${this.name}`);
  }
}

person.hello();
// => My name is my_name

前述した関数の呼び出し方ではcallメソッドを使うとthisを書けて, 通常の呼び出しではthisは書けずに実行環境側で設定されていました。
上記のコードでメソッドを呼び出す際もthisは書いていませんが実はthisとしてpersonが設定されているのです。
これはレキシカルスコープというものの働きによるもので、thisを使った際はその正体を知るためにスコープを登っていき、見つかればそれが設定され、見つからずにグローバルスコープまで登りきってしまえばwindowやglobalになってしまうのです。上記のコードではthisとしてpersonが見つかったのでそれが設定されています。

person.hello() === person.hello.call(person)

this値が渡されていることがわかれば, helloメソッドのthis.nameperson.name => "my_name"ということがわかりますね. だんだんthisの気持ちが理解できていませんか?

✅メソッドが持つthisはオブジェクトそのものである.

じゃあ最大の難所であるthisのバインドに話を移りましょう.

this値を明示するためにバインドする

メソッドのthisはもとのオブジェクトそのものである. ここまでは理解できましたか?
じゃあメソッドのthisをオブジェクト以外の別の値に変更することを考えてみます。
わざわざthisを変更する必要があるのかって? あるんです実は, これがReactのコンポーネントに関数を渡す際にthisのバインドがどうのこうのに関わってくる部分なのです。少しハードですがしっかりついてきてください.

(メソッドに出てくるthis) === (オブジェクト本体)
ではメソッドを変数に割り当てて関数に変えた場合thisはどうなるのでしょうか. レキシカルスコープなんていかつい言葉は使いたくないですがちゃんと説明しようと思えばレキシカル環境を見ていく必要があります。
まあ新しい言葉を覚えるのは大変なので, 今回のコードの場合の答えだけ。thisはundefinedが設定されます。要はthis値がなくなっているのです。person.helloを渡してるんだからpersonオブジェクトになるんじゃないのかって? 残念ながら今はメソッドから関数になっているので話が違います。

const hello = person.hello;
// これは ↓ と同じことです
const hello = function(){
  console.log(`My name is ${this.name}`)
}
// じゃあ関数単体が持っているthisでなんでしたか?
// そう! globalとかwindowとかでしたね。(スコープによって実際は違いますが今回の場合はグローバルスコープですのであしからず)
// まあメソッドを渡すとstrict modeのときと同じundefinedになるんですけどねー
// fuxx!!

メソッドを変数に格納するとメソッドから関数になり、レキシカルスコープを登っていってthisが設定されます. hello関数とpersonオブジェクトは同じスコープにいるので, hello関数のthisはその上のスコープ. つまりグローバルスコープ(Window, global、undefined)まで到達してるんですねー。

メソッドを関数に変換するとthisが思わぬ挙動を取ることがわかりました。そこでthisのバインド, つまりthisを明示的に渡す必要があるのです。いくつか方法があります。見ていきましょう.

this値としては``

クロージャー

const A = {name:"a"}
const hello = function(thing){
  return person.hello.call(A, thing)
}

従来のcallメソッドにわたすやり方ですね。ただしcallメソッドはthisと通常の引数も取るので、単純にperson.hello.call(A)とすれば良いというものではありません。これでは関数が実行されその返り値がhelloに入ってしまいます。helloは関数として取り扱いたいので関数を返す関数 = クロージャー を使っています。
でもthis値を渡す程度のことにクロージャーを使いたくないですよね。というかクロージャーとか知りません!関数を返す関数って何?怖い!ってなる人が続出しそうです。このくらいthis値を設定する範囲は初心者から熟練者まで幅広いのでもっとかんたんに設定する方法としてbindメソッドが登場しました。

const hello = person.hello.bind(A)

これだけです。bindメソッドの引数にthis値を渡すだけです。あっけないですがこれで任意のthisを設定できるのです。

✅bindメソッドにthis値を渡そう

3. アロー関数で関数は楽に書けるね. あれ? thisどこいった?

さあいよいよ大詰め。アロー関数に登場するthisです。こいつをしっかり理解しないとJavaScriptを使った開発は地獄を見るでしょう。
まずアロー関数とは関数をかんたんに定義するためのシンタックスシュガーです。コンソールに"Hello World"と出力するhello関数を作ってみます。

アロー関数
const hello = () => {
  console.log("Hello World");
};

hello()
// => Hello World

アロー関数を使って関数を作れましたね。次にこれまで見てきたメソッドにアロー関数を使ってみます。

メソッドの中のアロー関数
const person = {
  name: "my_name",
  hello:()=>{
    console.log(`My name is ${this.name}`)
  }
}

person.hello()
// => My name is 
//               ^^^ ???

あれ?おかしいなー。this.nameが表示されなくなってしまいました。これはundefindとなっているのでしょう。
だけどなんで?メソッドのthisはオブジェクト本体じゃないの?
そこがアロー関数の落とし穴です。アロー関数はthisをバインドしないのです。
は?どういうこと?
アロー関数自体にthisはないってことや、だから通常はレキシカルスコープを登っていってthisを見つけに行くんやけどこれがメソッドと相性が悪い。関数として使う分には問題はないんやけどね。
アロー関数はその記述の簡便さから主にコールバック関数として利用されることが多いんです。
ですのでなんでもかんでもアロー関数で記述しないようにしようね。
そもそもメソッド自体かんたんに記述できるんだからそうしようね。

const person = {
  bye(){
    console.log("good bye")
  }
}

person.bye()
// => good bye

参考

Understanding JavaScript Function Invocation and "this"
コンポーネントに関数を渡す-React