thisがよくわからないのthis[オレ得JavaScriptメモ]

JAVASCRIPT

オレ得JavaScriptメモの…えーと何枚目だっけ?4枚目?まぁ何枚目でもいや別に数管理してないし。ごろどくですどうも。

今回は「this」について調べてまとめてみた。文書ではわかりにくいところもあるので図も使って整理してみたよ。

あいまいなthis

JavaScriptにおけるthisとはなんなのか。いろんな解説

JavaScriptの「this」は「4種類」?? – Qiita

JavaScriptのthisの覚え方 – Qiita

があるけれど、下記の一文が(わかりにくいけど)一番素直に表現している気がする。

this – JavaScript | MDN

this キーワードはコンテキストオブジェクト (カレントオブジェクト) を参照します。一般的に、メソッド内では this は呼び出し元オブジェクト (calling object)を参照します。

コンテキスト、というのは日本語に訳すと「文脈」とかそういう意味になる。thisはご存じの通り指示代名詞だ。「これ」とか「この~」と訳すよね。JS上でもこれは同じで、thisは特定の何かを指していない。実行されるときに前後の脈絡を判断して初めて意味を持つ。

次の1行コードを考えてみよう。


console.log(this);//Window

実行環境上でいきなり「これ(this)」と言っている。だからこのthisはその実行環境そのもの(グローバルオブジェクト)を指している。ブラウザで実行していればWindowオブジェクトだ。そしてWindowオブジェクトの型はobject型。

ちょっと勘違いしていたがグローバルオブジェクトとはObjectオブジェクトのことではないよ。「実行環境」という大きなオブジェクトに含まれるオブジェクト(プロパティやメソッド)のひとつとしてObjectオブジェクトが存在する。

以下は上のコードをchromeで実行したときのコンソールのキャプチャだ。これをみたらわかるんじゃなかろうか。


cinsole.logのエコーとしてWindowの後に{なんちゃらかんちゃら…}と表示されている。これ、なにかというとWindowオブジェクトの持っているプロパティやメソッドの一覧。最後が「…」で省略されているけど、エコー行をクリックすると


ツリーが展開されて、行ごとにプロパティやメソッドがリスト表示される。下の方にスクロールしていくと


Objectオブジェクトがあったね。「JavaScriptのオブジェクトは全てObjectオブジェクトの派生(前回参照)」であったけれども、JavaScriptはこのようにそれ自身を包含する実行環境そのものとその実行環境に属するものも参照できる。そして一部については操作することもできる。

だからこそJavaScriptはHTMLやCSSを動的に操作するシステムとして機能するんだよね。あまり意識されないことかもしれないけど。

これでグローバルオブジェクトがObjectオブジェクトではなくて(ブラウザで実行する場合には)Windowであることは分かったと思う。

だいぶ寄り道したが話をthisに戻そう。冒頭の1行コードの例は図にするとこうなると思う。


文字ちっちゃいですか?ですね、すみません、クリックで別窓拡大表示しますのでそちらをご覧いただければ。

次に、関数を1つ宣言してみる。


function abc(){ this.p = 56; return this.p; } console.log(this);//Window

abcを宣言しただけなので何も起こらない。図にするとこう。


abcの中に書かれているthisはこの時点でなんなのか決まっていない。一方abcの外でthisと言えばWindowであることには変わりない模様。

次にabcを実行してみる。


function abc(){
this.p = 56;
return this.p;
}
console.log(abc());//56

56が返ってきたので、thisは関数abcを指しているような気がする…のだが、本当にそうなのか?


function abc(){
this.p = 56;
return this.p;
}
console.log(abc());//56
console.log(p);//56
console.log(window.p);//56
console.log(abc.p);//undefined

ご覧の通り関数abcにpなんてプロパティはない。abcの実行によってプロパティpが追加されたのはWindowに対してだ。

つまり関数を定義した時点ではthisの意味はあいまいだが、実行された時点で初めて意味を持つ(冒頭の例と同じように)。

そして関数abcが実行されたのはグローバルオブジェクト上でのことだ。この時点でthisは意味を持ち、グローバルオブジェクトを指すことになる。これも図示してみよう。

「thisは暗黙の引数」と言われることもあるね。Windowオブジェクト上でabcを実行するとき、abc隠れ引数thisにWindowオブジェクト自身を「渡している」感じ…とでも言えばいいかな。

