IT戦記

プログラミング、起業などについて書いているプログラマーのブログです😚

JavaScript の this について

WEB+DB PRESS 編集の R たんから、僕の連載記事に読者様から質問が来ていると教えていただいたので、その内容を教えていただきました。

以下、内容を転載

JavaScriptわくわく開発道」の記事に関して質問です。

今回の内容で特に興味を持ったのはthisキーワードの振る舞いでした。

thisキーワードの説明には、オブジェクト型の変数を別の変数にコピーしてから初期化すると、コピーした変数からメソッドを実行できなくなるという例が紹介されていました。

そこには「(1)の時点でobj0には{}が入っているため、hogeは未定義となってしまう」という説明があるのですが、誌面の都合上省略があるためか、thisキーワードを用いることで問題を回避できるという理由がなかなか理解できないでいます。

自分なりに考えてみたところ、内部的には以下のようなことが起きているのではないかと思いました。

(1) 変数obj0はオブジェクト型の変数なので、アドレス値(ポインタ)が格納されている。

(2) 変数obj0を別の変数obj1にコピーすると、obj1には値ではなくアドレス値が格納される。

(3) 変数obj0を{}(オブジェクトコンストラクタ)で初期化すると、新たなオブジェクトが生成(メモリ上の別の領域が確保)され、そのアドレス値が格納される。

(4) 変数obj0から参照されていたメモリ空間は、まだ変数obj1から参照されているため、ガーベジコレクションの対象にはならない。そのため、プロパティhogeメソッドfunc()は解放されておらず、メモリ上に残り続けている。

(5) 変数obj1を介してメソッドfunc()を実行すると、メソッドfunc()の中にobj0がリテラルで記述されているため、(3) で生成したオブジェクトのメモリ領域を参照してしまい、意図した結果とならない。

(6) ただし、thisキーワードを用いれば、ベースとなるアドレス値がずれず、意図した結果を得ることができる。

勉強を始めて日が浅く間違った認識もあると思いますが、JavaScriptを本気で習得したいと考えているので、上の考え方が正しいかどうか教えていただけると助かります。

メールで返信するのも良かったのですが、僕の記事を読んでいただいた他の方や読んでいない人にも読んでいただきたいと思いまして、ブログのほうで解答を書く事にしました。

了承してくださって、ありがとうございます><

まず、雑誌を買ってない人向けにどういう記事内容だったのかを紹介します。

WEB+DB PRESS vol.40 の 167 ページです。

■ 関数呼び出しの不思議、 this
次に、関数呼び出しの不思議な現象、 this について解説します。プログラミング経験があって、 this について偏見があるとしたら、一度今までの this は忘れてしまいましょう。

さて、 this について解説する前に [リスト 16] の例を見てください。

● リスト 16 プロパティに関数を持つ

var obj = {
  hoge: 1,
  func: function() {
    alert(obj.hoge);
  }
};
obj.func(); // 1 と表示される

このようにすると、データと関数を一緒のオブジェクトに閉じ込めることができて便利です。しかし、このやり方には一つ重大な問題があります。それは、変数 obj を別の用途に再利用した場合、動作しなくなってしまうという点です。 [リスト 17] を見てください。

● リスト 17 変数に依存するオブジェクト

var obj0 = {
  hoge: 1,
  func: function() {
    alert(obj0.hoge);
  }
};
obj0.func(); // 1 と表示される

var obj1 = obj0;
obj1.func(); // 1 と表示される

obj0 = {};
obj1.func(); // undefined と表示される ... (1)

(1) の個所が undefined になってしまいました。これは obj1.func の中で変数 obj0 を見ているからです。 (1) の時点で obj0 には {} が入っているため、 hoge は未定義となってしまうのです。

これを解決するために、 JavaScript では this というキーワードを使います。 リスト 18 では (2) のところを this としました。

● リスト 18 関数の中で this を使う

var obj0 = {
  hoge: 1,
  func: function() {
    alert(this.hoge); // ... (2)
  }
};
obj0.func(); // 1 と表示される

var obj1 = obj0;
obj1.func(); // 1 と表示される

obj0 = {};
obj1.func(); // 1 と表示される

これで変数 obj0 が別の値になっても期待どおりに動くようになりました。関数内の this は、関数呼び出し時に関数が保持されていたオブジェクトになるのです。

少し難しいことを言うと、関数呼び出し演算は関数と引数だけで行われるのではありません。関数がどのオブジェクトプロパティとして呼び出されたかということも、演算結果に影響を与えるのです。なぜそうなるかは、非常に難解でイメージしにくい解説になってしまいそうですので、本記事では省略します。

