Javascriptでオブジェクト指向(クラス,オーバーライド,カプセル化)

カタログ
  1. 1. Javascriptでオブジェクト指向
    1. 1.1. 関数はクラス
    2. 1.2. クラスの定義
  2. 2. constructor
    1. 2.1. constructorからコンストラクタを辿れるとは限らない
  3. 3. プロトタイプチェーン
    1. 3.1. __proto__に入るもの
    2. 3.2. instanceofと__proto__
    3. 3.3. prototype は別のオブジェクトに変更される可能性があるので…
    4. 3.4. __proto__ === null で止まる
    5. 3.5. 謎の Object.__proto__
      1. 3.5.1. プロトタイプに関する参考リンク
  4. 4. アクセサ
  5. 5. オーバーライド?
    1. 5.1. プロトタイプチェーンを使用しない場合
    2. 5.2. プロトタイプチェーンの利用
  6. 6. よく分からない prototype の挙動
    1. 6.1. 上のコードでエラーが出る理由
  7. 7. インターフェース
  8. 8. カプセル化
  9. 9. Wordpress時のコメントログ

※Javascriptにクラスは無いけどクラスという記述しています。

Javascriptでオブジェクト指向

Javascriptはプロトタイプベース(インスタンスベース、委譲による継承)なOOP。

JavaなどはクラスベースなOOP。

関数はクラス

1
2
3
4
5
6
7
8
function Foobar() {};

// 関数に new でインスタンスを生成できる。
// この関数はコンストラクタとして働く
var foobar = new Foobar();

alert( foobar.constructor ); // function Foobar() {}
alert( foobar instanceof Foobar ); // true

JavascriptではJavaのようなクラスは存在しないけど、関数をクラス的な存在として使うことができる。

クラスの定義

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var Foobar = function(arg) {
// このような定義にすると new した全てのインスタンス
// 対してここで代入している内容が別々に作られてしまう。
this.var = arg;
this.method = function(str) {
alert(str);
};
};

// プロトタイプに定義することでコンストラクタ内で関数定義する
// ときのような問題点は解消される。
Foobar.prototype.pVar = 123;
Foobar.prototype.pMethod = function (str) {
alert(str);
}

// fooインスタンス自身が持つメンバがコールされる
var foo = new Foobar(999);
foo.method(foo.var); // 999

// プロトタイプに定義したメンバにアクセスする。
// これは foo.pMethod == undefined の場合は
// Foobar.prototype.pMethod にアクセスする仕様になっている為…
// (正しくは foo.__proto__.pMethod : new で生成されたインスタンスの
// __proto__ プロパティには インスタンスの prototype の参照が入れられる)
// foo.pVar == undefined なら foo.__proto__.pVar にアクセス。
foo.pMethod(foo.pVar); // 123
foo.__proto__.pMethod(foo.__proto__.pVar); // 上の1行と同じの結果

プロトタイプを利用する場合は、生成したインスタンスの分だけメソッドが生成されるというような無駄が生じない。

constructor

constructor はコンストラクタを保持するためのプロパティ。 関数定義時に、その関数オブジェクト.prototype.constructor にその関数オブジェクトの参照が渡される。 constructor はインスタンス自身が持っているプロパティではなく、関数オブジェクトの.prototype.constructorをプロトタイプチェーンで読み取っている。

1
2
3
4
5
6
7
8
9
var Hoge = function() {};
alert( Hoge.prototype.constructor === Hoge ); // true

var instance = new Hoge; // HogeConstructor

// instance.__proto__ には Hoge.prototype が入っているため
// instance.constructor つまり instance.__proto__.constructor
// には Hoge が入っている。
alert( instance.constructor ); // Hoge.toString()の内容がアラート

constructorからコンストラクタを辿れるとは限らない

1
2
3
4
5
6
7
8
function A(){};
function B(){}; // この時点では B.prototype.constructor === B だが…
B.prototype = new A; // ここで B.prototype.constructor === A になっている
var b = new B;
var bConstructor = b.__proto__.constructor;

alert(bConstructor === A); // true
alert(bConstructor === B); // false

プロトタイプチェーン

アクセスしたプロパティが未定義の場合は__proto__が持つプロパティにアクセスする。

__proto__のプロパティにアクセスしても未定義だった場合は、さらに__proto__.__proto__

にアクセスする…それでも未定義なら更に上の__proto__にアクセスする。

この連鎖をプロトタイプチェーンという。

