読者です 読者をやめる 読者になる 読者になる
スポンサーリンク

JavaScriptの動かないコード (中級編) オブジェクトのprototypeを変更した時のエラー

javascript 動かないコード


以下のJavaScriptコードが意図した動作をしないのは,なぜですか。(制限時間1分)


やりたい事:

  • 1, a
  • 2, b
  • 3, b

という,3種類の値のペアを生成する。

<script language="JavaScript">

// オブジェクトを定義する。
// 2つのメンバ変数 x, y を持つと想定。
var A = function(){
	// show というメンバ関数
	this.show = function(){
		alert( this.x + "," + this.y );
	};
};

// prototype を3回書き換える事により,
// 3種類の異なった値を持たせる。

A.prototype = { x : 1, y : "a" };
var a1 = new A();

A.prototype = { x : 2, y : "b" };
var a2 = new A();

A.prototype.x = 3 ;
var a3 = new A();


a1.show(); // 1,a と表示
a2.show(); // 2,b と表示
a3.show(); // 3,b と表示

</script>



答え



「1, a」「2, b」「3, b」ではなく,

「1, a」「3, b」「3, b」と表示される。a2 の値がおかしい。


prototype.x に 3 を設定したタイミングは,a2 を生成したよりも後であるにも関わらず,なぜか a2.x にも 3 が代入されてしまっている。


上のコードでは,オブジェクトの prototype に関して誤解をしている可能性がある。

また,やはりJavaScriptにおけるオブジェクトの扱い方が見逃されている。


ここで,JavaScriptの prototype について,順を追って復習してみよう。

__proto__とは

オブジェクトのインスタンスが持つ__proto__とは,

アクセスされたメンバ変数が存在しない時に,かわりにどのオブジェクトを参照するか?

という参照先のこと。


例えば,下記のようなコードを考える。

<script language="JavaScript">

// オブジェクトを作る。
var A = function(){};
var a = new A();

// 困ったときには,p というオブジェクトに
// 代打をしてもらう事にする。
var p = {x:1};
a.__proto__ = p;

// p は x というプロパティを持つ。
alert( "x" in p ); // true
alert( p.x ); // 1

// a.x というプロパティはまだ定義していないので,
// a.x という記法は不可能なように見える。

// しかし,a 内に存在しないプロパティは,
// p がかわりに補ってくれる。
// a.x を取得しようとした時には,p.x が参照される。
alert( a.x ); // 1
alert( a.__proto__.x ); // 1
alert( a.__proto__ === p ); // true

// a は x というプロパティを既に持っているとみなされる。
alert( "x" in a ); // true

</script>

この例では,a 中で存在しない x というプロパティにアクセスがなされた時に,かわりのオブジェクトから x の値を引っ張ってきている。

どのオブジェクトを「代わりのオブジェクト」として参照するかは,オブジェクトのインスタンスの __proto__ プロパティが決める。


(注意点として,__proto__ プロパティはIEでは操作できない。Firefoxで動かすこと。*1
残念ながらIEでは __proto__ を使ったこのコードは走らない。しかし概念は同じである。)


なお,__proto__ の正式名称は"[[Prototype]]"である。(二重括弧をつける)

JavaScript によるオブジェクト指向プログラミング
http://nlp.kuee.kyoto-u.ac.jp/~murawa...

すべてのオブジェクトは、ECMA の仕様書が [[Prototype]] とする内部プロパティを持っている。

内部プロパティとは、処理系のために内部仕様を説明するもので、JavaScript のコードからはアクセスできない。

とは言うものの、実は、SpiderMonkey と Rhino では、[[Prototype]] は、独自拡張の __proto__ プロパティでアクセスできる。

prototype とは

オブジェクトの雛型が持つ prototype とは,

new でオブジェクトのインスタンスを生成する際に,デフォルトで __proto__ に代入されるオブジェクト

のこと。


つまり,「困ったときのデフォルトの参照先」である。


例えば,下記のようなコードを考える。

<script language="JavaScript">

// prototype付きのオブジェクトを作る。
var A = function(){};
A.prototype = {x:1};
var a = new A();

// インスタンスの __proto__ には,
// オブジェクトの prototype が自動的に代入されている。
alert( a.__proto__ === A.prototype ); // true


// a.x というプロパティはまだ明示的に定義していない。
// しかし,a は x というプロパティを既に持っているとみなされる。
alert( "x" in a ); // true

// a.x にアクセスがあったときには,
// a.__proto__.x つまり
// A.prototype.x が呼び出される。

alert( a.x ); // 1

</script>

上の例では,prototype に x というプロパティが定義されている。

そのおかげで,インスタンスに x を明示的にセットしなくても,インスタンスから x を参照できる。


同類のオブジェクト間で共通して使いたい関数や値があれば,コンストラクタやprototype 中で定義しておく事により,オブジェクトごとにいちいち同じコードを書く手間が省ける。

