目次


多忙な JavaScript 開発者のための ECMAScript 6 ガイド、第 3 回

JavaScript 内でのクラス

プロパティーと継承について理解する

Comments

コンテンツシリーズ

このコンテンツは全4シリーズのパート#です: 多忙な JavaScript 開発者のための ECMAScript 6 ガイド、第 3 回

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:多忙な JavaScript 開発者のための ECMAScript 6 ガイド、第 3 回

このシリーズの続きに乞うご期待。

Interactive code: When you see Run at the top of a code sample, you can run the code, check the results, make changes, and run it again.

編集者による注記: このシリーズは、対話式コーディング機能を追加して更新されています。コード・リストに「実行」ボタンがある場合、そのコードを実行して結果を確認し、コードを変更してから再度実行して、その成果を確認できます。

このシリーズの第 2 回では、新しく導入されたアロー関数とジェネレーター関数を含め、ECMAScript 6 での関数に関する機能強化を紹介しました。関数型要素を JavaScript コードに統合するには、いくつか見直さなければならない点がありますが、それは思うほど大々的な変更ではありません。実際、この数年間にわたって提案されてきたすべての変更のうち、最も論争の的になっている ECMAScript 6 に含まれる新しい要素は、オブジェクト指向の要素です。

JavaScript には長い間、従来型のクラスに基づく構文が欠けていましたが、その状況は ECMAScript 6 によって一変しています。シリーズ第 3 回となるこの記事で、JavaScript 内でクラスとプロパティーをどのようにして定義するのか、そしてプロトタイプ・チェーンを使用して JavaScript プログラムに継承をどのようにして導入するのかを説明します。

オブジェクトの歴史

JavaScript は当初、Java の軽量バージョンとして考え出されて市場に出されたことから、一般には従来型のオブジェクト指向の言語であると見なされています。さらに new キーワードのおかげで、構文的には Java や C++ で見慣れている内容と同じようにも見えます。

実際のところ、JavaScript はクラス・ベースの環境ではなく、オブジェクト・ベースの環境です。クラス・ベースであるかオブジェクト・ベースであるかは、オブジェクト指向の言語を初めて使う開発者や、たまにしか使わない開発者にとってはそれほど問題にならないかもしれませんが、少なくともこの違いを理解することは重要です。オブジェクト・ベースの環境には、クラスがありません。クラスからオブジェクトが派生するのではなく、各オブジェクトが別の既存のオブジェクトから複製されます。複製されたオブジェクトは、複製元のプロトタイプ・オブジェクトへの暗黙的参照を保持します。

オブジェクト・ベースの環境内で作業することには利点がありますが、プロパティーや継承などといったクラス・ベースの概念を使用できないという制約があります。ECMAScript 専門委員会ではかねてから、JavaScript ならではのスタイルを犠牲にすることなく、この言語にオブジェクト指向の要素を統合する方法を模索していました。ECMAScript 6 でついに、専門委員会はその方法を見つけたのです。

クラス定義

最初に取り組むのに最も簡単なのは、class キーワードです。以下に示すように、このキーワードは新しい ECMAScript クラスを定義することを意味します。

Show result

空のクラスだけでは興味深いものとは言えません。人には名前と年齢があるので、Person クラスにそれを反映させます。コンストラクターを導入することで、クラスのインスタンスを作成する際に、これらの詳細を追加できます。

Show result

コンストラクターは、作成プロセスの一環として呼び出される「特殊関数」です。new 演算子の一部として型に渡すパラメーターは、いずれもコンストラクターに渡されます。けれども間違いなく、constructor は ECMAScript 関数であることに変わりはありません。したがって、以下のように JavaScript 流の柔軟なパラメーターと暗黙的 arguments 引数を利用できます。

Show result

クラスを導入した目的は、JavaScript 開発者がより従来型に近いクラス指向のコードを作成できるようにすることである点は明らかですが、ECMAScript 専門委員会では、これまで ECMAScript を特徴付けてきた柔軟性と開放性も引き続きサポートする意向でいます。つまり理想的には、開発者がクラス・ベースとオブジェクト・ベースそれぞれの長所を利用できるようになるということです。

プロパティーとカプセル化

自身の状態を公開して維持できなければ、クラスとしての意味はほとんどありません。そのため、ECMAScript 6 では開発者がフィールドを装うプロパティー関数を定義できるようになっていて、ECMAScript 内でカプセル化のさまざまなフレーバーを利用するお膳立てが整っています。

Person クラスを例に取ると、本格的なプロパティーにするには firstNamelastNameage が妥当です。この場合、以下のようにプロパティーを定義できます。