プロトタイプチェーンという言葉の中のプロトタイプが指すものは コンストラクタ.prototype ではなくて、コンストラクタから渡されたインスタンス.__proto__プロパティ(FireFoxなどの拡張仕様で本来はアクセスできない)ので間違えないように…というか思いっきり間違えていた。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 親とするオブジェクト
MetaVar = function() {};
MetaVar.prototype.exec = function() {
alert("MetaVar");
};

Hoge = function() {
// 親のコンストラクタ実行
MetaVar.apply(this, arguments);
};
// Hoge に MetaVar を継承させる
Hoge.prototype = new MetaVar;

var instance = new Hoge();
instance.exec(); // alert「MetaVar」
/* instance.exec == undefined かつ
- instance.__proto__.exec == undefined だけど
- instance.__proto__.__proto__.exec != undefined なので
- instance.__proto__.__proto__.exec にアクセスする。
- この場合は MetaVar.prototype.exec と同じものにアクセスする。
*/


alert( instance instanceof MetaVar ); // alert「true」

instanceof演算子もプロトタイプチェーンを辿る。

__proto__に入るもの

IE では使えないけど プロトタイプチェーンで辿っているのは__proto__プロパティ(厳密には [[prototype]] らしい)。

new でインスタンスを生成した時に、そのインスタンスの__proto__プロパティに コンストラクタ.prototype の参照が代入される。

1
2
3
4
5
6
7
8
var A = function() {
this.piyo = null;
};
var a = new A;

alert( a.__proto__ === A.prototype ); // true
alert( a.__proto__ === a.constructor.prototype ); // true
alert( A.prototype === a.constructor.prototype ); // true

instanceofと__proto__

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function A(){};
function B(){};
B.prototype = new A;
var b = new B;

alert(b instanceof B); // true
alert(b instanceof A); // true

var buffer = b.__proto__.__proto__;
b.__proto__.__proto__ = null;
alert(b instanceof B); // true
alert(b instanceof A); // false

b.__proto__ = buffer;
alert(b instanceof B); // false
alert(b instanceof A); // true

buffer = A.prototype;
A.prototype = {};
alert(b instanceof B); // false
alert(b instanceof A); // false

A.prototype = buffer;
b.__proto__ = {};
alert(b instanceof B); // false
alert(b instanceof A); // false

b instanceof A は A.prototype と一致するものを bのプロトタイプチェーンから探し、見つかれば true を返す。

prototype は別のオブジェクトに変更される可能性があるので…

new 直後は ( Class.prototype === instance.__proto__ ) === true であるものの、この関係が常に保証されるわけではない。

1
2
3
4
5
6
7
8
function A(){};
A.prototype = {};

var instance = new A;
alert(A.prototype === instance.__proto__); // true

A.prototype = {};
alert(A.prototype === instance.__proto__); // false

__proto__ === null で止まる

インスタンスのプロトタイプチェーンは基本的には Object.prototype.__proto__ で止まる (Object.prototype.__proto__ は null)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var A = function() {};
var o = new Object();
A.prototype = o; // Objectを継承
var a = new A;

// a.__proto__ には o が入っている
alert( a.__proto__ === o ); // true

// o.__proto__(a.__proto__.__proto__) には Object.prototype が入っている
alert( a.__proto__.__proto__ === Object.prototype );

// Object.prototype.__proto__(a.__proto__.__proto__.__proto__)
// には null が入っている
alert(a.__proto__.__proto__.__proto__); // null
alert(a.piyo); // undefined

__proto__ を辿って最終的に __proto__ == null だった場合は undefined が返される。

謎の Object.__proto__

Object.__proto__ にも何らかのオブジェクトが入っている。

1
2
3
4
var enigma = Object.__proto__; // 謎のオブジェクト
alert( enigma ); // 空の関数
alert( enigma.__proto__ === Object.prototype ); // true
alert( enigma.__proto__.__proto__ ); // null

これは何もしない空の関数で __proto__ プロパティを持ち、この__proto__の中身は Object.prototype のものと同じ。

1
2
3
var A = function() {};
alert( A.__proto__ === Object.__proto__ ); // true
alert( A.__proto__ === Array.__proto__ ); // true

FunctionやArrayの __proto__ にもコレが入っているらしい。

プロトタイプに関する参考リンク

アクセサ

1
2
3
4
5
6
7
8
9
10
11
12
13
var xxx = {
yyy : undefined,
get zzz () { return this.yyy; },
set zzz (arg) { this.yyy = arg; }
};

