この記事は、オブジェクト(理論編) からの続編です。
この記事は、絵でわかる プログラムとは何か(6)~シンボル~ の一記事です。
この記事のポイント
- 「カプセル化されたデータ型」こそが、オブジェクトの本質。
- 個々のオブジェクトの生成には、new演算子と、オブジェクト生成専用の関数=コンストラクタを合わせて使う。
- オブジェクトを生成したら、オブジェクト変数へ、そのオブジェクトへの参照を代入する。
- 関数定義内のthis キーワードは、その関数の親オブジェクトを表す。
- オブジェクト指向プログラミングには、継承の違いからくる、クラスベースとプロトタイプベースの二つの流派がある。
- クラスベースは、生物の分類のような考え方で、プロトタイプベースは、生物の進化のような考え方。
プログラミングの視点から見たオブジェクト
オブジェクトの本質はカプセル化
オブジェクト(理論編)では、オブジェクトの三大要素をそれぞれ見てきましたが、これら3つの関係は対等ではありません。
多態性は継承が無ければ持てませんが、継承があっても全メソッドに多態性を持たせる必要はありません。
継承は、多種多様なオブジェクトを効率よく作るのに便利ですが、継承無しで一からオブジェクトを作れないわけではありません。
簡易なプログラムであれば、多態性も継承も不要ですが、最低限、オブジェクトの利用にはカプセル化が使われます。
逆に言えば、「カプセル化されたデータ型」こそが、オブジェクトの本質なのです。
例えるなら、ファイルをフォルダに入れて、そのフォルダにわかりやすい名前をつけて、整理するのと同じです。
同じグループに変数や関数をまとめ、そのグループにわかりやすい名前をつけられたら便利ですよね?
それが、オブジェクトなのです。
オブジェクトはよく「もの」に例えられますが、何か具体的な「もの」を想定してからでないとオブジェクトを作れないわけではないのです。
グループ化して、名前を付けるだけで、オブジェクトの一丁あがりです。
「もの」に例えるのが難しいオブジェクトの例
例えば、お絵描き系プログラムでは、『コンテキスト(context)』と言われるオブジェクトがよく使われています。
しかし、『コンテキスト』と言われて、どんなフィールドやメソッドを持つか、イメージできる人は少ないと思います。現実世界のお絵描きでは、『コンテキスト』と言われる「もの」が使われていないからです。
ちなみに、英語の context の、一般的な日本語訳は、「文脈」、「コンテキスト」です。語源は、「織柄」らしいです。
さっぱりイメージできないですね!
では、コンテキスト(context)オブジェクトの正体は何かというと、
- 塗りの色、描線の色、描線の太さ、…のようなフィールド
- 長方形描画、パス(下書き)描画、切り抜き、…のようなメソッド
を、ぜ~んぶ「ひとまとめ」にしたものです。
それぞれ個別のこととして見ると、現実世界でも使われることなので、イメージしやすいでしょう。
じゃ、それらを全部「ひとまとめ」にしたものを何と言いますか?
現実世界ではそれらを「ひとまとめ」にはしないですから、特にそれを表す言葉などありません。
でも、お絵描き系のプログラミングとしては、これらを「ひとまとめ」のグループにしておいた方が、名前空間的にも、他のシンボルとの重複を気にせず使えて、大変便利なのです。
それで仕方なくつけられたグループ名 = オブジェクト名 が、「context = 一連の情報」なのです。
繰り返しになりますが、オブジェクト指向プログラミングでは、オブジェクトは「もの」ではなく、変数と関数の「グループ」として考えましょう。
オブジェクト指向プログラミング以外で使われるオブジェクト(object)の一般的な意味としては、やはり「もの」であることが多いと思います。
コンストラクタ
個々のオブジェクトを作るための専用の関数(メソッド)を、コンストラクタと言います。
コンストラクタ名は、頭文字大文字で、オブジェクト名と同じ名前を付けるのが通例となっています。関数としてのコンストラクタの定義は、オブジェクトの定義そのもの(リテラル)になります。
コンストラクタによる実際のオブジェクト生成
個々のオブジェクトを生成する際には、各フィールド(変数)に対して、具体的な値を代入します。各フィールドへ代入する値は、コンストラクタの引数で指定します。
また、コンストラクタは、オブジェクトの定義になりますが、コンストラクタだけでは、個々のオブジェクト用のメモリ領域を確保できません。メモリ領域の確保には、言語処理系の力が必要で、new演算子がする仕事です。個々のオブジェクトの生成には、new演算子とコンストラクタを合わせて使います。
new Constructer(data)
なお、コンストラクタには通常、戻り値をつけられません。戻り値を指定しても、エラーにされたり、無視されたりします。生成したオブジェクトへの参照は、コンストラクタではなく new演算子が返してくれます。
オブジェクト変数
オブジェクトを生成したら、通常はそのオブジェクトを利用できるようにするために、オブジェクト変数という識別子(ID)へ、そのオブジェクトへの参照を代入します。
例) 新たな三毛猫に「たま」と名前を付ける場合
tama = new Cat(mike);
初期値 mike で Cat オブジェクトを作成し、オブジェクト変数 tama に、そのオブジェクトへの参照を代入。
ここでソースコードとしては、オブジェクトをオブジェクト変数へ代入する形式をとりますが、ここでの代入は必ず参照渡しになり、値渡しされることはありません。
this キーワード
this は、関数定義のリテラル内で使われ、その関数の親オブジェクトを表すキーワード(特殊な意味を持つ言葉)です。
this の代わりに self というキーワードを使う言語もあります。
特定の親オブジェクトを持たない、メソッドではない通常の関数の定義内で this が使われると、グローバルオブジェクトという、そのプログラム全体を表します。
コンストラクタの定義(リテラル)内で this が使われると、そのコンストラクタから作られるオブジェクトを、表します。
それを利用して、各オブジェクトへのメンバー設定には、コンストラクタ内で、thisキーワードを使います。
this.member = data;
クラスベースとプロトタイプベース
オブジェクト指向プログラミングには、継承の違いからくる、クラスベースとプロトタイプベースの二つの流派があります。クラスベースは、生物の分類のような考え方で、プロトタイプベースは、生物の進化のような考え方です。
クラスベースの代表例は、C++やJavaです。PythonやVBAもクラスベースです。一方、JavaScriptは、プロトタイプベースの代表例です。
それではまず、より一般的なクラスベースのオブジェクト指向の方から以下に説明していきます。
クラス(class)
クラスとは
オブジェクト型はその抽象性を活かして、様々なデータに使われることが、その存在意義になります。しかし同じ抽象データ型でも完全に具体性を持たないバイナリ型とは違い、「分類」によって、ある程度の具体性を段階的に持てるのが、クラスベースのオブジェクトの特徴です。クラスベースのオブジェクトが持つ、この段階的な分類をクラスと言います。クラスベースでは、上位層のクラスから下位層のクラスへ、クラスメンバー(フィールドとメソッド)が継承されます。
段階的な分類をすると、上図の生物の分類例でもわかるとおり、大分類の下に中分類があって、中分類の下にさらに小分類があって、…というように、分類が何段にも分けられることで、抽象的なイメージから、具体的なイメージへと、より限定されていくことになります。
クラス定義
クラスは分類ではありますが、プログラム中にすべてのクラス定義を記述する必要はありません。通常、プログラム中には、そのプログラムで必要なクラスと、そのフィールドやメソッドのみを記述します。
例えば、プログラムで必要なのが、哺乳類クラスだけであれば、魚類~鳥類クラスや、植物クラス、無脊椎動物クラスの定義は不要です。
クラス定義で、そのクラスの上位クラスを指定することにより、クラス階層構造が作られます。
←
subClass : superClass
: (コロン)で結ぶことにより、上位クラスsuperClassのメンバーが、下位クラスsubClassに継承されます。
また、よく利用されるクラスの多くは、プログラミング言語ごとに提供されるクラスライブラリに、そのクラス定義が含まれています。
クラスとインスタンス
データになれないクラス
クラスはオブジェクトの「分類」です。クラス自身は、具体的な個々のオブジェクト(実体)にはなれません。
データになれるインスタンス
それに対して、具体的な個々のオブジェクト(実体)として宣言されるのが、インスタンスです。
メモリへの領域確保は、プログラム中のnew演算子で行われ、続けてプログラム中のコンストラクタによって、具体的データが入ったフィールドとメソッドへの参照が、確保した領域へ記録されます。
instance1 = new Constructor( data1 );
instance2 = new Constructor( data2 );
instance3 = new Constructor( data3 );
つまりインスタンスは、具体的なフィールドデータとメソッド参照の集まりとして生成されます。フィールドデータは各インスタンスのメモリ領域へ個別に入りますが、メソッドは各インスタンス共通なので、通常、個別には入れず、メソッド定義への参照だけが入ることに注意してください。
インスタンスを生成したら、そのインスタンスを利用できるようにするために、オブジェクト変数へ、new が返すインスタンスへの参照を代入します。上の例では、 instance1, instance2, instance3 がオブジェクト変数です。
以降、このオブジェクト変数を使ってこのインスタンスを扱うことができるようになるので、このオブジェクト変数名をインスタンス名とも言います。上の例では、 instance1, instance2, instance3 がそれぞれインスタンス名となります。
プロトタイプ
プロトタイプとは
一方、プロトタイプからオブジェクトを作るのが、プロトタイプベースのプログラミングです。
プロトタイプとは、プロトタイプベースでクラスの代わりに継承元となるオブジェクトのことです。
ただし、クラスにクラス定義があるのとは違って、プロトタイプ定義というのは特にありません。普通のオブジェクトとして定義されたオブジェクトを、別のオブジェクトのプロトタイプとして参照設定するだけです。
これが、プロトタイプベースのやり方です。
プロトタイプはクラスと違い、プロトタイプ自身も、具体的なデータを持てるオブジェクトです。プロトタイプにもまた、別のプロトタイプが存在します。
クラスベースが分類なら、プロトタイプベースは生物の進化のイメージです。
プロトタイプとコンストラクタ
また、コンストラクタとの関係が、クラスとプロトタイプでは異なります。
クラスベースでのコンストラクタは、クラスのメソッドとして定義されます。
他方、プロトタイプベースでのコンストラクタは、グローバルな関数として定義されます。
戻り値が無く、コンストラクタ名が頭文字大文字であることは共通です。
そして、プロトタイプベースでのオブジェクトは、実はコンストラクタからではなく、プロトタイプから作られます。
正確には、継承元であるプロトタイプのコンストラクタから作られます。
オブジェクトを作るプロトタイプ
詳しく言うと、定義済みのオブジェクトを継承元(プロトタイプ)として、オブジェクトリテラルや、createメソッドなどの戻り値を、オブジェクト変数に代入すれば、コンストラクタ無しでオブジェクトが作れるのです。
定義済みのオブジェクトには、言語処理系によってあらかじめ定義された、組み込みオブジェクト(built-in object) も含まれます。
object1 = {data: “data1”};
object2 = Object.create(prototype2);
オブジェクトをカスタマイズするコンストラクタ
もちろん、コンストラクタを定義してnew演算子で処理しても、オブジェクトが作られます。これは実際には、組み込みオブジェクト(built-in object)を継承元(プロトタイプ)とする無名プロトタイプが作られ、そこからコンストラクタでカスタマイズされて、オブジェクトが作られているのです。
プロトタイプベースでの継承チェーン(委譲チェーン)を、プロトタイプチェーンと言います。
コンストラクタ(下の例ではSample)を new して、オブジェクト(下の例ではsample1, sample2)を作ると、作られたオブジェクトに、その時点でコンストラクタと相互参照するプロトタイプを参照するプロパティ __proto__ が作られます。
Sample.prototype = sampleProto1;
sample1 = new Sample();
Sample.prototype = sampleProto2;
sample2 = new Sample();
同じ Sample() コンストラクタで作っても、作ったその時点での Sample.prototype が異なれば、__proto__ の参照先プロトタイプも異なります。
それぞれ異なるプロトタイプから、共通のコンストラクタで進化した、異なるオブジェクトが生成された、ということです。
sample1.__proto__ → sampleProto1
sample2.__proto__ → sampleProto2
新種のオブジェクト生成
プロトタイプベースは生物の進化のような考え方です。よって新種のオブジェクトは、祖先であるプロトタイプを変更するか、進化を表すコンストラクタを変更するかで、創り出せます。
- プロトタイプは参照先なので、動的に、複数オブジェクト共通で変更でき、メモリの節約になります。
- コンストラクタはコピー元なので、これから生成するオブジェクト個別にしか反映されず、その分メモリを消費します。
さらにプロトタイプには、
- コンストラクタと相互参照する無名のプロトタイプ
- 他オブジェクト(祖先オブジェクト)を参照するプロトタイプ
の二種類があります。
無名のプロトタイプの内容変更は、通常、コンストラクタの prototype プロパティを介して行います。(無名のオブジェクトなので、直接アクセスできません。)
他オブジェクト(祖先オブジェクト)を参照するプロトタイプは、祖先オブジェクトの内容を直接変更することで、子孫オブジェクトに、その変更内容が反映されます。
この記事のまとめ
「カプセル化されたデータ型」こそが、オブジェクトの本質。
個々のオブジェクトの生成には、new演算子と、オブジェクト生成専用の関数=コンストラクタを合わせて使います。
オブジェクトを生成したら、オブジェクト変数へ、そのオブジェクトへの参照を代入します。
関数定義内のthis キーワードは、その関数の親オブジェクトを表します。
オブジェクト指向プログラミングには、継承の違いからくる、クラスベースとプロトタイプベースの二つの流派があります。
クラスベースは、生物の分類のような考え方で、プロトタイプベースは、生物の進化のような考え方です。
次は、メモリセグメント に進みましょう。
この記事は、絵でわかる プログラムとは何か(6)~シンボル~ の一記事です。
コンピューターのしくみ全体を理解したい場合は、以下の2コースがお勧めです。
日本全国 オンラインレッスン にも対応しています。
知りたいことだけ単発で聞きたい場合は、 オンラインサポート をご利用ください。