JavaScriptの動かないコード (中級編) オブジェクトのメソッドを変数代入でコピーできない
以下のJavaScriptコードが意図した動作をしないのは,なぜですか。(制限時間1分)
やりたい事:
- hello,world! と,2回表示したい。その際,出力用の処理を簡単に呼び出せるように,関数を変数に代入してから利用する。
<html> <body> <input type="button" onclick="f()" value="「hello,world」と2回表示"> <script language="JavaScript"> function f(){ var A = function(){}; A.prototype = { str : "hello,world!", // hello,worldと表示するためのメソッド say_hello_world : function(){ alert( this.str ); } }; var a = new A(); a.say_hello_world(); // hello,worldと表示 // メソッド名が長いので,簡単に呼び出せるように代入しておこう。 var x = a.say_hello_world; // 代入したメソッドを実行 x(); // hello,worldと表示 } </script> </body> </html>
答え
IEでもFFでも,一回目は「hello,world」が表示される。
しかし,二回目は「undefined」が出る。
JavaScriptでは,関数オブジェクトを「=」で変数に代入・保持できるはずだ。
どうしてだめなのか。
具体例
次のコードを動かしてみよう。
やりたいのは,「hello」と,「world」をそれぞれ表示することだ。
途中で,「world」を表示するための関数をコピーしようとしている。
<html> <body> <input type="button" onclick="f()" value="クリックして表示"> <script language="JavaScript"> function f(){ var A = function(){}; A.prototype = { str : "hello", // helloと表示するためのメソッド say_hello : function(){ alert( this.str ); } }; var B = function(){}; B.prototype = { str : "world", // worldと表示するためのメソッド say_world : function(){ alert( this.str ); } }; // インスタンス作成 var a = new A(); var b = new B(); // それぞれのオブジェクトが持っているメソッドを呼び出す a.say_hello(); // helloと表示 b.say_world(); // worldと表示 // BのメソッドをAにコピー a.say_world = b.say_world; // コピーしたメソッドを実行 a.say_world(); // worldと表示 } </script> </body> </html>
このコードの意図は「hello」「world」「world」の順番で文字列を表示させる事だ。
しかし,実際にはそうではなく,「hello」「world」「hello」と表示されてしまう。
say_world()が「hello」と表示してしまっている。
say_worldの中身のthisが,b.strではなく,a.strを参照してしまっているのだ。
原因
オブジェクトからメソッドをコピーした場合,もとのオブジェクトとの関わりは一切なくなる。
メソッドだけが切り離されてコピーされ,そのメソッドの中に書いてある「計算方法」だけが引き継がれるのだ。
thisを使っていた場合は,あくまでthisという計算方法だけが残る。
もとのオブジェクト内でthisが指していた実体と,コピー後にthisが指す実体とは別物になる。
冒頭のコードも,x()を実行した時点で,もとのクラス A の存在は完全に無視される。
say_hello_world中のthisが指すのは,x()を実行する時点でのコンテキストである。つまりthisはwindowを指す。
windowにstrは存在しないのでエラーになる。
解決策
代入ではなく,新規関数の定義ならばOK。
var x = function() { a.say_hello_world() };
この場合,say_hello_worldというメソッド呼び出しの「レシーバ」はaであり,a.strが参照され,hello,world!が出る。
補足
関数オブジェクトの代入にまつわる問題の詳細仕様については,以下のエントリに詳しい。
JavaScriptのメソッドコールの仕組みを深く理解する (参照型とは?)
http://d.hatena.ne.jp/mindcat/2010032...
- 全てはオブジェクトのメソッドであり,thisがグローバルの場合を特に関数と呼ぶ
- オブジェクト側とメソッド側を2つあわせた物を「アドレス」と考えるとよい
- 関数オブジェクト代入時に「参照外し」が行なわれ,参照型ではなくなってしまう