JavaScript の prototype オブジェクト入門

こんにちわ、2015年3月をもって所属会社が解散することになってしまった CYOKODOG です(悲) ただいま絶賛就活中、当方にご興味ございましたらお声かけのほどよろしくお願い致します ^^;
そんな背景もあり過去の資料を整理することも多々ありまして、今回は昔懐かしい JavaScript の prototype 周りについて自分なりにまとめてみました。

this と オブジェクトの関係

prototype の話の前に this と オブジェクトの関係をおさらいします。 JavaScript ではオブジェクトは以下のように作ります。

var Hello = {}


オブジェクトに対し、値やメソッドを追加したい場合は以下のように書きます。

var Hello = {}
Hello.msg = 'hello';
Hello.say = function(){
    alert(this.msg);
}


say() メソッドを実行してみます。

demo noAuto noReset code

script noCode
Hello = {}
Hello.msg = 'hello';
Hello.say = function(){
    alert(this.msg);
}
script
Hello.say(); /* hello */

this.msg の値である “hello” が表示されます。
this は、その処理を記述してる場所が属しているオブジェクトを指します。
ここでは say が属してる Hello オブジェクトが this にあたり、 this.msg は Hello.msg に相当するためその値である “hello” が表示されます。


では、script タグ内でいきなり this と記述した場合、あるいはどのオブジェクトにも属さない function 内で this と記述した場合、this はなにに相当するでしょうか?

demo noAuto noReset code

script
alert(this); /* [object Window] */
var f = function(){ alert(this) }
f(); /* [object Window] */

[object Window] と表示されます。
つまり DOM 階層のルート要素にあたる window オブジェクトに相当することとなり、実行してる場所は window オブジェクト に属しているということになります。

また、実行場所が window オブジェクト以外に属していたとしても、以下のように function のみ参照し実行した場合も this は window オブジェクトに属すことになります。

demo noAuto noReset code

script
var obj = {}
obj.showThis = function(){ alert(this) }
var f = obj.showThis;
f(); /* [object Window] */

window オブジェクトの汚染

変数や function を定義する際、var の記述を省略するとその属性が window オブジェクトに追加(汚染)されます。

demo noAuto noReset code

script
msg = 'hello';
say = function(){
    alert(this.msg);
}
window.say(); /* hello ← window オブジェクトに say がある */


また、html 内の要素に id 属性を振ることはよくあるかと思いますが、これも window オブジェクトを汚染することになります。

demo noAuto noReset code

html
<div id="myObj">Hello</div>
script
alert(myObj); /* [object HTMLDivElement] */
alert(window.myObj); /* [object HTMLDivElement] ← window オブジェクトに myObj がある */
alert(window.myObj.innerText); /* Hello */


window オブジェクトはさまざまな属性値を持っているので、これらの値を上書きしてしまわないよう var の記述や id の命名には配慮が必要です。

code + demo

html
window オブジェクトの持つ属性値
<div style="height:160px;overflow:auto">
    <ul></ul>
</div>
script
for(var i in window) $('<li style="float:left;margin-right:8px"/>').text(i).appendTo($DEMO.find('ul'));

よそ様の function を自分のメソッドのように使う

say() メソッドを持たない Bye オブジェクトがあったとします。

var Bye = {}
Bye.msg = 'bye';


以下のように記述することで、Hello オブジェクトのもつ say() メソッドを自分のメソッドのように実行できます。

demo noAuto noReset code

script noCode
Hello = {}
Hello.msg = 'hello';
Hello.say = function(){
    alert(this.msg);
}
Bye = {}
Bye.msg = 'bye';
script
Hello.say.apply(Bye); /* bye */
/* あるいは */
Hello.say.call(Bye); /* bye */


say() は引数を必要としないメソッドのため apply と call の違いが分かりませんが、apply と call では引数の渡し方が異なります。 2つの引数を必要とする function で試してみます。

demo noAuto noReset code

script noCode
Bye = {}
Bye.msg = 'bye';
script
var customMsg = function(prefix, safix){
    alert(prefix + this.msg + safix);
}
customMsg.apply(Bye, ['good ',' !']); /* good bye ! */
customMsg.call(Bye, 'good ',' !'); /* good bye ! */

apply の場合は配列でまとめて渡し、call の場合は引数を必要な分だけ記述します。

prototype から生成する オブジェクト

前置きが長くなりましたが、ここからが本題です。
JavaScript の function は prototype という名前のオブジェクトを持っています

demo noAuto noReset code

script
var f = function(){};
alert(typeof f.prototype); /* object */


prototype もオブジェクトなので、属性名を指定し function や 値を追加することができます。