この事を指して,「prototypeを使うとオブジェクトを初期化できる」と言われる。


プロトタイプの意義や__proto__との関係については,下記URLなどを参照。

しかしこの「初期化」という言葉に落とし穴が潜んでいる。

よくある間違い

怖いのは,prototypeがオブジェクトであるのを忘れてしまう場合である。

確かに prototype をあらかじめ設定しておけばインスタンスを「初期化」できるのだが,あくまで値渡しではなく,「参照渡しによる初期化」である事に注意しなければならない。


prototype オブジェクト中に書かれている内容は,インスタンスにコピーされるのではない。

インスタンスから参照できるようになるだけである。


prototype だと思って使っていたオブジェクトの中身が変更されれば,そのオブジェクトを参照している全てのインスタンスに影響が及ぶ。

なおJavaScriptにおける値渡しと参照渡しの違いについては,前回の記事で詳しく触れた。

JavaScriptの動かないコード (初級編) 関数に配列を渡すときのエラー
http://language-and-engineering.hatenablog.jp/entry/20080921/1221926545


この事を踏まえて,冒頭のコードを見直してみよう。適宜コメント等を追加してある。

<script language="JavaScript">

// オブジェクトを定義する。
var A = function(){
	// show というメンバ関数
	this.show = function(){
		alert( this.x + "," + this.y );
	};
};

// { x : 1, y : "a" } というオブジェクト(以下「オブ1」)を新規生成し,
// 今後はそれを参照させる。
A.prototype = { x : 1, y : "a" };
var a1 = new A();

a1.show(); // 1, a と表示される。オブ1の内容。


// { x : 2, y : "b" } というオブジェクト(「オブ2」)を新規生成し,
// 今後はそれを参照させる。
A.prototype = { x : 2, y : "b" };
var a2 = new A();

a2.show(); // 2, b と表示される。オブ2の内容。

// A.prototype = { x : 2, y : "b" };
// というのは,参照渡しである。

// オブ2は,オブ1を上書きしたのではない。
// オブ1はまだ別領域で生きている。
// prototype としてオブ1を指しているインスタンスには,
// 何の影響もない。

a1.show(); // 1, a と表示される。オブ1の内容。


// オブジェクトを新規生成しないで,
// 既存のオブジェクト領域(オブ2)の内容を書き換える。
// 現在このオブジェクトを prototype として指している
// すべてのインスタンスに影響が及ぶ。
A.prototype.x = 3 ;
var a3 = new A();


// a3 に加えたつもりの操作は,a2にも影響する。
a1.show(); // 1,a
a2.show(); // 3,b
a3.show(); // 3,b


// a2 と a3 で,prototypeとして指しているオブジェクトが同じオブ2。
// a1 の場合はオブ1を指している。
// オブ2の変更はオブ1に影響しない。
alert( a1.__proto__ === A.prototype );//false
alert( a2.__proto__ === A.prototype );//true
alert( a3.__proto__ === A.prototype );//true

</script>


要するに,

  • prototype の内容を,丸ごと新規オブジェクトで書き換える場合は
    • 新規オブジェクトを生成し,それをprototypeとして使い始めている。
    • 既存の prototype を使っているインスタンスには影響なし
  • prototype の内容を,一部ずつ書きかえる場合は
    • 既存のオブジェクトの内容を一部変更しただけ。
    • 既存の prototype を使っている全てのインスタンスに影響あり

これらの2つのケースで,挙動が異なるのだ。
前者のように丸ごと書き換えれば既存のインスタンスに影響がなく,後者のようにすれば影響が生じる。


下記URLでも,冒頭のコードと同じような例が扱われている。

ちなみに,prototype の中味を丸ごとではなく一部ずつ書き変えた方がよい場合の一つとして,オブジェクト間で継承を行なっているような場合が挙げられる。


A.prototype = B.prototype; のような書き方もよくないので,かわりに A.prototype = new B(); とするのがよい。



 

*1:とは言っても,実際には「 __proto__ はFirefoxの独自拡張である」という言い方が正しい。IEでは__proto__が隠ぺいされており,おかげで,__proto__にprototype以外のオブジェクトが入っているのではないかと心配する必要がない。 しかし,この記事で取り上げているように,prototypeだと思って利用しているオブジェクトが実は違うものだったり,あるいは同じだったりして,混乱が起きやすい。この混乱を解決するためには,__proto__の値を直接操作することによって,prototypeの挙動を実感してみるのが一番良い方法だ。 従って,__proto__にアクセスする事を許しているFirefoxの存在は非常にありがたい。 ついでに言うと,現時点では「__proto__とは何か」をわかりやすく述べているサイトがない(もしくは,見つかりにくい)。そのため,このあたりの話題について調査・学習するのが困難だった記憶がある。微力ながら,この記事が一助になれば幸いと思う。