JavaScriptの「オブジェクト指向」はやわかり
JavaScriptには、ES6(ES2015)からクラスが導入されました。
つまりそれ以前にはクラスはなかったわけで、当時のJavaScriptプログラマは「プロトタイプベースのオブジェクト指向」の機能を使ってプログラムを書いていました。
「そんなの、今のJavaScriptプログラマが知る必要はないのでは?」と思うかもしれませんが、JavaScriptの「クラスベースの」オブジェクト指向は、元のプロトタイプベースのオブジェクト指向を元にしたものなので、やはり知らないと困ることがあります。
ひととおり、見ておくことにしましょう。
その1: JavaScriptのオブジェクトは連想配列だ
まず、JavaScriptのオブジェクトは、「文字列をキーにすることができる配列」、つまり連想配列です。こちらで説明したとおりです。ES2015からはシンボル(symbol)もキーにできるようになりましたが、そう広く使われているわけではないと思いますのでここでは無視します。
たとえば、以下のように書くと、x、yというふたつのキーを持ち、それぞれに10, 20という値が格納されたオブジェクトを作れます。これは、たとえばこちらのUFOゲームにおいて、「ビーム」の座標を保持するのに使えます。
この、キーと値の組を、プロパティ(property)と呼びます。
図にするとこうです。
このオブジェクトに対し、beam.xまたはbeam["x"]と書けば、xというキーに紐づけられた値、つまり10が取得できます。
その2: JavaScriptの関数はオブジェクトだ
JavaScriptの関数はオブジェクトです。「その1」で書いたようにJavaScriptのオブジェクトは連想配列なので、関数にもプロパティを追加することができます(こんなことをする機会はそうそうないと思いますが)。
そして、JavaScriptの関数がオブジェクトであるということは、通常のオブジェクトと同じように、その参照値を変数に代入したり、関数の引数にしたり、関数から戻り値として返したりできるということです※1。
たとえば、JavaScriptでは、ポップアップでメッセージを表示するalertという関数がありますが、これを別の変数に代入すれば、その変数に関数呼び出し演算子「( )」を適用することで、同じようにメッセージ表示ができます(ここにも書きました)。
これを図にすれば、以下のようになります。
JavaScriptでは、オブジェクトは参照経由でアクセスします。関数もオブジェクトですから、参照で扱います。ここでは、alertとmyAlertは、どちらも同じ関数オブジェクトを「指して」います。つまり、alertやmyAlertが保持しているのは関数そのものに対する参照値です。
さて、JavaScriptでは、関数への参照値を値として扱えるので、当然、プロパティの値として、関数への参照値を保持することもできます。
リスト4は、hello()という関数を作って、それをリスト1で作ったbeamオブジェクトのsayプロパティに代入しています(say(セイ)というのは、英語で「言う」という意味です)。
最後の「beam.say();」で、hello()関数が呼び出されて、「こんにちは!」と表示されます。
この「beam.say();」は、単にオブジェクトのプロパティに代入された関数を呼び出しているだけなのですが、オブジェクトのメソッド(method)を呼び出しているように見えるでしょう。
――JavaScriptにおける「メソッド」は、実のところこういうものなのであって、単にオブジェクトのプロパティに代入された関数にすぎません。
その3: thisは、メソッド呼び出し時の、ピリオドの左側
そうは言ってもJavaScriptのオブジェクトのメソッドは、「そのオブジェクト」に対して何らかの処理を行うものだろう、と思うでしょう。メソッドの中で、「そのオブジェクト」を意味するのがthis(ディス)です。thisは英語で「これ」という意味です※2。
そして、JavaScriptにおけるthisは、メソッドを呼び出したときに、ピリオドの左側にあったオブジェクトを意味します。
これは、たとえば以下のようなプログラムで実験できます。
この例では、「ビーム」と「敵」のふたつのオブジェクトを作り、その両方のshowXプロパティに、「this.x」を表示する関数showXFunctionを代入しています。
そしてこれを実行すると、「beam.showX();」では、beam.xである10が、「enemy.showX();」ではenemy.xである100が表示されます。呼び出しの際のピリオドの左側にあったオブジェクトのxが表示されていることがわかります。
このように、JavaScriptにおけるthisは、その関数がメソッドとして呼び出されて初めて決まります。実際、上の例では、showXFunctionという関数は、beamとenemyの両方のオブジェクトのプロパティに保持されていて、thisがどちらを指すのかは、呼び出しが行われて初めて決まっています。
他の意味のthis
上では、thisについて、「メソッド呼び出し時のピリオドの左側」と書いてしまっていますが、実のところthisが意味するのはこれだけではありません。他のケースのthisについて、以下に挙げてみます。
- 関数の外側では、thisはグローバルオブジェクト(global object)を指す※3。
- メソッドとしてではなく、普通に呼ばれた関数の中では、strictモードかどうかで挙動が異なる。strictモードでない場合はグローバルオブジェクトを指すが、strictモードではundefined。
- コンストラクタ関数の中では、新たに生成したオブジェクトを指す。これについては後述します。
- DOMのイベントハンドラとして設定された関数の中では、設定先のDOM要素を指す。イベントハンドラでDOM要素の見た目等を変えたいときに使う。
- アロー関数(後述)では、その外側の構文のthisが使用される。アロー関数はforEach()とかmap()とかで気楽に使うものなので、外側のthisが見える方が都合がよいため。
詳細は、MDNのページあたりを参照してください。
関数式
リスト4やリスト5では、ここで説明した方法で関数定義を行うことで、hello()とかshowXFunction()とかの関数を作りました。ただし、プロパティに関数を設定するなら、このような方法で関数定義を行うよりも、関数式(関数定義式/function defining expression)を使う方が便利です。
関数式は、以下のように使います。
この例では、「function() {alert("こんにちは!")};」の部分が関数式です。この式は、関数を定義するとともに、式そのものの値としてその関数を返します。そして、上の例なら、その関数がbeam.sayに代入されているわけです。
こうして作られた関数は、名前を持ちません(無名関数と呼びます)。名前を付けたい場合は普通の関数定義と同じようにfunctionの後ろに名前を書くこともできますが、その名前は、その関数の中でしか使えません。これは再帰呼び出しをするための機能です。
また、通常の関数定義では、関数を定義する前に(ソースファイル中で、関数定義より上で)その関数を呼び出すことができますが(これを関数の「巻き上げ(hoisting)」と言います)、関数式の場合、巻き上げは発生しません。
ES6から、アロー関数(arrow function)というものが使えるようになりました。これを使うと、上のfunctionによる関数式の部分を、以下のように書き換えられます。
脱線:ここまでの知識で、JavaScriptで「オブジェクト指向」してみる
JavaScriptのオブジェクト指向的な機能はこれだけではないですが、ここまで出てきたその1~その3までの知識で、そこそこオブジェクト指向っぽいことはできます。やってみましょう。
リスト7は、こちらのシューティングゲームのように、敵(Enemy)クラスを継承して、エイリアン(Alien)クラスを作るということに近いことをしています。
2行目からのnewEnemy関数が、Enemyのコンストラクタ(という役目と決めた、ただの関数)です。出現地点のX座標、Y座標を引数として与えると、Enemyのオブジェクトを返します。8~13行目で、自身を描画するdraw()という関数と、移動させるmove()という関数をプロパティとして設定しています(上の補足で説明した、関数式の記法を使っています)。
19行目からは、Enemyの「サブクラス」であるAlienクラスのコンストラクタを作っています。まず最初に、スーパークラスであるEnemyのコンストラクタを呼び出してオブジェクトを作ってから(21行目)、オーバーライドしたいメソッドについてはプロパティを上書きしています。
こうしてやれば、32行目のようにnewAlien()関数を呼び出すことでエイリアンのオブジェクトが作れますし、そのメソッドを呼び出せば、オーバーライドされていなければスーパークラスのメソッドが(36行目)、オーバーライドされていればサブクラスのメソッドが(40行目)呼び出されます。Alienクラスのmove()メソッドではthisを参照していますが(25行目)、このthisは、40行目の「alien.move()」という呼び出しにおけるピリオドの左側、つまりalienであり、そのオブジェクトは、newAlien()関数を経由したnewEnemy()関数内で(3行目で)作られたものであることもわかるでしょう。たとえばnewAlien()関数を100回呼び出してエイリアンを100匹生成したら、それぞれ個別のx, yを持ちますが、適切な変数をピリオドの左に置いてmove()を呼び出せば、その変数が指しているエイリアンが移動する、ということになります。
――ここまでできるなら、もうこれでいいじゃん、と正直私には思えますが、JavaScriptにはもうちょっとオブジェクト指向向けの機能があります。以下で説明します。
その4: JavaScriptのオブジェクトは、「プロトタイプ」も検索する
上のリスト7では、エイリアンのオブジェクトそれぞれに、drawとmoveというプロパティを持たせています。
まあこれで動きますし、問題ないといえばないのですが、エイリアンを100匹作ったとき、それぞれがdrawとmoveというプロパティを持っているのは無駄ではあります。まあ、各プロパティで持っているのは関数への参照値だけなので、たいしたことはないでしょうが、メソッドがもっとたくさんあったら気になることでしょう。ゲームの画面にエイリアンが100匹登場したとき、X座標やY座標はそれぞれのエイリアンごとに違う値である必要がありますが、drawやmoveはどうせ同じ関数しか指さないからです。
JavaScriptには、『オブジェクトのプロパティを探すとき、もしそのオブジェクトに指定された名前のプロパティがなかったら、「__proto__」という名前のプロパティが指す先のオブジェクトに探しに行く』という機能があります。そこにもなければ、さらにそのオブジェクトの__proto__の先を探しに行きます。あるオブジェクトの__proto__が指す先のオブジェクトのことを、そのオブジェクトのプロトタイプオブジェクト(prototype object)と呼び、プロトタイプオブジェクトを順に辿ってプロパティを探す仕組みのことをプロトタイプチェーン(prototype chain)と呼びます。
プロトタイプチェーンがあれば、エイリアンがたくさんいても、それらがすべて共通のプロトタイプオブジェクトを持っていれば、メソッドを参照するプロパティは共通にできます。
リスト8のようなプログラムで、「__proto__」の動きを直接確かめることができます。
この例では、obj1に存在しないbというプロパティを、「obj1.b」として参照できています。プロトタイプチェーンを辿ってobj2に探しに行っているためです。
その5: 関数にnew演算子を適用すると、「コンストラクタ関数」になる
newといえば、たとえばAlienというクラスがあったとしてnew Alien()と書けば新しいAlienのオブジェクト(インスタンス)が作られる、ということをこちらで説明しました。しかしこれは2015年のES6から導入された比較的新しい機能であり、それより前は、JavaScriptのnew演算子といえば、関数に対して適用するものでした。
たとえば以下のような関数Alien()を作ったとします。
この関数に対して、「new Alien(10, 20)」のようにnew演算子を適用します。
関数へのnewの適用は、以下のように動作します。
- ① まず、空のオブジェクトを作る。
- ② 上記①で作ったオブジェクトの__proto__に、new演算子の適用対象の関数の「prototype」というプロパティに設定されているオブジェクトをセットする。
- ③ 上記①で作ったオブジェクトをthisに設定して、関数を実行する。
- ④ この関数がオブジェクト型の戻り値をreturnしない場合、thisを返す。
リスト9のAlien関数の場合なら、①で空のオブジェクト({})が作られ、③でthisにそれが設定された後で、関数の中身の「this.x = x;」と「this.y = y;」が実行されて、④により、そのthisが返却されます。これにより、引数で渡されたxとyをそれぞれx, yプロパティに設定したAlienのオブジェクトが生成されるわけです。
Alienのオブジェクトならメソッドもいるだろう、メソッドはどうするんだ、という声が聞こえてきそうですが、そこで生きてくるのが②のルールです。メソッドはあらかじめ関数のprototypeプロパティに設定しておけばよいのです。たとえばAlienにdraw()とmove()というメソッドを付けたければ、以下のように書けます。
上のルールの②に「上記①で作ったオブジェクトの__proto__に、new演算子の適用対象の関数の「prototype」というプロパティに設定されているオブジェクトをセットします。」あります。このルールにより、新たに作られたオブジェクトの__proto__はmoveとかdrawとかを持っているオブジェクトを指すようになりますから、プロトタイプチェーン経由で普通にdraw()とかmove()が呼べるようになります。もちろん、エイリアンのインスタンスをたくさん作っても、prototypeの指す先のオブジェクトは共有されます(図8)。
ここまでやったら継承が欲しい、と思うかもしれません。たとえばAlienクラスをEnemyクラスのサブクラスにしたいとして、これを雑に実現するなら、AlienのprototypeにEnemyのインスタンスを設定すればできます。ここでは、例として、Enemyのmove()メソッドだけオーバーライドし、draw()メソッドはEnemyのものをそのまま使うことにしましょう。
Alien関数のprototypeにEnemyのオブジェクトを紐づけていますから、ここでエイリアンをnewすれば、生成されたエイリアンの__proto__はEnemyオブジェクトを指します。このEnemyのmoveは書き換えられているのでエイリアンを動かす関数を指しています。drawは書き換えられていないので、Enemyオブジェクトの__proto__を辿って、Enemyのprototypeのdrawが指す関数、つまりオーバーライド前のEnemy共通の関数が呼び出されます。
この方法では、Alienがたくさん作られても、それに紐づけられたEnemyオブジェクトはひとつしかありません。よって、スーパークラスであるEnemyに、それぞれの敵個別で保持したいデータのプロパティを保持することはできません。この方法が雑な実現だと書いたのはそのためです。
雑でない、(ある程度)まともな方法は結構面倒ですし、クラスが使える今そんな方法を覚える価値もないと思いますが、以下の記事が参考になるかと思います(こんなことをいまどきやろうとする人はいないので、どちらもかなり古い記事です)。
まとめとして、クラスを使わずに作ったシューティングゲームのリストを以下に載せます。
このリストをちゃんと読んだ人は、以下の部分を見て、
「あれ? この雑な継承では、それぞれの敵個別で保持したいデータをスーパークラスに持たせてはいけなかったのでは? hitLimitは敵の種類ごとに固定だからいいけど、hitCounterをここで持ったらまずいだろう」と思ったかもしれません。
JavaScriptでは、指定したプロパティがそのオブジェクトになかったら、__proto__を辿って探しに行きますが、そのオブジェクトにあれば、そのオブジェクトのプロパティを参照します。そして、プロパティは、代入によって作り出されます。よって、スーパークラスのプロパティに新しい値を代入しようとしても、それはサブクラス側へのプロパティの追加を意味します。この例でいえば、127行目の「this.hitCounter++;」により、Alienとかのサブクラス側にhitCounterが新設されるので、インスタンスごとに個別のhitCounterを持つことになります。
インスタンスベース? オブジェクトベース?
「クラスベース」のオブジェクト指向言語では、クラスというひな型をもとにインスタンスを作ります。たとえばJavaなら、同じクラスから作られたインスタンスは、すべて同じメソッドと同じデータフィールドを持ちます。RubyやPythonだとデータフィールド(インスタンス変数)は代入により実行時に作られるのでインスタンスごとに異なる可能性がありますが、メソッドについては、同じクラスであれば、基本的に共通です(基本的に、と書いているのは、Rubyは特異メソッドという機能でインスタンス固有のメソッドを作れるからです)。
それに対し、JavaScriptでは、オブジェクトは単なる連想配列です。class構文やコンストラクタ関数を使って「特定のメソッド群を持つオブジェクト」をいくつも生成することはできますが、そんなものを使わなくてもオブジェクトは作れますし、メソッドを付けることもできます。よって、インスタンスに対するメソッドは、すべてのインスタンスごとに個別に変更できます。
これがJavaScriptのような、クラスベースではないオブジェクト指向言語の特徴であると私は思います。
ここまで、「プロトタイプ」という言葉は出てきていません。このページで言えば、「プロトタイプ」という言葉が出てきたのは『その4: JavaScriptのオブジェクトは、「プロトタイプ」も検索する』からで、その前の『脱線:ここまでの知識で、JavaScriptで「オブジェクト指向」してみる』において普通にオブジェクト指向っぽいことは(継承含めて)実現できています。プロトタイプは、たくさんのオブジェクトが同じメソッドを持つときにちょっとメモリを節約できるとか、プロトタイプをいじることでたくさんのオブジェクトにまとめてメソッドを追加したり動きを変えたりできるとか※4というメリットはありますが、正直、これが本質だとは私には思えません。
なので、このような言語のことを「プロトタイプベース」と呼ぶのはやめて、「インスタンスベース」とか「オブジェクトベース」と呼ぶようにしよう、という意見もあります。実際、JavaScriptの作者であるブレンダン・アイクも、「"Object-based"」という言葉を使っていたわけですし。
そういえば、前述の通り、Rubyには特異メソッドという、インスタンスごとのメソッドを定義する機能がありますが、そのユーザーズガイドを見ると、以下のように書いてあります。
特異メソッドは別にrubyだけのオリジナルじゃない.珍しいのは確かだけど.rubyの他に特異メソッドを持っているのはCLOSやDylan などがある.中にはSelfやNewtonScriptのように特異メソッドしか持たないものもある.そういう言語をプロトタイプベースという.
JavaScriptも、メソッドはインスタンスごとなので、ここで言う「特異メソッドしか持たない」言語に相当します(ここで例に挙がっていないのは、このユーザーズガイドがかなり古く、まだJavaScriptの本格的な普及前だったからでしょう)。「特異メソッドしか持たない」言語をプロトタイプベースという、と言われても、「特異メソッドしか持たない」言語にプロトタイプは必須ではないので、これはやっぱり用語が混乱しているのだと思います。
公開日: 2022/10/10
ひとつ上のページに戻る |
トップページに戻る