alert(xxx.yyy); // undefined
alert(xxx.zzz); // undefined

xxx.zzz = "piyopiyo";
alert(xxx.yyy); // piyopiyo
alert(xxx.zzz); // piyopiyo
alert(xxx.zzz = "hogehoge"); // hogehoge
1
2
3
4
5
// このスクリプトは動かない
var ppp = {
qqq : undefined,
get qqq () {} // フィールドと同じ名前はエラー
};

get, set でゲッター・セッターの定義はできるものの、隠蔽できるわけではない。

オーバーライド?

プロトタイプチェーンを利用することでオーバーライドっぽい動作を実現できる

プロトタイプチェーンを使用しない場合

ただ単に関数オブジェクトを別のものにするだけでも似たようなことはできる。

1
2
3
4
5
6
7
8
9
10
11
12
// ただ単に関数自体を別物にするパターン
var f;

f = function() {
alert("HOGE");
};
f(); // HOGE

f = function() {
alert("PIYO");
};
f(); // PIYO

ただし、この場合は最初の関数は消えてしまう。

※これをオーバーライドと呼べるのかは知らない

プロトタイプチェーンの利用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var Super = function(isAlert) {
// コンストラクタ
if ( isAlert === false ) {
alert("SuperConstructor");
}
};
Super.prototype.func = function() {
alert("Super.prototype.func");
};

var Sub = function() {
// スーパークラスのコンストラクタを呼び出す
Super.apply(this, arguments);

// 派生クラスのコンストラクタ
alert("SubConstructor");
};
Sub.prototype = new Super(false);
Sub.prototype.func = function() {
alert("Sub.prototype.func");
};

var obj = new Sub();
obj.func(); // Sub.prototype.func

// 削除すると…
delete Sub.prototype.func;
obj.func(); // Super.prototype.func
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var Super = function() {
this.number = 123;
};
Super.prototype = {
number : 123,
getNumber : function() {
return this.number;
}
};

var A = function(number) {
if ( number != null ) {
alert( "number changed" );
this.number = number;
}
};
A.prototype = new Super();
A.prototype.getNumber = function() {
return this.number * 10;
};

alert( new A().getNumber() ); // 1230
alert( new A(987).getNumber() ); // number changed -> 9870

var B = function() {
this.number = 567;
};
B.prototype = new Super();
alert( new B().getNumber() ); // 567

よく分からない prototype の挙動

1
2
3
4
5
6
7
8
var A = function() { alert("A"); };
A.prototype.hoge = "tesu";

var B = function() { alert("B"); };
B.prototype = { hoge : "tesu" };

alert( new A().constructor );
alert( new B().constructor );

new A().constructor の結果が function() { alert(“A”); } なのに new B().constructor の結果が function Object() { [native code] } になってしまう…なんで???

おかげで以下のようなエラーが出ることがある。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var A = function() {};
A.prototype.exec = function() { alert("A"); };

var B = function() {};
B.prototype = {
exec : function() { alert("B"); }
};

var C = function() {};
C.prototype.exec = function() { alert("C"); };

C.prototype = new A();
new C().constructor.prototype.exec.call(C); // 「A」とアラートが出る

C.prototype = new B();
new C().constructor.prototype.exec.call(C); // execメソッド無いとブラウザ激怒

スーパークラスのメソッドを呼べないじゃん… 何が起きてるんだろう。いろいろと気持ち悪い。 誰か教えてください orz

上のコードでエラーが出る理由

コメントを頂いて理解できた! コメントして頂いたひよこさん、解説ありがとうございました!

1
2
3
4
5
6
7
8
9
10
A = function(){};
B.prototype = new A();
B.prototype = {
doSomething : function(){/*何かする*/};
};

C.prototype = new Object();
C.prototype.doSomething = function(){/*何かする*/}

// B と C は意味的には等価 (もちろん別インスタンスという違いはある)

上のコードで B と C が等価になるのは、B.prototype = { /* 略 */ } としてしまうと new A() で生成したインスタンスの代わりに { /* 略 */ } が入ってしまうので、new A() で生成したインスタンスは使われず無意味に消えてしまうから。prototypeプロパティはただのオブジェクトに過ぎない。結果的には B は Object を継承したコンストラクタになってしまい A は無関係のものとなってしまう。当然 B に継承されたオブジェクトがあってもアクセス不能になる。

インターフェース

Javascriptの機能としてインターフェースは存在しないので、インターフェースっぽいものを勝手に実装して下さいってことらしい。

