@ 古曳 純基
どうもどうも、じゅんきです。
今回は、エンジニアを悩ませるthisの挙動について僕なりの理解をまとめてみました。
それでは早速いってみよう
まず最初にメンタルモデルを作っておきます。
この記事で重要なのは、以下の3点です。
もっと詳しくいうと、thisは「関数が実行されるコンテキスト(前後関係、文脈)であるオブジェクトへの参照が格納されている暗黙の引数」のことです。
重要なのは、
・関数が実行された際にその関数が参照しているオブジェクトは前後関係によって決まること
・JavaScriptにおいて、このthisは暗黙的な引数であること
です。
詳しいことは本文で話しますが、概要をまとめると以下のようになります。
1.new演算子によって新規生成されるオブジェクト
2.メソッド実行時、アクセス演算子の直前にあるオブジェクト
3.非Strictモードにおけるグローバル空間でのthis = グローバルオブジェクト
4.Strictモードにおけるグローバル空間でのthis = undefined
本文で紹介しますが、thisには結構やばい挙動があります。
でも安心してください。以下に示す2つのルールを守ればきっと大丈夫でしょう。
ルール1:this はクラス構文内での使用に限定
ルール2:クラス構文内ではメソッドも、メソッド内で定義する関数もアロー関数を使用する
まず、そもそもthisってなんぞやってところから話していきます。
関数が実行されたコンテキストのオブジェクトへの参照を暗黙の引数としてもちますが、その「暗黙の引数」のことをここでthisと呼んでいます。JavaScriptにおいて、全ての関数はthisを持つことができ、場合によっては危険な挙動をします。
今回はPythonとJSにおける実行コンテキストのオブジェクトの取り扱いについて比較をしてみたいと思います。
まず、Pythonのコードを以下に示します。
#クラスの定義
class Person:
#コンストラクタの定義
def __init__( self , name ):
self.name = name
#メソッドの定義
def introduce(self):
print("My name is " + self.name + " !")
#クラスのインスタンス生成
person1 = Person("Junki")
#メソッドの実行
person1.introduce()
#My name is Junki !
上から4行目のコンストラクタ(初期化関数)の部分を見てもらうとわかりますが、selfという引数によって実行コンテキストのオブジェクトを明示的に受け取っていることがわかります。
次にJavaScriptのコードを以下に示します。
//クラスの定義
class Person{
constructor(name){
this.name = name;
}
//メソッドの定義
introduce(){
console.log("My name is " + this.name + " !");
}
}
//インスタンスの生成
person1 = new Person(Junki);
person1.introduce(); //My name is Junki !
コンストラクタとintroduce()メソッドの定義の部分に注目してください。JSではPythonのように明示的に実行コンテキストのオブジェクトを受けるような記述がありません。
ですが、person1.introdece()によって実行はうまくいっているので、メソッドは実行コンテキストのオブジェクトを引数として暗黙的に受け取ることができていると言えます。
もう少し深掘りしてthisがなんなのかを見ていきましょう。
const Person = function(name){
this.name = name;
return this; //thisを返す
}
//callによってfunction Personのthisに{age: 100}というオブジェクトをセットする
const taro = Person.call({age: 100},' Taro ');
console.log(taro); //{ age : 100 , name : taro }
「コードは簡単だから理解できるよ。んで、何が言いたいん?」って思ったかもしれません。
この例でわかることはthisの正体は実行コンテキストのオブジェクトであるということです。
JSのコードを2つ紹介しましたが、結局this とは
・暗黙の引数である
・関数が実行された際にその関数が参照しているオブジェクトは前後関係によって決まる
ということです。
ここからはthisの中身について深ぼっていくっ!
まず、new演算子はコンストラクタ関数を呼ぶときに使用するものでした。この辺りの話は爲西さんが記事を書いているのでぜひ参考にしてみてください。(https://unblog.unreact.jp/blog/msq-2n8fs8_w)
実はこのnew演算子ですが、JSにおいて全ての関数に適用可能なんです。
関数の呼び出しにnewをつけると以下のようなことが起きます。
1. newがつけられた関数のprototypeオブジェクトをコピーして新規オブジェクトを作成
2.作成したオブジェクトを関数の暗黙の引数 thisに渡す
3.関数がreturn this で終わってなければreturn thisを実行する
const func1 = function(){ console.log( this ) }
const obj1 = new func1() // ここで上記の1,2,3 が実行される
if( obj1 !== func1.prototype ){
console.log("true");
}//true → obj1 と func1 のオブジェクトは違うメモリに格納されている(なぜなら、obj1は新規オブジェクトだから)
prototypeオブジェクトなど難しい単語が出てきました。これはプロトタイプチェーンという概念も関わってくるので、説明は他の記事でします(多分、これだけでも1記事できると思います)。
要するに何が言いたいのっていうと、「new演算子を関数に対して使うと新規オブジェクトが生成され、関数の暗黙の引数thisにぶち込んでる」ってことです。
メソッドとは、クラスやオブジェクトのなかで定義されている関数のことですね。オブジェクトは状態と動作をもちますが、メソッドはオブジェクトの動作にあたります。これも例を見た方が早いので以下のコードを見てみてください。
const person = {
name : "Taro",
showObj(){
console.log(this);
}
};
person.showObj(); //{ name : "Taro", showObj : [Function: showObj] }
このようにthisにはメソッドのアクセス演算子の直前にあるオブジェクトが代入されています。
グローバルな空間で関数を実行したとき、グローバルオブジェクトを暗黙の引数として受け取る性質があります。
グローバルオブジェクトとは、ブラウザ環境であればwindowオブジェクトのことです。先程からコード内でちょくちょく使ってるconsole.log()メソッドですが、実はこれwindowオブジェクトのなかで定義されてる関数なんです。
this がグローバルオブジェクトになり得るということは、こういったブラウザ環境そのものに定義されている関数や値を書き換えてしまうことができます(グローバル汚染)。これって危険ですよね。
(3)の説明でthisがグローバルオブジェクトになり得ることの危険性がわかったところで、Strictモードについて説明します。
Strictモードとは、ES2015で追加された機能です。関数がメソッドでない場合、つまり任意のオブジェクトのコンテキストにない場合にthisにundefinedが入るようになっています。こうすることで、thisがグローバルオブジェクトになることがないので、グローバル汚染を防ぐことができます。
素晴らしい!
ですが、これは言い換えてみれば「thisは実行コンテキストのオブジェクト」という一貫性を捨てた代わりに安全性を確保したとも言えます。
何かを得るためには何かを捨てる必要があるんですね。。。
メソッドの中で関数が実行されているというケースを考えてみましょう。
//クラスの定義
class Person {
//コンストラクタの定義
constructor( name ){
this.name = name;
}
//メソッドの定義
introduce(){
//メソッド内で関数の実行
const introduction = function(){
console.log("My name is " + this.name " !");
};
introduction();
}
}
const person1 = new Person("Junki");
person1.introduce();//エラーをはく
一見、ちゃんと実行されそうですよね。これがthisの落とし穴なんです。
メソッド内で実行されている関数はただの関数であるため、オブジェクトのコンテキストにありません。そして、Strictモードを有効としている場合は、任意のオブジェクトのコンテキストにない関数はthisにundefinedが入ります。undefinedにアクセスしたためエラーをはいたということになります。
対処法をコードとともに紹介します。お楽しみください。
class Person {
constructor(name){
this.name = name;
}
introduce(){
//メソッド内で関数の実行
const introduction = function(){
console.log("My name is " + this.name " !");
};
const bindIntroduction = introduction.bind(this); //関数にthisを拘束してみた
bindIntroduction();
}
}
class Person {
constructor(name){
this.name = name;
}
introduce(){
//メソッド内で関数の実行
const introduction = function(){
console.log("My name is " + this.name " !");
};
introdution.call(this);//callで実行コンテキストのオブジェクトに対して関数を実行している
}
}
class Person {
constructor(name){
this.name = name;
}
introduce(){
const this1 = this ; // this1にthisを格納している
//メソッド内で関数の実行
const introduction = function(){
//thisを関数定義の前にメモリにストックしておくことで実行コンテキストのオブジェクトを受け取る
console.log("My name is " + this1.name " !");
};
introduction();
}
}
アロー関数はES2015に追加された機能です。こいつの凄いところは、暗黙の引数thisを持たない点です。関数内でthisを使った場合、関数スコープ外のthisを参照してくれます。
また、アロー関数でメソッドを定義したときは、さっきの話からすると関数スコープ外のthisを参照するから、オブジェクトのコンテキストから外れてしまうのではと思うかもしれません。ですが心配ありません。アロー関数でクラスのメソッドが定義された時点で、裏側では(3)で紹介した一時的な変数によってthisを移し替えが発生するようなので、正常に動作します。
class Person {
constructor(name){
this.name = name;
}
introduce = () => {
const introduction = () => {
console.log("My name is " + this1.name " !");
}
introduction();
}
}
thisの挙動を色々紹介してきましたが、これから紹介する二つのルールを守ることによってthisのやばい挙動は防ぐことができるようです。
1.thisはクラス構文内でしか使わない(グローバル汚染を防ぐ観点から)
2.クラス構文内ではメソッドもメソッド内の関数もアロー関数によって記述する
ここまで読んで下さった方、本当にお疲れ様です。正直、僕自身が完全にthisの挙動を理解しているのかというとそういうわけではないので、各々情報の多様化をしてください。
それではさらば!
・https://qiita.com/takkyun/items/c6e2f2cf25327299cf03
・大岡由佳(2020.12.26)『リアクト!TypeScriptで始めるつらくないReact開発 第3.1版【Ⅰ . 言語・環境編】』pp94-106 くるみ割り書房
@ 古曳 純基