第8章: オブジェクト指向プログラミング
¶ 90年代初頭、オブジェクト指向プログラミングと呼ばれるものがソフトウェア業界を騒乱させました。その背後にあるアイデアのほとんどは当時本当に新しいものではありませんでしたが、ついに勢いを増して転がり始め、流行するようになりました。書籍が書かれ、コースが提供され、プログラミング言語が開発されました。突然、誰もがオブジェクト指向の利点を称賛し、あらゆる問題に熱心に適用し、ついにプログラムを書く正しい方法を見つけたと思い込んでいました。
¶ このようなことはよく起こります。プロセスが難しくて混乱しているとき、人々は常に魔法の解決策を探しています。そのような解決策のように見えるものが現れると、彼らは熱心な信者になる準備ができています。多くのプログラマーにとって、今日でもオブジェクト指向(または彼らによるオブジェクト指向の解釈)は福音です。プログラムが「真のオブジェクト指向」でない場合、それが何を意味するにしても、明らかに劣っていると考えられます。
¶ しかし、この流行ほど長く人気を保ち続けたものはほとんどありません。オブジェクト指向の長寿は、その中核にあるアイデアが非常に堅実で有用であるという事実によって largely 説明できます。この章では、これらのアイデアと、JavaScriptによる(やや風変わりな)解釈について説明します。上記の段落は、これらのアイデアを discredit することを意図したものではありません。私がしたいことは、読者にそれらに不健全な執着をすることに対して警告することです。
¶ その名前が示すように、オブジェクト指向プログラミングはオブジェクトに関連しています。これまでのところ、私たちはオブジェクトを値の緩やかな集合体として使用し、必要に応じてプロパティを追加したり変更したりしてきました。オブジェクト指向のアプローチでは、オブジェクトはそれ自体が小さな世界と見なされ、外部の世界は限られた明確に定義されたインターフェース、つまり特定のメソッドとプロパティを通じてのみそれらに触れることができます。第7章の最後に使用した「到達リスト」は、この例です。私たちは、makeReachedList
、storeReached
、findReached
の3つの関数のみを使用して、それと対話しました。これらの3つの関数は、そのようなオブジェクトのインターフェースを形成します。
¶ これまでに見てきた Date
、Error
、BinaryHeap
オブジェクトもこのように動作します。オブジェクトを操作するための通常の関数を提供する代わりに、new
キーワードを使用してそのようなオブジェクトを作成する方法と、残りのインターフェースを提供する多数のメソッドとプロパティを提供します。
¶ オブジェクトにメソッドを与える1つの方法は、単に関数値をそれに添付することです。
var rabbit = {}; rabbit.speak = function(line) { print("The rabbit says '", line, "'"); }; rabbit.speak("Well, now you're asking me.");
¶ ほとんどの場合、メソッドはそれが誰に作用すべきかを知る必要があります。たとえば、異なるウサギがいる場合、speak
メソッドはどのウサギが話しているかを示す必要があります。この目的のために、this
と呼ばれる特別な変数があり、これは関数が呼び出されると常に存在し、関数がメソッドとして呼び出されるときに関連するオブジェクトを指します。関数は、プロパティとして検索され、object.method()
のようにすぐに呼び出されると、メソッドとして呼び出されます。
function speak(line) { print("The ", this.adjective, " rabbit says '", line, "'"); } var whiteRabbit = {adjective: "white", speak: speak}; var fatRabbit = {adjective: "fat", speak: speak}; whiteRabbit.speak("Oh my ears and whiskers, how late it's getting!"); fatRabbit.speak("I could sure use a carrot right now.");
¶ 第6章で常に null
を使用していた、apply
メソッドへの不思議な最初の引数を明確にすることができます。この引数を使用して、関数を適用する必要があるオブジェクトを指定できます。メソッド以外の関数の場合、これは無関係であるため、null
になります。
speak.apply(fatRabbit, ["Yum."]);
¶ 関数には、apply
に似た call
メソッドもありますが、関数の引数を配列としてではなく個別に指定できます。
speak.call(fatRabbit, "Burp.");
¶ new
キーワードは、新しいオブジェクトを作成するための便利な方法を提供します。関数の前に new
という単語を付けて呼び出すと、その this
変数は*新しい*オブジェクトを指し、(明示的に何かを返さない限り)自動的にそれを返します。このように新しいオブジェクトを作成するために使用される関数は、コンストラクターと呼ばれます。ウサギのコンストラクターを次に示します。
function Rabbit(adjective) { this.adjective = adjective; this.speak = function(line) { print("The ", this.adjective, " rabbit says '", line, "'"); }; } var killerRabbit = new Rabbit("killer"); killerRabbit.speak("GRAAAAAAAAAH!");
¶ JavaScriptプログラマーの間では、コンストラクターの名前を大文字で始めるのが慣例です。これにより、他の関数と簡単に区別できます。
¶ なぜ new
キーワードが必要なのでしょうか?結局のところ、単に次のように書くこともできたはずです。
function makeRabbit(adjective) { return { adjective: adjective, speak: function(line) {/*etc*/} }; } var blackRabbit = makeRabbit("black");
¶ しかし、それはまったく同じではありません。new
は舞台裏でいくつかのことを行います。1つは、killerRabbit
には constructor
と呼ばれるプロパティがあり、それがそれを作成した Rabbit
関数を指しています。blackRabbit
にもそのようなプロパティがありますが、それは Object
関数を指しています。
show(killerRabbit.constructor); show(blackRabbit.constructor);
¶ constructor
プロパティはどこから来たのでしょうか?それは、ウサギの プロトタイプの一部です。プロトタイプは、JavaScriptオブジェクトの仕組みの中で強力ですが、やや混乱しやすい部分です。すべてのオブジェクトはプロトタイプに基づいており、プロトタイプはオブジェクトに一連の固有のプロパティを提供します。これまでに使用してきた単純なオブジェクトは、Object
コンストラクターに関連付けられている最も基本的なプロトタイプに基づいています。実際、{}
と入力することは new Object()
と入力することと同じです。
var simpleObject = {}; show(simpleObject.constructor); show(simpleObject.toString);
¶ toString
は、Object
プロトタイプの一部であるメソッドです。これは、すべての単純なオブジェクトに toString
メソッドがあり、それらを文字列に変換することを意味します。ウサギオブジェクトは、Rabbit
コンストラクターに関連付けられたプロトタイプに基づいています。コンストラクターの prototype
プロパティを使用して、そのプロトタイプにアクセスできます。
show(Rabbit.prototype); show(Rabbit.prototype.constructor);
¶ すべての関数は自動的に prototype
プロパティを取得し、その constructor
プロパティは関数を指します。ウサギプロトタイプはそれ自体がオブジェクトであるため、Object
プロトタイプに基づいており、その toString
メソッドを共有します。
show(killerRabbit.toString == simpleObject.toString);
¶ オブジェクトはプロトタイプのプロパティを共有しているように見えますが、この共有は一方向です。プロトタイプのプロパティはそれに基づくオブジェクトに影響を与えますが、このオブジェクトのプロパティはプロトタイプを決して変更しません。
¶ 正確なルールは次のとおりです。プロパティの値を検索するとき、JavaScriptは最初にオブジェクト*自体*が持つプロパティを調べます。探している名前のプロパティがある場合、それが私たちが得る値です。そのようなプロパティがない場合、オブジェクトのプロトタイプ、次にプロトタイプのプロトタイプなどを検索し続けます。プロパティが見つからない場合、値 undefined
が与えられます。一方、プロパティの値を*設定*するとき、JavaScriptはプロトタイプに移動することはなく、常にオブジェクト自体にプロパティを設定します。
Rabbit.prototype.teeth = "small"; show(killerRabbit.teeth); killerRabbit.teeth = "long, sharp, and bloody"; show(killerRabbit.teeth); show(Rabbit.prototype.teeth);
¶ これは、プロトタイプを使用して、それに基づくすべてのオブジェクトにいつでも新しいプロパティとメソッドを追加できることを意味します。たとえば、ウサギが踊る必要が生じるかもしれません。
Rabbit.prototype.dance = function() { print("The ", this.adjective, " rabbit dances a jig."); }; killerRabbit.dance();
¶ そして、ご想像のとおり、プロトタイプのウサギは、speak
メソッドなど、すべてのウサギに共通の値に最適な場所です。Rabbit
コンストラクターへの新しいアプローチを次に示します。
function Rabbit(adjective) { this.adjective = adjective; } Rabbit.prototype.speak = function(line) { print("The ", this.adjective, " rabbit says '", line, "'"); }; var hazelRabbit = new Rabbit("hazel"); hazelRabbit.speak("Good Frith!");
¶ すべてのオブジェクトがプロトタイプを持ち、このプロトタイプからいくつかのプロパティを受け取るという事実は、注意が必要です。これは、第4章の猫など、一連のものを格納するためにオブジェクトを使用すると、問題が発生する可能性があることを意味します。たとえば、「constructor」という猫がいるかどうか疑問に思った場合、次のようにチェックしたでしょう。
var noCatsAtAll = {}; if ("constructor" in noCatsAtAll) print("Yes, there definitely is a cat called 'constructor'.");
¶ これは問題があります。関連する問題は、Object
や Array
などの標準コンストラクターのプロトタイプを新しい便利な関数で拡張することがしばしば実用的であるということです。たとえば、すべてのオブジェクトに properties
と呼ばれるメソッドを提供できます。これは、オブジェクトが持つ(非表示の)プロパティの名前を含む配列を返します。
Object.prototype.properties = function() { var result = []; for (var property in this) result.push(property); return result; }; var test = {x: 10, y: 3}; show(test.properties());
¶ そして、それはすぐに問題を示しています。Object
プロトタイプに properties
と呼ばれるプロパティができたので、for
と in
を使用してオブジェクトのプロパティをループすると、その共有プロパティも取得されます。これは一般的に私たちが望んでいるものではありません。オブジェクト自体が持つプロパティにのみ関心があります。
¶ 幸いなことに、プロパティがオブジェクト自体に属しているのか、それともプロトタイプの1つに属しているのかを調べる方法があります。残念ながら、オブジェクトのプロパティをループすることは少し面倒になります。すべてのオブジェクトには hasOwnProperty
と呼ばれるメソッドがあり、オブジェクトに指定された名前のプロパティがあるかどうかを知らせます。これを使用して、properties
メソッドを次のように書き直すことができます。
Object.prototype.properties = function() { var result = []; for (var property in this) { if (this.hasOwnProperty(property)) result.push(property); } return result; }; var test = {"Fat Igor": true, "Fireball": true}; show(test.properties());
¶ もちろん、それを高階関数に抽象化できます。action
関数は、プロパティの名前とオブジェクト内での値の両方を使用して呼び出されることに注意してください。
function forEachIn(object, action) { for (var property in object) { if (object.hasOwnProperty(property)) action(property, object[property]); } } var chimera = {head: "lion", body: "goat", tail: "snake"}; forEachIn(chimera, function(name, value) { print("The ", name, " of a ", value, "."); });
¶ しかし、もし`hasOwnProperty`という名前の猫が見つかったらどうなるでしょうか?(そんなことはあり得ないでしょうが。)その猫はオブジェクトに格納され、次に猫のコレクションを調べたいときに`object.hasOwnProperty`を呼び出すと、そのプロパティがもはや関数値を指していないため、失敗します。これは、さらに醜いことをすることで解決できます。
function forEachIn(object, action) { for (var property in object) { if (Object.prototype.hasOwnProperty.call(object, property)) action(property, object[property]); } } var test = {name: "Mordecai", hasOwnProperty: "Uh-oh"}; forEachIn(test, function(name, value) { print("Property ", name, " = ", value); });
¶ (注:この例は、Internet Explorer 8 では現在正しく動作しません。組み込みのプロトタイププロパティのオーバーライドに問題があるようです。)
¶ ここでは、オブジェクト自体にあるメソッドを使用する代わりに、`Object`プロトタイプからメソッドを取得し、`call`を使用して正しいオブジェクトに適用します。誰かが実際に`Object.prototype`のメソッドをいじらない限り(しないでください)、これは正しく動作するはずです。
¶ `hasOwnProperty`は、オブジェクトが特定のプロパティを持っているかどうかを確認するために`in`演算子を使用していた状況でも使用できます。ただし、もう1つ注意点があります。第4章で、`toString`などの一部のプロパティは「隠されて」おり、`for`/`in`でプロパティを調べても表示されないことを確認しました。Geckoファミリーのブラウザ(最も重要なのはFirefox)は、すべてのオブジェクトに`__proto__`という名前の隠しプロパティを与え、それがそのオブジェクトのプロトタイプを指していることがわかりました。`hasOwnProperty`は、プログラムが明示的に追加していなくても、これに対して`true`を返します。オブジェクトのプロトタイプにアクセスできることは非常に便利ですが、それをそのようなプロパティにするのはあまり良い考えではありませんでした。それでも、Firefoxは広く使用されているブラウザであるため、Web用のプログラムを作成する場合は、これに注意する必要があります。隠しプロパティに対して`false`を返し、`__proto__`のような奇妙なものを除外するために使用できる`propertyIsEnumerable`というメソッドがあります。次のような式を使用して、これを確実に回避できます。
var object = {foo: "bar"}; show(Object.prototype.hasOwnProperty.call(object, "foo") && Object.prototype.propertyIsEnumerable.call(object, "foo"));
¶ 素晴らしくシンプルですね?これは、JavaScriptのあまりよく設計されていない側面の1つです。オブジェクトは、「メソッドを持つ値」(プロトタイプが非常に効果的)と「プロパティのセット」(プロトタイプが邪魔になるだけ)の両方の役割を果たします。
¶ オブジェクトにプロパティが存在するかどうかを確認する必要があるたびに上記の式を書くことは現実的ではありません。関数を定義することもできますが、さらに良いアプローチは、オブジェクトを単なるプロパティのセットとして扱いたい場合など、このような状況に特化したコンストラクタとプロトタイプを作成することです。名前で検索できるため、`Dictionary`と呼びます。
function Dictionary(startValues) { this.values = startValues || {}; } Dictionary.prototype.store = function(name, value) { this.values[name] = value; }; Dictionary.prototype.lookup = function(name) { return this.values[name]; }; Dictionary.prototype.contains = function(name) { return Object.prototype.hasOwnProperty.call(this.values, name) && Object.prototype.propertyIsEnumerable.call(this.values, name); }; Dictionary.prototype.each = function(action) { forEachIn(this.values, action); }; var colours = new Dictionary({Grover: "blue", Elmo: "orange", Bert: "yellow"}); show(colours.contains("Grover")); show(colours.contains("constructor")); colours.each(function(name, colour) { print(name, " is ", colour); });
¶ これで、オブジェクトをプレーンなプロパティのセットとして扱うことに関連するすべての混乱が、便利なインターフェース(1つのコンストラクタと4つのメソッド)に「カプセル化」されました。 `Dictionary`オブジェクトの`values`プロパティはこのインターフェースの一部ではなく、内部的な詳細であることに注意してください。`Dictionary`オブジェクトを使用している場合は、直接使用する必要はありません。
¶ インターフェースを作成するときは、その機能と使用方法の簡単な概要をコメントとして追加することをお勧めします。こうすることで、作成者本人を含め、3か月後にインターフェースを使用したい人が、使用方法をすばやく理解でき、プログラム全体を学習する必要がなくなります。
¶ ほとんどの場合、インターフェースを設計すると、すぐに考案したものに限界や問題が見つかり、変更することになります。時間を無駄にしないために、インターフェースのドキュメント化は、いくつかの実際の状況で使用され、実用的であることが証明された*後*でのみ行うことをお勧めします。― もちろん、これはドキュメントを完全に忘れてしまう誘惑になるかもしれません。個人的には、ドキュメントの作成をシステムに追加する「最後の仕上げ」と考えています。準備が整ったと感じたら、それについて何かを書き、JavaScript(または任意のプログラミング言語)と同じくらい英語(または任意の言語)でうまく聞こえるかどうかを確認します。
¶ オブジェクトの外部インターフェースとその内部詳細の区別は、2つの理由で重要です。まず、小さく明確に記述されたインターフェースを持つことで、オブジェクトを使いやすくなります。インターフェースだけを念頭に置いておけば、オブジェクト自体を変更しない限り、残りの部分を気にする必要はありません。
¶ 第二に、オブジェクトタイプの内部実装を、たとえば効率を向上させたり、問題を修正したりするために変更する必要がある場合や、変更するのが実際的な場合があります1。外部コードがオブジェクトのすべてのプロパティと詳細にアクセスしている場合、他の多くのコードも更新せずにそれらのいずれかを変更することはできません。外部コードが小さなインターフェースのみを使用している場合、インターフェースを変更しない限り、必要な操作を行うことができます。
¶ これに関して、非常に徹底的な人もいます。たとえば、彼らはオブジェクトのインターフェースにプロパティを含めることは決してなく、メソッドのみを含めます。つまり、オブジェクトタイプに長さがある場合、`length`プロパティではなく、`getLength`メソッドでアクセスできます。こうすることで、たとえば、内部配列の長さを返す必要があるため、`length`プロパティを持たない方法でオブジェクトを変更したい場合、インターフェースを変更せずに関数を更新できます。
¶ 私自身の考えでは、ほとんどの場合、これはそれだけの価値はありません。`return this.length;`のみを含む`getLength`メソッドを追加すると、ほとんどの場合、無意味なコードが追加されるだけであり、ほとんどの場合、無意味なコードは、オブジェクトのインターフェースを gelegentlich 変更する必要があるリスクよりも大きな問題だと考えています。
¶ 既存のプロトタイプに新しいメソッドを追加すると非常に便利です。特に、JavaScriptの`Array`および`String`プロトタイプには、さらに基本的なメソッドが必要です。たとえば、`forEach`と`map`を配列のメソッドに置き換え、第4章で作成した`startsWith`関数を文字列のメソッドにすることができます。
¶ ただし、プログラムが、`for`/`in`を単純に使用している別のプログラム(自分または他の人が作成したもの)(これまではそのように使用してきました)と同じWebページで実行する必要がある場合、プロトタイプ、特に`Object`および`Array`プロトタイプに何かを追加すると、これらのループが突然これらの新しいプロパティを見るようになるため、確実に問題が発生します。このため、一部の人はこれらのプロトタイプにはまったく触れないことを好みます。もちろん、注意深く、コードが質の悪いコードと共存する必要があるとは思わない場合は、標準のプロトタイプにメソッドを追加することは完全に優れた手法です。
¶ この章では、仮想テラリウム、つまり昆虫が動き回る水槽を作成します。いくつかのオブジェクトが関係します(結局のところ、これはオブジェクト指向プログラミングに関する章です)。かなり単純なアプローチを採用し、第7章の2番目のマップのように、テラリウムを2次元グリッドにします。このグリッドには、多くのバグがあります。テラリウムがアクティブになると、すべてのバグは0.5秒ごとに移動などのアクションを実行する機会を得ます。
¶ したがって、時間と空間の両方を固定サイズの単位(空間の場合は正方形、時間の場合は0.5秒)に分割します。これは通常、プログラムでモデリングするのを容易にしますが、もちろん、非常に不正確であるという欠点があります。幸いなことに、このテラリウムシミュレータは、いかなる方法でも正確である必要はないため、これで問題ありません。
¶ テラリウムは、「プラン」である文字列の配列で定義できます。単一の文字列を使用することもできましたが、JavaScriptの文字列は1行にとどまる必要があるため、入力するのがはるかに難しかったでしょう。
var thePlan = ["############################", "# # # o ##", "# #", "# ##### #", "## # # ## #", "### ## # #", "# ### # #", "# #### #", "# ## o #", "# o # o ### #", "# # #", "############################"];
¶ `"#"`文字はテラリウムの壁(およびその中にある装飾用の岩)を表すために使用され、`"o"`はバグを表し、スペースは、ご想像のとおり、空のスペースです。
¶ このようなプラン配列を使用して、テラリウムオブジェクトを作成できます。このオブジェクトは、テラリウムの形状と内容を追跡し、中のバグを移動させます。4つのメソッドがあります。まず、`toString`は、テラリウムを基にしたプランに似た文字列に戻すため、内部で何が起こっているかを確認できます。次に、`step`があり、テラリウム内のすべてのバグが、必要に応じて1ステップ移動できるようにします。そして最後に、テラリウムが「実行中」かどうかを制御する`start`と`stop`があります。実行中は、`step`が0.5秒ごとに自動的に呼び出されるため、バグは移動し続けます。
¶ グリッド上の点は再びオブジェクトで表されます。第7章では、点の操作に`point`、`addPoints`、`samePoint`の3つの関数を使用しました。今回は、コンストラクタと2つのメソッドを使用します。点のx座標とy座標の2つの引数を取り、`x`と`y`プロパティを持つオブジェクトを生成するコンストラクタ`Point`を作成します。このコンストラクタのプロトタイプに、別の点を引数として取り、2つの指定された点の`x`と`y`の合計である*新しい*点を返すメソッド`add`を与えます。また、点を取得し、`this`点が指定された点と同じ座標を参照しているかどうかを示すブール値を返すメソッド`isEqualTo`を追加します。
¶ 2つのメソッドに加えて、`x`および`y`プロパティもこのタイプのオブジェクトのインターフェースの一部です。点オブジェクトを使用するコードは、`x`および`y`を自由に取得および変更できます。
function Point(x, y) { this.x = x; this.y = y; } Point.prototype.add = function(other) { return new Point(this.x + other.x, this.y + other.y); }; Point.prototype.isEqualTo = function(other) { return this.x == other.x && this.y == other.y; }; show((new Point(3, 1)).add(new Point(2, 4)));
¶ `add`のバージョンが`this`点をそのままにして、新しい点オブジェクトを生成していることを確認してください。代わりに現在の点を変更するメソッドは`+=`演算子に似ていますが、これは`+`演算子に似ています。
¶ 特定のプログラムを実装するためにオブジェクトを作成する場合、どの機能がどこに配置されるかは必ずしも明確ではありません。オブジェクトのメソッドとして記述するのが最適なものもあれば、個別の関数として表現する方が良いものもあり、新しいタイプのオブジェクトを追加することで実装するのが最適なものもあります。物事を明確かつ整理された状態に保つために、オブジェクトタイプが持つメソッドと責務の量をできるだけ少なくすることが重要です。オブジェクトが多すぎることを行うと、機能が大きく混乱し、混乱の恐ろしい原因となります。
¶ 上記で、テラリウムオブジェクトはその内容を保存し、中のバグを移動させる役割を担うと述べました。まず、それは彼らに移動を*許可*するものであり、移動を*強制*するものではないことに注意してください。バグ自体もオブジェクトであり、これらのオブジェクトは何をしたいかを決定する役割を担います。テラリウムは単に、0.5秒ごとに何をすべきかを尋ねるインフラストラクチャを提供し、移動することを決定した場合、それが確実に発生するようにします。
¶ テラリウムの内容を保持するグリッドの保存は非常に複雑になる可能性があります。何らかの表現方法、その表現にアクセスする方法、「プラン」配列からグリッドを初期化する方法、toString
メソッドのためにグリッドの内容を文字列に書き込む方法、そしてグリッド上でのバグの動きを定義する必要があります。この一部を別のオブジェクトに移動できれば、テラリウムオブジェクト自体が大きくて複雑になりすぎるのを防ぐことができます。
¶ 1つのオブジェクトにデータ表現と問題固有のコードを混在させようとしている場合は、データ表現コードを別のタイプのオブジェクトに配置することをお勧めします。この場合、値のグリッドを表す必要があるため、テラリウムに必要な操作をサポートする Grid
タイプを作成しました。
¶ グリッド上の値を格納するには、2つのオプションがあります。次のように配列の配列を使用できます
var grid = [["0,0", "1,0", "2,0"], ["0,1", "1,1", "2,1"]]; show(grid[1][2]);
¶ または、すべての値を単一の配列に配置することもできます。この場合、x
、y
の要素は、配列内の x + y * width
の位置にある要素を取得することで見つけることができます。ここで、width
はグリッドの幅です。
var grid = ["0,0", "1,0", "2,0", "0,1", "1,1", "2,1"]; show(grid[2 + 1 * 3]);
¶ 配列の初期化がはるかに簡単になるため、2番目の表現を選択しました。 new Array(x)
は、undefined
値で満たされた、長さ x
の新しい配列を生成します。
function Grid(width, height) { this.width = width; this.height = height; this.cells = new Array(width * height); } Grid.prototype.valueAt = function(point) { return this.cells[point.y * this.width + point.x]; }; Grid.prototype.setValueAt = function(point, value) { this.cells[point.y * this.width + point.x] = value; }; Grid.prototype.isInside = function(point) { return point.x >= 0 && point.y >= 0 && point.x < this.width && point.y < this.height; }; Grid.prototype.moveValue = function(from, to) { this.setValueAt(to, this.valueAt(from)); this.setValueAt(from, undefined); };
¶ 移動する必要があるバグを見つけたり、全体を文字列に変換したりするために、グリッドのすべての要素を調べる必要もあります。これを簡単にするために、アクションを引数として取る高階関数を使用できます。2つの引数の関数を取る each
メソッドを Grid
のプロトタイプに追加します。グリッド上のすべてのポイントに対してこの関数を呼び出し、そのポイントのポイントオブジェクトを最初の引数として、そのポイントのグリッド上の値を2番目の引数として渡します。
¶ 0
、0
から開始して、1行ずつポイントを調べます。そのため、1
、0
は 0
、1
の前に処理されます。これにより、後でテラリウムの toString
関数を簡単に記述できます。(ヒント:y
座標のループ内に x
座標の for
ループを配置します。)
¶ グリッドオブジェクトの cells
プロパティを直接操作するのではなく、valueAt
を使用して値を取得することをお勧めします。こうすることで、(何らかの理由で)値を格納する別の方法を使用することにした場合、valueAt
と setValueAt
を書き直すだけで済み、他のメソッドは変更せずに済みます。
Grid.prototype.each = function(action) { for (var y = 0; y < this.height; y++) { for (var x = 0; x < this.width; x++) { var point = new Point(x, y); action(point, this.valueAt(point)); } } };
¶ 最後に、グリッドをテストするには
var testGrid = new Grid(3, 2); testGrid.setValueAt(new Point(1, 0), "#"); testGrid.setValueAt(new Point(1, 1), "o"); testGrid.each(function(point, value) { print(point.x, ",", point.y, ": ", value); });
¶ Terrarium
コンストラクターの作成を開始する前に、その中に住む「バグオブジェクト」についてもう少し具体的に説明する必要があります。前述したように、テラリウムはバグにどのような行動をとるかを尋ねます。これは次のように機能します。各バグオブジェクトには act
メソッドがあり、呼び出されると「アクション」を返します。アクションは、バグが実行したいアクションのタイプ(たとえば、"move"
)を指定する type
プロパティを持つオブジェクトです。ほとんどのアクションでは、アクションには、バグが移動したい方向などの追加情報も含まれています。
¶ バグは非常に近視眼的で、グリッド上で周囲のマスしか見ることができません。しかし、これらを使用して、行動の Grundlage とすることができます。 act
メソッドが呼び出されると、問題のバグの周囲に関する情報を含むオブジェクトが渡されます。8つの方向それぞれに、プロパティが含まれています。バグの上にあるものを示すプロパティは、北を表す "n"
と呼ばれ、右上にあるものを示すプロパティは、北東を表す "ne"
と呼ばれます。これらの名前が参照する方向を調べるには、次の辞書オブジェクトが役立ちます
var directions = new Dictionary( {"n": new Point( 0, -1), "ne": new Point( 1, -1), "e": new Point( 1, 0), "se": new Point( 1, 1), "s": new Point( 0, 1), "sw": new Point(-1, 1), "w": new Point(-1, 0), "nw": new Point(-1, -1)}); show(new Point(4, 4).add(directions.lookup("se")));
¶ バグが移動することを決定すると、結果のアクションオブジェクトにこれらのいずれかの方向を指定する direction
プロパティを指定することにより、どの方向に移動したいかを示します。常に南に、「光に向かって」移動する、シンプルで愚かなバグを次のように作成できます
function StupidBug() {}; StupidBug.prototype.act = function(surroundings) { return {type: "move", direction: "s"}; };
¶ これで、Terrarium
オブジェクトタイプの作業を開始できます。まず、プラン(文字列の配列)を引数として取り、グリッドを初期化するコンストラクターです。
var wall = {}; function Terrarium(plan) { var grid = new Grid(plan[0].length, plan.length); for (var y = 0; y < plan.length; y++) { var line = plan[y]; for (var x = 0; x < line.length; x++) { grid.setValueAt(new Point(x, y), elementFromCharacter(line.charAt(x))); } } this.grid = grid; } function elementFromCharacter(character) { if (character == " ") return undefined; else if (character == "#") return wall; else if (character == "o") return new StupidBug(); }
¶ wall
は、グリッド上の壁の位置をマークするために使用されるオブジェクトです。本物の壁のように、あまり何もせず、ただそこに座ってスペースを占有します。
¶ テラリウムオブジェクトの最も簡単なメソッドは toString
で、テラリウムを文字列に変換します。これを簡単にするために、wall
と StupidBug
のプロトタイプの両方に、それらを表す文字を保持するプロパティ character
をマークします。
wall.character = "#"; StupidBug.prototype.character = "o"; function characterFromElement(element) { if (element == undefined) return " "; else return element.character; } show(characterFromElement(wall));
¶ これで、Grid
オブジェクトの each
メソッドを使用して文字列を作成できます。ただし、結果を読みやすくするために、各行の最後に改行があると便利です。グリッド上の位置の x
座標を使用して、行の終わりに達した Zeitpunkt を判断できます。引数を取らず、print
に渡すとテラリウムの素敵な2次元ビューを表示する文字列を返す toString
メソッドを Terrarium
プロトタイプに追加します。
Terrarium.prototype.toString = function() { var characters = []; var endOfLine = this.grid.width - 1; this.grid.each(function(point, value) { characters.push(characterFromElement(value)); if (point.x == endOfLine) characters.push("\n"); }); return characters.join(""); };
¶ そして、試してみるには...
var terrarium = new Terrarium(thePlan); print(terrarium.toString());
¶ 上記の演習を解こうとしたときに、グリッドの each
メソッドに引数として渡す関数内で this.grid
にアクセスしようとした可能性があります。これは機能しません。関数を呼び出すと、メソッドとして使用されていない場合でも、常にその関数内に新しい this
が定義されます。したがって、関数の外部にある this
変数は表示されません。
¶ 内部関数で *表示* される endOfLine
のような変数に必要な情報を格納することで、これを回避するのは簡単な場合があります。this
オブジェクト全体にアクセスする必要がある場合は、それも変数に格納できます。このような変数には、多くの場合、self
(または that
)という名前が使用されます。
¶ しかし、これらの余分な変数はすべて面倒になる可能性があります。第6章 の partial
に似た関数を使用するという、もう1つの優れた解決策があります。引数を関数に追加する代わりに、関数の apply
メソッドの最初の引数を使用して this
オブジェクトを追加します
function bind(func, object) { return function(){ return func.apply(object, arguments); }; } var testArray = []; var pushTest = bind(testArray.push, testArray); pushTest("A"); pushTest("B"); show(testArray);
¶ こうすることで、内部関数を this
に `bind` することができ、外部関数と同じ this
になります。
¶ 式 bind(testArray.push, testArray)
では、名前 `testArray` はまだ2回出現します。オブジェクトを2回名前付けずに、オブジェクトをそのメソッドの1つにバインドできる関数 method
を設計できますか?
¶ メソッドの名前を文字列として指定できます。こうすることで、method
関数は、正しい関数値を自分で検索できます。
function method(object, name) { return function() { return object[name].apply(object, arguments); }; } var pushTest = method(testArray, "push");
¶ テラリウムの `step` メソッドを実装するときは、`bind`(または `method`)が必要になります。このメソッドは、グリッド上のすべてのバグを調べ、アクションを要求し、指定されたアクションを実行する必要があります。グリッドで `each` を使用して、遭遇したバグだけを処理したいと思うかもしれません。しかし、バグが南または東に移動すると、同じターンで再び遭遇し、再び移動させることができます。
¶ 代わりに、最初にすべてのバグを配列に収集してから、それらを処理します。このメソッドは、バグ、または `act` メソッドを持つ他のものを収集し、現在の位置も含まれるオブジェクトに格納します
Terrarium.prototype.listActingCreatures = function() { var found = []; this.grid.each(function(point, value) { if (value != undefined && value.act) found.push({object: value, point: point}); }); return found; };
¶ バグにアクションを要求するときは、現在の周囲に関する情報を含むオブジェクトを渡す必要があります。このオブジェクトは、前述の方向名(`"n"`、`"ne"` など)をプロパティ名として使用します。各プロパティは、`characterFromElement` によって返されるように、バグがその方向に見えるものを示す1文字の文字列を保持します。
¶ `listSurroundings` メソッドを `Terrarium` プロトタイプに追加します。バグが現在立っているポイントを引数として1つ取り、そのポイントの周囲に関する情報を含むオブジェクトを返します。ポイントがグリッドの端にある場合は、グリッドの外側に向かう方向に `"#"` を使用します。そのため、バグはそこに移動しようとしません。
¶ ヒント:すべての方向を書き出すのではなく、`directions` 辞書で `each` メソッドを使用します。
Terrarium.prototype.listSurroundings = function(center) { var result = {}; var grid = this.grid; directions.each(function(name, direction) { var place = center.add(direction); if (grid.isInside(place)) result[name] = characterFromElement(grid.valueAt(place)); else result[name] = "#"; }); return result; };
¶ `this` 問題を回避するために `grid` 変数を使用していることに注意してください。
¶ 上記の両方のメソッドは、`Terrarium` オブジェクトの外部インターフェースの一部ではなく、内部の詳細です。一部の言語では、特定のメソッドとプロパティを明示的に「プライベート」として宣言し、オブジェクトの外部から使用するとエラーになるようにする方法が用意されています。JavaScriptにはありませんので、オブジェクトへのインターフェースを記述するにはコメントに頼る必要があります。外部プロパティと内部プロパティを区別するために、たとえばすべての内部プロパティにアンダースコア(`_`)を付けるなど、何らかの命名スキームを使用すると便利な場合があります。これにより、オブジェクトのインターフェースの一部ではないプロパティの誤用を容易に発見できます。
¶ 次は、もう1つの内部メソッドです。これは、バグにアクションを要求して実行するメソッドです。`listActingCreatures` によって返されるように、`object` プロパティと `point` プロパティを持つオブジェクトを引数として取ります。今のところ、`"move"` アクションについてのみ知っています
Terrarium.prototype.processCreature = function(creature) { var surroundings = this.listSurroundings(creature.point); var action = creature.object.act(surroundings); if (action.type == "move" && directions.contains(action.direction)) { var to = creature.point.add(directions.lookup(action.direction)); if (this.grid.isInside(to) && this.grid.valueAt(to) == undefined) this.grid.moveValue(creature.point, to); } else { throw new Error("Unsupported action: " + action.type); } };
¶ 選択した方向がグリッド内であり、空であるかどうかを確認し、そうでない場合は無視することに注意してください。こうすることで、バグは好きなアクションを要求できます。アクションは実際に可能な場合にのみ実行されます。これは、バグとテラリウムの間に絶縁層として機能し、バグの `act` メソッドを記述する際の精度を落とすことができます。たとえば、`StupidBug` は、邪魔になる可能性のある壁に関係なく、常に南に移動します。
¶ これら3つの内部メソッドにより、最終的に `step` メソッドを記述できます。このメソッドは、すべてのバグ(`act` メソッドを持つすべての要素 ― 必要に応じて `wall` オブジェクトにも追加して、壁を歩かせることもできます)に何かを行う機会を与えます。
Terrarium.prototype.step = function() { forEach(this.listActingCreatures(), bind(this.processCreature, this)); };
¶ それでは、テラリウムを作成し、バグが移動するかどうかを確認しましょう...
var terrarium = new Terrarium(thePlan); print(terrarium); terrarium.step(); print(terrarium);
¶ ちょっと待ってください。なぜ上記のコードは `print(terrarium)` を呼び出して、`toString` メソッドの出力を表示するのでしょうか? `print` は `String` 関数を使用して、引数を文字列に変換します。オブジェクトは `toString` メソッドを呼び出すことで文字列に変換されるため、独自のオブジェクトタイプに意味のある `toString` を提供することは、出力時に読みやすくするための良い方法です。
Point.prototype.toString = function() { return "(" + this.x + "," + this.y + ")"; }; print(new Point(5, 5));
¶ 約束どおり、`Terrarium` オブジェクトにもシミュレーションを開始または停止するための `start` メソッドと `stop` メソッドが追加されます。このために、ブラウザが提供する`setInterval` と`clearInterval` という2つの関数を使用します。最初の関数は、最初の引数(関数、または JavaScript コードを含む文字列)を定期的に実行するために使用されます。2番目の引数は、呼び出し間隔をミリ秒(1/1000秒)単位で指定します。この関数は、`clearInterval` に渡して効果を停止できる値を返します。
var annoy = setInterval(function() {print("What?");}, 400);
¶ そして...
clearInterval(annoy);
¶ ワンショットの時間ベースのアクションにも同様の関数があります。`setTimeout` は、指定されたミリ秒後に関数または文字列を実行し、`clearTimeout` はそのようなアクションをキャンセルします。
Terrarium.prototype.start = function() { if (!this.running) this.running = setInterval(bind(this.step, this), 500); }; Terrarium.prototype.stop = function() { if (this.running) { clearInterval(this.running); this.running = null; } };
¶ これで、単純なバグを含むテラリウムが作成され、実行できるようになりました。しかし、何が起こっているかを確認するには、繰り返し `print(terrarium)` を実行する必要があります。そうしないと、何が起こっているかがわかりません。これはあまり実用的ではありません。自動的に印刷される方が良いでしょう。また、何千ものテラリウムを上下に印刷する代わりに、テラリウムの1つの印刷物を更新できれば、見栄えも良くなります。2番目の問題については、このページでは `inPlacePrinter` という関数が提供されています。これは、出力に追加するのではなく、以前の出力を置き換える `print` のような関数を返します。
var printHere = inPlacePrinter(); printHere("Now you see it."); setTimeout(partial(printHere, "Now you don't."), 1000);
¶ テラリウムが変更されるたびに再印刷されるようにするには、`step` メソッドを次のように変更します。
Terrarium.prototype.step = function() { forEach(this.listActingCreatures(), bind(this.processCreature, this)); if (this.onStep) this.onStep(); };
¶ これで、`onStep` プロパティがテラリウムに追加されると、すべてのステップで呼び出されるようになります。
var terrarium = new Terrarium(thePlan); terrarium.onStep = partial(inPlacePrinter(), terrarium); terrarium.start();
¶ `partial` の使用方法に注目してください。テラリウムに適用されるインプレースプリンターを生成します。このようなプリンターは1つの引数しか取らないため、部分的に適用した後は引数がなくなり、引数のない関数になります。これは `onStep` プロパティに必要なものです。
¶ テラリウムが面白くなくなったとき(すぐにそうなるはずです)は、コンピューターのリソースを無駄に消費し続けないように、停止することを忘れないでください。
terrarium.stop();
¶ しかし、1種類のバグしかいない、しかも愚かなバグしかいないテラリウムを誰が望むでしょうか?私は望みません。さまざまな種類のバグを追加できると良いでしょう。幸いなことに、`elementFromCharacter` 関数をより汎用的にするだけで済みます。現在、直接入力された、つまり「ハードコーディング」された3つのケースが含まれています。
function elementFromCharacter(character) { if (character == " ") return undefined; else if (character == "#") return wall; else if (character == "o") return new StupidBug(); }
¶ 最初の2つのケースはそのままにしておくことができますが、最後のケースはあまりにも具体的すぎます。より良いアプローチは、文字と対応するバグコンストラクターを辞書に格納し、そこで検索することです。
var creatureTypes = new Dictionary(); creatureTypes.register = function(constructor) { this.store(constructor.prototype.character, constructor); }; function elementFromCharacter(character) { if (character == " ") return undefined; else if (character == "#") return wall; else if (creatureTypes.contains(character)) return new (creatureTypes.lookup(character))(); else throw new Error("Unknown character: " + character); }
¶ `register` メソッドが `creatureTypes` にどのように追加されるかに注目してください。これは辞書オブジェクトですが、追加のメソッドをサポートできない理由はありません。このメソッドは、コンストラクターに関連付けられた文字を検索し、辞書に格納します。プロトタイプに実際に `character` プロパティがあるコンストラクターに対してのみ呼び出す必要があります。
¶ `elementFromCharacter` は、`creatureTypes` 内で指定された文字を検索し、不明な文字が見つかった場合は例外を発生させます。
¶ 新しいバグタイプと、`creatureTypes` にその文字を登録するための呼び出しを次に示します。
function BouncingBug() { this.direction = "ne"; } BouncingBug.prototype.act = function(surroundings) { if (surroundings[this.direction] != " ") this.direction = (this.direction == "ne" ? "sw" : "ne"); return {type: "move", direction: this.direction}; }; BouncingBug.prototype.character = "%"; creatureTypes.register(BouncingBug);
¶ それが何をするか理解できますか?
¶ `DrunkBug` と呼ばれるバグタイプを作成します。これは、壁があるかどうかに関係なく、毎回ランダムな方向に移動しようとします。第7章の `Math.random` のトリックを覚えておいてください。
¶ ランダムな方向を選択するには、方向名の配列が必要です。もちろん、`["n", "ne", ...]` と入力することもできますが、これは情報を複製しており、複製された情報は不安になります。`directions` の `each` メソッドを使用して配列を作成することもできます。これはすでに改善されています。
¶ しかし、ここには明らかに発見すべき一般性があります。辞書のプロパティ名のリストを取得することは、便利なツールのように聞こえるため、`Dictionary` プロトタイプに追加します。
Dictionary.prototype.names = function() { var names = []; this.each(function(name, value) {names.push(name);}); return names; }; show(directions.names());
¶ 真の神経質なプログラマーは、辞書に格納されている値のリストを返す `values` メソッドも追加することで、すぐにシンメトリーを復元します。しかし、それは必要になるまで待つことができると思います。
¶ 配列からランダムな要素を取得する方法は次のとおりです。
function randomElement(array) { if (array.length == 0) throw new Error("The array is empty."); return array[Math.floor(Math.random() * array.length)]; } show(randomElement(["heads", "tails"]));
¶ そして、バグ自体は次のとおりです。
function DrunkBug() {}; DrunkBug.prototype.act = function(surroundings) { return {type: "move", direction: randomElement(directions.names())}; }; DrunkBug.prototype.character = "~"; creatureTypes.register(DrunkBug);
¶ それでは、新しいバグをテストしてみましょう。
var newPlan = ["############################", "# #####", "# ## ####", "# #### ~ ~ ##", "# ## ~ #", "# #", "# ### #", "# ##### #", "# ### #", "# % ### % #", "# ####### #", "############################"]; var terrarium = new Terrarium(newPlan); terrarium.onStep = partial(inPlacePrinter(), terrarium); terrarium.start();
¶ 跳ね返るバグが酔っ払ったバグに跳ね返っているのがわかりますか?純粋なドラマです。とにかく、この魅力的なショーを見終わったら、シャットダウンしてください。
terrarium.stop();
¶ これで、`act` メソッドと `character` プロパティの両方を持つ2種類のオブジェクトができました。これらの特性を共有しているため、テラリウムは同じ方法でそれらにアプローチできます。これにより、テラリウムコードを変更することなく、あらゆる種類のバグを持つことができます。この手法はポリモーフィズムと呼ばれ、オブジェクト指向プログラミングの最も強力な側面と言えるでしょう。
¶ ポリモーフィズムの基本的な考え方は、特定のインターフェースを持つオブジェクトを操作するようにコードが記述されている場合、このインターフェースをサポートするあらゆる種類のオブジェクトをコードにプラグインでき、`toString` メソッドがどのように文字列を作成することを選択したかに関係なく、正しい文字列が生成されます。オブジェクトの `toString` メソッドのような、この単純な例をすでに見てきました。意味のある `toString` メソッドを持つすべてのオブジェクトは、`print` や値を文字列に変換する必要がある他の関数に渡すことができ、 `toString` メソッドがどのように文字列を作成することを選択したかに関係なく、正しい文字列が生成されます。
¶ 同様に、`forEach` は、必要なのは `length` プロパティと、配列の要素である `0`、`1` などのプロパティだけなので、実際の配列と `arguments` 変数にある疑似配列の両方で機能します。
¶ テラリウムでの生活をよりリアルにするために、食物と生殖の概念を追加します。テラリウム内のすべての生物は、`energy` という新しいプロパティを取得します。これは、アクションを実行することで減少し、何かを食べることで増加します。十分なエネルギーがあれば、生物は繁殖し2、同じ種類の新しい生物を生成できます。
¶ バグだけがいて、動き回ったりお互いを食べたりしてエネルギーを浪費している場合、テラリウムはすぐにエントロピーの力に屈し、エネルギーを使い果たし、生命のない荒れ地になります。これが起こるのを防ぐために(少なくとも 너무早くは)、テラリウムに地衣類を追加します。地衣類は移動せず、光合成を使用してエネルギーを集めて繁殖します。
¶ これを機能させるには、`processCreature` メソッドが異なるテラリウムが必要です。`Terrarium` プロトタイプのメソッドを置き換えることもできますが、跳ね返るバグと酔っ払ったバグのシミュレーションに非常に愛着があり、古いテラリウムを壊したくありません。
¶ できることは、`Terrarium` プロトタイプに基づくプロトタイプを持つが、`processCreature` メソッドが異なる新しいコンストラクター `LifeLikeTerrarium` を作成することです。
¶ これを行うには、いくつかの方法があります。`Terrarium.prototype` のプロパティを調べて、`LifeLikeTerrarium.prototype` に1つずつ追加することができます。これは簡単で、場合によっては最良の解決策ですが、この場合はよりクリーンな方法があります。古いプロトタイプオブジェクトを新しいプロトタイプオブジェクトのプロトタイプにすると(数回読み直す必要があるかもしれません)、自動的にすべてのプロパティが継承されます。
¶ 残念ながら、JavaScriptには、特定の他のオブジェクトをプロトタイプとするオブジェクトを作成する簡単な方法がありません。ただし、以下のトリックを使用することで、これを行う関数を作成することは可能です。
function clone(object) { function OneShotConstructor(){} OneShotConstructor.prototype = object; return new OneShotConstructor(); }
¶ この関数は、プロトタイプが指定されたオブジェクトである、空のワンショットコンストラクタを使用します。このコンストラクタでnew
を使用すると、指定されたオブジェクトに基づいて新しいオブジェクトが作成されます。
function LifeLikeTerrarium(plan) { Terrarium.call(this, plan); } LifeLikeTerrarium.prototype = clone(Terrarium.prototype); LifeLikeTerrarium.prototype.constructor = LifeLikeTerrarium;
¶ 新しいコンストラクタは、古いコンストラクタと異なる処理を行う必要がないため、this
オブジェクトに対して古いコンストラクタを呼び出すだけです。また、新しいプロトタイプでconstructor
プロパティを復元する必要があります。そうしないと、コンストラクタがTerrarium
であると主張されます(もちろん、これはこのプロパティを使用する場合にのみ実際に問題となりますが、使用しません)。
¶ これで、LifeLikeTerrarium
オブジェクトのメソッドの一部を置き換えたり、新しいメソッドを追加したりできます。古いオブジェクトタイプに基づいて新しいオブジェクトタイプを作成したため、Terrarium
とLifeLikeTerrarium
で同じであるすべてのメソッドを書き直す手間が省けました。この手法は「継承」と呼ばれます。新しいタイプは、古いタイプのプロパティを継承します。ほとんどの場合、これは、新しいタイプが古いタイプのインターフェースを引き続きサポートすることを意味しますが、古いタイプにはないいくつかのメソッドもサポートする場合があります。このようにして、新しいタイプのオブジェクトは、古いタイプのオブジェクトを使用できるすべての場所で(ポリモーフィックに)使用できます。
¶ オブジェクト指向プログラミングを明示的にサポートするほとんどのプログラミング言語では、継承は非常に簡単なことです。JavaScriptでは、言語は実際にはそれを行う簡単な方法を指定していません。このため、JavaScriptプログラマーは継承に対して多くの異なるアプローチを考案してきました。残念ながら、それらのどれも完全ではありません。幸いなことに、このような幅広いアプローチにより、プログラマーは解決しようとしている問題に最適なアプローチを選択でき、他の言語ではまったく不可能な特定のトリックが可能になります。
¶ この章の最後で、継承を行うための他のいくつかの方法と、それらに伴う問題を示します。
¶ 新しいprocessCreature
メソッドを以下に示します。大きいです。
LifeLikeTerrarium.prototype.processCreature = function(creature) { if (creature.object.energy <= 0) return; var surroundings = this.listSurroundings(creature.point); var action = creature.object.act(surroundings); var target = undefined; var valueAtTarget = undefined; if (action.direction && directions.contains(action.direction)) { var direction = directions.lookup(action.direction); var maybe = creature.point.add(direction); if (this.grid.isInside(maybe)) { target = maybe; valueAtTarget = this.grid.valueAt(target); } } if (action.type == "move") { if (target && !valueAtTarget) { this.grid.moveValue(creature.point, target); creature.point = target; creature.object.energy -= 1; } } else if (action.type == "eat") { if (valueAtTarget && valueAtTarget.energy) { this.grid.setValueAt(target, undefined); creature.object.energy += valueAtTarget.energy; valueAtTarget.energy = 0; } } else if (action.type == "photosynthese") { creature.object.energy += 1; } else if (action.type == "reproduce") { if (target && !valueAtTarget) { var species = characterFromElement(creature.object); var baby = elementFromCharacter(species); creature.object.energy -= baby.energy * 2; if (creature.object.energy > 0) this.grid.setValueAt(target, baby); } } else if (action.type == "wait") { creature.object.energy -= 0.2; } else { throw new Error("Unsupported action: " + action.type); } if (creature.object.energy <= 0) this.grid.setValueAt(creature.point, undefined); };
¶ この関数は、依然として、エネルギーが不足していない(死んでいない)ことを前提として、クリーチャーにアクションを要求することから始まります。次に、アクションにdirection
プロパティがある場合、この方向がグリッド上のどのポイントを指しているか、および現在どの値がそこにあるかをすぐに計算します。サポートされている5つのアクションのうち3つはこれを知る必要があり、すべてのアクションが個別に計算した場合、コードはさらに醜くなります。direction
プロパティがない場合、または無効なプロパティがある場合、変数target
とvalueAtTarget
は未定義のままになります。
¶ この後、すべてのアクションを実行します。一部のアクションでは、実行前に追加のチェックが必要です。これは、別のif
を使用して行われます。そのため、たとえば、クリーチャーが壁を通り抜けようとした場合、"Unsupported action"
例外は生成されません。
¶ "reproduce"
アクションでは、親クリーチャーは新生クリーチャーが得るエネルギーの2倍を失い(出産は容易ではありません)、新しいクリーチャーは、親がそれを生成するのに十分なエネルギーを持っていた場合にのみグリッドに配置されることに注意してください。
¶ アクションが実行された後、クリーチャーのエネルギーが不足しているかどうかを確認します。不足している場合、クリーチャーは死に、削除されます。
¶ 地衣類はそれほど複雑な生物ではありません。文字"*"
を使用して表します。演習8.6のrandomElement
関数を定義していることを確認してください。ここでは再び使用されます。
function Lichen() { this.energy = 5; } Lichen.prototype.act = function(surroundings) { var emptySpace = findDirections(surroundings, " "); if (this.energy >= 13 && emptySpace.length > 0) return {type: "reproduce", direction: randomElement(emptySpace)}; else if (this.energy < 20) return {type: "photosynthese"}; else return {type: "wait"}; }; Lichen.prototype.character = "*"; creatureTypes.register(Lichen); function findDirections(surroundings, wanted) { var found = []; directions.each(function(name) { if (surroundings[name] == wanted) found.push(name); }); return found; }
¶ 地衣類は20エネルギーを超えて成長しません。そうでない場合、他の地衣類に囲まれて繁殖する余地がない場合、*巨大*になります。.
¶ LichenEater
クリーチャーを作成します。10のエネルギーで開始し、次のように動作します。
- エネルギーが30以上で、近くに余裕がある場合は繁殖します。
- そうでない場合、近くに地衣類がある場合は、ランダムに1つ食べます。
- そうでない場合、移動するスペースがある場合は、近くのランダムな空の正方形に移動します。
- そうでない場合は、待ちます。
¶ findDirections
とrandomElement
を使用して周囲を確認し、方向を選択します。地衣類を食べる人に文字「c」(パックマン)を付けます。
function LichenEater() { this.energy = 10; } LichenEater.prototype.act = function(surroundings) { var emptySpace = findDirections(surroundings, " "); var lichen = findDirections(surroundings, "*"); if (this.energy >= 30 && emptySpace.length > 0) return {type: "reproduce", direction: randomElement(emptySpace)}; else if (lichen.length > 0) return {type: "eat", direction: randomElement(lichen)}; else if (emptySpace.length > 0) return {type: "move", direction: randomElement(emptySpace)}; else return {type: "wait"}; }; LichenEater.prototype.character = "c"; creatureTypes.register(LichenEater);
¶ そして、試してみてください。
var lichenPlan = ["############################", "# ######", "# *** **##", "# *##** ** c *##", "# *** c ##** *#", "# c ##*** *#", "# ##** *#", "# c #* *#", "#* #** c *#", "#*** ##** c **#", "#***** ###*** *###", "############################"]; var terrarium = new LifeLikeTerrarium(lichenPlan); terrarium.onStep = partial(inPlacePrinter(), terrarium); terrarium.start();
¶ おそらく、地衣類がテラリウムの大部分を急速に覆い尽くし、その後、豊富な食物が原因で食べる人が非常に多くなり、すべての地衣類を、ひいては自分自身を消してしまうでしょう。ああ、自然の悲劇。
terrarium.stop();
¶ テラリウムの住人が数分後に絶滅するのは少し depressing です。これに対処するために、地衣類を食べる人に長期的な持続可能な農業について教える必要があります。近くに少なくとも2つの地衣類が見えない限り、どれだけお腹が空いていても食べないようにすることで、地衣類を根絶することはありません。これにはある程度の規律が必要ですが、その結果、biotope は自らを破壊しません。新しいact
メソッドを以下に示します。唯一の変更点は、lichen.length
が少なくとも2つの場合にのみ食べるようになったことです。
LichenEater.prototype.act = function(surroundings) { var emptySpace = findDirections(surroundings, " "); var lichen = findDirections(surroundings, "*"); if (this.energy >= 30 && emptySpace.length > 0) return {type: "reproduce", direction: randomElement(emptySpace)}; else if (lichen.length > 1) return {type: "eat", direction: randomElement(lichen)}; else if (emptySpace.length > 0) return {type: "move", direction: randomElement(emptySpace)}; else return {type: "wait"}; };
¶ 上記のlichenPlan
テラリウムをもう一度実行して、どのように動作するかを確認してください。非常に幸運でない限り、地衣類を食べる人はしばらくすると絶滅する可能性があります。なぜなら、大量飢餓の時代には、角にある地衣類を見つけるのではなく、空の空間を目的もなく前後に這うからです。
¶ LichenEater
を変更して、生き残る可能性を高める方法を見つけてください。不正行為はしないでください。this.energy += 100
は不正行為です。コンストラクタを書き直す場合は、creatureTypes
辞書に再登録することを忘れないでください。そうでない場合、テラリウムは古いコンストラクタを使い続けます。
¶ 1つのアプローチは、動きのランダム性を減らすことです。常にランダムな方向を選択することにより、どこにも着くことなく前後に移動することがよくあります。最後に行った方向を覚えておいて、その方向を優先することで、食べる人は時間を無駄にすることなく、より早く食べ物を見つけることができます。
function CleverLichenEater() { this.energy = 10; this.direction = "ne"; } CleverLichenEater.prototype.act = function(surroundings) { var emptySpace = findDirections(surroundings, " "); var lichen = findDirections(surroundings, "*"); if (this.energy >= 30 && emptySpace.length > 0) { return {type: "reproduce", direction: randomElement(emptySpace)}; } else if (lichen.length > 1) { return {type: "eat", direction: randomElement(lichen)}; } else if (emptySpace.length > 0) { if (surroundings[this.direction] != " ") this.direction = randomElement(emptySpace); return {type: "move", direction: this.direction}; } else { return {type: "wait"}; } }; CleverLichenEater.prototype.character = "c"; creatureTypes.register(CleverLichenEater);
¶ 前のテラリウムプランを使用して試してみてください。
¶ 1つの食物連鎖はまだ少し初歩的です。地衣類を食べる人を食べることで生き残る新しいクリーチャー、LichenEaterEater
(文字"@"
)を書くことができますか?あまりにも早く絶滅することなく、生態系に適合させる方法を見つけてください。lichenPlan
配列を変更して、これらのいくつかを含め、試してみてください。
¶ ここではあなたは一人です。私は、これらの生き物がすぐに絶滅したり、すべての地衣類を食べる人を食い尽くして絶滅したりするのを防ぐ本当に良い方法を見つけることができませんでした。2つの食べ物が1か所にあることはめったにないため、2つの食べ物が見えたときにのみ食べるというトリックは、彼らにはあまりうまくいきません。役に立つと思われるのは、食べる人を食べる人を本当に太らせる(高エネルギー)ことで、地衣類を食べる人が少ない時期を乗り切ることができ、ゆっくりとしか繁殖しないため、食料源をすぐに絶滅させることができないことです。
¶ 地衣類と食べる人は周期的な動きをします。地衣類が豊富になると、多くの食べる人が生まれ、地衣類が不足し、食べる人が飢え、地衣類が豊富になります。数ターンの間食べ物が足りない場合、地衣類を食べる人を食べる人を「冬眠」させる(しばらくの間"wait"
アクションを使用する)ことができます。この冬眠のターン数を正しく選択するか、たくさんの食べ物の匂いがしたときに自動的に目を覚ますようにすると、これは良い戦略になる可能性があります。
¶ これで、テラリウムについての議論は終わりです。章の残りの部分は、継承、およびJavaScriptにおける継承に関連する問題の詳細な考察に当てられています。
¶ まず、いくつかの理論。オブジェクト指向プログラミングの学生は、継承の正しい使用法と誤った使用法について、長くて微妙な議論をしているのをよく耳にします。結局のところ、継承は、怠惰な3プログラマーがコードを少なく書くことを可能にするトリックにすぎないことを覚えておくことが重要です。したがって、継承が正しく使用されているかどうかの問題は、結果のコードが正しく機能し、無駄な繰り返しを回避しているかどうかの問題に要約されます。それでも、これらの学生が使用する原則は、継承について考え始める良い方法を提供します。
¶ 継承とは、既存のタイプである「スーパーツール」に基づいて、新しいタイプのオブジェクトである「サブタイプ」を作成することです。サブタイプは、スーパーツープのすべてのプロパティとメソッドから始まり、それらを継承し、次にこれらのいくつかを変更し、オプションで新しいものを追加します。継承は、サブタイプによってモデル化されたものがスーパーツープのオブジェクト*である*と言える場合に最適に使用されます。
¶ したがって、Piano
型は Instrument
型のサブタイプになり得ます。なぜなら、ピアノは楽器*である*からです。ピアノには鍵盤の配列があるため、Piano
を Array
のサブタイプにしたくなるかもしれませんが、ピアノは配列*ではない*ため、そのように実装すると、あらゆる種類の不都合が生じます。たとえば、ピアノにはペダルもあります。なぜ piano[0]
は最初のペダルではなく、最初の鍵盤を返すのでしょうか?もちろん、ピアノは鍵盤を*持っている*ため、keys
プロパティと、場合によっては配列を保持する別の pedals
プロパティを与える方が良いでしょう。
¶ サブタイプがさらに別のサブタイプのスーパータイプになる可能性があります。複雑な型のファミリーツリーを構築することで、最もよく解決できる問題もあります。ただし、継承を多用しすぎないように注意する必要があります。継承の過剰使用は、プログラムを大きな醜い混乱にするための素晴らしい方法です。
¶ new
キーワードとコンストラクターの prototype
プロパティの動作は、オブジェクトを使用する特定の方法を示唆しています。テラリウムの生き物のような単純なオブジェクトの場合、この方法はうまく機能します。残念ながら、プログラムが継承を本格的に使い始めると、このオブジェクトへのアプローチはすぐに扱いにくくなります。一般的な操作を処理するための関数をいくつか追加すると、物事を少しスムーズにすることができます。たとえば、多くの人はオブジェクトに inherit
メソッドと method
メソッドを定義します。
Object.prototype.inherit = function(baseConstructor) { this.prototype = clone(baseConstructor.prototype); this.prototype.constructor = this; }; Object.prototype.method = function(name, func) { this.prototype[name] = func; }; function StrangeArray(){} StrangeArray.inherit(Array); StrangeArray.method("push", function(value) { Array.prototype.push.call(this, value); Array.prototype.push.call(this, value); }); var strange = new StrangeArray(); strange.push(4); show(strange);
¶ 'JavaScript' と '継承' という単語をウェブで検索すると、これに関する何十もの異なるバリエーションが見つかります。その中には、上記よりもはるかに複雑で巧妙なものもあります。
¶ ここで記述された push
メソッドが、親タイプのプロトタイプから push
メソッドをどのように使用しているかに注目してください。これは、継承を使用する場合によく行われることです。サブタイプのメソッドは内部でスーパータイプのメソッドを使用しますが、それを何らかの形で拡張します。
¶ この基本的なアプローチの最大の問題は、コンストラクターとプロトタイプの二重性です。コンストラクターは非常に中心的な役割を果たし、オブジェクト型に名前を付けるものであり、プロトタイプを取得する必要がある場合は、コンストラクターにアクセスしてその prototype
プロパティを取得する必要があります。
¶ これは*多くの*タイピング("prototype"
は9文字)につながるだけでなく、混乱を招きます。上記の例では、StrangeArray
に空の役に立たないコンストラクターを記述する必要がありました。私は、誤ってプロトタイプではなくコンストラクターにメソッドを追加したり、実際には Array.prototype.slice
を意味しているときに Array.slice
を呼び出そうとしたりするのを何度も経験しました。私としては、プロトタイプ自体がオブジェクト型の最も重要な側面であり、コンストラクターはその拡張、特別な種類のメソッドにすぎません。
¶ Object.prototype
にいくつかの単純なヘルパーメソッドを追加することで、オブジェクトと継承への代替アプローチを作成できます。このアプローチでは、型はそのプロトタイプで表され、これらのプロトタイプを格納するために大文字の変数を使用します。「構築」作業を行う必要がある場合は、construct
というメソッドによって行われます。new
キーワードの代わりに使用される create
というメソッドを Object
プロトタイプに追加します。オブジェクトを複製し、construct
メソッドがある場合はそれを呼び出し、create
に渡された引数を渡します。
Object.prototype.create = function() { var object = clone(this); if (typeof object.construct == "function") object.construct.apply(object, arguments); return object; };
¶ 継承は、プロトタイプオブジェクトを複製し、そのプロパティの一部を追加または置き換えることで実行できます。このための便利な shorthand、オブジェクトを複製し、引数として与えられたオブジェクトのプロパティをこの複製に追加する extend
メソッドも提供します。
Object.prototype.extend = function(properties) { var result = clone(this); forEachIn(properties, function(name, value) { result[name] = value; }); return result; };
¶ Object
プロトタイプをいじることが安全でない場合は、これらを通常の(メソッドではない)関数として実装できます。
¶ 例。あなたが十分な年齢であれば、かつて「テキストアドベンチャー」ゲームをプレイしたことがあるかもしれません。コマンドを入力して仮想世界を移動し、周囲のものや実行するアクションのテキストによる説明を取得します。当時は素晴らしいゲームでした!
¶ そのようなゲームのアイテムのプロトタイプをこのように書くことができます。
var Item = { construct: function(name) { this.name = name; }, inspect: function() { print("it is ", this.name, "."); }, kick: function() { print("klunk!"); }, take: function() { print("you can not lift ", this.name, "."); } }; var lantern = Item.create("the brass lantern"); lantern.kick();
¶ このように継承します...
var DetailedItem = Item.extend({ construct: function(name, details) { Item.construct.call(this, name); this.details = details; }, inspect: function() { print("you see ", this.name, ", ", this.details, "."); } }); var giantSloth = DetailedItem.create( "the giant sloth", "it is quietly hanging from a tree, munching leaves"); giantSloth.inspect();
¶ 必須の prototype
部分を省略すると、DetailedItem
のコンストラクターから Item.construct
を呼び出すなどの操作が少し簡単になります。 DetailedItem.construct
で this.name = name
を実行するのは悪い考えです。これは行を複製します。確かに、行の複製は Item.construct
関数を呼び出すよりも短いですが、後でこのコンストラクターに何かを追加することになると、2か所に追加する必要があります。
¶ ほとんどの場合、サブタイプのコンストラクターは、スーパータイプのコンストラクターを呼び出すことから始める必要があります。このようにして、スーパータイプの有効なオブジェクトから始まり、それを拡張できます。プロトタイプへのこの新しいアプローチでは、コンストラクターを必要としない型はそれを省略できます。それらはスーパータイプのコンストラクターを自動的に継承します。
var SmallItem = Item.extend({ kick: function() { print(this.name, " flies across the room."); }, take: function() { // (imagine some code that moves the item to your pocket here) print("you take ", this.name, "."); } }); var pencil = SmallItem.create("the red pencil"); pencil.take();
¶ SmallItem
は独自のコンストラクターを定義していませんが、name
引数を使用して作成すると、Item
プロトタイプからコンストラクターを継承しているため、機能します。
¶ JavaScript には instanceof
と呼ばれる演算子があり、オブジェクトが特定のプロトタイプに基づいているかどうかを判断するために使用できます。オブジェクトを左側に、コンストラクターを右側に指定すると、ブール値が返されます。コンストラクターの prototype
プロパティがオブジェクトの直接または間接のプロトタイプである場合は true
、そうでない場合は false
です。
¶ 通常のコンストラクターを使用していない場合、この演算子の使用はかなり扱いにくくなります。2番目の引数としてコンストラクター関数を想定していますが、プロトタイプしかありません。 clone
関数と同様のトリックを使用して回避できます。「偽のコンストラクター」を使用し、それに instanceof
を適用します。
Object.prototype.hasPrototype = function(prototype) { function DummyConstructor() {} DummyConstructor.prototype = prototype; return this instanceof DummyConstructor; }; show(pencil.hasPrototype(Item)); show(pencil.hasPrototype(DetailedItem));
¶ 次に、詳細な説明が付いた小さなアイテムを作成したいと思います。このアイテムは、DetailedItem
と SmallItem
の両方から継承する必要があるようです。 JavaScript では、オブジェクトに複数のプロトタイプを持たせることは許可されておらず、たとえ許可されていても、問題はそれほど簡単に解決できるわけではありません。たとえば、SmallItem
が何らかの理由で inspect
メソッドも定義する場合、新しいプロトタイプはどの inspect
メソッドを使用する必要がありますか?
¶ 複数の親タイプからオブジェクトタイプを派生させることを 多重継承といいます。一部の言語はしり込みしてそれを完全に禁止し、他の言語はそれを明確に定義された実用的な方法で機能させるための複雑なスキームを定義しています。 JavaScript で適切な多重継承フレームワークを実装することは可能です。実際、いつものように、これには複数の優れたアプローチがあります。しかし、それらはすべてここで説明するには複雑すぎます。代わりに、ほとんどの場合に十分な非常に簡単なアプローチを紹介します。
¶ mixin は、他のプロトタイプに「mixin」できる特定の種類のプロトタイプです。 SmallItem
はそのようなプロトタイプと見なすことができます。その kick
メソッドと take
メソッドを別のプロトタイプにコピーすることで、小ささをそのプロトタイプに mixin します。
function mixInto(object, mixIn) { forEachIn(mixIn, function(name, value) { object[name] = value; }); }; var SmallDetailedItem = clone(DetailedItem); mixInto(SmallDetailedItem, SmallItem); var deadMouse = SmallDetailedItem.create( "Fred the mouse", "he is dead"); deadMouse.inspect(); deadMouse.kick();
¶ forEachIn
はオブジェクトの*独自の*プロパティのみを処理するため、kick
と take
はコピーされますが、SmallItem
が Item
から継承したコンストラクターはコピーされないことに注意してください。
¶ mixin にコンストラクターがある場合、またはそのメソッドの一部が mixin されるプロトタイプのメソッドと「衝突」する場合、プロトタイプの mixin はより複雑になります。場合によっては、「手動 mixin」を実行するのが適切です。独自のコンストラクターを持つプロトタイプ Monster
があり、それを DetailedItem
と mixin したいとします。
var Monster = Item.extend({ construct: function(name, dangerous) { Item.construct.call(this, name); this.dangerous = dangerous; }, kick: function() { if (this.dangerous) print(this.name, " bites your head off."); else print(this.name, " runs away, weeping."); } }); var DetailedMonster = DetailedItem.extend({ construct: function(name, description, dangerous) { DetailedItem.construct.call(this, name, description); Monster.construct.call(this, name, dangerous); }, kick: Monster.kick }); var giantSloth = DetailedMonster.create( "the giant sloth", "it is quietly hanging from a tree, munching leaves", true); giantSloth.kick();
¶ ただし、これにより、DetailedMonster
を作成するときに Item
コンストラクターが2回呼び出されることに注意してください。1回は DetailedItem
コンストラクターを介して、もう1回は Monster
コンストラクターを介してです。この場合、それほど害はありませんが、これが問題を引き起こす状況があります。
¶ しかし、これらの複雑さのために継承の利用をためらってはいけません。多重継承は、状況によっては非常に役立ちますが、ほとんどの場合、安全に無視できます。そのため、Java のような言語は多重継承を禁止することで逃げることができます。そして、いつか本当に必要だとわかったら、ウェブを検索して調査を行い、自分の状況に合ったアプローチを見つけることができます。
¶ 考えてみると、JavaScript はおそらくテキストアドベンチャーを構築するための素晴らしい環境でしょう。プロトタイプ継承が私たちに与える、オブジェクトの動作を自由に 변경できる機能は、これによく適しています。蹴られると丸くなるという独特の習慣を持つオブジェクト hedgehog
がある場合、その kick
メソッドを変更するだけです。
¶ 残念ながら、テキストアドベンチャーは レコード盤と同じ道をたどり、かつては非常に人気がありましたが、今では少数の 愛好家 だけがプレイしています。