Ruby脳が理解するJavaScriptのオブジェクト指向 error」の続きです。


引き続きJavaScriptのオブジェクト指向における「継承」について学んだので、自分の理解を書いてみます。当然に、間違いが含まれています。ご指摘助かります。

前回のまとめ

前回の記事では、JavaScriptの「オブジェクトの生成」、「プロトタイプチェーン」、「オブジェクトコンストラクタ」、「new演算子」の各概念について順にみました。これらを通したJavaScriptのオブジェクト指向に対する僕の理解は概ね次のようなものです。

  1. すべてのオブジェクトは__proto__プロパティ(非標準)を持っていて、プロパティ探索に関し、ここにセットされたオブジェクトを順次たどりこれを解決する(プロトタイプチェーン)。__proto__にはデフォルトで空オブジェクトがセットされている。
  2. 同種オブジェクト(属性値だけが異なるオブジェクト)を複数作るには、コンストラクタ関数を使うのが便利である。JavaScriptにはその目的のために、(クラスっぽい)専用記法によるコンストラクタ関数とnew演算子が用意されている。
  3. new演算子は、(1)コンストラクタの引数をプロパティとしたオブジェクトを生成して返し、(2)その際に、この生成オブジェクトの__proto__プロパティが、コンストラクタのprototypeプロパティにセットした唯一のオブジェクトを参照するようにリンクする、という処理を実行する。
  4. 専用記法コンストラクタはnewを忘れるとバグを生むので、「newは良くない部品」とする意見がある。

new 演算子を使う

同種のオブジェクトを多数作るためには差し当たり、関数コンストラクタ + new演算子が便利だということがわかりました。もう一度これらを使ってオブジェクトを生成してみます。諸般の事情によりiPhone5が買えないので(泣)、ここでiPhone5コンストラクタを作って独自ルートからiPhone5をゲットしようと思います。

iPhone5コンストラクタから生成されるiPhone5オブジェクトは固有のidを持ちます。それからマックユーザが、購入したデバイスに名前をつける習慣にならって(嘘です)、nameプロパティを用意します。idは本来は自動生成されるべきですが、ここでは手打ちとします。

function iPhone5 (id, name) {
  this.id = id;
  this.name = name;
};

iPhone5は、電話を掛けるcall、音楽を聞くiTunes、写真を撮るcamera、およびパノラマを撮るpanoramaの4つの機能を持っているので、これらのプロパティを持ったオブジェクトを、コンストラクタのprototypeプロパティにセットします。

iPhone5.prototype = {
  call: function(number) {
    return "Calling to " + number + " ...";
  },
  iTunes: function(title, artist) {
    return "Playing: => `" + title + "` of " + artist;
  },
  camera: function() {
    return this.name + " Take a Photo!";
  },
  panorama: function() {
    return this.name + " Take a Panorama Photo!!";
  }
};

さあiPhone5生成器ができました。早速newして、誰よりも早くiPhone5を手に入れます。

var jonathan = new iPhone5(12345, 'Jonathan');
var scott = new iPhone5(12346, 'Scott');

jonathan.id; // 12345
jonathan.name; // 'Jonathan'
jonathan.call('800-692-7753'); // 'Calling to 800-692-7753 ...'
jonathan.iTunes('Imagine', 'John Lennon'); // 'Playing: => `Imagine` of John Lennon'
jonathan.panorama(); // 'Jonathan Take a Panorama Photo!!'

scott.id; // 12346
scott.name; // 'Scott'
scott.call('800-275-2273'); // 'Calling to 800-275-2273 ...'
scott.iTunes('My Hero', 'Foo Fighters'); // 'Playing: => `My Hero` of Foo Fighters'
scott.panorama(); // 'Scott Take a Panorama Photo!!'

いいですね!とりわけnewっていうのが、新品ができる感じで。

オブジェクト関係図

ここで上記コードに係るオブジェクトの関係を図にしたので見てみます。グラフはruby-graphvizをラップした拙作graphazで作りました。

JS prototype chain noshadow

iPhone5コンストラクタ関数から3つのiPhone5オブジェクト、すなわちJonathanscottpeterが作成されています。各オブジェクトはそれぞれ固有のidおよびnameプロパティを持っています。iPhone5コンストラクタのprototypeプロパティには、別のオブジェクト(プロトタイプオブジェクト)がセットされています。プロトタイプオブジェクトは、calliTunescamerapanoramaの各プロパティを持っています。

今、jonathanオブジェクトでcallプロパティが呼ばれると、Jonathanオブジェクト自身はそれを持っていないので、iPhone5のprototypeプロパティにセットされたプロトタイプオブジェクトのcallが参照され、その結果が返されます。つまり、iPhone5コンストラクタは、Jonathanオブジェクトとプロトタイプオブジェクトとを繋ぐ導管の役割をしているのです。