もうひとつ例を見てみよう。


function Abc(){
this.p = 56;
return this.p;
}
var xyz = new Abc;
console.log(xyz.p);//56
console.log(xyz);//Abc {p: 56}
console.log(window.p);//undefined

Abcはコンストラクタ(function型オブジェクト)でxyzはインスタンス(object型オブジェクト)だ。今度はpがWindowオブジェクトではなくインスタンスxyzに作られたようだ。

「コンストラクタ(関数)abcがインスタンスxyzを通じて実行されたときに、関数内のthisが初めて意味を持った。これはグローバルオブジェクトの例と同じだ。

つまり「thisを含むステートメントを(定義ではなく)実行したときにthisとして実行元のobject型オブジェクト自身を渡す」と考えていいんじゃないだろうか。

グローバルオブジェクトでthisといえばグローバルオブジェクト自身だし、インスタンスからそのモデルであるコンストラクタが参照されれば参照しているインスタンス自身を指している、ということ。


またこのときWindowを指すようなthisはインスタンス生成にに全くかかわっていない。青ラインと紫ラインは全く別の「コンテキスト」だ。冒頭の説明文で「コンテキストオブジェクト(カレントオブジェクト)」って書いてたね。

Windowオブジェクト上で直接関数を実行しているときはコンテキストオブジェクトはWindowオブジェクトだし、コンストラクタとしての関数からインスタンスを生成する場合には生成されるインスタンス自身がコンテキストオブジェクトだ。それぞれのコンテキストで注目しているオブジェクトは異なり、thisはその時注目しているオブジェクトを指してるってことだ。

ちょっと複雑な例

ということで、これらを踏まえてこんな例。


function Abc(){
this.p = 56;
this.getp = function(){
return this.p;
};
}
var xyz = new Abc;
var p = 78;
var lmn = xyz.getp;
console.log(xyz.p);//56 ----- (1)
console.log(xyz.getp());//56 ----- (2)
console.log(lmn());//78 ----- (3)
console.log(window.p);//78 ----- (4)

関数abcを宣言する。abcにはプロパティpとメソッドgetpが設定されている。プロパティpは56という数値だ。メソッドgetpはプロパティpを返せ、となっている。

これ、なんでコンソールのエコー(3)が78なるかうまく説明できる?順に見てみよう。

(1)xyzのプロパティとして

インスタンスxyzはコンストラクタabcを参照している。参照元はxyzということになる。こいつはもちろんobject型オブジェクトだ。ここでは「thisをインスタンスxyz自身」としてコンストラクタabcを実行しているのだから、プロパティ「this.p」は「xyz.p」だ。それが56としているのだからxyz.pは56だ。

(2)xyzのメソッドとして

これも(1)の場合と同様だ。プロパティがメソッドになっただけ。「this.p = ~」、「this.getp = ~」、「return this.p;」のthisはいずれもこのコンストラクタabcを参照しているインスタンスxyzだ。thisをxyzに置き換えてメソッドgetpを実行すれば当然56を返すよね。

(3)参照の参照?

「lmn = xyz.getp」はxyzとしてインスタンス化したときのgetpの結果56を返し…そうだけどそうじゃない。lmnはxyzを通じて間接的に関数abcのthis.getpの「実行方法」を参照しているだけ。参照の参照だ。

だからlmnは単なる関数だ。上のソースに書いてないけどtypeof(lmn)ってやってみたらわかるよ。

lmnを実行(lmn();)しているのはWindowオブジェクト(グローバルオブジェクト)上でのことなので、この場合のthisはWindowオブジェクトとして渡され、それに属するpプロパティの値、78を返している。

(4)オマケ

(3)の確認。Windowオブジェクトにはpというプロパティが本当にあるの?あるね。その値は78。だってその前にvarしてるもの。確認おしまい。


というか、何気なく「グローバルな変数」とか「グローバルなオブジェクト」を宣言したり定義したりしてるのは、それがどこに属しているのかこれで明確になるね。

もちろん「グローバルオブジェクト」と「グローバルなオブジェクト」は全然違う。グローバルオブジェクトは実行環境そのものであり、グローバルなオブジェクトはグローバルオブジェクトの下に設定された、どこからでも参照できるオブジェクトだ。