var Hello = function(){};
Hello.prototype.msg = 'Hello';
Hello.prototype.say = function(){
    alert(this.msg);
}


当然それらを参照したり、実行することもできます。

demo noAuto noReset code

script noCode
Hello = function(){};
Hello.prototype.msg = 'Hello';
Hello.prototype.say = function(){
    alert(this.msg);
}
script
Hello.prototype.say(); /* Hello */
Hello.prototype.msg = 'Good Morning !';
Hello.prototype.say(); /* Good Morning ! */


new でインスタンス化することで、新しいオブジェクトを生成できます。

demo noAuto noReset code

script noCode
Hello = function(){};
Hello.prototype.msg = 'Hello';
Hello.prototype.say = function(){
    alert(this.msg);
}
script
var hello1 = new Hello();
var hello2 = new Hello();
hello2.msg = 'Good Morning !';
hello1.say(); /* Hello */
hello2.say(); /* Good Morning ! */


prototype に設定した値や function がインスタンス化した各オブジェクトにコピーされたように見えますが、実際はそうではなく prototype オブジェクトの所有物が自分のもののように見えてるだけです。

demo noAuto noReset code

script noCode
Hello = function(){};
Hello.prototype.msg = 'Hello';
Hello.prototype.say = function(){
    alert(this.msg);
}
script
var hello = new Hello();
alert(hello.hasOwnProperty('msg')); /* false */
alert(hello.hasOwnProperty('say')); /* false */


但し、インスタンス化したオブジェクトに対し同じ名前の属性値を追加すると、prototype 上の同名の属性値は見えなくなり、追加した属性値は自分のものになります。

demo noAuto noReset code

script noCode
Hello = function(){};
Hello.prototype.msg = 'Hello';
Hello.prototype.say = function(){
    alert(this.msg);
}
script
var hello1 = new Hello();
var hello2 = new Hello();
hello1.msg = 'Good Night !'; /* 上書き */
alert(hello1.hasOwnProperty('msg')); /* true */
alert(hello2.hasOwnProperty('msg')); /* false */


逆に言えば同名の属性値の追加をしてない状態で prototype の属性値を書き換えると、インスタンス化したオブジェクトたちはその影響を受けます。

demo noAuto noReset code

script noCode
Hello = function(){};
Hello.prototype.msg = 'Hello';
Hello.prototype.say = function(){
    alert(this.msg);
}
script
var hello1 = new Hello();
var hello2 = new Hello();
hello1.say(); /* Hello */
hello2.say(); /* Hello */
Hello.prototype.msg = 'Bye !';
hello1.say(); /* Bye ! */
hello2.say(); /* Bye ! */

インスタンス、コンストラクタ、prototype の関係を調べる

new で生成されたオブジェクトは constructor 属性で自分の生成元となった function(コンストラクタ)を参照することができます。

demo noAuto noReset code

script
var Hello = function(){/* Hello 関数 */}
var hello = new Hello();
alert(hello.constructor); // function(){/* Hello 関数 */}
alert(hello.constructor === Hello); /* true */


instanceof 関数を使用することで指定 function が、自オブジェクトの constructor かどうかを知る事ができます。

demo noAuto noReset code

script
var Hello = function(){}
var hello = new Hello();
alert(hello instanceof Hello); /* true */
alert(hello instanceof hello.constructor); /* true */


自分のコンストラクタを知る事ができるので、当然、prototype も知る事ができます。

demo noAuto noReset code

script noCode
Hello = function(){}
hello = new Hello();
script
alert(hello.constructor.prototype); /* [object Object] */


_proto_ 属性でコンストラクタを経由せず prototype を参照することもできます。

demo noAuto noReset code

script noCode
Hello = function(){}
hello = new Hello();
script
alert(hello.__proto__); /* [object Object] */
alert(hello.constructor.prototype === hello.__proto__); /* true */


arguments オブジェクトの callee 属性を参照することで、 自身のコンストラクタ(new してない場合は所属する function)を処理内で知ることができます。

demo noAuto noReset code

script
var f = function(){
    alert(f === arguments.callee)
    alert(this instanceof arguments.callee)
};
new f(); /* true, true */
f(); /* true, false */

prototype オブジェクトの継承

prototype オブジェクトでの継承方法を考えてみます。
前述の動作原理から、継承先 prototype に継承元の prototype のメンバー属性をコピーすれば継承は可能かと考えられます。

/* 継承元 */
var From = function(param){
    this.param = param;
};
From.prototype = {
    id : 'from',
    desc : 'Extend Test',
    param : null
}
/* 継承先 */
var To = function(){};
for(var i in From.prototype) To.prototype[i] = From.prototype[i]; /* 継承 */
To.prototype.id = 'to';

