jumble

アクセスカウンタ

zoom RSS JavaScript 継承

<<   作成日時 : 2011/06/29 17:51   >>

ブログ気持玉 0 / トラックバック 1 / コメント 0

JavaScript 継承


以前、似たようなテーマで記事を投稿していたが、内容がイマイチなので削除した。
で、改めて投稿することにした。
テーマはJavaScriptの継承である。
JavaScriptにはクラスという概念はないが、Javaのクラスベースの継承と似たことがやりたい。
まず、次のようなコンストラクタ関数を定義する。
実際にはクラスではないが、これをクラスに見立てる。

function ClassA(p1, p2){
    this.p1 = p1;
    this.p2 = p2;
};

次に、メソッドに相当する関数をプロトタイプオブジェクトに追加する。

var prot = ClassA.prototype;
prot.m1 = function(){
    return "* " + this.p1;
};
prot.m2 = function(){
    return "* " + this.p2;
};

これでClassAという擬似クラスが完成した。もちろん実体は単なるコンストラクタ関数だが。
これを継承してClassBという擬似クラスを定義することを考える。
(もちろん、ClassBの実体もコンストラクタ関数。)
なお、ここのコードは即時関数などの中で記述し、不要なグローバル変数を定義しないようにする方がいいだろう。

定番の方法


まずはいろんな資料で紹介されている定番の方法。

function ClassB(p1, p2, p3){
    ClassA.call(this, p1, p2);
    this.p3 = p3;
};
ClassB.prototype = new ClassA(); // (1)
var prot = ClassB.prototype;
prot.m2 = function(){ // override
    return "** " + this.p2;
};
prot.m3 = function(){
    return "** " + this.p3;
};

さて、確認しよう。

var cb = new ClassB(10, 20, 30);
alert(cb.m1());
alert(cb.m2());
alert(cb.m3());

"* 10", "** 20", "** 30"と順に出力される。
とりあえずOKのようだ。オーバーライドもうまくいっている。
ただし、ここままでは cb.costructor == ClassB が false となるので、次を追加する方がいいだろう。

prot.constructor = ClassB; // (2)

まあ、定番の方法だからうまくいくのは当たり前だけど。
ただし、ここまでだとある欠点が残るのだ。
それは、ClassB.prototype に p1 と p2 という不要なプロパティができてしまう点だ。
そのことは、"p1" in ClassB.prototype が true となることから確認できる。p2 も同様。
致命的ではないかもしれないが、やっぱりこれは避けたい。
よって、万全を期すためには次のコードが必要となる。

delete prot.p1;
delete prot.p2;

このことは、O'REILLYの「JavaScript 第5版(David Flanagan 著, 村上列 訳)」の170-171ページに例9-3として書いてある。
うーん。この最後のdeleteが自分としては許せない。
100個のプロパティがあれば100回削除するハメになる。
もちろん、ループを組むことになるとは思うが。

__proto__を使用する方法


IE以外の主要ブラウザ(FireFox, Chrome, Opera, Safari)では、全オブジェクトに__proto__というプロパティがある。
これは、そのオブジェクトのプロトタイプオブジェクトを参照している。
これが使えると、継承は綺麗で簡潔に行うことができる。
上記の(1)の部分を次のように書き換える。

ClassB.prototype.__proto__ = ClassA.prototype;

この場合、コンストラクタ関数を設定するための(2)も不要。
かつ、最後の delete も当然不要。
継承としてはこれが一番綺麗で簡潔だと思う。
ただ、悲しいかな__proto__は非標準だ。
それでもIEさえ実装してくれれば事実上の標準になるのだが。
削除した記事ではこの__proto__にこだわりすぎた。
実際、FireFoxのJavaScriptの説明のページに、定番の方法をわざわざ否定し、__proto__ を使う方法が紹介されていたのだ。
でもそれは過去の話。
今は、ECMA-262-5thで標準に追加された、Object.create(...)を使用する方法が紹介されている。
ちなみにこのページ

Object.create(...)を使用する方法