それは時に単なる変数(プリミティブな型のオブジェクト)であったり、object型オブジェクトであったり、function型オブジェクトであったりする。

そして、いままで散々「プロパティ」とか「メソッド」とかいう言葉を使ってきて説明は前後になったけど、何らかのオブジェクトに属するプリミティブ型オブジェクトやobject型オブジェクトは特に「プロパティ」と呼び、何らかのオブジェクトに属するfunction型オブジェクトを「メソッドと呼ぶことになっている。

だからthisは「thisを含むステートメントで構成されたメソッド(function型オブジェクト)を実行・解釈するとき、その実行起点となっているプロパティ(object型オブジェクト)」と言ってもいいかもしれない。

あと、関数の中のメソッド定義はインスタンス化されたからと言ってそのインスタンスに固定されているわけではない、ということも間接参照によって値が変わることからわかるね。

一見複雑そうに見える次のような例も(たぶん)大丈夫。


function Abc(){
this.p = 56;
this.getp = function(){
return this.p;
};
}
var p = 78;
var lmn = new Abc();
var xyz = new Object;
xyz.p = 34;
xyz.getp = new Abc();
console.log(this.p);//?? ----- (1)
console.log(lmn.p);//?? ----- (2)
console.log(lmn.getp());//?? ----- (3)
console.log(xyz.p);//?? ----- (4)
console.log(xyz.getp.p);//?? ----- (5)
console.log(xyz.getp.getp());//?? ----- (6)

コンソールにはどうエコーされるだろう?同じ名前のオブジェクトがあちこちに作られて大変紛らわしい。どこにどういうプロパティやメソッドが設定されたのか、メソッドはどのプロパティ上で実行されているのか。予想を立てて実行してみよう。

正解は(1)~(6)まで順に78、56、56、34、56、56になる。

(1)はグローバルオブジェクト上で直接thisを問うているのでthisはグローバルオブジェクト。そのプロパティpは「var p = 78」とされているから78だ。

(2)と(3)はインスタンスlmnがコンストラクタAbcを参照した場合のthisなので参照元のlmn自身がthis。コンストラクタのpプロパティとgetpメソッドはそれぞれlmnのプロパティとメソッドと解釈するのでlmn.pの値は56であり、lmn.getpは56を返す。

(4)はxyzを新しいobject型オブジェクトとして定義し、次の行でそのプロパティpを34と設定しているから34。thisはまったく関係なかったぜ。

(5)の場合、xyzがgetpというプロパティを持っているが、それはAbcをコンストラクタとしてインスタンス化したものだ。Abcの定義から、xyz.getpはさらにpというプロパティを内包しているという形だが、xyz.getpはobject型オブジェクトだ。だから関数の実行起点はxyz.getpとなり、thisはこれを指している。this.pは結局xyz.getp.pということになるので56になる。

(6)も(5)と同様。xyz.getpはpプロパティのほかにgetpメソッドを持ち、このときthisは実行起点のxyz.getpを指すのだから、this.getp()はxyz.getp.getp()というメソッドを実行していることになるのでxyz.getp.pの値56を返す。


コンテキストが移るたびにthisが指すものが変わるってのがこれでよくわかるんじゃないかな。

関数が入れ子になる場合

関数の中に関数を内包している(いわゆる入れ子)場合はやや挙動が不自然に見えることがあるので確認しておく。


function outerfunc(){
this.p = 56;
function innerfunc(){
this.q = 78;
};
innerfunc();
}
outerfunc();
console.log(p);//56
console.log(window.p);//56
console.log(q);//78
console.log(window.q);//78

関数outerfuncの中で関数innerfuncを定義している。で、outerfuncを実行する。どのthisもグローバルオブジェクトと解釈される…のは自然に見える。問題ない。はい次。


function outerfunc(){
this.p = 56;
function innerfunc(){
this.q = 78;
};
innerfunc();
}
var xyz = new outerfunc;
console.log(xyz.p);//56
console.log(xyz.q);//undefined
console.log(window.p);//undefined
console.log(window.q);//78

関数定義は上と同じ構造だけど、今度はインスタンスxyzのコンストラクタとして使ってみた。outerfunc直下のthisはインスタンスxyzを指している。問題なさそう。

