穂乃果「穂乃果、決めたよ」
海未「はい」
穂乃果「後で天国が待っているなら、穂乃果は地獄に身を投じるよっ!」
ことり「穂乃果ちゃん・・・!」
海未「よい覚悟です。それでこそ穂乃果、私たちのリーダーですね」
穂乃果「えへへ・・・」
海未「では今回は地獄を見てもらいましょう。・・・といっても、できるだけ簡潔になるよう整理してきましたし、入門の範囲を超える複雑性はカットしますから」
ことり「血の池地獄で、いい湯だな~、ってできるくらい・・・?」
海未「まあ、少なくとも舌抜いたりはしませんよ。最近の地獄は昔と比べると平和になったということです」
海未「まず、継承という考え方からいきましょう。まずは人を表すコンストラクタ関数を用意します」
|
1 2 3 4 5 |
function Person(name, age) { this.name = name; this.age = age; } |
海未「人間の持つ属性は性別とか出身地とかマイナンバーとかいろいろありますが、ここでは名前と年齢ということにしましょう」
ことり「これを使うと、穂乃果ちゃんとか海未ちゃんとか、山内先生とかお母さんとか・・・アルパカさんは、無理かなぁ」
海未「・・・まあ扱えなくはないですが・・・やめておきましょう」
穂乃果「これは前回やったやつだよね?」
海未「そうです。話はここからで、世の中には2種類の人間がいる、アイドルかそうでないかだ、というわけで、アイドルを表すコンストラクタ関数を用意します」
ことり「じゃ、作ってみるね」
|
1 2 3 4 5 6 7 8 9 10 11 12 |
function Idol(name, age, group) { this.name = name; this.age = age; this.group = group; this.sing = function() { console.log("Sing!!"); }, this.dance = function() { console.log("Dance!!"); } } |
ことり「こんなのでいいかな?」
海未「そうですね。ではこれでいきましょう。さて、アイドルは人間の種類の1つだという話をしました。このコンストラクタを見ると、nameとageは両方にあります」
穂乃果「えーと、つまり?」
海未「アイドルは人間を拡張したものと見なすことができるわけです。Personに対して、グループに所属していること、歌や踊りができることが付加されていますね」
ことり「言い方はアレだけど、特別な人、みたいな感覚なんだ」
海未「それを表現するのが継承です。Personを継承してIdolを作るという実装をします」
|
1 2 3 4 5 6 7 8 9 10 11 12 |
function Idol(name, age, group) { Person.call(this, name, age); this.group = group; this.sing = function() { console.log("Sing!!"); }, this.dance = function() { console.log("Dance!!"); } } Idol.prototype = Object.create(Person.prototype); |
海未「この関係を、Idol is a Personが成り立つ、is-aの関係といいます」
穂乃果「prototypeって見慣れないのが出てきたんだけど・・・」
海未「それは非常に重要な概念です。JavaScriptはプロトタイプベースオブジェクト指向言語、というのは一番始めに話しましたが、そのプロトタイプベースがこれのことです」
穂乃果「む、なんか難しそうなの来たぞ・・・」
海未「prototypeは関数オブジェクト、この例ではPersonやIdolに自動的に付加されるプロパティです。親オブジェクトというか拡張元オブジェクトが入っていると考えればよいでしょう」
ことり「親?」
海未「このPersonとIdolの関係であれば、Personを親、Idolを子といいます。なので、IdolのprototypeにはPersonが入っています。というか、入れています」
ことり「最後の行だよね。これが、継承関係を作っている、ってこと?」
海未「そうです。それによって、IdolはPersonの持つnameやageも自身のプロパティとして利用できるのです」
|
1 2 3 4 5 |
var honoka = new Idol("Honoka Kosaka", 16, "μ's"); honoka.name; // Honoka Kosaka honoka.age; // 16 honoka.group; // μ's |
ことり「じゃあ、Idolをもっと拡張するときは、追加するプロパティだけコンストラクタ関数に書いて、Idol.prototypeを新しいコンストラクタ関数のprototypeに入れればいいんだね」
海未「そうです。・・・穂乃果、ついてきていますか?」
穂乃果「と、とりあえず書き方はわかったよ・・・けど、Person.call(this, name, age);ってところは何?」
海未「callは関数オブジェクトの持つメソッドで、その関数自身を呼び出すものです。第1引数でthisを渡すと、Person関数内のthisはそのthis、つまりIdol関数になります」
穂乃果「むむむ・・・ややこしいよ」
海未「要は継承元オブジェクトのプロパティの初期化の書き方だと考えれば大丈夫です」
海未「プロトタイプの話が出てきたので、メソッドの定義の仕方について少し補足しておきます」
|
1 2 3 4 5 6 7 8 9 10 11 12 |
function Idol(name, age, group) { Person.call(this, name, age); this.group = group; this.sing = function() { console.log("Sing!!"); }, this.dance = function() { console.log("Dance!!"); } } Idol.prototype = Object.create(Person.prototype); |
海未「先ほどのコードの再掲ですが、これまでこのように定義をしてきました。しかしこの場合、Idolから生成した全てのオブジェクトが、singやdanceといった関数を個別に持つ形になります」
穂乃果「それって、何が困るの?」
海未「メモリの効率性の面でよくないとされています。ですから、メソッドはオブジェクトそのものではなく、プロトタイプに定義しておくと共有ができるので望ましいです」
|
1 2 3 4 5 6 7 8 9 10 11 12 |
function Idol(name, age, group) { Person.call(this, name, age); this.group = group; } Idol.prototype = Object.create(Person.prototype); Idol.prototype.sing = function() { console.log("Sing!!"); } Idol.prototype.dance = function() { console.log("Dance!!"); } |
海未「このような書き方が、無駄なリソースを食わずにすむので一般的なようです」
ことり「ちょっとごちゃごちゃしてきたね」
穂乃果「オブジェクトがたくさんあっても、プロトタイプは1つだけだから、これでいいんだね」
海未「もう1階層追加してみました。全体像を改めて見ておきましょう」
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
function Person(name, age) { this.name = name; this.age = age; } function Idol(name, age, group) { Person.call(this, name, age); this.group = group; } Idol.prototype = Object.create(Person.prototype); Idol.prototype.sing = function() { console.log("Sing!!"); } Idol.prototype.dance = function() { console.log("Dance!!"); } function SchoolIdol(name, age, group, school) { Idol.call(this, name, age, group); this.school = school; } SchoolIdol.prototype = Object.create(Idol.prototype); SchoolIdol.prototype.sing = function(title = "Snow halation") { console.log(`Sings: ${title}`); } function ProfessionalIdol(name, age, group, production) { Idol.call(this, name, age, group); this.production = production; } ProfessionalIdol.prototype = Object.create(Idol.prototype); ProfessionalIdol.prototype.talk = function() { console.log("Talk!!"); } var honoka = new SchoolIdol("Honoka Kosaka", 16, "μ's", "Otonokizaka High"); honoka.name; // Honoka Kosaka honoka.age; // 16 honoka.group; // μ's honoka.school; // Otonokizaka HS honoka.sing(); var uzuki = new ProfessionalIdol("Uzuki Shimamura", 17, "CINDERELLA PROJECT", "346PRO"); uzuki.name; // Uzuki Shimamura uzuki.age; // 17 uzuki.group; // CINDERELLA PROJECT uzuki.production; // 346PRO uzuki.sing(); |
海未「増えたのはSchoolIdolとProfessionalIdolですが、実装の内容については大丈夫でしょう」
ことり「さっきと同じだね」
穂乃果「あ、卯月ちゃんだ」
海未「これを例に、プロトタイプチェーンの説明をします。honoka.nameやhonoka.schoolのようにプロパティにアクセスするとき、どうやって探しているかという話です」
ことり「継承すると、同じ名前のプロパティがいくつかあるかもしれないから・・・」
海未「そうです。例えばこの例では、singメソッドがIdolとSchoolIdolの2箇所で定義されていますね」
海未「まずhonoka.sing()ですが、singという名前のプロパティを探す際、最初にローカルにその名前のプロパティがないかを探します」
穂乃果「ローカル?」
海未「この場合であればhonoka.sing = function() {...}のようにhonokaに直接メソッドを追加しているコードがあれば、そこで追加されたメソッドが実行されます」
穂乃果「えーと・・・このコードにはないね」
海未「はい。ですから、次の探し方に移ります。次は、honokaのプロトタイプにメソッドがあるかどうか、です」
穂乃果「プロトタイプはSchoolIdol.prototype・・・でいいんだっけ?」
海未「そうです。SchoolIdol.prototypeを見るとsingメソッドが定義されていますから、それが実行されてSings: Snow halationと出力されます」
穂乃果「ふむふむ・・・」
海未「では島村さんはどうでしょう。uzukiにはローカルのsingメソッドはなく、プロトタイプのProfessionalIdol.prototypeにもsingメソッドはありません」
ことり「たぶん、その上のIdol.prototypeのsingが実行されるんだよね」
海未「はい。動きとしては、ローカルになくプロトタイプにもないと、プロトタイプのプロトタイプへ探しに行きます。この場合はIdol.prototypeですね」
ことり「そっか、それでプロトタイプチェーンなんだ」
海未「目的のプロパティが見つかるまでプロトタイプを辿り続けますから、それでプロトタイプチェーンという呼び方をします」
穂乃果「もし、Personまで辿っても見つからなかったらエラーになるの?」
海未「Personになかった場合、組み込みのObject.prototypeを見に行きます。Personのようにプロトタイプを指定していない場合は、Objectがプロトタイプと見なされるからです」
穂乃果「根っこは必ずObjectなんだね」
海未「そのObjectにもなかったら、undefinedになります」
海未「継承関係をコードで見るとこうなります」
|
1 2 3 4 5 |
honoka.__proto__ == SchoolIdol.prototype; // true honoka.__proto__.__proto__ == Idol.prototype; // true honoka.__proto__.__proto__.__proto__ == Person.prototype; // true honoka.__proto__.__proto__.__proto__.__proto__ == Object.prototype; // true |
穂乃果「な、何これ・・・」
海未「オブジェクトの__proto__というプロパティには、そのオブジェクトのプロトタイプが入っています。これを利用してプロトタイプチェーンを遡ったものがこれです」
ことり「たしかに、Objectが最後にくるんだね」
海未「さて、オブジェクトにはいつでも自由にプロパティを追加できました。ここでも同じことが言えます」
|
1 2 3 4 5 6 7 8 9 10 |
var honoka = new SchoolIdol("Honoka Kosaka", 16, "μ's", "Otonokizaka High"); var uzuki = new ProfessionalIdol("Uzuki Shimamura", 17, "CINDERELLA PROJECT", "346PRO"); var mio = new ProfessionalIdol("Mio Honda", 15, "CINDERELLA PROJECT", "346PRO"); ProfessionalIdol.prototype.salary = 100000; uzuki.salary; // 100000 mio.salary; // 100000 honoka.salary; // undefined |
海未「ProfessionalIdolには給料が発生しました」
穂乃果「ずるい!穂乃果も!」
海未「私たちは部活動ではないですか」
ことり「巨大な財布はあるけどね・・・」
海未「とにかく。ProfessionalIdol.prototypeにプロパティを追加すると、それは全てのProfessionalIdolオブジェクトに反映されます」
穂乃果「で、SchoolIdolには反映されないんだよね・・・」
海未「色々整理したり過度に難解な部分を割愛したりしたので思いの外地獄ではありませんでしたが」
穂乃果「そうでもないよ・・・」
海未「JavaScriptにおけるオブジェクト指向プログラミングは、これが伝統的な方法でした。互換性の面からも、広く使われているスタイルだと思います」
ことり「なんとなく、なんとな~くわかったと思うな・・・」
海未「後は場数でしょう。さて、ECMAScript6では、この複雑さを軽減するための新しい構文が導入されました。次回はそれを見てみましょう」