実行してみます。

demo noAuto noReset code

script noCode
/* 継承元 */
From = function(param){
    this.param = param;
};
From.prototype = {
    id : 'from',
    desc : 'Extend Test',
    param : null
}
/* 継承先 */
To = function(){};
for(var i in From.prototype) To.prototype[i] = From.prototype[i]; /* 継承 */
To.prototype.id = 'to';
script
var to = new To();
alert(to.id); /* to */
alert(to.desc); /* Extend Test */
alert(to.param); /* null */

問題なさそうですが、param が null のままです。

param に 値をセットするのは、From コンストラクタの役割なので、To コンストラクタから From コンストラクタ を call するようにしてみます。

demo noAuto noReset code

script noCode
/* 継承元 */
From = function(param){
    this.param = param;
};
From.prototype = {
    id : 'from',
    desc : 'Extend Test',
    param : null
}
/* 継承先 */
To = function(){};
for(var i in From.prototype) To.prototype[i] = From.prototype[i]; /* 継承 */
To.prototype.id = 'to';
script
To = function(param){
    From.call(this, param);
};
var to = new To('Send Param');
alert(to.param); /* Send Param */

3階層以上の継承

3階層以上の継承も試してみます。

まずは継承元である Greeting コンストラクタを定義します。 say() メソッドを実行するとオブジェクト生成時の引数として指定した名前に msg 属性値で挨拶をします。

demo noAuto noReset code

script
/* Greeting */
var Greeting = function(name){
    this.name = name;
}
Greeting.prototype.name = '';
Greeting.prototype.msg = 'Hi';
Greeting.prototype.say = function(){
    alert(this.msg + ', ' + this.name)
}
/* Run */
var greeting = new Greeting('Taro');
greeting.say(); /* Hi, Taro*/


次に Greeting を継承する Hello コンストラクタを定義します。 talk() メソッドを実行すると Greeting の msg 属性値を使用し confirm() 関数で挨拶してきます。 OK を選択した時のみ自身の msg 属性値で挨拶し返します。

demo noAuto noReset code

script noCode
/* Greeting */
Greeting = function(name){
    this.name = name;
}
Greeting.prototype.name = '';
Greeting.prototype.msg = 'Hi';
Greeting.prototype.say = function(){
    alert(this.msg + ', ' + this.name)
}
script
/* Hello */
var Hello = function(name){
    Greeting.call(this, name);
}
for(var i in Greeting.prototype) Hello.prototype[i] = Greeting.prototype[i];
Hello.prototype.msg = 'Hello';
Hello.prototype.talk = function(){
    if(confirm(Greeting.prototype.msg + ', I\'m ' + this.name)){
        this.say()
    }
}
/* Run */
var hello = new Hello('Taro');
hello.talk(); /* Hi, I'm Taro → Hello, Taro */


次に Hello を継承する Bye コンストラクタを定義します。 msg 属性値のみ “Bye” で上書きし、その他の拡張は行いません。

demo noAuto noReset code

script noCode
/* Greeting */
Greeting = function(name){
    this.name = name;
}
Greeting.prototype.name = '';
Greeting.prototype.msg = 'Hi';
Greeting.prototype.say = function(){
    alert(this.msg + ', ' + this.name)
}
script noCode
Hello = function(name){
    Greeting.call(this, name);
}
for(var i in Greeting.prototype) Hello.prototype[i] = Greeting.prototype[i];
Hello.prototype.msg = 'Hello';
Hello.prototype.talk = function(){
    if(confirm(Greeting.prototype.msg + ', I\'m ' + this.name)){
        this.say()
    }
}
script
/* Bye */
var Bye = function(name){
    Hello.call(this, name);
}
for(var i in Hello.prototype) Bye.prototype[i] = Hello.prototype[i];
Bye.prototype.msg = 'Bye';
/* Run */
var bye = new Bye('Taro');
bye.talk(); /* Hi, I'm Taro → Bye, Taro */

親コンストラクタの参照

次は、動的に親コンストラクタを参照する方法を考えてみます。
talk() メソッドで最初に表示される “Hi” は、Hello コンストラクタから見ると親コンストラクタに当たる Greeting の属性値ですが、 Bye コンストラクタから見ると親の親に当たります。
bye.talk() とした場合は、親コンストラクタの属性値である “Hello” が表示されるようにしてみます。

継承処理の一環として以下記述を追加し、インスタンス化されたオブジェクト自身が継承元のコンストラクタを参照できるようにします。。

Hello.prototype.__super = Greeting;
Bye.prototype.__super = Hello;


talk() メソッドを以下のように書き換えます。

