オブジェクト指向を徹底して、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 フレームワークを使用するとき) などのプログラミング言語に採用されました。1
JavaScript のオブジェクト指向プログラミング
ネームスペース
ネームスペースは、開発者がユニークなアプリケーション固有の名前のもとに、機能性をまとめることができるコンテナです。JavaScript のネームスペースは、メソッド、プロパティ、オブジェクトを包含する別のオブジェクトです。
注記: 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 リファレンスの Global Objects をご覧ください。
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!" // Check that instanceof works correctly console.log(student1 instanceof Person); // true console.log(student1 instanceof Student); // true
Student.prototype = Object.create(Person.prototype);
について: Object.create
が存在しない古い JavaScript エンジンでは、"ポリフィル" ("shim" とも呼ばれます。リンク先の記事をご覧ください) または同様の結果になる以下のような関数を使用できます。:
function createObject(proto) { function ctor() { } ctor.prototype = proto; return new ctor(); } // 使用法: Student.prototype = createObject(Person.prototype);
オブジェクトをインスタンスる化する方法に関わらず this
の参照先を適切にすることは難しいでしょう。ただし、これを容易にするシンプルなイディオムがあります。
var Person = function(firstName) { if (this instanceof Person) { this.firstName = firstName; } else { return new Person(firstName); } }
カプセル化
前の例で、Student
は Person
クラスの walk()
メソッドがどのように実装されているかを知る必要がありませんが、そのメソッドを使用できます。すべてのクラスはデータとメソッドをひとつのユニットに収めることから、これを カプセル化 と呼びます。
情報を隠蔽することは、他の言語でも 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)" (日本語版)