Javascript入門 クラス継承、new演算子、プロトタイプチェーン Q&A

  • 37
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

分からなかったので、備忘録がてら、Q&A形式でまとめてみた。
「どうすりゃいいのか」から始めて「どういう意味なのか」に踏み込んでいったつもりです。
ご意見、ご感想、ご要望、間違いの指摘などございましたら、コメント欄まで。

Q1: Javascriptでは、クラスは関数で作ると聞きました。どういうことですか?

こういうことです。

クラスの作り方
/* コンストラクタとなる関数。
 クラス変数は、this.a = ... のようにして初期化します。
 この関数は、通常は何も返しません。
 */
function Foo(a){
  /* 初期化処理 */
  this.a = a;
}
/* Foo関数をコンストラクタとして作られたインスタンスは、
 Foo.prototypeが持つプロパティにアクセスできます。
 次のようにメソッドや定数を定義するとよいでしょう。
*/
Foo.prototype.func = function(){ return this.a * 10; }
インスタンスの作り方
/* Fooをコンストラクタとして、インスタンスfooを得る。 */
foo = new Foo(1);
foo.a  // --> 1
foo.func()  // --> 10

ちなみに。コンストラクタに引数をつけない場合は、new Foo()new Fooと書いても構いません。
また、Javascriptでは関数名は小文字から始めることが多いですが、コンストラクタとなる関数は名前を大文字から始めることも多いようです。

Q2: では、クラスの継承はどのようにするのですか?

いろいろとやり方があって、どれを書いても誰かからは文句を言われそうなのですが。
まずは、これを知っておくべきです。

クラスの継承をする
// 親クラス
function Animal(name){ this.name = name; }
Animal.prototype.hello = function(){ return "Hello, I'm " + this.name; }
Animal.prototype.sleep = function(){ return 'Zzz...'; }

// 子クラス
function Dog(name){
  /* なんぞこれ: func.call(thisArg, args...) は、
     func(args...) と同じだが、funcの中で指すthisを、thisArgに置き換える。

     親クラスのコンストラクタAnimal()を呼び出したいが、普通に呼ぶと、
     Animal()内のthisは、Dog()内のthisと違うものとなる。
     なので、下記のように、Animal()内でのthisをDog()内でのthisに置き換える。 */
  Animal.call(this, name);

  /* その他の初期化処理... */
}
/* Animalのインスタンスを、Dogのprototypeプロパティにセットする。 */
Dog.prototype = new Animal;
/* Dog.prototypeに適宜、追加する。 */
Dog.prototype.hello = function(){ return 'ぼく' + this.name + 'だワン。'; }
継承したクラスの挙動を確認する
doubutsu = new Animal('DOUBUTSU');
doubutsu.hello();  // --> "Hello, I'm DOUBUTSU"
pochi = new Dog('ポチ');
pochi.hello();  // --> 'ぼくポチだワン。'
pochi.sleep();  // --> 'Zzz...'

Q3: Dog.prototype = Animal.prototype として継承している例を見ました。この方法ではダメですか?

あなたが期待している通りには動きません。
具体的に言いますと。この場合、Dog.prototypeとAnimal.prototypeは同一のオブジェクトを指しているわけですから、Dog.prototypeにプロパティを追加・変更すると、Animal.prototypeも同じく追加・変更したことになってしまいます。コンストラクタが違っているだけで、これは継承とは呼べません。

不適切な継承
// 親クラス
function Animal(name){ this.name = name; }
Animal.prototype.hello = function(){ return "Hello."; }

// 子クラス
function Dog(name){ Animal.call(this, name); }
Dog.prototype = Animal.prototype;  // 不適切な継承

Dog.prototype.hello = function(){ return "ワンワン!"; }

beef = new Animal('牛');
beef.hello();  // --> "ワンワン!"

この挙動は、うれしくないのでは?

Q4: Q2の方法で継承をすると、コンストラクタに副作用がある場合は問題があるかもしれないと聞きました。どういうことですか?

Dog.prototype = new Animal の部分が悪いのです。
なんか微妙な例ですが、Animal()を呼ばれる度にカウンターをインクリメントして、これまでに生み出された動物の数を数えているとしましょう。

