#プロトタイプと、あとクラス、継承、ミックスインと呼ばれている物の説明
この文書はなるべく正確な情報を書きたいのでちょくちょく更新されます。あと、ちょくちょくキャラが変わるのは気にしないでください。修正した部分に関する情報は コメント やリビジョンを参照してください。
Javascriptの変数にはプリミティブ型とオブジェクト型が存在する。
###プリミティブ型
数字、文字列、真偽値、undefined、nullの5つ。値型。
###オブジェクト型
上で書いた型以外の型。参照型。いわゆるハッシュというデータ構造。
//fooオブジェクトを持つhogeプロパティを作る例
var foo = {
hoge: 1
};
foo.hoge === foo['hoge']; //同じ
####プロパティの属性
実際のところプロパティは以下のような属性をもっている。
属性名 | 型 | 説明 |
---|---|---|
`value` | 全ての型 | プロパティの実際の値 |
`get` | 関数か `undefined` | プロパティを読み込んだときに `get` に関数が登録されているのならそれが呼ばれる |
`set` | 関数か `undefined` | プロパティに値を書き込むときに `set` に関数が登録されているのならそれが呼ばれる |
`writable` | `boolean` | 書き換え可能かどうか |
`enumerable` | `boolean` | `for in` で列挙されるかどうか |
`configurable` | `boolean` | プロパティの削除、属性の変更、データプロパティからアクセッサープロパティへ(後述)の変更ができなくなる |
さらにプロパティが持つ属性によってデータプロパティ、アクセッサープロパティ(と、隠しプロパティというものもある)に分類できる。
- データプロパティが持つ属性:
value
,writable
,enumerable
,configurable
- アクセッサープロパティが持つ属性:
get
,set
,enumerable
,configurable
ちなみに value
と get
のような両方にまたがる属性を持つプロパティを作ることはできない。
####プロパティを操作する関数
Object
オブジェクトにはプロパティを操作する便利な関数がたくさん登録されている。例えば Object.getOwnPropertyDescriptor
でオブジェクト自身のプロパティの属性を調べたり、
Object.defineProperty
で属性を設定しながらプロパティの追加ができる。
var foo = {};
foo.bar = 1;
//fooオブジェクトのbarプロパティの属性を調べる
Object.getOwnPropertyDescriptor(foo, 'bar');
/*
{
value: 1,
writable: true,
enumerable: true,
configurable: true
}
*/
//fooオブジェクトにhogeプロパティを属性を設定しながら追加する
Object.defineProperty(foo, 'hoge', {
value: 3,
writable: true,
enumerable: false,
configurable: true
});
他のプロパティを操作する方法やその他の詳しい情報については以下を参考にして欲しい。
##プロトタイプ
プロトタイプとはオブジェクト型の変数が持つ隠しプロパティで、通常は読み書きすることができないが、 foo.__proto__
や Object.getPrototypeOf(foo)
で読み書きすることができる。
var foo = {};
var bar = {hoge: 1};
foo.__proto__ = bar;
Object.getPrototypeOf(foo) === bar; //true
また foo = Object.create(bar)
でプロトタイプが bar
オブジェクトの foo
オブジェクトを作ることができる。
var bar = {hoge: 1};
var foo = Object.create(bar);
Object.getPrototypeOf(foo) === bar; //true
###プロトタイプチェーン
上の例では foo.hoge
は1を返す。 foo
自身には hoge
というプロパティがないにもかかわらず。この例で foo.hoge
が1を返すまでどうやって処理を行なっているのかを見てみよう。
foo
にhoge
というプロパティがあるかチェックする- なかった!
foo
のプロトタイプであるbar
を調べよう bar
にhoge
というプロパティがあるかチェックする- あった!
bar
のプロパティhoge
を返そう(1が返る)
このようにプロトタイプを辿ることをプロトタイプチェーンと呼ぶ。
プロトタイプチェーンは再帰的に書ける(オブジェクト o
からプロパティ名が name
というプロパティを探すとして)
o
自身にname
という名前のプロパティが存在するか
- ある :
o[name]
を返す - ない :
- プロトタイプがオブジェクト型 :
o
のプロトタイプに対して1.からやり直す - null :
undefined
を返す
- プロトタイプがオブジェクト型 :
コードにすると以下のようになる。
function getProp(o, name) {
//oにnameが存在するか
if(o.hasOwnProperty(name)) { //ある
return o[name];
} else { //ない
var proto = Object.getPrototypeOf(o);
var type = typeof proto;
if(proto !== null && type === 'function' || type === 'object') { //プロトタイプがオブジェクト型
//`o` のプロトタイプに対して1.からやり直す
return getProp(proto, name);
} else { //それ以外
return undefined;
}
}
}
###プロトタイプチェーン上のthis
あるオブジェクト o
のプロトタイプチェーン上のプロパティに関数 f
が存在したときに o.f()
で f
を読み込んだ時のfの中で使っている this
は o
。
var oo = {
f: function() {
return this;
}
};
var o = Object.create(oo);
o === o.f(); //true
##関数オブジェクト
Function型のオブジェクト(以下、関数オブジェクト)は以下のようにして定義することができる。
//function文を用いた定義
function foo() {
//何か
}
//無名関数を用いた定義
var bar = function () {
//何か
};
//無名関数には名前をつけることができる
var baz = function baz() {
//何か
};
//Functionをnewする
var hoge = new Function();
###prototypeプロパティ
関数オブジェクトは prototype
という特別なプロパティを持つ。この prototype
というプロパティは上で説明したプロトタイプとは違うものだということに注意してほしい。ちなみに実際の関数オブジェクトのプロトタイプは Function.prototype
を指す。
Object.getPrototypeOf(foo) === foo.prototype; //false
Object.getPrototypeOf(foo) === Function.prototype: //true
さらに、この prototype
は constructor
というプロパティを持ち、それは関数オブジェクト自体を指す。
foo.prototype.constructor === foo; //true
###Function.prototypeの便利な関数
関数オブジェクトのプロトタイプは Function.prototype
なので、そこに登録されている便利な関数を使うことができる。ここでは call
, apply
, bind
について紹介する。
####call
call
は理解しにくのでとりあえず具体的な例を見て欲しい。
var o = {
hoge: 1,
fuga: 2
};
//プロトタイプチェーン上にhogeやfugaプロパティは無いはず
var foo = function(x, y) {
return this.hoge + this.fuga + x + y;
};
//第一引数に渡されたオブジェクトoがfooの中のthisとして、
//第二引数以降の引数がfooの引数として使われている
foo.call(o, 3, 4) === 10 //true
処理の流れは以下のようになる。
foo
がプロトタイプチェーン上に存在するcall
関数を呼び出す- プロトタイプチェーン上の
this
はfoo
を指すのでcall
関数はfoo
を呼び出す関数に決定する - 第一引数
o
を呼び出す関数のthis
を指すことにする - 第二引数以降を呼び出す関数の引数とする
- 実際に呼び出す
####apply
call
は第二引数以降を呼び出す関数の引数に対して、 apply
は第二引数に配列を入れてそれを引数とする。
//同じ
foo.call(o, 1, 2);
foo.apply(o, [1, 2]);
####bind
TODO
##名状しがたきクラスのようなもの
Javascriptでは new
と関数オブジェクトを使っていわゆるクラス的なものからインスタンス的なものを作るようなことができる。このとき関数オブジェクトはコンストラクタ、 prototype
が持つプロパティはメソッド的な役割を果たす。
//コンストラクタ
var Animal = function(name) {
this.name = name;
};
//メソッド
Animal.prototype.walk = function() {
console.log(this.name + ' is walking.');
};
//newを使ってAnimalのインスタンスを作る
var pochi = new Animal('pochi');
pochi.walk(); //pochi is walkingと表示される
new
で生成された pochi
というオブジェクトはオブジェクト自体に name
、プロトタイプに walk
、 constructor
というプロパティを持つ。ちなみにオブジェクト自身が持つプロパティは Object.getOwnPropertyNames
関数で調べることができる。
Object.getOwnPropertyNames(pochi); //["name"]
Object.getOwnPropertyNames(Object.getPrototypeOf(pochi)); //["walk", "constructor"]
似たものとしては Object.keys
という関数があり、こちらはオブジェクト自身が持つプロパティの中で、enumerable
属性が true
のプロパティ名だけを返す。
Object.keys(Object.getPrototypeOf(pochi)); //["walk"]
###実際にnewがしていること
ある関数オブジェクト F
があったとしたとき new F
は次のような処理を行なう。
- プロトタイプが
F.prototype
であるオブジェクトo
をつくる o
のコンテキスト(F
の中で使っているthis
をo
ということにすること)でF
を呼ぶo
を返す
コードにすると
function new_(F) {
return function() {
var o = Object.create(F.prototype);
F.apply(o, arguments);
return o;
};
}
var Foo = function(a, b, c) {
this.a = a;
this.b = b;
this.c = c;
};
Foo.prototype.sum = function() {
return this.a + this.b + this.c;
};
//同じ
var foo1 = new Foo(1, 2, 3);
var foo2 = new_(Foo)(1, 2, 3);
##名状しがたき継承のようなもの
上で説明した知識を使うとクラスベースのオブジェクト指向言語的な継承を行うことができる。
###最も簡単な例
最も簡単に継承を実現するには子にしたい関数オブジェクトの prototype
に親にしたい関数オブジェクトを new
したものを代入すればいい。
var A = function() {
this.hoge = 'a';
};
A.prototype.walk = function() {
return this.hoge + ' is walking';
};
var B = function() {
this.hoge = 'b';
};
//子にしたい関数オブジェクトのprototypeに親にしたい関数オブジェクトをnewしたものを代入する
B.prototype = new A();
var a = new A();
var b = new B();
a.walk(); //'a is walking'
b.walk(); //'b is walking'
//プロトタイプチェーンを確認してみよう
Object.getPrototypeOf(b) === B.prototype;
Object.getPrototypeOf(B.prototype) === A.prototype;
b
のプロトタイプは B.prototype
で、 B.prototype
のプロトタイプは A.prototype
なので確かに b.walk()
は問題なく呼べることが確認できる。
ただし、この方法だと以下のような問題が生じる。
- 引数をチェックしてエラーを出すような関数オブジェクトを親にすることができない
- 子の関数オブジェクトの
prototype
のconstructor
プロパティが消える
###改良版
上の問題を解決する inherits
関数を作ってみよう。
function inherits(child, parent) {
var F = function() {};
F.prototype = parent.prototype;
child.prototype = new F();
Object.defineProperty(child.prototype, 'constructor', {
value: child,
configurable: true,
enumerable: false,
writable: true
});
}
var A = function(hoge) {
if(typeof hoge === 'undefined') throw new Error('hoge is undefiend!');
this.hoge = hoge;
};
A.prototype.walk = function() {
return this.hoge + ' is walking';
};
var B = function() {
this.hoge = 'b';
};
inherits(B, A);
var a = new A();
var b = new B();
a.walk(); //'a is walking'
b.walk(); //'b is walking'
この方法では一度ダミーの関数オブジェクトを作り、親の関数オブジェクトの prototype
をコピーすることにより1.の問題を解決する。
さらに child.prototype = new F()
したあとに 子の prototype
の constructor
プロパティを追加することで2.の問題を解決する。
###inherits関数の改良版1
そういえば Object.create
で parent.prototype
をプロトタイプにしたオブジェクトを作れたので、 inherits
関数は以下のように書き換えられる。
function inherits(child, parent) {
child.prototype = Object.create(parent.prototype);
Object.defineProperty(child.prototype, 'constructor', {
value: child,
configurable: true,
enumerable: false,
writable: true
});
}
###親コンストラクタを呼ぶ
例えば new
するときに引数を指定することがよくある。今までの知識で継承を実現しようとするとこのようなる。
var A = function(a, b, c) {
this.a = a;
this.b = b;
this.c = c;
};
var B = function(a, b, c) {
this.a = a;
this.b = b;
this.c = c;
};
inherits(B, A);
親と子のどちらにも同じことが書いてあって無駄だし、親の中身を変更したら子の方も変更しなければならない。例えばJavaだとsuperで親コンストラクタを呼べるがJavascriptには今のところそういう構文が無いのでできない。しかし call
や apply
を使えば擬似的に親コンストラクタを呼ぶような動作を実現することができる。
var A = function(a, b, c) {
this.a = a;
this.b = b;
this.c = c;
};
var B = function() {
A.apply(this, arguments);
};
/*
callの例
var B = function(c) {
A.call(this, 1, 2);
this.c = c;
};
*/
inherits(B, A);
###親のメソッドを呼ぶ
同じようにJavaで言うところの親のメソッドを呼ぶようなこともできる。
var A = function(hoge) {
this.hoge = hoge;
};
A.prototype.walk = function() {
console.log(this.hoge + ' is walking');
};
var B = function() {
A.call(this, 'b');
};
inherits(B, A);
B.prototype.walk = function() {
A.prototype.walk.call(this);
console.log(this.hoge + 'は歩いています');
};
var a = new A('a');
var b = new B();
a.walk(); //'a is walking'
b.walk(); //'b is walking'と'bは歩いています'
実現はできたが、親自体を変えたときのコストが高そうだ。
###inherits関数の改良版2
inherits
関数を実行したときに子の関数オブジェクトが親の関数オブジェクトを参照できるようにすればこの問題を解決できる。
function inherits(child, parent) {
child.prototype = Object.create(parent.prototype);
Object.defineProperty(child.prototype, 'constructor', {
value: child,
configurable: true,
enumerable: false,
writable: true
});
child.__super__ = parent.prototype; //__super__というプロパティから親のprototypeプロパティを読み込める
}
var A = function(hoge) {
this.hoge = hoge;
};
A.prototype.walk = function() {
console.log(this.hoge + ' is walking');
};
var B = function() {
B.__super__.constructor.call(this, 'b');
//関数オブジェクトのprototypeプロパティのconstructorプロパティは
//その関数オブジェクト自身という性質を利用する
};
inherits(B, A);
B.prototype.walk = function() {
B.__super__.walk.call(this);
console.log(this.hoge + 'は歩いています');
};
var a = new A('a');
var b = new B();
a.walk(); //'a is walking'
b.walk(); //'b is walking'と'bは歩いています'
ちなみにcoffeescriptではこれと同じような方法を用いている。
###親の関数オブジェクトから直接子の関数オブジェクトを生成する
親の関数オブジェクトから直接子の関数オブジェクトを生成する extend
関数を作ってみよう。
function extend(parent, props) {
parent = parent || Object;
props = props || {};
var child = props.__init__ || function(){ parent.apply(this, arguments) }; //省略したら親コンストラクタを呼ぶ関数を登録
inherits(child, parent);
Object.keys(props).forEach(function(key) {
if(key !== '__init__') child.prototype[key] = props[key];
});
return child;
}
var A = extend(null, {
__init__: function(a, b, c) {
this.a = a;
this.b = b;
this.c = c;
},
sum: function() {
return this.a + this.b + this.c;
}
});
var B = extend(A, {
mul: function() {
return this.a * this.b * this.c;
}
});
var a = new A(1, 2, 3);
var b = new B(2, 3, 4);
上の例では第一引数を親、第二引数に渡すオブジェクトの __init__
というプロパティを子のコンストラクタ、それ以外を子の prototype
プロパティのプロパティにしている。
enchant.jsとかがこれと同じだった気がする。
##名状しがたきミックスインのようなもの
今までのいわゆる継承的なもので親のメソッドを呼ぼうとするとプロトタイプを2回辿るので処理が遅くなるという問題がある。もしもメソッドだけ(いわゆるミックスイン的なもの)が欲しいのなら関数オブジェクトの prototype
プロパティに直接追加してしまえばよい。
###オブジェクトに関数を入れておいてそれを使いまわす
オブジェクトに関数を入れておいて専用の mixin
関数を使ってミックスインを実現してみよう。
function mixin(proto, methods) {
Object.getOwnPropertyNames(methods).forEach(function(key) {
if(key === 'constructor') return; //constructorプロパティは変えないようにする。
proto[key] = methods[key]; //methodsオブジェクトの内容全てをprotoオブジェクトに追加する。
});
}
var A = extend(null, {
__init__: function(a, b, c) {
this.a = a;
this.b = b;
this.c = c;
}
});
var B = extend(A);
var FooMixin = {
sum: function() {
return this.a + this.b + this.c;
},
mul: function() {
return this.a * this.b * this.c;
}
}
mixin(A.prototype, FooMixin);
mixin(B.prototype, FooMixin);
var a = new A(1, 2, 3);
var b = new B(1, 3, 4);
a.sum(); //6
b.sum(); //8
a.mul(); //6
b.mul(); //12
###ミックスインするためだけの関数を作る
call
を駆使ことによってミックスインするためだけの関数を作ることもできる。
function FooMixin() {
function sum() {
return this.a + this.b + this.c;
}
function mul() {
return this.a * this.b * this.c;
}
this.sum = sum;
this.mul = mul;
}
FooMixin.call(A.prototype);
FooMixin.call(B.prototype);
//ただしA.prototype.sumとB.prototype.sum (mulも)は違うもの
A.prototype.sum === B.prototype.sum //false
###ミックスインするためだけの関数(キャッシュ版)
クロージャを使ってキャッシュしてみよう。
var FooMixin = (function() {
function sum() {
return this.a + this.b + this.c;
}
function mul() {
return this.a * this.b * this.c;
}
return function() {
this.sum = sum;
this.mul = mul;
};
})();
FooMixin.call(A.prototype);
FooMixin.call(B.prototype);
A.prototype.sum === B.prototype.sum //true
キャッシュできるということは直接プロパティに追加してもメモリ負荷が少なくなり、さらにプロトタイプを辿る必要がなくなるので動作も速くなる(頻繁に new
するものだと逆に遅くなるかもしれない)。パフォーマンスの章を参照。
var A = function(a, b, c) {
this.a = a;
this.b = b;
this.c = c;
FooMixin.call(this);
};
var a = new A(1, 2, 3);
Object.keys(a); //["a", "b", "c", "sum", "mul"] 順番はこうならないかもしれない
##パフォーマンス
- オブジェクト生成 http://jsperf.com/new-vs-object-create-vs-mixin-create
- メソッド呼び出し http://jsperf.com/new-vs-object-create-vs-mixin-call-method
###考察
- 頻繁に
new
するものはクラス的な継承一択 - あまり
new
しないものについては、ブラウザによってキャッシュされたミックスインを使ったほうが良い場合がある - プロトタイプチェーンが意外に速い
Object.create
は今のところ微妙- というよりか今まで
new
でやっていたことをわざわざObject.create
でやる必要はない
##おわり
疲れたー。間違ってたらコメントちょーだい。
##参考文献
それぞれの話題のより詳しい情報については以下を参照。
名状しがたき継承のようなもの > 改良版 のコードにnewするコードがないようです。
の部分です。