Show result

ゲッターとセッターが (ECMAScript 仕様に含まれて公式に知られているように) アンダースコアによってプレフィックスを付けたフィールド名をどのようにして参照しているのかに注目してください。つまり、これで Person には 6 つの関数と 3 つのフィールドが定義されたことになります (プロパティーごとに 2 つの関数と 1 つのフィールド)。他の言語とは異なり、ECMAScript ではプロパティーを作成する際にバッキングストア・フィールドをサイレントに導入することはしません (バッキングストアとは、データが格納される場所のことなので、別の言い方をすれば、実際のフィールドそのものです)。

プロパティーにクラス内部の状態を 1 対 1 の関係を使用して反映させる必要はありません。実のところ、プロパティーに伴うカプセル化の特性は主に、クラス内部の状態の一部またはすべてを隠すことにあります。

Show result

ただし、このプロパティー構文の存在により、フィールドを直接取得できなくなるわけではありません。引き続き ECMAScript のお馴染みの方法を使ってオブジェクトを列挙し、その内容を取得することができます。

Show result

別の方法として、Object に定義された getAllPropertyNames() 関数を使用して同じリストを取得することもできます。

ここで興味深い疑問が浮かんできます。firstNamelastNameage のゲッター関数とセッター関数がオブジェクト自体に定義されていないとしたら、「ted.firstName」のような表現をインタープリターの巧みな技なしで解決するためにどのような仕組みがあるのでしょうか?

その答えは簡潔なだけに簡単です。Person のインスタンスである ted は、元の Person クラスへのプロトタイプ・リンクを保持することから、このオブジェクトのストリング表現が解決されます。

プロトタイプ・チェーン

JavaScript は当初から、オブジェクト間のプロトタイプ・チェーンを維持するようになっています。プロトタイプ・チェーンは Java や C++/C# での継承と同様のものであると思うかもしれませんが、この 2 つの手法の真の類似点は 1 つしかありません。JavaScript で、オブジェクトにまだ直接定義されていないシンボルを解決する必要がある場合は、プロトタイプ・チェーンを辿って有効な一致を探します。

これを理解するのは少々難しいので、要点をまとめましょう。例えば、昔流の JavaScript スタイルに従って、以下の極めて単純なオブジェクトを定義するとします。
var obj = {};

このオブジェクトのストリング表現を取得するとします。通常、それには toString() を使用しますが、obj にはそのような関数が定義されていません。実際のところ、このオブジェクトに定義されているものは何もありませんが、それでもこのコードは動作するだけでなく、結果も返します。
var obj = {};
console.log(obj.toString()); // prints "[object Object]"

インタープリターが obj オブジェクトに名前として定義された toString を探しても、一致は見つかりません。一方、オブジェクトのプロトタイプ・オブジェクトはすぐに見つかるので、インタープリターはそのプロトタイプ・オブジェクトで toString を探します。それでも一致が見つからなければ、そのプロトタイプのプロトタイプを探すといった具合に続いていきます。この特定の例では、obj のプロトタイプである Object オブジェクトに toString が定義されています。

以上の説明を Person クラスに当てはめると、どのような仕組みかはかなり明白なはずです。オブジェクト ted はそのプロトタイプとしてオブジェクト Person を参照しています。Person では firstNamelastNameage のそれぞれに、ゲッターとセッターとして 2 つのメソッドが定義されています。ゲッターまたはセッターのいずれかを使用すると、この言語は単純にプロトタイプに従って、ted インスタンス自体に代わってそのメソッドを実行します。

以下のように新しいメソッドを追加すると明らかになるように、この仕組みは Person クラスに定義されているどのメソッドにも当てはまります。

Show result

上記で使用されている新しいメソッドでは、Person プロトタイプ・インスタンスを徐々にエージング処理できます。

getOlder メソッドは Person オブジェクトに定義されているため、ted.getOlder() が呼び出されると、インタープリターは ted から Person へとプロトタイプ・チェーンを辿り、Person オブジェクトの getOlder メソッドを見つけて実行します。

Java または C++/C# 開発者のほとんどにとって、クラスが実際にはオブジェクトであるという概念に慣れるには時間がかかります。Smalltalk 開発者にとっては、これまで常にクラスはオブジェクトであったため、他の開発者がどうしてこの概念になかなか慣れないのか不思議に思うでしょう。この概念を取り込みやすくなるのであれば、ECMAScript 内のクラスは型オブジェクトであると考えてください。つまり型定義の体裁を提供するために存在するオブジェクト・インスタンスです。