動物カウンター
var counter = 0;

function Animal(name){
  this.name = name; counter++;
}
function animal_count(){
  return 'これまでに' + counter + '匹の動物が生まれました。';
}

new Animal('a');
new Animal('b');
animal_count();  // --> 'これまでに2匹の動物が生まれました。'
犬の定義がカウントを狂わせる!
animal_count();  // --> 'これまでに2匹の動物が生まれました。'

function Dog(name){ Animal.call(this, name); }
Dog.prototype = new Animal;  // !!!

animal_count();  // --> 'これまでに3匹の動物が生まれました。'

new Dog('c');
new Dog('d');

animal_count();  // --> 'これまでに5匹の動物が生まれました。'

Q5: 継承時の副作用の影響を避けるにはどうしたらいいですか?

方法1: ダミーのコンストラクタを用意することで、親クラスのコンストラクタを呼ばずにnewする。ややこしいのが欠点。
方法2: Object.create()を使う。古いブラウザ(IE8など)では動かないのが欠点。

まずは方法1から。

方法1
// 親クラス
function Animal(name){ this.name = name; }
Animal.prototype.hello = function(){ return "Hello."; }
// 子クラス
function Dog(name){ Animal.call(this, name); }
function Dummy(){}
Dummy.prototype = Animal.prototype;
Dog.prototype = new Dummy();

/* いくつもクラス継承をするのなら、関数化した方がいいでしょう。 */
function get_prototype(cls){
  function Dummy(){}
  Dummy.prototype = cls.prototype
  return new Dummy();
}
Dummy.prototype = get_prototype(Animal)

方法2のObject.create()ですが、これは、新たなオブジェクトを作成する関数で、第1引数に指定したオブジェクトが、新たに作成されるオブジェクトのプロトタイプとなります。
これは、newとかなり似たことをしています。第2引数については、ややこしいのでこちらをご参照下さい。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/create

方法2
// 親クラス
function Animal(name){ this.name = name; }
Animal.prototype.hello = function(){ return "Hello."; }
// 子クラス
function Dog(name){ Animal.call(this, name); }
Dog.prototype = Object.create(Animal.prototype);

とてもシンプルですね。

Q6: ~~.prototypeって、よく出てきますけど、何なんですか?

Javascriptでは、オブジェクトは何らかのプロトタイプオブジェクト(あるいはnull)とつながりを持ちます。new Fooとしてオブジェクトを作った場合は、作成されたオブジェクトはFoo.prototypeとつながりを持ちます。
他にも、Object.createを使った場合は、その第1引数のプロトタイプオブジェクトとつながりを持ちます。また、obj = {} とした場合はObject.prototype, ary = []とした場合はArray.prototype, function func(){}とした場合はFunction.prototypeとつながりを持ちます。このつながりをプロトタイプと呼んでいます。

「プロトタイプ」という言葉は、2つの意味で使われている場合があります。混同しないように注意して下さい。
1. ~~.prototypeのこと。オブジェクトが持っている、prototypeプロパティ。
2. プロトタイプオブジェクトとのつながり。[[Prototype]] あるいは __proto__ と記述される場合もある。
この文章では、カタカナの「プロトタイプ」は2の意味で使い、「~~.prototype」あるいは「prototypeプロパティ」を1の意味で使っています。

Q7: プロトタイプチェーンって何ですか?

Q6にて、すべてのオブジェクトはプロトタイプオブジェクトへのつながりを持ち、foo = new Fooと作られたオブジェクトfooは、Foo.prototypeとつながりを持つことを述べました。
一方、Foo.prototypeもまたオブジェクトなので、別のプロトタイプオブジェクト(あるいはnull)へのつながりを持っていて、さらにその別のプロトタイプオブジェクトもつながりを持っています。nullにたどり着くまでのつながりの連鎖を「プロトタイプチェーン」と呼びます。

オブジェクトのプロトタイプを調べるには、Object.getPrototypeOf(obj)を使うか、非標準ですが多くのブラウザでobj.__proto__がプロトタイプを指しています。

