スポンサーリンク

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つあわせた物を「アドレス」と考えるとよい
  • 関数オブジェクト代入時に「参照外し」が行なわれ,参照型ではなくなってしまう