参考:JavaScriptでJavaのInterfaceを実現する方法 - yinkywebの日記

カプセル化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var Class = (function() {
// privateっぽいもの
// この環境でのみアクセス可能
var privateField = "private!!!";
var privateMethod = function() {
alert(privateField);
};

var clazz = function() {
alert("コンストラクタ");
};
clazz.prototype.publicMethod = function() {
privateMethod();
};

return clazz;
})();

var obj = new Class();
obj.publicMethod();
obj.privateMethod(); // エラー

可視性を指定するものなんて無いのでクロージャで隠蔽する。protected なものって作れるのかな。

ってかこのコードだと static private な感じの中途半端なものになってしまう…どうしよう

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var Class = (function() {

var num = 0;

var C = function() {};

C.prototype.func = function() {
alert(++num);
};

return C;
})();

var a = new Class();
var b = new Class();

a.func(); // 1
b.func(); // 2
alert(a.m); // undefined

num は インスタンスメンバ的なものになるかと思ってたけど… ここで num は static メンバ的な動作になってしまう…駄目じゃん orz

だよなぁ…隠蔽してかつ static ではない private をやろうとすると… どうなるんだってばよ?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
var Class = (function() {
/*
- この var は static private な動作をする
*/

var staticPrivateNum = 0;

// クラス
var clazz = function() {
/*
- この var は private な動作をする
*/

var privateNum = 0;

// インスタンスごとに別のprivateNumに
// アクセスさせようと思うと
// prototype ではないプロパティに
// 関数定義しないと無理だよな…
// new毎にメソッドができてしまうのはしかたがないのか…
this.func1 = function() {
alert( ++privateNum );
};

// これはダメ…
// 最後に new Class したときにできた
// privateNum にアクセスしてしまう
clazz.prototype.func3 = function() {
alert( ++privateNum );
};
};

// staticメンバにアクセスするものであれば
// prototype に定義すればOK
clazz.prototype.func2 = function() {
alert( ++staticPrivateNum * 100 );
};

return clazz;
})();

var a = new Class();
var b = new Class();

a.func1(); // 1
b.func1(); // 1
a.func1(); // 2
b.func1(); // 2
a.func1(); // 3
b.func1(); // 3
alert( a.privateNum ); // undefined

a.func2(); // 100
b.func2(); // 200
a.func2(); // 300
b.func2(); // 400
a.func2(); // 500
b.func2(); // 600
alert( a.staticPrivateNum ); // undefined

a.func3(); // 4
b.func3(); // 5

b.func1(); // 6

うーん…なんか微妙。

参考 : Private Members in JavaScript

Wordpress時のコメントログ

ひよこ より: 2012年9月10日 12:04 PM

var A = function() {};
// この時 A.prototype.constructorに”自動で”Aが代入される。

A.prototype = {methodA: function(){}};
// prototype にオブジェクトを代入することで、
// 自動で代入されたA.prototype.constructorはA.prototypeごと上書きされる。

A.prototype = {constructor: A, methodA: function(){}};
// もしconstructorプロパティを利用するなら、このようにしておく

var B = function() {};
B.prototype = new A(); // EcmaScript5な環境ならば Object.create(A.prototype); とするべき
var b = new B();
// このとき b.constructor が A になるのは new A() のconstructorプロパティを読んでいるから

// b.constructor は A ではなく B を返すのが正しい(と思う)ので、
B.prototype = new A();
// の後で
B.prototype.constructor = B;
// としておいたほうがいいかもしれない。

// C は A を継承して、 methodAを書き換える
var C = function(){};
C.prototype = {constructor: C, methodA: function(){ /* override */ }};
var c = new C();
// もし C の methodA ではなくて A のmethodAを呼び出したいなら
// constructorプロパティは利用せず
A.prototype.methodA.call(c);
// のようにする。

// constructorプロパティは文字通りコンストラクタを保持するプロパティ。
// 本来は、継承元との関係はない。

Fernweh より: 2012年9月10日 11:09 PM
__proto__が使えない場合にどうやって親にアクセスするのかと思いましたが、親.prototype.メソッド.call(インスタンス); のように 親オブジェクトを直接指定するのですね。これをやりたくなくて constructor から呼び出していたのですが、ひよこさんの説明を見ていろいろ勘違いしすぎていることが分かりました orz

とはいえコメントのおかげでどんな挙動をしているのかなんとなく分かりました。
わかり易い説明をしていただいてとても嬉しいです。

関連があるかもしれない記事