今回は JavaScript の特徴である「プロトタイプベース」のオブジェクト指向機能について説明します。現在のオブジェクト指向はクラスでインスタンス変数やメソッドを定義する「クラスベース」が主流です。ところがプロトタイプベースにはクラスが存在しません。このため、「雛形 (プロトタイプ) 」となるオブジェクトから、新しいオブジェクトを生成する方法が必要になります。この場合、次の 2 通りの方法が考えられます。
2 の方法はクローン (clone) と呼ばれることもあります。JavaScript の場合、基本的には 1 の方法を使います。まず最初にオブジェクトの生成から説明します。
JavaScript では、new 演算子を使って空のオブジェクトを生成し、それを関数に渡してオブジェクトに必要なデータをセットします。この関数を「コンストラクタ (constructor) 」と呼びます。JavaScript の場合、オブジェクトはハッシュで実装されています。ハッシュのキーのことを「プロパティ (property) 」と呼びます。プロパティは「属性」という意味で、クラスベースのオブジェクト指向では「インスタンス変数」に相当します。
たとえば、コンストラクタを Foo とすると、new Foo() で新しいオブジェクトが生成されます。次の例を見てください。
リスト : コンストラクタ function Foo(a, b) { this.a = a; this.b = b; }
JavaScript の場合、コンストラクタの中ではキーワード this を使ってオブジェクトを参照することができます。new Foo() の場合、new で生成されたオブジェクトが this に渡されるので、関数 Foo の中では this を使ってオブジェクトのプロパティにアクセスすることができます。
プロパティのアクセス方法は [ ] だけではなく、object.name でもアクセスすることができます。this.a は this["a"] と同じ意味です。ただし、this.1 のように数値を直接指定することはできません。この場合は [ ] を使って this[1] とします。
それでは実際にオブジェクトを生成してみましょう。
> x = new Foo(10, 20) Foo {a: 10, b: 20} > x.a 10 > x.b 20 > y = new Foo(100, 200) Foo {a: 100, b: 200} > y.a 100 > y.b 200
JavaScript の場合、プロパティのアクセスを制限する機能はありません。どこからでもアクセスすることができます。
JavaScript の場合、オブジェクトのプロパティに関数オブジェクトをセットすれば、それをメソッドとして呼び出すことができます。メソッドの呼び出しは object.method() とします。このとき、メソッドの中ではキーワード this を使って呼び出したオブジェクト (object) を参照することができます。次の例を見てください。
リスト : メソッドの定義 (1) function Foo(a, b) { this.a = a; this.b = b; this.get_a = function() { return this.a; }; this.get_b = function() { return this.b; }; this.set_a = function(x) { this.a = x; }; this.set_b = function(x) { this.b = x; }; }
コンストラクタ Foo の中でメソッド get_a(), get_b(), set_a(), set_b() を定義します。匿名関数で関数オブジェクトを生成してプロパティにセットするだけなので簡単です。
それでは実際に試してみましょう。
> x = new Foo(10, 20) Foo {a: 10, b: 20, get_a: function, get_b: function, set_a: function…} > x.get_a() 10 > x.get_b() 20 > x.set_a(100) undefined > x.get_a() 100 > x.set_b(200) undefined > x.get_b() 200
このように、関数オブジェクトをプロパティにセットすれば、それをメソッドとして呼び出すことができます。
ところで、この方法ではオブジェクトを生成するたびに、新たな関数オブジェクトが生成されてプロパティにセットされます。同じ処理を行う関数をいくつも作るのは無駄ですね。そこで、メソッド用の関数を定義して、それをプロパティにセットすることにしましょう。次の例を見てください。
リスト : メソッドの定義 (2) function Foo(a, b) { function get_a() { return this.a; }; function get_b() { return this.b; }; function set_a(x) { this.a = x; }; function set_b(x) { this.b = x; }; this.a = a; this.b = b; this.get_a = get_a this.get_b = get_b this.set_a = set_a this.set_b = set_b }
Foo の中で局所関数 get_a(), get_b(), set_a(), set_b() を定義し、それをプロパティにセットします。これで、無駄な関数オブジェクトの生成を抑えることができます。ただし、この方法でもメソッドは個々のオブジェクトに格納されるので、メモリを余分に使うことになります。この問題は「プロトタイプチェーン」という機能を使うと解決することができます。
JavaScript の場合、new 演算子でオブジェクトを生成するとき、プロトタイプとなるオブジェクトを指定することができます。もし、オブジェクトの中でプロパティが見つからない場合はプロトタイプからプロパティを探します。そのプロトタイプにも、プロトタイプとなるオブジェクトが存在する場合があります。つまり、複数のプロトタイプがつながっている場合があるのです。これをプロトタイプチェーンといいます。
ようするに、JavaScript はプロトタイプをコピーするのではなく、プロトタイプをリンクでつないでおくわけです。そして、プロトタイプチェーンをたどってプロパティを探します。また、プロトタイプチェーンを使って「継承」を実現することもできます。
コンストラクタで生成されるオブジェクトのプロトタイプは、コンストラクタ (関数オブジェクト) のプロパティ prototype で指定します。次の例を見てください。
リスト : メソッドの定義 (3) function Foo(a, b) { this.a = a; this.b = b; } Foo.prototype.get_a = function() { return this.a; }; Foo.prototype.get_b = function() { return this.b; }; Foo.prototype.set_a = function(x) { this.a = x; }; Foo.prototype.set_b = function(x) { this.b = x; };
関数はコンストラクタになる可能性があるので、関数オブジェクトにはプロパティ prototype が必ず用意され、その値は空オブジェクトで初期化されています。たとえば上記プログラムの場合、コンストラクタ Foo の prototype には空オブジェクトがセットされています。そして、演算子 new でオブジェクトを生成するとき、新しいオブジェクトのプロトタイプチェーンに Foo.prototype のオブジェクトがリンクされます。
ここで、Foo.prototype で設定するオブジェクトは、new 演算子で生成されるオブジェクトのプロトタイプを指定するものであり、Foo のプロトタイプではないことに注意してください。プロトタイプチェーンを表すプロパティ名は処理系に依存していて、Google Chrome の場合は __proto__ になります。つまり、new 演算子は生成したオブジェクトの __proto__ に、コンストラクタの prototype の値をセットするわけです。これを図に示すと次のようになります。
したがって、Foo.prototype にセットされているオブジェクトにメソッドを追加すれば、new Foo() で生成したオブジェクトからそれらのメソッドを呼び出すことができるわけです。また Foo.prototype には、次のように { } でオブジェクトを生成してセットすることもできます。
リスト : メソッドの定義 (4) function Foo(a, b) { this.a = a; this.b = b; } Foo.prototype = { get_a: function() { return this.a; }, get_b: function() { return this.b; }, set_a: function(x) { this.a = x; }, set_b: function(x) { this.b = x; } }
{ } の中では name: value でプロパティ名とその値を設定することができます。このように、prototype のオブジェクトでメソッドを設定すると、コンストラクタで生成されたオブジェクトからそのメソッドを呼び出すことができます。メソッドを持っているオブジェクトは一つだけなので、メモリを余分に使うこともありません。
それでは簡単な例題として、点と表すオブジェクトを作ってみましょう。コンストラクタ Point で作成するオブジェクトは 2 次元座標を表し、Point3D で作成するオブジェクトは 3 次元座標を表します。それぞれ、2 点間の距離を計算するメソッド distance() を定義します。プログラムは次のようになります。
リスト : 座標を表すオブジェクト // 2 次元座標 function Point(x, y) { this.x = x; this.y = y; } // 距離を求める Point.prototype.distance = function(p) { var dx = this.x - p.x; var dy = this.y - p.y; return Math.sqrt(dx * dx + dy * dy); } // 3 次元座標 function Point3D(x, y, z) { this.x = x; this.y = y; this.z = z; } // 距離を求める Point3D.prototype.distance = function(p) { var dx = this.x - p.x; var dy = this.y - p.y; var dz = this.z - p.z; return Math.sqrt(dx * dx + dy * dy + dz * dz); }
コンストラクタ Point, Point3D は座標を受け取り、それをオブジェクトにセットします。メソッド distance() は引数 p にオブジェクトを受け取り、this と p の距離を計算します。sqrt() は平方根を求める関数で、JavaScript のグローバルオブジェクト Math に定義されています。
それでは実行してみましょう。
> p1 = new Point(0, 0) Point {x: 0, y: 0, distance: function} > p2 = new Point(10, 10) Point {x: 10, y: 10, distance: function} > p3 = new Point3D(0, 0, 0) Point3D {x: 0, y: 0, z: 0, distance: function} > p4 = new Point3D(10, 10, 10) Point3D {x: 10, y: 10, z: 10, distance: function} > p1.distance(p2) 14.142135623730951 > p3.distance(p4) 17.32050807568877
このように、ドットの左側のオブジェクトによって適切なメソッドが呼び出され、ポリモーフィズム (polymorphism) がきちんと働いていることがわかります。
クラスベースのオブジェクト指向に慣れている方ならば、prototype で指定したオブジェクトを「クラス」と考えるとわかりやすいかもしれません。たとえば、オブジェクトで共通に使用する変数が必要な場合、prototype のオブジェクトに変数を定義することで実現することができます。これはクラスベースでいうところの「クラス変数」と同じ機能です。
簡単な例を示しましょう。
> function Foo() {} undefined > Foo.prototype = {bar: 1} Object {bar: 1} > a = new Foo() Foo {bar: 1} > b = new Foo(); Foo {bar: 1} > a.bar 1 > b.bar 1
プロパティ bar がクラス変数になります。new Foo() でオブジェクトを生成して、変数 a, b にセットします。a.bar と b.bar は同じプロパティ bar を参照するので、同じ値 1 になります。
bar の値を書き換える場合は注意が必要です。次の例を見てください。
> Foo.prototype.bar = 10 10 > a.bar 10 > b.bar 10 > a.bar = 20 20 > a.bar 20 > b.bar 10
最初の例は prototpye のオブジェクトを指定して、プロパティ bar の値を書き換えています。この場合、a.bar と b.bar は書き換えた値 10 になります。次に、a.bar = 20 を実行します。このとき、オブジェクト a にプロパティ bar が存在しないことに注意してください。
JavaScript の場合、オブジェクトにプロパティが存在しない場合、そのオブジェクトにプロパティを登録し、そこに値を代入します。つまり、オブジェクト a にプロパティ bar が作られて、そこに 20 がセットされるのです。この場合、prototype の bar は 10 のままなので、b.bar の値は 10 になります。a.bar の値は a のプロパティ bar を参照するので 20 になります。
このため、クラス変数を使用する場合は、prototype のオブジェクトに直接アクセスするよりも、次のようにアクセサを定義した方がよいでしょう。
リスト : クラス変数のアクセスメソッド function Foo() {} Foo.prototype = { bar: 1, show: function() { return Foo.prototype.bar; }, update: function(x) { Foo.prototype.bar = x; } }
メソッド show() で Foo.prototype の bar の値を参照し、update() で bar の値を更新します。簡単な例を示しましょう。
> a = new Foo(); Foo {bar: 1, show: function, update: function} > b = new Foo() Foo {bar: 1, show: function, update: function} > a.show() 1 > b.show() 1 > a.update(10) undefined > a.show() 10 > b.show() 10 Foo.prototype.show() 10 Foo.prototype.update(100) undefined Foo.prototype.show() 100 a.show() 100 b.show() 100
show() と update() は new Foo() で生成したオブジェクトから呼び出すことができますし、Foo.prototype に格納されたオブジェクトからも呼び出すこともできます。show() と update() は、クラスベースオブジェクト指向でいうところの「クラスメソッド」という機能に相当します。
クラスメソッドを定義するとき、this の使用には十分に注意してください。Foo.prototype からメソッドを呼び出すと、this の値は Foo.prototype のオブジェクトになります。ところが、オブジェクト a から呼び出すと this の値は a になります。たとえば、update() の処理を this.bar = x; とした場合、オブジェクト a から呼び出すと a のプロパティ bar に x の値を代入することになり、Foo.prototype の bar の値を書き換えることはできません。
今回は簡単な例題として、「連結リスト (Linked List) 」という基本的なデータ構造を作ってみましょう。なお、今回のプログラムは お気楽 Python プログラミング入門 連結リスト のプログラムを JavaScript で書き直したものです。内容は重複していますが、あしからずご了承ください。
連結リストはデータを一方向につなげたデータ構造です。リストを操作するプログラミング言語では Lisp が有名ですが、Lisp で扱うリストが連結リストです。下図に連結リストの構造を示します。
図 : 連結リスト
連結リストはセル (cell) というデータを繋げて作ります。セルにはデータを格納する場所と、次のセルを指し示す場所から構成されます。上図でいうと、箱がひとつのセルを表していて、左側にデータを格納し、右側に次のセルへの参照を格納します。リストの終わりを示すため、最後のセルの右側には特別な値(たとえば null)を格納します。そして、図 (1) のように先頭セルへの参照を変数に格納しておけば、この変数を使って連結リストにアクセスすることができます。また、図 (2) のようにヘッダセルを用意する方法もあります。
それではプログラムを作りましょう。まずは、セル Cell と連結リスト List のコンストラクタを作成します。
リスト : コンストラクタの定義 // セル function Cell(data, link) { this.data = data; this.link = link; } // 連結リスト function List() { var cp = new Cell(null, null); this.top = cp; for(var i = 0; i < arguments.length; i++) { cp.link = new Cell(arguments[i], null); cp = cp.link; } }
Cell はセルを作成します。プロパティ data にデータを格納し、link に接続するセルへの参照を格納します。List は連結リストのオブジェクトを生成します。今回は図 (2) の方法でプログラムします。List が生成したオブジェクトのプロパティ top にはヘッダーセルを格納します。List() は可変個の引数を受け取るようにすると便利です。配列 arguments から要素 x を取り出し、連結リストの最後尾に x を追加していきます。
あとはメソッドを定義するだけです。今回作成するメソッドを下表に示します。
メソッド | 機能 |
---|---|
ls.at(n) | n 番目の要素を求める |
ls.insert(n, x) | n 番目の位置にデータ x を挿入する |
ls.remove(n) | n 番目の要素を削除する |
ls.isEmpty() | 連結リストが空の場合は真を返す |
ls.each(func) | 要素に関数 func を適用する |
プログラムは次のようになります。
リスト : メソッドの定義 (1) List.prototype = { // 作業用メソッド : n 番目のセルを返す _nth: function(n) { var cp = this.top; var i = -1; while(cp != null){ if(n == i) return cp; cp = cp.link; i += 1; } return null; }, // n 番目の要素を返す at: function(n) { var cp = this._nth(n); if(cp) return cp.data; return null; }, // n 番目にデータを挿入 insert: function(n, x) { var cp = this._nth(n - 1); if (cp) { cp.link = new Cell(x, cp.link); return x; } return null; }, // n 番目の要素を削除 remove: function(n) { var cp = this._nth(n - 1); if (cp && cp.link) { var data = cp.link.data; cp.link = cp.link.link; return data; } return null; }, // イテレータ each: function(func) { var cp = this.top.link; while (cp != null) { func(cp.data); cp = cp.link; } }, // 空リストか isEmpty: function() { return this.top.link == null; } }
メソッドはオブジェクト { } の中で定義して、それを List.prototype にセットします。_nth() は n 番目のセルを求める作業用のメソッドです。at(), insert(), remove() は _nth() を使うと簡単にプログラムすることができます。詳しい説明は お気楽 Python プログラミング入門 連結リスト をお読みください。
それでは、簡単な実行例を示しましょう。
> a = new List() List {top: Cell, _nth: function, at: function, insert: function, remove: function…} > for (i = 0; i < 5; i++) a.insert(0, i) 4 > a.at(0) 4 > a.at(4) 0 > a.each(function(x){ console.log(x); }) 4 3 2 1 0 undefined > a.remove(0) 4 > a.each(function(x){ console.log(x); }) 3 2 1 0 undefined
正常に動作していますね。
このほかに、連結リストを配列に変換するメソッド toArray() と文字列に変換するメソッド toString() を定義すると便利です。次のリストを見てください。
リスト : データの変換 // 配列への変換 toArray: function(){ var ary = []; this.each(function(x){ ary.push(x); }); return ary; }, // 文字列への変換 toString: function(){ return "(" + this.toArray().join(", ") + ")"; }
toArray() はメソッド each() を使うと簡単です。each() で先頭から順番に要素にアクセスし、それを配列 ary に push() で追加するだけです。最後に配列を返します。
toString() も簡単です。連結リストを toArray() で配列に変換し、要素を配列のメソッド join() で連結します。各要素は join() で文字列に変換され、カンマ ( , ) をはさんで連結されます。
それでは簡単な実行例を示しましょう。
> a = new List(1,2,3,4,5) List {top: Cell, _nth: function, at: function, insert: function, remove: function…} > a.toArray() [1, 2, 3, 4, 5] > a.toString() "(1,2,3,4,5)"
ところで、今回のプログラムは作業用の関数 _nth() をメソッドとしてオブジェクトに登録したので、次のように呼び出すことが可能です。
> x = a._nth(0) Cell {data: 1, link: Cell} > x.data 1 > x.link Cell {data: 2, link: Cell}
クロージャを使うと作業用の関数を隠蔽することができます。次のリストを見てください。
リスト : メソッドの定義 (2) List.prototype = function() { // n 番目のセルを返す function nth(cp, n){ var i = -1; while(cp != null){ if (n == i) return cp; cp = cp.link; i += 1; } return null; }; var obj = { // n 番目の要素を返す at: function(n) { var cp = nth(this.top, n); if(cp) return cp.data; return null; }, // n 番目にデータを挿入 insert: function(n, x) { var cp = nth(this.top, n - 1); if (cp) { cp.link = new Cell(x, cp.link); return x; } return null; }, // n 番目の要素を削除 remove: function(n) { var cp = nth(this.top, n - 1); if (cp && cp.link) { var data = cp.link.data; cp.link = cp.link.link; return data; } return null; }, // イテレータ each: function(func) { var cp = this.top.link; while (cp != null) { func(cp.data); cp = cp.link; } }, // 空リストか isEmpty: function() { return this.top.link == null; }, // 配列への変換 toArray: function() { var ary = []; this.each(function(x){ ary.push(x); }); return ary; }, // 文字列への変換 toString: function() { return "(" + this.toArray().join(", ") + ")"; } }; return obj; }();
匿名関数の中で作業用関数 nth() を局所関数として定義します。そして、{ } の中でメソッドを定義します。メソッドは匿名関数を使って定義しているので、当然ですが局所関数 nth() にアクセスすることができます。そして、最後に生成したオブジェクトを返せばいいわけです。
なお、この匿名関数を実行しないと List.prototype にオブジェクトがセットされません。function(){ ... } の後ろに () を付けることをお忘れなく。
// // list.js : 連結リスト // // Copyright (C) 2010 Makoto Hiroi // // セル function Cell(data, link) { this.data = data; this.link = link; } // 連結リスト function List() { var cp = new Cell(null, null); this.top = cp; for (var i = 0; i < arguments.length; i++) { cp.link = new Cell(arguments[i], null); cp = cp.link; } } // メソッドの定義 List.prototype = { // 作業用メソッド : n 番目のセルを返す _nth: function(n) { var cp = this.top; var i = -1; while (cp != null) { if (n == i) return cp; cp = cp.link; i += 1; } return null; }, // n 番目の要素を返す at: function(n) { var cp = this._nth(n); if (cp) return cp.data; return null; }, // n 番目にデータを挿入 insert: function(n, x) { var cp = this._nth(n - 1); if (cp) { cp.link = new Cell(x, cp.link); return x; } return null; }, // n 番目の要素を削除 remove: function(n) { var cp = this._nth(n - 1); if (cp && cp.link) { var data = cp.link.data; cp.link = cp.link.link; return data; } return null; }, // イテレータ each: function(func) { var cp = this.top.link; while (cp != null) { func(cp.data); cp = cp.link; } }, // 空リストか isEmpty: function() { return this.top.link == null; }, // 配列への変換 toArray: function() { var ary = []; this.each(function(x) { ary.push(x); }); return ary; }, // 文字列への変換 toString: function() { return "(" + this.toArray().join(",") + ")"; } }
// // list.js : 連結リスト // // Copyright (C) 2010 Makoto Hiroi // // セル function Cell(data, link) { this.data = data; this.link = link; } // 連結リスト function List() { var cp = new Cell(null, null); this.top = cp; for (var i = 0; i < arguments.length; i++) { cp.link = new Cell(arguments[i], null); cp = cp.link; } } // メソッドの定義 List.prototype = function() { // n 番目のセルを返す function nth(cp, n) { var i = -1; while (cp != null) { if (n == i) return cp; cp = cp.link; i += 1; } return null; }; var obj = { // n 番目の要素を返す at: function(n) { var cp = nth(this.top, n); if (cp) return cp.data; return null; }, // n 番目にデータを挿入 insert: function(n, x) { var cp = nth(this.top, n - 1); if (cp) { cp.link = new Cell(x, cp.link); return x; } return null; }, // n 番目の要素を削除 remove: function(n) { var cp = nth(this.top, n - 1); if (cp && cp.link) { var data = cp.link.data; cp.link = cp.link.link; return data; } return null; }, // イテレータ each: function(func) { var cp = this.top.link; while (cp != null) { func(cp.data); cp = cp.link; } }, // 空リストか isEmpty: function() { return this.top.link == null; }, // 配列への変換 toArray: function() { var ary = []; this.each(function(x) { ary.push(x); }); return ary; }, // 文字列への変換 toString: function() { return "(" + this.toArray().join(",") + ")"; } }; return obj; }();