指しているプロトタイプが何のオブジェクトのprototypeプロパティかを調べるには、constructorプロパティを見て当たりをつけ、 foo.__proto__ === Foo.prototypeなどで調べるとよいでしょう。

次のような例を考え、コードと図を対比させてみましょう。

プロトタイプチェーンを考える
function Animal(){}

function Dog(){}
Dog.prototype = new Animal();

function Cat(){}
/* Animal()を呼び出さずに継承。Q5参照。 */
function Dummy(){}
Dummy.prototype = Animal.prototype;
dummy = new Dummy;
Cat.prototype = dummy;

function Bat(){}
Bat.prototype = Animal.prototype; /* 不適切な継承。 */

animal = new Animal;
dog = new Dog;
cat = new Cat;
bat = new Bat;
dog2 = Object.create(Dog.prototype);

JavascriptPrototype.png
まだごちゃごちゃしていますが。実線の矢印がプロトタイプチェーンで、破線の矢印が、関数が持つprototypeオブジェクトです。
*1 とあるのは、本当は関数であるコンストラクタは全て Function.prototype をプロトタイプに持つのですが、それを図示すると非常に見づらくなるので、このように割愛しています。
また、本来なら、プロトタイプオブジェクトには名前はついていないはずですが、それではあまりに不親切なので、便宜上、名前をつけています。dummy = new Dummy;として作ったdummyと、Cat.prototype = dummyとしてセットしたCat.prototypeは、同一のオブジェクトを指していますので、名前を併記する形にしています。
コンストラクタの破線矢印と、インスタンスの実線矢印に注目してください。コンストラクタのprototypeプロパティが指し示すオブジェクトが、そのコンストラクタより作られたインスタンスのプロトタイプとなっていることが分かります。
また、Batの継承方法がなぜ不適切なのか、Dummyを使った継承はどのように動くのか、プロトタイプチェーンの観点から、考えてみて下さい。

Q8: プロトタイプチェーンは一体、何のためにあるのですか?

オブジェクトのプロパティを参照した際、そのオブジェクト自身が、参照されたプロパティを持っていなかった場合、プロトタイプチェーンを辿り、プロパティを検索します。一番最初に見つかったものを返し、プロトタイプチェーンを最後まで(nullにたどり着くまで)辿ってもなければundefinedを返します。
ただし、オブジェクトのプロパティに値をセットする場合は、プロトタイプチェーンは辿られず、そのオブジェクト自身にセットされます。

プロトタイプチェーンを辿る
obj1 = {a: 1, b: 1}
/* obj1: {a: 1, b: 1, __proto__: Object.prototype} */

obj2 = Object.create(obj1);
obj2.a = 2;
obj2.c = 2;
/* obj2: {a: 2, c: 2, __proto__: obj1} */

obj1.a;  // --> 1
obj1.b;  // --> 1
obj1.c;  // --> undefined

obj2.a;  // --> 2
obj2.b;  // --> 1
obj2.c;  // --> 2

obj2.bが、プロトタイプチェーンを辿った先の、obj1のプロパティを参照した値を返すことに注目してください。
また、obj2.a, obj2.cに値をセットしても、obj1には影響を及ぼしていないことにも注目してください。

もちろん、関数呼び出しにおいても、プロトタイプチェーンが辿られます。

プロトタイプチェーンを辿る(関数編)
obj1 = { func: function(){ return 1; } }
obj2 = Object.create(obj1);

obj2.func();  // --> 1
obj2.func = function(){ return 2; }
obj2.func();  // --> 2
obj1.func();  // --> 1

ちなみに。この場合の挙動は、予測できるでしょうか?

プロトタイプチェーンを辿る(funcが返す値を当てよ)
obj1 = { a: 1, func: function(){ return this.a; } }
obj2 = Object.create(obj1);
obj2.a = 2;

obj2.func();  // 1か2、どちらを返すでしょうか?

答えは、2です。obj2.func()という形で呼び出した場合は、thisはobj2と同一のオブジェクトを参照しています。

Q9: プロトタイプチェーンの仕組みが、クラスを作る上でどのように役に立つのですか?