Hello.prototype.talk = function(){
    //if(confirm(Greeting.prototype.msg + ', I\'m ' + this.name)){
    //↓
      if(confirm(this.__super.prototype.msg + ', I\'m ' + this.name)){


実行してみます。

demo code noAuto

script
//Greeting
var Greeting = function(name){
    this.name = name;
}
Greeting.prototype.name = '';
Greeting.prototype.msg = 'Hi';
Greeting.prototype.say = function(){
    alert(this.msg + ', ' + this.name)
}
//Hello
var Hello = function(name){
    Greeting.call(this, name);
}
for(var i in Greeting.prototype) Hello.prototype[i] = Greeting.prototype[i];
Hello.prototype.__super = Greeting;
Hello.prototype.msg = 'Hello';
Hello.prototype.talk = function(){
    if(confirm(this.__super.prototype.msg + ', I\'m ' + this.name)){
        this.say()
    }
}
//Bye
var Bye = function(name){
    Hello.call(this, name);
}
for(var i in Hello.prototype) Bye.prototype[i] = Hello.prototype[i];
Bye.prototype.__super = Hello;
Bye.prototype.msg = 'Bye';
//RUN
var hello = new Hello('Taro'); // Hi, I'm Taro → Hello, Taro
hello.talk();
var bye = new Bye('Jiro'); // Hello, I'm Jiro → Bye, Jiro
bye.talk();

hello.talk()、bye.talk() の実行時、それぞれ継承元の msg 属性値である “Hi” と “Hello” が表示されることが確認できます。

コンストラクタ生成処理を汎用化してみる

継承処理の for 文や __super への格納等の冗長な処理を汎用化してみます。

var Constructor = function(__super, constructor, prototype){
    if(!prototype) {
        prototype = constructor;
        constructor = __super;
    }
    if(__super){
        for(var i in __super.prototype) constructor.prototype[i] = __super.prototype[i]
        constructor.prototype.__super = __super;
    }
    for(var i in prototype) constructor.prototype[i] = prototype[i];
    return constructor;
}


以下のようにコンストラクタの定義を行います。

//Greeting
var Greeting = Constructor(function(name){
    this.name = name;
},{
    name : '',
    msg : 'Hi !',
    say : function(){
        alert(this.msg + ' ' + this.name)
    }
});
//Hello
var Hello = Constructor(Greeting, function(name){
    Greeting.call(this, name);
},{
    msg : 'Hello !',
    talk : function(){
        if(confirm(this.__super.prototype.msg + ' I\'m ' + this.name)){
            this.say()
        }
    }
});
//Bye
var Bye = Constructor(Hello, function(name){
    Hello.call(this, name);
}, {
    msg : 'Bye !'
});

継承を行うときのみ第一引数に継承元のコンストラクタを指定します。


実行してみます。

demo code noAuto

script noCode
var Constructor = function(__super, constructor, prototype){
    if(!prototype) {
        prototype = constructor;
        constructor = __super;
    }
    if(__super){
        for(var i in __super.prototype) constructor.prototype[i] = __super.prototype[i]
        constructor.prototype.__super = __super;
    }
    for(var i in prototype) constructor.prototype[i] = prototype[i];
    return constructor;
}
//Greeting
Greeting = Constructor(function(name){
    this.name = name;
},{
    name : '',
    msg : 'Hi !',
    say : function(){
        alert(this.msg + ' ' + this.name)
    }
});
//Hello
Hello = Constructor(Greeting, function(name){
    Greeting.call(this, name);
},{
    msg : 'Hello !',
    talk : function(){
        if(confirm(this.__super.prototype.msg + ' I\'m ' + this.name)){
            this.say()
        }
    }
});
//Bye
Bye = Constructor(Hello, function(name){
    Hello.call(this, name);
}, {
    msg : 'Bye !'
});
script
var hello = new Hello('Taro'); // Hi, I'm Taro → Hello, Taro
hello.talk();
var bye = new Bye('Jiro'); // Hello, I'm Jiro → Bye, Jiro
bye.talk();

汎用化前と同じ処理結果が得られることが確認できます。

最後に

実用の際にはパラメータのディープコピー等、他にも必要そうなものはあるかと思いますがとりあえずここまでとしときます。

最後のコンストラクタ生成処理の汎用化はすごい懐かしい気持ちになりました・・・ UI コンポーネントをこれと継承処理で独自定義してよく活用していました。

あと prototype とか this についてはいろいろな意見があったような気がしますが、個人的にはあのローコスト感とか apply や call を活用できる柔軟性は結構好きです。 変に他の言語を意識することなく JavaScript は JavaScript らしさを貫いてほしいと思う次第です。