継承

さて、Apple社内でも同種の方法でiPhone5を生産していると予想されますが、仮に僕がSteve JobsもといTim Cookだとしたら、上記コードには口を挟まざるを得ません。

なぜなら、iPhone5の生産においてAppleの過去の資産を全く活用していないからです。Apple社には当然にiPhone4を生産した次のようなコードが残っているはずです。

function iPhone4 (id, name) {
  this.id = id;
  this.name = name;
};

iPhone4.prototype = {
  call: function(number) {
    return "Calling to " + number + " ...";
  },
  iTunes: function(title, artist) {
    return "Playing: => `" + title + "` of " + artist;
  },
  camera: function() {
    return this.name + " Take a Photo!";
  }
};

var phil = new iPhone4(10101, 'Phil');

phil.id; // 10101
phil.name; // 'Phil'
phil.call('408-974-5050'); // 'Calling to 408-974-5050 ...'
phil.iTunes('Valentine', 'Fiona Apple'); // 'Playing: => `Valentine` of Fiona Apple'
phil.camera(); // 'Phil Take a Photo!'

iPhone5は基本的にiPhone4にpanorama機能を付加しただけのものですから1、これを活用しない手はありません。

iPhone5コンストラクタをこの資産を継承した形に書き換えます。

function iPhone5 (id, name) {
  this.id = id;
  this.name = name;
};

iPhone5.prototype = new iPhone4;
iPhone5.prototype.panorama = function() {
  return this.name + " Take a Panorama Photo!!";
};

iPhone5.prototype = new iPhone4としている点がポイントです。つまりiPhone5のprototypeプロパティに、iPhone4コンストラクタから生成されるオブジェクトをそのプロトタイプオブジェクトとしてセットしています。そしてそのプロトタイプオブジェクトに、iPhone5の独自機能であるpanoramaを追加しました。

さあもう一度、iPhone5オブジェクトを作ってみます。

var jonathan = new iPhone5(12345, 'Jonathan');
var scott = new iPhone5(12346, 'Scott');

jonathan.id; // 12345
jonathan.name; // 'Jonathan'
jonathan.call('800-692-7753'); // 'Calling to 800-692-7753 ...'
jonathan.iTunes('Imagine', 'John Lennon'); // 'Playing: => `Imagine` of John Lennon'
jonathan.panorama(); // 'Jonathan Take a Panorama Photo!!'

scott.id; // 12346
scott.name; // 'Scott'
scott.call('800-275-2273'); // 'Calling to 800-275-2273 ...'
scott.iTunes('My Hero', 'Foo Fighters'); // 'Playing: => `My Hero` of Foo Fighters'
scott.panorama(); // 'Scott Take a Panorama Photo!!'

うまくいったようですね。

オブジェクト関係図

さて、先の例と同様に新たなiPhone5生産システムにおけるオブジェクトの関係図も見てみます。

JS prototype chain noshadow

iPhone5コンストラクタのprototypeプロパティにはiPhone4コンストラクタで作ったオブジェクトがセットされています。このオブジェクトはあとで追加したpanoramaプロパティを持っています。

今、jonathanオブジェクトでcallプロパティが呼ばれると、Jonathanオブジェクト自身はそれを持っていないので、iPhone5のprototypeプロパティにセットされたプロトタイプオブジェクトのプロパティを参照します。ところが、このオブジェクトもcallプロパティを持っていないので、iPhone4のprototypeプロパティにセットされたオブジェクトを参照します。そしてこのオブジェクトにおけるcallが参照され、その結果が返されることになります。つまり、iPhone5およびiPhone4コンストラクタは、プロトタイプチェーンを形成する導管の役割をしています。

更に継承

僕自身は上記生産システムに不満はありませんが、超効率主義のあなたは納得しないでしょうね。

あなた「iPadはどうなってんのよ。別に作れってか」

蛇足と思われますが折角ここまで来たので、ご要望にお答えしまして上記生産システムにiPadの生産ラインも載せることにします。iPhoneとiPadのコアはご存知iOSですから、これをベースにしてiPadコンストラクタのラインも追加します。

function iOS () { };

iOS.prototype = {
  call: function(number) {
    return "Calling to " + number + " ...";
  },
  iTunes: function(title, artist) {
    return "Playing: => `" + title + "` of " + artist;
  }
};

function iPhone4 (id, name) {
  this.id = id;
  this.name = name;
};

iPhone4.prototype = new iOS;
iPhone4.prototype.camera = function() {
  return this.name + " Take a Photo!"
};

function iPhone5 (id, name) {
  this.id = id;
  this.name = name;
};