プロトタイプチェーンを辿ることで、インスタンスは、コンストラクタ関数のprototypeプロパティを参照できます。さらに、そのコンストラクタ関数のプロトタイプを参照することで、クラスの継承のようになります。
下記のコードのプロトタイプチェーンを考えて下さい。

プロトタイプチェーンとクラス
function Animal(){}
Animal.prototype.hello = function(){ return 'ガオー'; }
Animal.prototype.sleep = function(){ return 'Zzz...'; }

function Dog(){}
Dog.prototype = new Animal;
Dog.prototype.hello = function(){ return 'わんわん'; }

animal = new Animal;
dog = new Dog;

/* 矢印でプロトタイプチェーンを表すと、こうなります。

dog    → Dog.prototype: { hello }

animal → Animal.prototype: { hello, sleep }

         Object.prototype

         null
*/

プロトタイプチェーンを通じて、
animalがAnimal.prototypeの関数や変数にアクセスできること、
dogがDog.prototypeの関数や変数、さらにはAnimal.prototypeの関数や変数にもアクセスできることが分かります。

Q10: Foo.prototype = {/* ... */}のようにしてprototypeプロパティを作っている例を見かけたのですが、いいのですか?

派生クラスでは使えません。基底クラスではこの方法でも構いません。
この方法をとった場合は、プロトタイプ(__proto__)がObject.prototypeになります。これは、いわゆる基底クラスのような状態です。

プロトタイプにオブジェクトを直接セット
function Foo(){}
Foo.prototype = {a: 1};

foo = new Foo;
foo.a;  // --> 1

function Bar(){}
/* デフォルトのprototypeオブジェクトを捨てて、Foo.prototypeをプロトタイプに持つオブジェクトをセットする。 */
Bar.prototype = new Foo;
/* Foo.prototypeをプロトタイプに持つオブジェクトを捨てて、Foo.prototypeをプロトタイプに持たないオブジェクトをセットする。 */
Bar.prototype = {};

bar = new Bar;
bar.a;  // --> undefined

Q11: クラスメソッドやクラス変数を作りたい場合、どうすればいいでしょう?

Javascriptでは関数もオブジェクトです。コンストラクタに持たせておけば、それらしきことができます。

クラスメソッドやクラス変数らしきもの
function Foo(){}
Foo.func = function(){ return 'Foo.func()'; }
Foo.x = 1;
Foo.y = 1;
Foo.prototype.x = 2;

foo = new Foo;
Foo.func();  // --> 'Foo.func()'
Foo.x;  // --> 1
Foo.y;  // --> 1

foo.x;  // --> 2
foo.y;  // --> undefined

Fooを継承したクラスを作っても、これらのクラスメソッドやクラス変数は継承されないことにも注意して下さい。(コンストラクタそのもののプロトタイプチェーン上にFooがないため、継承されません。)

Q12: クラスを作る際、メンバ変数やメソッドを、~~.prototypeに書いてもthis.~~で書いても、実用上はあまり変わらなくないですか? どう使い分けたらいいですか?

~~.prototypeに書くと、すべてのインスタンスがプロトタイプチェーンを通じて同一の値を参照します。コンストラクタでthis.~~で書くと、すべてのインスタンスでそれぞれ独立に値を持ちます。実用上は、~~.prototypeに書くと、インスタンスを作ってから~~.prototypeを変更した場合、すべてのインスタンスにその変更が反映されます。また、メソッドや定数など独立に持たせる意義のないものは、~~.prototypeに書いた方が、インスタンスをたくさん作るときや、サイズの大きいもの作るのに時間がかかるものがある場合はリソースの節約になります。使い分け方は、これが絶対というものはないですが、例えばメソッドは~~.prototypeに、定数は~~.prototypeか、Q11のようにコンストラクタのメンバとして。インスタンス内で変更する変数はコンストラクタ内でthis.~~、とするのがよいでしょう。

おわり

気が向いたり、何か調べたり、何かコメントで言われたら追記します。
constructorプロパティをまじめに作る、プライベートメンバを模倣する、Object.createの2個目の引数、などは付け加えようかなと思ってます。