Objectクラス(正確にはコンストラクタ関数)にcreate(...)というメソッドが追加されたことは知っていた。
それは、削除した記事を書いている時点で、既にその仕様も知っていた。
ただ、それが継承とどう結びつくのか分からなかった。
鳴り物入りで追加された感があったので、それを使えばもっと綺麗にできるかもしれないとは思っていた。
でも、確証もなかったし、どうしても思いつかなかった。
で最近、上記のFireFoxの説明のページをみて、なるほど、となったわけだ。
こうなると自分もまだまだだなと思ってしまう。
でも、言い訳がましいが、このObject.create(...)を使った継承はどこ見てもあまり見かけない。
上記のFireFoxのページと、O'REILLYの「JavaScriptパターン(Stoyan Stefanov 著, 豊福剛 訳)」という本に似た記述がある。
JavaScriptの大権威である、Douglas Crockford氏さえ言及していないと思う。
その著書である、O'REILLYの「JavaScript The Good Parts(Douglas Crockford 著, 水野貴明 訳)」にもなかった(と思う)。
その本が書かれたときは、まだObject.create(...)は標準ではなかったかもしれない。
でも、本の中でわざわざ同じ仕様のObject.create(...)を自力で定義しているのだ。
それでいて、それを使った擬似クラスベースのスマートな方法を書いてくれていないのだ。
それってどうなのと思ってしまう。
上記著書で紹介されているクラスベースではない継承の方法は、オブジェクトを大量生産するとメモリが大量に無駄になりそう。
そう思うのは自分だけだろうか?
それとも、根本的に何か勘違いしている部分があるのかな?

何はともあれ、Object.create(...)を使用して継承してみよう。
上記の(1)を次のように変更する。

ClassB.prototype = Object.create(ClassA.prototype);

言われてみれば簡単だ。
基本的には、new ClassA()を使用した定番的なやり方と同じ。
コンストラクタ関数を設定する(2)は必要となる。
ただ、余計なプロパティが入らないので、最後の delete は不要になる。
コンストラクタ関数の設定はまだ許せるので、この方法が使えるなら、__proto__に強硬にこだわる必要もない気がする。

プロトタイプオブジェクトを使用しない方法


これは、継承と言うより擬似クラスとなるコンストラクタ関数の定義方法について。
メソッドに相当する関数をデータのプロパティと同様に、生成するオブジェクトに直接設定している例が結構ある。
例えば、ClassA の例だと次のような感じ。

function ClassA(p1, p2){
    this.p1 = p1;
    this.p2 = p2;
    this.m1 = function(){
        return "* " + this.p1;
    };
    this.m2 = function(){
        return "* " + this.p2;
    };
}

で、ClassBは次のような感じ。

function ClassB(p1, p2, p3){
    ClassA.call(this, p1, p2);
    this.p3 = p3;
    this.m2 = function(){ // override
        return "** " + this.p2;
    };
    this.m3 = function(){
        return "** " + this.p3;
    };
}

困ったことに、この例が一番コードが短く、綺麗に見える。
しかも、動作も問題ない。
強いて欠点を挙げると、cb instanceof ClassA が false になるくらいか。
ただし、自分自身はこの方法は論外だと思っている。
もしこれらのコンストラクタにより大量のオブジェクトを生成すると、大量のメモリが無駄に消費される。
理由は、オブジェクトを生成する度にメソッドに相当する関数を新たに生成するからだ。
メソッドは、よほど特別な事情がない限り、オブジェクトごとに処理が変わることがない。
とすると、オブジェクトごとに関数を新たに生成する理由はどこにもないのだ。
メソッドに相当する関数は1つで十分のはず。
なので、プロトタイプオブジェクトに設定するべきだろう。

Object.create(...)を自力で定義


今現在、主要ブラウザの最新バージョンでObject.create(...)が実装されていないのは、Operaくらいだった。
ただ、バージョンアップが頻繁なので、既に嘘になっているかもしれない。
まあ、近いうちに実装されるだろう。
ただ、古いバージョンのブラウザでは当然そんなものは使えない。
よって、自分は次のようなコードを実行している。

(function(){
    function Cons(){}
    if(!Object.create){
        Object.create = function(prot){
            Cons.prototype = prot;
            return new Cons();
        }
    }
})();

このコードは、前述の「JavaScript The Goods Parts」で紹介されていた方法を少し改良した。
本の通りだと、オブジェクトを生成する度に関数が生成されるから。
その関数は即座に不要になってガーベッジコレクションに回収されるだろう。
その関数は、生成したオブジェクトとそのプロトタイプとなるべきオブジェクトを結びつけるだけのダミー関数だ。
よって、毎回生成しなおす必要もないので、少し変更してある。

テーマ

関連テーマ 一覧


月別リンク

ブログ気持玉

クリックして気持ちを伝えよう!
ログインしてクリックすれば、自分のブログへのリンクが付きます。
→ログインへ

トラックバック(1件)

タイトル (本文) ブログ名/日時
レイバン サングラス
JavaScript 継承 jumble/ウェブリブログ ...続きを見る
レイバン サングラス
2013/07/05 18:20

トラックバック用URL help


自分のブログにトラックバック記事作成(会員用) help

タイトル
本 文

コメント(0件)

内 容 ニックネーム/日時

コメントする help

ニックネーム
本 文
JavaScript 継承 jumble/BIGLOBEウェブリブログ
文字サイズ:       閉じる