プロトタイプによる継承

「プロトタイプ・チェーンを辿る」ことをパターンとして採用すると、ECMAScript 6 の継承に関するルールに従うことはいとも簡単になります。別のクラスを継承するクラスを作成する場合、その派生クラス上でインスタンスのメソッドを呼び出すとどうなるのかは、極めて簡単にわかります。

Show result

最初に呼び出しの処理を試みるのは、インスタンス自体です。それが失敗すると、型オブジェクト (この例では Author) がチェックされます。次に、型オブジェクトの「継承」オブジェクト (Person) がチェックされるというように、元の型オブジェクト (常に Object です) に戻るまでチェックが続けられます。

さらに、上記のコードの Author コンストラクターを見るとわかるように、super というキーワードがプロトタイプ・チェーンを遡って特定の継承元プロトタイプのメソッド・バージョンを呼び出します。上記の例の場合、コンストラクターが呼び出されると、Person コンストラクターがメソッドを実行します。プロトタイプ・チェーンを辿るだけで、まったく単純になります。

プロトタイプ委任を扱い慣れるにつれ、このソリューションの簡潔さに対する私の評価はますます高くなっています。すべては基本的に単一の概念に従っていますが、「古いルール」も引き続き有効です。これまでのようにメタオブジェクト方式の中で ECMAScript オブジェクトを使用したいとしたら、オブジェクト自体のメソッドを追加および削除して、以下のようなコードを作成できます。

Show result

私見ですが、新しいクラス・ベースの構文はケーキをとっておきながら、それを食べてもいることに似ています。つまり、Java を使用しながら、同じ言語で Lisp もそのまま維持することもできるということです。

静的プロパティーおよびフィールド

オブジェクト指向にしない方法を考えることなく、オブジェクト指向を語ることはできません。コード内でクラスを扱い始める際には、グローバル変数やグローバル関数をどのようにして処理するのかを把握していることが必須です。ほとんどの言語では、それぞれ静的変数、静的関数とも呼ばれています。あるいは、パターン指向の開発者はシングルトンとも呼んでいます。

ECMAScript 6 には静的プロパティーや静的フィールドに関する明示的な規定はありませんが、これまでの説明を基に、ECMAScript オブジェクトの仕組みについて多少知っていれば、静的要素の価値をどのようにして実現するのかを想像するのはそれほど難しくないはずです。

Show result

Person クラスは実際にはオブジェクトであるため、ECMAScript 内での静的フィールドは本質的に Person 型オブジェクトに定義されたフィールドです。したがって、静的フィールドを定義する明示的な構文はないとは言え、型オブジェクトのフィールドを直接参照することができます。上記の例では、Person コンストラクターがまず、Person に既存の population フィールドがあるかどうかをチェックします。ない場合は、population を 0 に設定して暗黙的にフィールドを作成します。population フィールドがある場合は、フィールドの値を増やすだけのことです。Person クラスの population プロパティーにアクセスすると、シングルトン・パターンに従った値が返されます。

フィールドを定義するのは簡単ですが、ECMAScript 6 仕様では静的メソッドの定義がもう少し明示的なものになっています。静的メソッドを定義するには、以下のように、関数を定義するクラス宣言に static キーワードを追加します。

Show result

クラス・オブジェクトを介して静的メソッド haveBaby() が呼び出されると、population プロパティーの値が 1 増えます。これで、Person クラスにシングルトンのプロパティーと、このプロパティーにアクセスするための静的関数が定義されました。

まとめ

ECMAScript 専門委員会は、初期の頃、重要ないくつかの課題に取り組みましたが、そのいずれにしても、JavaScript へのクラスの導入ほど重要な課題ではありませんでした。今のところ、新しい構文は成功を収め、ECMAScript 全体を支える原則を維持しつつ、大半のオブジェクト指向開発者の期待に応えているようです。

専門委員会は、TypeScript などの言語で見られるような堅牢な静的型チェックを統合することはしませんでしたが、それが最終目標であったわけでは決してありません。少なくとも今回は静的型チェックを適用しようとしなかったことは、委員会の称賛に値します。

このシリーズの最終回では、モジュールを明示的に宣言して使用するための新しい構文を含め、ECMAScript 6 のライブラリーに関する機能拡張をいくつか取り上げて説明します。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Web development
ArticleID=1041331
ArticleTitle=多忙な JavaScript 開発者のための ECMAScript 6 ガイド、第 3 回: JavaScript 内でのクラス
publish-date=07132017