2008年9月13日土曜日

Inheritance on JavaScript

しばらく夏休みなので、以前から読みたかった、Pro JavaScript Design Patterns を読みながら、JavaScirpt の勉強をしてます。

これまでこのブログでも何回か JavaScript の継承について書いてきたのですが、とにかくややこしい印象でした。

JavaScript の継承は、大きく分けて
  • Classical Inheritance
  • Prototypeal Inheritance
の 2通りで実現できます。

今回の記事では、この 2通りのパターンをもう一度おさらいしてみます。

Classical Inheritance

一般的に継承というと、Super Class 作って、それを継承する、というパターンを想像します。
Classical Inheritance では、その方法を JavaScript なりに実現します。

例として、Person というクラスを作成して、それを継承する Author というクラスを作成します。
/* Class Person */
function Person(name) {
this.name = name;
}

Person.prototype.getName = function() {
return this.name;
};

/* Class Author */
function Author(name, books) {
Person.call(this, name);
this.books = books;
}

Author.prototype = new Person(); // Set up the prototype chain
Author.prototype.constructor = Author; // Set the constructor attribute to Author
Author.prototype.getBooks = function() { // Add a method to Author
return this.books;
};
以下の例で、継承が実現されているのが分かります。
var author = [];
author[0] = new Author('Larry Wall', ['Perl']);
author[1] = new Author('Yukihiro Matsumoto', ['Ruby']);

author[1].getName(); // 'Yukihiro Matsumoto'
author[1].getBooks(); // ['Ruby']
この例から、以下のような extend 関数を定義しておけば、Classical Inheritance のパターンを汎用的に利用できることが分かります。
/* Extend function. */
function extend(subClass, superClass) {
var F = function() {};
F.prototype = superClass.prototype;
subClass.prototype = new F();
subClass.prototype.constructor = subClass;

subClass.superClass = superClass.prototype;
if (superClass.prototype.constructor == Object.prototype.constructor) {
superClass.prototype.constructor = superClass;
}
}
この extend 関数を以下のように利用することで、継承を簡単に実現することができるようになります。
/* Class Person */
function Person(name) {
this.name = name;
}

Person.prototype.getName = function() {
return this.name;
};

/* Class Author */
function Author(name, books) {
Author.superClass.constructor.call(this, name);
this.books = books;
}

extend(Author, Person);

Author.prototype.getBooks = function() {
return this.books;
};

Prototypal Inheritance

Prototypal Inheritance とは、プロトタイプとなるオブジェクトを作成し、それを clone (複製) したものにメソッドを追加しながら、継承を実現するものです。

※ clone で生成されるのは deep copy ではないので注意してください。

Prototypal Inheritance は、JavaScript の prototype チェーンの性質を最大限に活用することによって実現します (ただし、実装自体は非常にシンプルなものです) 。

では例を見てみましょう。
/* Person Prototype Object. */
var Person = {};
Person.name = 'default name';
Person.getName = function() {
return this.name;
};

/* Author Prototype Object */
var Author = clone(Person);
Author.books = [];
Author.getBooks = function() {
return this.books;
}

var author = clone(Author);
author.name = 'Yukihiro Matsumoto';
author.books.push('Ruby');

author.getBooks();
このとき利用されている clone 関数は以下のような定義です。
/* Clone Function */
function clone(obj) {
var F = function (){};
F.prototype = obj;
return new F;
}

Classical and Prototypal Inheritance の比較

この 2 つの 継承パターンの pros & cons は以下の通りになります。

Classical Inheritance については、
  • (Pros)JavaScript やプログラマのコミュニティにて理解しやすい
  • (Cons)クラスベースの継承だが、object に prototype を結びつけるので混乱しやすい
Prototypical Inheritance の方は、
  • (Pros)メモリを効率的に利用できる
    ※ prototype chain の性質上、全ての clone オブジェクトは、プロパティやメソッドを上書きしない限り、ひとつのオブジェクト (プロトタイプとしたオブジェクト) の属性またはメソッドを参照するだけで済むからです。
  • (Cons) prototype の概念に慣れていないプログラマには理解されづらい
となります。

Mix-in

継承のおまけとして、Mix-in を実現することもできます。
ここでは Classical Inheritance パターンの場合にどのように Mix-in を実現するかを見てみます。

※ Prototype Inheritance を利用する場合、Mix-in は Prototype Object の prototype にメソッドを実装すれば良いだけなので省略します。

オブジェクトをシリアライズするクラスを、任意のオブジェクトに実装することを考えてみましょう。

まず、任意のオブジェクトをシリアライズのメソッドは以下のように書けます。
/* Mix in class implemented to any other object. */
var Mixin = function() {};
Mixin.prototype = {
serialize: function() {
var output = new Array();
for (key in this) output.push(key + ': ' + this[key]);
return output.join(', ');
}
};
このクラスを Author クラスに以下のように Mix in します。
augment(Author, Mixin);
var author = new Author('Larry Wall', ['Perl']);
author.serialize();
結果、以下のようなシリアライズされた出力を得ることができます。
"name: Larry Wall, books: Perl, constructor: function Author(name, books) { Person.call(this, name); this.books = books; }, getBooks: function () { return this.books; }, serialize: function () { var output = new Array; for (key in this) { output.push(key + ": " + this[key]); } return output.join(", "); }, getName: function () { return this.name; }"
ここで利用された augment 関数は以下のようになります。
/* Augument function */
function augment(receivingClass, givingClass) {
if (arguments[2]) {
for (var i = 2, len = arguments.length; i < len; i++)
receivingClass.prototype[arguments[i]] = givingClass.prototype[arguments[i]];
}
else {
for (methodName in givingClass.prototype) {
if (!receivingClass.prototype[methodName])
receivingClass.prototype[methodName] = givingClass.prototype[methodName];
}
}
}