オブジェクト指向を追求することで、JavaScript は強力かつ柔軟なオブジェクト指向プログラミング能力を特色としています。この記事ではまずオブジェクト指向プログラミングの入門から始め、JavaScript のオブジェクトモデルの復習、そして最後に JavaScript のオブジェクト指向プログラミングの概念を説明します。
JavaScript の復習
変数、型、関数、スコープといった JavaScript の概念について自信がないのでしたら、JavaScript「再」入門で該当するトピックをご覧いただくとよいでしょう。また、JavaScript ガイドもご覧ください。
オブジェクト指向プログラミング
オブジェクト指向プログラミング (OOP) は、実世界を元にしたモデルの作成に抽象化を使用する、プログラミングのパラダイムです。OOP はモジュラリティ、ポリモーフィズム、カプセル化といった、これまでに確立されたパラダイム由来の技術を複数使用しています。今日、人気がある多くのプログラミング言語 (Java、JavaScript、C#、C++、Python、PHP、Ruby、Objective-C など) が OOP をサポートしています。
OOP はソフトウェアを関数の集まりや単なるコマンドのリスト(これまでの伝統的な見方)としてではなく、協調して動作するオブジェクトの集まりであると考えます。OOP では、各々のオブジェクトがメッセージを受信し、データを処理し、また他のオブジェクトへメッセージを送信できます。各々のオブジェクトは明確な役割や責任を持つ、独立した小さな機械であると見なせます。
OOP はプログラミングにおける柔軟性や保守性の向上を促し、大規模ソフトウェアエンジニアリングにおいて広く普及しています。OOP はモジュラリティを強く重視しているため、オブジェクト指向によるコードは開発をシンプルにします。また、コードを後から理解することが容易になります。オブジェクト指向によるコードはモジュラリティが低いプログラミング方法よりも、直接的な分析、コーディング、複雑な状況や手続きの理解を促進します。1
用語集
- ネームスペース (名前空間)
- 開発者があらゆる機能をアプリケーション固有の一意な名前にまとめることができる一種の容器のことです。
- クラス
- オブジェクトの特性を定義するものです。クラスは、オブジェクトのプロパティやメソッドを定義するテンプレートです。
- オブジェクト
- クラスの実体です。
- プロパティ
- 「色」などといったオブジェクトの特性です。
- メソッド
- 「歩く」などといった、オブジェクトの能力です。これは、クラスに関連付けられたサブルーチンや関数です。
- コンストラクタ
- インスタンス化するときに呼び出されるメソッドです。コンストラクタの名前は通常、クラスの名前と同じです。
- 継承
- あるクラスが別のクラスから特性を引き継ぐことを指します。
- カプセル化
- データと、そのデータを使用するメソッドとをまとめる手法のことです。
- 抽象化
- 実世界のモデルが、オブジェクトの複雑な継承、メソッド、プロパティの集合体によって適切に再現されている状態を指します。
- ポリモーフィズム
- Poly は "many"、morphism は "forms" を意味します。別々のクラスが同じメソッドやプロパティを定義可能であることを表します。
オブジェクト指向プログラミングのより広範な説明については、Wikipedia の オブジェクト指向プログラミング をご覧ください。
プロトタイプベースプログラミング
プロトタイプベースのプログラミングはクラスを使用せず、既存のプロトタイプオブジェクトをデコレート(あるいは拡張)してそのオブジェクトの持つ挙動を再利用する(クラスベースの言語における継承と同等)ことで実現される OOP モデルです(クラスレス、プロトタイプ指向、あるいはインスタンスベースプログラミングとも呼ばれます)。
プロトタイプベース言語として先駆けの(そしてもっとも正統な)代表例は、David Ungar 氏と Randall Smith 氏によって開発された Self です。とはいえ、クラスレスのプログラミングスタイルは最近ますます人気が高まり、JavaScript、Cecil、NewtonScript、Io、MOO、REBOL、Kevo、Squeak (ソフトウェア Morphic のコンポーネント操作の際の Viewer フレームワークとして使われています)などのプログラミング言語に採用されました。
JavaScript のオブジェクト指向プログラミング
ネームスペース
ネームスペース(名前空間)とは、開発者が一意なアプリケーション固有の名前を付けて、機能をまとめることができる一種の容器です。JavaScript では、ネームスペースはメソッド、プロパティ、オブジェクトを包含する別のオブジェクトとなります。
JavaScript でネームスペースを作成する考え方はシンプルです。グローバルオブジェクトをひとつ作成して、すべての変数、メソッド、関数をそのオブジェクトのプロパティとすればよいのです。ネームスペースを使用すると、アプリケーション内で名前が衝突する可能性が低下します。これは各アプリケーションのオブジェクトが、アプリケーションで定義したグローバルオブジェクトのプロパティとなるからです。
MYAPP という名前のグローバルオブジェクトを作成しましょう :
// グローバルネームスペース var MYAPP = MYAPP || {};
上記のサンプルコードでは、始めに MYAPP が(同じファイルまたは別のファイルで)すでに定義されているかを確認します。定義されている場合は、既存の MYAPP グローバルオブジェクトを使用します。定義されていない場合はメソッド、関数、変数、オブジェクトをカプセル化する、MYAPP という名前の空のオブジェクトを作成します。
サブネームスペースも作成できます(グローバルオブジェクトを最初に定義する必要があることに注意):
// サブネームスペース MYAPP.event = {};
ネームスペースを作成して変数、関数、メソッドを追加する構文は以下のようになります :
// 共通のメソッドやプロパティ向けに MYAPP.commonMethod という名前のコンテナを作成 MYAPP.commonMethod = { regExForName: "", // 名前を検証するための正規表現を定義 regExForPhone: "", // 電話番号を検証するための正規表現を定義 validateName: function(name){ // 電話番号に対してなんらかの処理を行う。"this.regExForName" を使用して // 変数 regExForName にアクセス可能 }, validatePhoneNo: function(phoneNo){ // 電話番号に対してなんらかの処理を行う } } // オブジェクトとともにメソッドを定義する MYAPP.event = { addListener: function(el, type, fn) { // 処理 }, removeListener: function(el, type, fn) { // 処理 }, getEvent: function(e) { // 処理 } // 他のメソッドやプロパティを追加できる } // addListener メソッドを使用する構文: MYAPP.event.addListener("yourel", "type", callback);
標準ビルトインオブジェクト
JavaScript は、例えば Math
、Object
、Array
、String
といったコアに組み込まれたオブジェクトがあります。以下の例では、乱数を取得するために Math
オブジェクトの random()
メソッドを使用する方法を示したものです。
console.log(Math.random());
console.log()
という名前の関数がグローバルで定義されていると仮定しています。実際は、console.log()
関数は JavaScript そのものの一部ではありませんが、多くのブラウザがデバッグ用に実装しています。JavaScript におけるコアオブジェクトの一覧については、JavaScript リファレンスの標準ビルトインオブジェクトをご覧ください。
JavaScript ではすべてのオブジェクトが Object
オブジェクトのインスタンスであり、それゆえに Object の全プロパティおよび全メソッドを継承します。
カスタムオブジェクト
クラス
JavaScript はプロトタイプベースの言語であり、C++ や Java でみられる class
文がありません。これは時に、class
文を持つ言語に慣れているプログラマを混乱させます。その代わりに、JavaScript ではクラスのコンストラクタとして関数を使用します。クラスの定義は、関数の定義と同じほど簡単です。以下の例では、空のコンストラクタを使って Person という名前の新たなクラスを定義しています。
var Person = function () {};
オブジェクト(クラスのインスタンス)
obj
オブジェクトの新たなインスタンスを生成するには new obj
文を使用し、その結果(obj
型を持つ)を、後からアクセスするための変数に代入します。
前出の例で、Person
という名前のクラスを定義しました。以下の例では、2 つのインスタンス(person1
と person2
)を生成しています。
var person1 = new Person(); var person2 = new Person();
Object.create()
をご覧ください。コンストラクタ
コンストラクタは、インスタンス化の際(オブジェクトのインスタンスが生成されたとき)に呼び出されます。コンストラクタは、クラスのメソッドです。JavaScript では、関数がオブジェクトのコンストラクタとして働きます。したがって、コンストラクタメソッドを明示的に定義する必要はありません。クラス内で定義されたすべてのアクションが、インスタンス化の際に実行されます。
コンストラクタはオブジェクトのプロパティの設定や、オブジェクトの使用準備を行うメソッドの呼び出しを行うために使用されます。クラスのメソッドの追加やメソッドの定義は別の構文を使用して行うことについては、後ほど説明します。
以下の例では Person
をインスタンス化する際に、コンストラクタがメッセージをログに出力します。
var Person = function () { console.log('instance created'); }; var person1 = new Person(); var person2 = new Person();
プロパティ(オブジェクトの属性)
プロパティは、クラス内にある変数です。オブジェクトのインスタンスはすべて、それらのプロパティを持ちます。プロパティがそれぞれのインスタンスで作成されるように、プロパティはコンストラクタ(関数)内で設定されます。
カレントオブジェクトを示す this
キーワードを使用して、クラス内でプロパティを扱うことができます。クラス外からプロパティにアクセス(読み取りや書き込み)するには、InstanceName.Property
という構文を使用します。これは C++、Java、その他の言語と同じ構文です(クラスの内部では、プロパティの値の取得や設定に this.Property
構文を使用します)。
以下の例では、Person
クラスをインスタンス化する際に firstName
プロパティを定義しています:
var Person = function (firstName) { this.firstName = firstName; console.log('Person instantiated'); }; var person1 = new Person('Alice'); var person2 = new Person('Bob'); // オブジェクトの firstName プロパティを表示する console.log('person1 is ' + person1.firstName); // "person1 is Alice" と出力 console.log('person2 is ' + person2.firstName); // "person2 is Bob" と出力
メソッド
メソッドは関数です(また、関数と同じように定義されます)が、他はプロパティと同じ考え方に従います。メソッドの呼び出しはプロパティへのアクセスと似ていますが、メソッド名の終わりに ()
を付加して、引数を伴うことがあります。メソッドを定義するには、クラスの prototype
プロパティの名前付きプロパティに、関数を代入します。関数を代入した名前を使用して、オブジェクトのメソッドを呼び出すことができます。
以下の例では、Person
クラスで sayHello()
メソッドを定義および使用しています。
var Person = function (firstName) { this.firstName = firstName; }; Person.prototype.sayHello = function() { console.log("Hello, I'm " + this.firstName); }; var person1 = new Person("Alice"); var person2 = new Person("Bob"); // Person の sayHello メソッドを呼び出す person1.sayHello(); // "Hello, I'm Alice" と出力 person2.sayHello(); // "Hello, I'm Bob" と出力
JavaScript のメソッドはオブジェクトにプロパティとして割り付けられた通常の関数であり、「状況に関係なく」呼び出せます。以下のサンプルコードについて考えてみましょう:
var Person = function (firstName) { this.firstName = firstName; }; Person.prototype.sayHello = function() { console.log("Hello, I'm " + this.firstName); }; var person1 = new Person("Alice"); var person2 = new Person("Bob"); var helloFunction = person1.sayHello; // "Hello, I'm Alice" と出力 person1.sayHello(); // "Hello, I'm Bob" と出力 person2.sayHello(); // "Hello, I'm undefined" と出力 // (strict モードでは TypeError で失敗する) helloFunction(); // true と出力 console.log(helloFunction === person1.sayHello); // true と出力 console.log(helloFunction === Person.prototype.sayHello); // "Hello, I'm Alice" と出力 helloFunction.call(person1);
この例で示すように、sayHello
関数を参照しているもの(person1
、Person.prototype
、helloFunction
変数など)すべてが、同一の関数を示しています。関数を呼び出しているときの this
の値は、関数の呼び出し方に依存します。もっとも一般的な、オブジェクトのプロパティから関数にアクセスする形式 (person1.sayHello()
) で this
を呼び出すときは、その関数を持つオブジェクト (person1
) を this
に設定します。これが、person1.sayHello()
で名前として "Alice"、person2.sayHello()
で名前として "Bob" が使用される理由です。一方、他の方法で呼び出す場合は this
に設定されるものが変わります。変数 (helloFunction()
) から this
を呼び出すと、グローバルオブジェクト(ブラウザでは window
)を this
に設定します。このオブジェクトは(おそらく)firstName
プロパティを持っていないため、"Hello, I'm undefined" になります(これは loose モードの場合です。strict モードでは異なる結果(エラー)になりますが、ここでは混乱を避けるために詳細は割愛します)。あるいは、例の最後で示したように Function#call
(または Function#apply
)を使用して、this
を明示的に設定できます。
継承
継承は、1 つ以上のクラスを特化したバージョンとしてクラスを作成する方法です(JavaScript は単一継承のみサポートしています)。特化したクラスは一般的に子と呼ばれ、またそれ以外のクラスは一般的に親と呼ばれます。JavaScript では親クラスのインスタンスを子クラスに代入して、特化させることにより継承を行います。現代のブラウザでは、継承の実装に Object.create
を使用することもできます。
註: JavaScript は子クラスの prototype.constructor
(Object.prototype
をご覧ください)を検出しないため、手動で明示しなければなりません。Stackoverflow に投稿された質問 "Why is it necessary to set the prototype constructor?" をご覧ください。
以下の例では、Person
の子クラスとして Student
クラスを定義しています。そして、sayHello()
メソッドの再定義と sayGoodBye()
メソッドの追加を行っています。
// Person コンストラクタを定義する var Person = function(firstName) { this.firstName = firstName; }; // Person.prototype にメソッドを 2 つ追加する Person.prototype.walk = function(){ console.log("I am walking!"); }; Person.prototype.sayHello = function(){ console.log("Hello, I'm " + this.firstName); }; // Student コンストラクタを定義する function Student(firstName, subject) { // 親のコンストラクタを呼び出す。呼び出しの際に "this" が // 適切に設定されるようにする (Function#call を使用) Person.call(this, firstName); // Student 固有のプロパティを初期化する this.subject = subject; }; // Person.prototype を継承する、Student.prototype オブジェクトを作成する // 註: ここでよくある間違いが、Student.prototype を生成するために // "new Person()" を使用することです。これは様々な理由で間違っていますが、 // まずこれでは Person の "firstName" 引数に渡すものがありません。 // Person を呼び出す正しい場所はこれより前の、 // Student から呼び出します。 Student.prototype = Object.create(Person.prototype); // 以下の注釈を参照 // "constructor" プロパティが Student を指すように設定する Student.prototype.constructor = Student; // "sayHello" メソッドを置き換える Student.prototype.sayHello = function(){ console.log("Hello, I'm " + this.firstName + ". I'm studying " + this.subject + "."); }; // "sayGoodBye" メソッドを追加する Student.prototype.sayGoodBye = function(){ console.log("Goodbye!"); }; // 使用例: var student1 = new Student("Janet", "Applied Physics"); student1.sayHello(); // "Hello, I'm Janet. I'm studying Applied Physics." student1.walk(); // "I am walking!" student1.sayGoodBye(); // "Goodbye!" // instanceof が正常に機能するかをチェック console.log(student1 instanceof Person); // true console.log(student1 instanceof Student); // true
Student.prototype = Object.create(Person.prototype);
という行について :
Object.create
が存在しない古い JavaScript エンジンでは、「ポリフィル (polyfill)」 ("shim" とも呼ばれます。リンク先の記事をご覧ください)または同様の結果になる以下のような関数を使用できます。:
function createObject(proto) { function ctor() { } ctor.prototype = proto; return new ctor(); } // 使用法: Student.prototype = createObject(Person.prototype);
Object.create
をご覧ください。オブジェクトをインスタンス化する方法を問わずに、this
の参照先を適切に指定するのは時に難しいものです。ですが、これを容易にするシンプルなイディオムがあります。
var Person = function(firstName) { if (this instanceof Person) { this.firstName = firstName; } else { return new Person(firstName); } }
カプセル化
前の例では、Person
クラスによる walk()
メソッドの実装状況を Student
が知らなくても、そのメソッドを使用できました。Student
クラスは変更の必要がない限り、そのメソッドを明示的に定義する必要はありません。すべてのクラスのデータとメソッドがひとつのユニットに収められていることから、これをカプセル化と呼びます。
情報を隠蔽することは、他の言語でも private
または protected
なメソッドやプロパティという形で一般的な機能です。JavaScript でも同様のことをシミュレートできますが、オブジェクト指向プログラミングに必須というわけではありません。2
抽象化
抽象化は、取り組んでいる問題の箇所を継承(特殊化)や合成によってモデル化することを可能にする仕組みです。JavaScript では継承によって特殊化を、クラスのインスタンスを別のオブジェクトの属性値にすることで合成を実現しています。
JavaScript の Function
クラスは Object
クラスから継承しています(これはモデルを特殊化している一例です)。また、Function.prototype
プロパティは Object
のインスタンスです (これは合成の一例です)。
var foo = function () {}; // "foo is a Function: true" と出力 console.log('foo is a Function: ' + (foo instanceof Function)); // "foo.prototype is an Object: true" と出力 console.log('foo.prototype is an Object: ' + (foo.prototype instanceof Object));
ポリモーフィズム
すべてのメソッドやプロパティが prototype
プロパティの内部で実装されているのと同じように、異なるクラスで同じ名前のメソッドを定義できます。メソッドは 2 つのクラスに親子関係(すなわち、あるクラスが別のクラスから継承されている)がない限り、自身が定義されたクラスに収められます。
注記
これらは JavaScript でオブジェクト指向プログラミングを実装する唯一の方法ではありません。この点で JavaScript はとても融通がききます。同様に、ここで示した技術は言語ハックをまったくしていませんし、他言語のオブジェクト理論における実装を模倣してもいません。
このほかにも、JavaScript によるより高度なオブジェクト指向プログラミングのテクニックがありますが、この入門記事で扱う範囲を超えます。
参考情報
- Wikipedia: "Object-oriented programming" (日本語版)
- Wikipedia: "Encapsulation (object-oriented programming)" (日本語版)