iPhone5.prototype = new iPhone4;
iPhone5.prototype.panorama = function() {
  return this.name + " Take a Panorama Photo!!";
};

function iPad (id, name) {
  this.id = id;
  this.name = name;
  this.call = function() { return 'not implemented yet'};
};

iPad.prototype = new iOS;

このコードのポイントは、iOSコンストラクタで生成したオブジェクトをiPhone4のprototypeプロパティと、iPadのprototypeプロパティにセットしている点です。またiOSコンストラクタのprototypeプロパティにセットしたオブジェクトは、calliTunesプロパティを持っていますが、iPadコンストラクタでは同名のcallプロパティを定義してこれを無効化しています。

これでiPadも同時に手に入れることができます。iPhone5とiPadを共にゲットしましょう。

var jonathan = new iPhone5(12345, 'Jonathan');
var scott = new iPhone5(12346, 'Scott');

jonathan.id; // 12345
jonathan.name; // 'Jonathan'
jonathan.call('800-692-7753'); // 'Calling to 800-692-7753 ...'
jonathan.iTunes('Imagine', 'John Lennon'); // 'Playing: => `Imagine` of John Lennon'
jonathan.panorama(); // 'Jonathan Take a Panorama Photo!!'

scott.id; // 12346
scott.name; // 'Scott'
scott.call('800-275-2273'); // 'Calling to 800-275-2273 ...'
scott.iTunes('My Hero', 'Foo Fighters'); // 'Playing: => `My Hero` of Foo Fighters'
scott.panorama(); // 'Scott Take a Panorama Photo!!'

var tim = new iPad(8765, 'Tim');

tim.id; // 8765
tim.name; // 'Tim'
tim.iTunes('Black Hourse And The Cherry Tree', 'KT Tunstall'); // 'Playing: => `Black Hourse And The Cherry Tree` of KT Tunstall'
tim.call('800-694-7466'); // 'not implemented yet'

いいですね!

オブジェクト関係図

新たなiPhone5およびiPad生産システムにおけるオブジェクトの関係図を見てみます。

JS prototype chain noshadow

iPhone4コンストラクタのprototypeプロパティにセットしたオブジェクトにはcameraプロパティがセットされる一方で、iPadコンストラクタのprototypeプロパティにセットしたオブジェクトには対応プロパティは無いので、このコンストラクタで生成されるiPadにはカメラ機能はないということになります。

また、iPadコンストラクタで生成されるiPadオブジェクトは、callプロパティを持ちます(このcallは’not implemented yet’を返します)。このためiPadオブジェクトからcallプロパティを呼ぶと、iOSコンストラクタのprototypeプロパティにおけるcallプロパティではなくiPadオブジェクトに実装されたcallプロパティが呼ばれることになります。

以上のようにして、私は誰よりも早くiPhone5を手に入れたのでした^ ^;


JavaScriptでは異なる実装による複数の継承が実現できるようですが、ここではnew演算子を使った最も一般的な方法による継承を説明してみました。


(追記:2012-09-16) 以下を追記しました。

Rubyにおける継承

対比のためRubyにおける対応コードを貼っておきます。Rubyにはクラスと似たしかしインスタンスを生成しないモジュールというエンティティがあります。IOSはこれを使って実装し、IPhone4クラスおよびIPadクラスにincludeすることで、その機能を各クラスに付与するようにしています。

オブジェクト関係図

上記コードに対応する図は次のようになります。

Ruby class inheritance noshadow

Rubyではオブジェクトに対するアクセスはすべてメソッド呼び出しで、基本的にその実体はクラスが保持しています。各オブジェクトはその状態情報(ここではname, id)だけを保持しています。

ここでは詳しい説明は割愛しますが、JavaScriptにおける図と対比して頂けると、2つの言語におけるオブジェクト指向の実現方法の違いが見えてくるかもしれません。

サンプルコードは以下においておきます。

iPhone5 constructor for describing JavaScript prototype chain — Gist


JS OOP Ebook

電子書籍「Ruby脳が理解するJavaScriptのオブジェクト指向」EPUB版

このリンクはGumroadにおける商品購入リンクになっています。クリックすると、オーバーレイ・ウインドウが立ち上がって、この場でクレジットカード決済による購入が可能です。購入にはクレジット情報およびメールアドレスの入力が必要になります。購入すると、入力したメールアドレスにコンテンツのDLリンクが送られてきます。


(追記:2012-09-18) 続きを書きました。

Ruby脳が理解するJavaScriptのオブジェクト指向(その3)


よくわかるiPhoneアプリ開発の教科書【iOS 5&Xcode 4.2対応版】 by 森 巧尚


  1. 冗談に怒らないように!


blog comments powered by Disqus
ruby_pack8

100円〜で好評発売中!
M'ELBORNE BOOKS