では、質問にある、 (1) 〜 (6) について一つずつ答えていきますー。

(1)

変数obj0はオブジェクト型の変数なので、アドレス値(ポインタ)が格納されている。

そうです。JavaScript にはアドレス値という概念がないので、あくまで内部の話(僕の想像)ですが、変数 obj0 (を表すデータの中)にはオブジェクト(の情報を持つメモリ領域)のアドレス値が格納されているんだと思います。

(2)

変数obj0を別の変数obj1にコピーすると、obj1には値ではなくアドレス値が格納される。

これもそのとおりです。代入によってオブジェクト(の情報を持つメモリ領域)のアドレス値は変数 obj0 (を表すデータの中)と変数 obj1 (を表すデータの中)で共有されると思います。

(3)

変数obj0を{}(オブジェクトコンストラクタ)で初期化すると、新たなオブジェクトが生成(メモリ上の別の領域が確保)され、そのアドレス値が格納される。

はい。そうです。変数 obj0 に {} を代入すると {} で生成されたオブジェクトが変数 obj0 に格納されます。内部的には変数 obj0 (を表すデータの中)には {} で作られたオブジェクト(の情報を持つメモリ領域)のアドレス値が格納されているんだと思います。

(4)

変数obj0から参照されていたメモリ空間は、まだ変数obj1から参照されているため、ガーベジコレクションの対象にはならない。そのため、プロパティhogeメソッドfunc()は解放されておらず、メモリ上に残り続けている。

そうですね。変数に格納されている限りはガーベジコレクションの対象にはなりません。

(5)

変数obj1を介してメソッドfunc()を実行すると、メソッドfunc()の中にobj0がリテラルで記述されているため、(3) で生成したオブジェクトのメモリ領域を参照してしまい、意図した結果とならない。

ここで一つ間違いがあります、メソッド func は (3) で生成したオブジェクトのメモリ領域を直接参照しているわけではありません。 func という関数は "obj0" という文字列を「知っている」だけなのです。
JavaScript の変数参照は、ハッシュへのアクセスです。つまり、 func が obj0 を参照する際は、実行時に "obj0" というキーでハッシュにアクセスし、 obj0 に入っているデータ(ここではオブジェクト)を取得しているのです。
以下を試してみると、分かるのではないでしょうか。

var variable = { property: 1 };

// func は variable という文字列を知っているだけでオブジェクトのアドレスは知らない
var func = function() {
  alert(variable.property);
};

func(); // 1 と表示される

variable = { property: 'hoge' };

func(); // hoge と表示される

delete variable;

func(); // エラー (variable is not defined)
(6)

ただし、thisキーワードを用いれば、ベースとなるアドレス値がずれず、意図した結果を得ることができる。

this キーワードは実行時に与えられるもので、自分が属するオブジェクトのアドレス値を直接参照するようなものではありません。意図した結果が得られるのは、 obj0.func() この演算によって obj0.func の this に obj0 が与えられているからです。
もちろん、実行時だけはオブジェクトの(ベースとなる?)アドレス値を持っている(とも言える)訳ですが。
以下の例を見てください。

var obj0 = { hoge: 1 };
var obj1 = { hoge: 2 };
var obj2 = { hoge: 3 };

obj0.func = obj1.func = obj2.func = function() { alert(this.hoge) };

alert(obj0.func === obj1.func); // true と表示
alert(obj0.func === obj2.func); // true と表示
alert(obj1.func === obj2.func); // true と表示

obj0.func(); // 1 と表示
obj1.func(); // 2 と表示
obj2.func(); // 3 と表示

この三つのオブジェクトに代入された関数はまったく同じものであるのに、実行結果(つまり this の値)が違うことから this は静的にオブジェクトのアドレス値を持つものではなく、実行時に this となるべきデータが与えられているということが分かるかと思います。

どうでしょうか><?

なんだか、自分でも小難しい説明だなあと思います><
すみません。。。

僕の WEB+DB PRESS の記事について

もし、分からないことがあればいくらでも質問してください。
全てに答えられるかは分かりませんが、答えられることにはこういう形で答えていきたいと思っています><
次回の連載は jQuery のことなんか書こうかと思っています。まだ確実ではないですが。
という訳で、今から jQuery の勉強しないと><
間に合うかなくぁwせdrfrtgyひゅじこlp;@

追記:併せてブクマのコメント欄もどうぞ

はてなブックマーク - IT戦記 - JavaScript の this について