innerfuncのthisはグローバルオブジェクトになってるね。newがコンストラクタを元にインスタンスへ追加してるのはプロパティとメソッドであって、関数(ここではinnerfunc)実行は「コンストラクタ-new-インスタンス」の一連のコンテキストとは別ってことだ。

newしたときにouterfuncの中は一覧されて、関数実行のステートメントがあれば実行するけど、それはnewのコンテキストとは関連を持たない、その部分だけ独立したグローバルオブジェクト上の関数実行として扱われている、と考えれば納得できる気がする。

innerfunc内でどーーーーーーーしてもouterfuncが指すthisを共有したい場合は


function outerfunc(){
var that = this;
that.p = 56;//この行はthis.p = 56;でもよい
function innerfunc(){
that.q = 78;
};
innerfunc();
}
var xyz = new outerfunc;
console.log(xyz.p);//56
console.log(xyz.q);//78
console.log(window.p);//undefined
console.log(window.q);//undefined

outerfunc直下で一旦thisを別の変数(ここではthat)に置き換えてinnerfuncでは置き換えた方の変数を使う、という手法がよく紹介されてる。

よく紹介されてるけど、だからと言って本当に良い方法なのかどうかはよくわからん。はい次。


function outerfunc(){
this.p = 56;
this.innerfunc = function(){
this.q = 78;
};
}
var xyz = new outerfunc;
console.log(xyz.p);//56
console.log(xyz.q);//undefined
console.log(window.p);//undefined
console.log(window.q);//undefined

似たようなやつでinnerfuncがメソッド、さらにその中にthisがある場合。this.pもthis.innerfuncもインスタンス化されたときにxyzのプロパティとメソッドとして追加されている。

が、インスタンス化した時点ではメソッド内のthisは決まってない。メソッドの動きが「this.qに78を入れるよ」という動作を決めただけ。なのでいったんメソッドを実行してみる…


function outerfunc(){
this.p = 56;
this.innerfunc = function(){
this.q = 78;
};
}
var xyz = new outerfunc;
xyz.innerfunc();
console.log(xyz.p);//56
console.log(xyz.q);//78
console.log(window.p);//undefined
console.log(window.q);//undefined

…と、ここで初めてメソッド内のthisが具体的に意味づけされる。インスタンスxyzのメソッドとして実行してるからthisはxyzだよね。

thisを強制的に特定したい場合

thisがなんなのかは呼び出し元を確認しないとわからない、thisを決め打ちしたい!という時は「.call」メソッドを使うとそういうことができちゃう。

「.call」メソッドはどんな関数にも適用できる。使い方はこんな感じ。


実行する関数.call(thisとして指定したいオブジェクト,実行する関数の実際の引数1,実行する関数の実際の引数2…);

以下具体例。


var p = 56;
var xyz = {};
xyz.p = 78;
function abc(){
console.log(this.p);
}
abc();//56

普通に関数を呼び出して実行すればthisはグローバルオブジェクトを指しているので56を返すけど


var p = 56;
var xyz = {};
xyz.p = 78;
function abc(){
console.log(this.p);
}
abc.call(xyz);//78

abcに.callメソッドを付けて、第1引数でthisにしたいオブジェクトとしてxyzを渡しているので、this.pはxyz.pになる。

グローバルオブジェクト上で直接abcを実行しているのでコンテキストオブジェクトはグローバルオブジェクトなのだが、引数で強制的にthisをxyzに変えてるの。

.callじゃなくて.applyでも同じことができるらしいけど、これ使う時は第2引数以降(実行する関数の実際の引数)を配列の形で渡すんですって。例は省略。

まとめ

thisは関数の呼び出し元。以上!

あ、乱暴ですか?まぁでも結果的にはこんだけですよね。もう少し丁寧に言えば

  • thisが何かは実行されるときに初めて決まる
  • 関数自身はthisになりえない
  • 関数を実行するしているobject型オブジェクトがthis
  • インスタンス化の文脈から外れた関数はグローバルオブジェクト上で実行されているとみなす
  • .callや.applyで強制的に指定もできる

こんな感じですか。thisひとつでもちゃんと理解しようとすると結構深い。深いけどあまり難しく考えない方がいいね。これでやっとprototypeの話ができそうだ。んじゃまたー。

コメント

タイトルとURLをコピーしました