第4章: データ構造: オブジェクトと配列

この章では、いくつかの簡単な問題を解くことに専念します。その過程で、配列とオブジェクトという2つの新しい型の値について説明し、それらに関連するいくつかのテクニックを見ていきます。

次の状況を考えてみてください。50匹以上の猫(あなたは数え切れたことがありません)と暮らしていると噂されている、あなたの風変わりなエミリーおばさんは、定期的にあなたにメールを送って、彼女の近況を知らせてくれます。それらは通常、次のようになります。

甥っ子へ

お母さんから、あなたがスカイダイビングを始めたと聞きました。これは本当ですか?気をつけなさいよ、若者!私の夫に何が起こったか覚えていますか?あれはたった2階からだったんですよ!

とにかく、ここはとてもエキサイティングです。私は一週間ずっと、隣に引っ越してきた素敵な紳士、ドレイク氏の注意を引こうとしてきましたが、彼は猫を怖がっているようです。それともアレルギーがあるのでしょうか?今度彼に会ったら、ファット・イゴールを彼の肩に乗せてみようと思っています。何が起こるかとても楽しみです。

それと、あなたに話した詐欺は予想以上にうまくいっています。すでに5回の「支払い」を受け取り、苦情は1件だけでした。でも、少し悪い気がしてきました。そして、あなたがおっしゃる通り、これはおそらく何らかの形で違法なのでしょう。

(... など ...)

愛を込めて、エミリーおばさんより

死亡 2006/04/27: ブラック・レクレール

誕生 2006/04/05 (母 レディ・ペネロープ): レッド・ライオン、ドクター・ホブルズ3世、リトル・イロコイ

このおばあちゃんを喜ばせるために、あなたは彼女の猫の系図を記録しておきたいと思っています。そうすれば、「追伸:ドクター・ホブルズ2世がこの土曜日の誕生日を楽しんでくれたらいいのですが!」とか「老レディ・ペネロープはどうしていますか?彼女はもう5歳ですよね?」といったことを、うっかり死んだ猫について尋ねることなく、付け加えることができます。あなたは叔母からの大量の古いメールを所有しており、幸いなことに彼女は常に猫の出生と死亡に関する情報をメールの最後に全く同じ形式で記載しています。

あなたはそれらのメールすべてを手作業で調べる気にはなれません。幸いなことに、私たちはちょうど例題が必要としていたので、私たちのために仕事をしてくれるプログラムを作成してみましょう。まずは、最後のメールの後にまだ生きている猫のリストをくれるプログラムを書いてみます。

あなたが尋ねる前に、文通の開始時には、エミリーおばさんはスポットという猫を1匹だけ飼っていました。(当時はまだかなり伝統的でした)。



通常、入力する前に、自分のプログラムが何をするのかについて何らかの手がかりを持っていると役に立ちます。ここに計画があります。

  1. "スポット"のみを含む猫の名前のセットから始めます。
  2. アーカイブ内のすべてのメールを時系列順に調べます。
  3. "誕生"または"死亡"で始まる段落を探します。
  4. "誕生"で始まる段落の名前を名前のセットに追加します。
  5. "死亡"で始まる段落の名前を名前のセットから削除します。

段落から名前を取得する方法は次のとおりです。

  1. 段落のコロンを見つけます。
  2. このコロンの後の部分を取得します。
  3. カンマを探して、この部分を別々の名前に分割します。

エミリーおばさんが常にこの正確な形式を使用し、名前を忘れたり綴りを間違えたりしたことがないと信じるには、いくらか信じられないかもしれませんが、あなたのおばさんはそういう人なのです。


まず、プロパティについて説明しましょう。多くのJavaScriptの値には、他の値が関連付けられています。これらの関連付けはプロパティと呼ばれます。すべての文字列にはlengthと呼ばれるプロパティがあり、これは数値、つまりその文字列の文字数を表します。

プロパティには2つの方法でアクセスできます。

var text = "purple haze";
show(text["length"]);
show(text.length);

2番目の方法は1番目の方法の省略形であり、プロパティの名前が有効な変数名である場合にのみ機能します。つまり、スペースや記号が含まれておらず、数字で始まらない場合です。

nullundefinedにはプロパティがありません。このような値からプロパティを読み取ろうとすると、エラーが発生します。このような場合にブラウザが生成するエラーメッセージ(一部のブラウザでは非常に謎めいたものになる可能性があります)について理解するために、次のコードを試してみてください。

var nothing = null;
show(nothing.length);

文字列値のプロパティは変更できません。私たちが見てきたように、length以外にもかなりの数がありますが、追加や削除はできません。

これはオブジェクト型の値とは異なります。それらの主な役割は、他の値を保持することです。いわば、プロパティという形で独自の触手を持っていると言えるでしょう。これらは自由に修正、削除、または新しいものを追加できます。

オブジェクトは次のように記述できます。

var cat = {colour: "grey", name: "Spot", size: 46};
cat.size = 47;
show(cat.size);
delete cat.size;
show(cat.size);
show(cat);

変数と同様に、オブジェクトにアタッチされている各プロパティは文字列でラベル付けされています。最初のステートメントは、プロパティ"colour"が文字列"grey"を保持し、プロパティ"name"が文字列"Spot"にアタッチされ、プロパティ"size"が数値46を参照するオブジェクトを作成します。2番目のステートメントは、名前がsizeのプロパティに新しい値を与えます。これは変数を変更するのと同じ方法で行われます。

キーワードdeleteはプロパティを切り離します。存在しないプロパティを読み取ろうとすると、値undefinedが返されます。

まだ存在しないプロパティが=演算子で設定されると、オブジェクトに追加されます。

var empty = {};
empty.notReally = 1000;
show(empty.notReally);

有効な変数名ではない名前のプロパティは、オブジェクトを作成するときに引用符で囲み、角かっこを使用してアクセスする必要があります。

var thing = {"gabba gabba": "hey", "5": 10};
show(thing["5"]);
thing["5"] = 20;
show(thing[2 + 3]);
delete thing["gabba gabba"];

ご覧のとおり、角かっこの間の部分は任意の式にすることができます。それが参照するプロパティ名を決定するために、文字列に変換されます。変数を使用してプロパティに名前を付けることさえできます。

var propertyName = "length";
var text = "mainline";
show(text[propertyName]);

演算子inは、オブジェクトに特定のプロパティがあるかどうかをテストするために使用できます。ブール値を生成します。

var chineseBox = {};
chineseBox.content = chineseBox;
show("content" in chineseBox);
show("content" in chineseBox.content);

オブジェクト値がコンソールに表示されると、クリックしてプロパティを調べることができます。これにより、出力ウィンドウが「検査」ウィンドウに変わります。右上の小さな「x」を使用して出力ウィンドウに戻り、左矢印を使用して以前に検査したオブジェクトのプロパティに戻ることができます。

show(chineseBox);

例 4.1

猫の問題の解決策は、名前の「集合」について述べています。集合とは、値が2回以上出現することがない値のコレクションです。名前が文字列の場合、オブジェクトを使用して名前の集合を表す方法を思いつきますか?

この集合に名前を追加する方法、名前を削除する方法、および名前に含まれているかどうかを確認する方法を示してください。

これは、集合の内容をオブジェクトのプロパティとして格納することで実行できます。名前の追加は、その名前のプロパティを値(任意の値)に設定することで行われます。名前の削除は、このプロパティを削除することで行われます。in演算子を使用して、特定の名前が集合の一部であるかどうかを判断できます1

var set = {"Spot": true};
// Add "White Fang" to the set
set["White Fang"] = true;
// Remove "Spot"
delete set["Spot"];
// See if "Asoka" is in the set
show("Asoka" in set);

オブジェクト値は、明らかに変更可能です。第2章で説明した型の値はすべて不変であり、これらの型の既存の値を変更することはできません。それらを組み合わせて新しい値を派生させることはできますが、特定の文字列値を取得する場合、その中のテキストは変更できません。一方、オブジェクトでは、プロパティを変更することで値の内容を変更できます。

2つの数値、120120がある場合、それらは実際にはまったく同じ数値と見なすことができます。オブジェクトの場合、同じオブジェクトへの2つの参照を持つことと、同じプロパティを含む2つの異なるオブジェクトを持つことには違いがあります。次のコードについて考えてみます。

var object1 = {value: 10};
var object2 = object1;
var object3 = {value: 10};

show(object1 == object2);
show(object1 == object3);

object1.value = 15;
show(object2.value);
show(object3.value);

object1object2は、同じ値を把握する2つの変数です。実際のオブジェクトは1つしかないため、object1を変更するとobject2の値も変更されます。変数object3は別のオブジェクトを指しています。これは最初はobject1と同じプロパティを含んでいますが、別々に存在しています。

JavaScriptの==演算子は、オブジェクトを比較する場合、与えられた両方の値がまったく同じ値である場合にのみtrueを返します。同一の内容を持つ異なるオブジェクトを比較すると、falseが返されます。これは状況によっては便利ですが、そうでない場合は実用的ではありません。


オブジェクト値は、さまざまな役割を果たすことができます。集合のように振る舞うことは、それらのうちの1つにすぎません。この章では他のいくつかの役割を見て、第8章ではオブジェクトを使用する別の重要な方法を示します。

猫の問題の計画、というか、計画ではなくアルゴリズムと呼びましょう。そうすると、私たちが何について話しているのかわかっているように聞こえます。アルゴリズムでは、アーカイブ内のすべてのメールを調べることについて述べています。このアーカイブはどのようなものですか?そして、それはどこから来るのですか?

今のところ2番目の質問については心配しないでください。第14章では、プログラムにデータをインポートするいくつかの方法について説明しますが、今のところ、メールは魔法のようにそこにあることがわかるでしょう。コンピュータの中では、魔法は本当に簡単です。


アーカイブの保存方法については、まだ興味深い問題が残っています。アーカイブには多数のEメールが含まれています。Eメールは文字列で表せることは明らかです。アーカイブ全体を1つの巨大な文字列にすることもできますが、それは実用的ではありません。必要なのは、個別の文字列のコレクションです。

オブジェクトは、まさにそのようなもののコレクションを扱うために使用されます。以下のようにオブジェクトを作成することができます。

var mailArchive = {"the first e-mail": "Dear nephew, ...",
                   "the second e-mail": "..."
                   /* and so on ... */};

しかし、これではEメールを最初から最後まで順番に処理するのが難しくなります。プログラムはこれらのプロパティの名前をどのように推測すればよいのでしょうか?これは、より予測可能なプロパティ名を使用することで解決できます。

var mailArchive = {0: "Dear nephew, ... (mail number 1)",
                   1: "(mail number 2)",
                   2: "(mail number 3)"};

for (var current = 0; current in mailArchive; current++)
  print("Processing e-mail #", current, ": ", mailArchive[current]);

幸運なことに、この種の用途に特化した特別なオブジェクトがあります。それは配列と呼ばれ、配列内の値の数を示すlengthプロパティや、この種のコレクションに役立つ多くの操作など、いくつかの便利な機能を提供します。

新しい配列は、角括弧([])を使用して作成できます。

var mailArchive = ["mail one", "mail two", "mail three"];

for (var current = 0; current < mailArchive.length; current++)
  print("Processing e-mail #", current, ": ", mailArchive[current]);

この例では、要素の番号は明示的に指定されていません。最初の要素は自動的に番号0、2番目の要素は番号1、というように番号が割り当てられます。

なぜ0から始まるのでしょうか?人は通常1から数え始めます。直感的ではないように思えますが、コレクションの要素を0から番号付けする方が実用的な場合が多いです。今はそのまま受け入れてください。いずれ慣れるでしょう。

要素0から開始するということは、X個の要素を持つコレクションでは、最後の要素が位置X - 1にあることを意味します。そのため、例のforループでは、current < mailArchive.lengthかどうかをチェックしています。位置mailArchive.lengthには要素が存在しないため、currentがこの値になるとループを停止します。


例 4.2

正の数値を引数に取り、0からその数値までのすべての数値を含む配列を返す関数rangeを作成してください。

空の配列は、単に[]と入力することで作成できます。また、オブジェクト、つまり配列にプロパティを追加するには、=演算子を使用して値を代入すればよいことを覚えておいてください。lengthプロパティは、要素が追加されると自動的に更新されます。

function range(upto) {
  var result = [];
  for (var i = 0; i <= upto; i++)
    result[i] = i;
  return result;
}
show(range(4));

これまでのようにループ変数にcountercurrentという名前を付ける代わりに、ここでは単にiという名前を使用しています。ループ変数に1文字、通常はij、またはkを使用するのは、プログラマーの間で広く普及している習慣です。これは主に怠惰に由来しています。7文字入力するよりも1文字入力する方が楽ですし、countercurrentのような名前は変数の意味をそれほど明確にするわけではありません。

プログラムで無意味な1文字変数を使いすぎると、非常に分かりにくくなる可能性があります。私自身のプログラムでは、いくつかの一般的な場合にのみこれを行うようにしています。小さなループはそのような場合の1つです。ループの中に別のループがあり、そのループでもiという名前の変数が使用されている場合、内側のループは外側のループが使用している変数を変更してしまい、すべてが壊れてしまいます。内側のループにjを使用することもできますが、一般的に、ループの本体が大きい場合は、明確な意味を持つ変数名を付けるべきです。


文字列オブジェクトと配列オブジェクトはどちらも、lengthプロパティに加えて、関数値を参照する多くのプロパティを含んでいます。

var doh = "Doh";
print(typeof doh.toUpperCase);
print(doh.toUpperCase());

すべての文字列にはtoUpperCaseプロパティがあります。これを呼び出すと、すべての文字が大文字に変換された文字列のコピーが返されます。toLowerCaseもあります。これが何をするか想像してみてください。

toUpperCaseの呼び出しでは引数を渡していませんが、関数はどういうわけか文字列"Doh"にアクセスできています。これがどのように機能するかは、第8章で詳しく説明されています。

関数を格納するプロパティは、一般的にメソッドと呼ばれます。「toUpperCaseは文字列オブジェクトのメソッドです」のようにです。

var mack = [];
mack.push("Mack");
mack.push("the");
mack.push("Knife");
show(mack.join(" "));
show(mack.pop());
show(mack);

配列に関連付けられているpushメソッドは、配列に値を追加するために使用できます。前回の演習では、result[i] = iの代わりに使用することもできました。また、pushの反対のpopは、配列の最後の値を取り出して返します。joinは、文字列の配列から1つの大きな文字列を作成します。渡されたパラメータは、配列内の値の間に挿入されます。


猫の話に戻りますが、Eメールのアーカイブを保存するには配列が適していることが分かりました。このページでは、関数retrieveMailsを使用して、(魔法のように)この配列を取得できます。それらを順番に処理していくことは、もはや難しいことではありません。

var mailArchive = retrieveMails();

for (var i = 0; i < mailArchive.length; i++) {
  var email = mailArchive[i];
  print("Processing e-mail #", i);
  // Do more things...
}

また、生きている猫の集合を表す方法も決定しました。次の問題は、Eメールの中で"born"または"died"で始まる段落を見つけることです。


最初に浮かぶ疑問は、段落とは正確には何かということです。この場合、文字列値自体はあまり役に立ちません。JavaScriptのテキストの概念は「文字のシーケンス」という考え方に限定されているため、段落をこれらの用語で定義する必要があります。

前述したように、改行文字というものがあります。ほとんどの人はこれを使用して段落を分割します。そこで、段落とは、改行文字またはコンテンツの先頭で始まり、次の改行文字またはコンテンツの末尾で終わるEメールの一部であると見なします。

さらに、文字列を段落に分割するアルゴリズムを自分で書く必要もありません。文字列にはすでにsplitというメソッドがあり、これは配列のjoinメソッドの(ほぼ)反対のものです。これは、引数として指定された文字列を使用して、文字列を配列に分割し、どこで分割するかを決定します。

var words = "Cities of the Interior";
show(words.split(" "));

したがって、改行("\n")で分割することで、Eメールを段落に分割できます。


例 4.3

splitjoinは、厳密には互いに逆ではありません。string.split(x).join(x)は常に元の値を生成しますが、array.join(x).split(x)はそうではありません。.join(" ").split(" ")が異なる値を生成する配列の例を挙げてください。

var array = ["a", "b", "c d"];
show(array.join(" ").split(" "));

"born"または"died"で始まらない段落は、プログラムによって無視できます。文字列が特定の単語で始まるかどうかをどのようにテストすればよいでしょうか?charAtメソッドを使用して、文字列から特定の文字を取得できます。x.charAt(0)は最初の文字、1は2番目の文字、というようになります。文字列が"born"で始まるかどうかを確認する1つの方法は次のとおりです。

var paragraph = "born 15-11-2003 (mother Spot): White Fang";
show(paragraph.charAt(0) == "b" && paragraph.charAt(1) == "o" &&
     paragraph.charAt(2) == "r" && paragraph.charAt(3) == "n");

しかし、これは少し面倒です。10文字の単語を確認することを想像してみてください。ただし、ここで学ぶべきことがあります。行が途方もなく長くなった場合は、複数行に分割できます。新しい行の先頭を、元の行で同様の役割を果たす最初の要素に揃えることで、読みやすくすることができます。

文字列にはsliceというメソッドもあります。これは、最初の引数で指定された位置にある文字から始まり、2番目の引数で指定された位置にある文字の直前(含まない)までの文字列の一部をコピーします。これにより、チェックをより短い方法で記述できます。

show(paragraph.slice(0, 4) == "born");

例 4.4

2つの引数(どちらも文字列)を取り、最初の引数が2番目の引数の文字で始まる場合はtrueを、そうでない場合はfalseを返す関数startsWithを作成してください。

function startsWith(string, pattern) {
  return string.slice(0, pattern.length) == pattern;
}

show(startsWith("rotation", "rot"));

charAtまたはsliceを使用して、存在しない文字列の一部を取得しようとするとどうなりますか?パターンが一致対象の文字列よりも長い場合、私が示したstartsWithは引き続き機能するでしょうか?

show("Pip".charAt(250));
show("Nop".slice(1, 10));

指定された位置に文字がない場合、charAt""を返し、sliceは単に存在しない新しい文字列の部分を省略します。

つまり、そうです。そのバージョンのstartsWithは機能します。startsWith("Idiots", "Most honoured colleagues")が呼び出されると、stringに十分な文字がないため、sliceの呼び出しは常にpatternよりも短い文字列を返します。そのため、==による比較はfalseを返しますが、これは正しいです。

プログラムの異常な(ただし有効な)入力について常に少し時間を取って考えることは役に立ちます。これらは通常コーナーケースと呼ばれ、「通常の」入力ではすべて完璧に動作するプログラムがコーナーケースで失敗することは非常に一般的です。


猫の問題でまだ解決されていない部分は、段落からの名前の抽出だけです。アルゴリズムは次のとおりです。

  1. 段落のコロンを見つけます。
  2. このコロンの後の部分を取得します。
  3. カンマを探して、この部分を別々の名前に分割します。

これは、"died"で始まる段落と"born"で始まる段落の両方で発生する必要があります。これらの異なる種類の段落を処理する2つのコードが両方とも使用できるように、関数の形にすることをお勧めします。


例 4.5

段落を引数に取り、名前の配列を返す関数catNamesを作成できますか?

文字列には、文字列内のある文字または部分文字列の(最初の)位置を見つけるために使用できるindexOfメソッドがあります。また、sliceに引数を1つだけ指定すると、指定された位置から末尾までの文字列の部分が返されます。

関数を「探索」するためにコンソールを使用すると役立つ場合があります。たとえば、"foo: bar".indexOf(":")と入力して、何が得られるかを確認してください。

function catNames(paragraph) {
  var colon = paragraph.indexOf(":");
  return paragraph.slice(colon + 2).split(", ");
}

show(catNames("born 20/09/2004 (mother Yellow Bess): " +
              "Doctor Hobbles the 2nd, Noog"));

アルゴリズムの元の説明では無視されていましたが、扱いが難しいのはコロンとコンマの後のスペースです。文字列をスライスする際に使用される + 2 は、コロン自体とその後のスペースを除外するために必要です。split の引数にはコンマとスペースの両方が含まれています。これは、名前が実際にはコンマだけでなく、コンマとスペースで区切られているためです。

この関数は問題のチェックを行いません。この場合、入力は常に正しいと仮定します。


残っているのは、それらをまとめることだけです。そのための1つの方法は次のようになります。

var mailArchive = retrieveMails();
var livingCats = {"Spot": true};

for (var mail = 0; mail < mailArchive.length; mail++) {
  var paragraphs = mailArchive[mail].split("\n");
  for (var paragraph = 0;
       paragraph < paragraphs.length;
       paragraph++) {
    if (startsWith(paragraphs[paragraph], "born")) {
      var names = catNames(paragraphs[paragraph]);
      for (var name = 0; name < names.length; name++)
        livingCats[names[name]] = true;
    }
    else if (startsWith(paragraphs[paragraph], "died")) {
      var names = catNames(paragraphs[paragraph]);
      for (var name = 0; name < names.length; name++)
        delete livingCats[names[name]];
    }
  }
}

show(livingCats);

これはかなり大きな密度の高いコードです。少し軽くする方法をすぐに検討します。しかし、まずは結果を見てみましょう。特定の猫が生き残るかどうかを確認する方法はわかっています。

if ("Spot" in livingCats)
  print("Spot lives!");
else
  print("Good old Spot, may she rest in peace.");

しかし、生きている猫をすべてリストするにはどうすればよいでしょうか?in キーワードは、for と一緒に使用すると、少し異なる意味を持ちます。

for (var cat in livingCats)
  print(cat);

このようなループは、オブジェクトのプロパティの名前を反復処理するため、セット内のすべての名前を列挙できます。


コードの一部は、侵入不可能なジャングルのようです。猫の問題の解決策の例は、この問題を抱えています。光を当てる1つの方法は、戦略的な空白行を追加することです。これは見栄えを良くしますが、実際には問題を解決するわけではありません。

ここで必要なのは、コードを分割することです。問題の小さく理解しやすい部分を処理する2つのヘルパー関数、startsWithcatNames を既に作成しました。これを続けましょう。

function addToSet(set, values) {
  for (var i = 0; i < values.length; i++)
    set[values[i]] = true;
}

function removeFromSet(set, values) {
  for (var i = 0; i < values.length; i++)
    delete set[values[i]];
}

これらの2つの関数は、セットへの名前の追加と削除を処理します。これで、ソリューションから最も内側の2つのループがなくなります。

var livingCats = {Spot: true};

for (var mail = 0; mail < mailArchive.length; mail++) {
  var paragraphs = mailArchive[mail].split("\n");
  for (var paragraph = 0;
       paragraph < paragraphs.length;
       paragraph++) {
    if (startsWith(paragraphs[paragraph], "born"))
      addToSet(livingCats, catNames(paragraphs[paragraph]));
    else if (startsWith(paragraphs[paragraph], "died"))
      removeFromSet(livingCats, catNames(paragraphs[paragraph]));
  }
}

私が言うまでもなく、かなりの改善です。

addToSetremoveFromSet はなぜセットを引数として取るのでしょうか?必要であれば、変数 livingCats を直接使用できます。理由は、このようにして、それらが現在の問題に完全に結び付けられていないためです。addToSetlivingCats を直接変更した場合、addCatsToCatSet などと呼ぶ必要があります。現在の方法では、より一般的に有用なツールです。

これらの関数を他の目的で使用することがない場合でも(これは非常に可能性が高いです)、このように記述すると便利です。それらは「自己完結型」であるため、livingCats と呼ばれる外部変数について知る必要なく、単独で読み取って理解できます。

関数は純粋ではありません。set 引数として渡されたオブジェクトを変更します。これにより、真の純粋関数よりも少し扱いにくくなりますが、好きな値や変数を変更する暴走する関数よりもはるかに混乱が少なくなります。


アルゴリズムをさらに分割していきます。

function findLivingCats() {
  var mailArchive = retrieveMails();
  var livingCats = {"Spot": true};

  function handleParagraph(paragraph) {
    if (startsWith(paragraph, "born"))
      addToSet(livingCats, catNames(paragraph));
    else if (startsWith(paragraph, "died"))
      removeFromSet(livingCats, catNames(paragraph));
  }

  for (var mail = 0; mail < mailArchive.length; mail++) {
    var paragraphs = mailArchive[mail].split("\n");
    for (var i = 0; i < paragraphs.length; i++)
      handleParagraph(paragraphs[i]);
  }
  return livingCats;
}

var howMany = 0;
for (var cat in findLivingCats())
  howMany++;
print("There are ", howMany, " cats.");

アルゴリズム全体が関数にカプセル化されました。これは、実行後に混乱を残さないことを意味します。livingCats は、トップレベルの変数ではなく、関数内のローカル変数になったため、関数の実行中のみ存在します。このセットを必要とするコードは、findLivingCats を呼び出して、返された値を使用できます。

handleParagraph を別の関数にすることで、事態が明確になったように思えました。しかし、これは猫のアルゴリズムに密接に関連付けられているため、他の状況では意味がありません。その上、livingCats 変数にアクセスする必要があります。したがって、関数内関数に最適な候補です。findLivingCats 内にある場合、そこでのみ関連があり、親関数の変数にアクセスできることが明らかです。

このソリューションは、実際には以前のものよりも*大きい*です。それでも、より整理されており、読みやすいことに同意していただけることを願っています。


プログラムは、電子メールに含まれている情報の多くをまだ無視しています。そこには、生年月日、死亡日、母親の名前があります。

日付から始めましょう。日付を保存する良い方法は?yearmonthday の3つのプロパティを持つオブジェクトを作成し、それらに数値を格納できます。

var when = {year: 1980, month: 2, day: 1};

しかし、JavaScript は既にこの目的のためのオブジェクトを提供しています。このようなオブジェクトは、キーワード new を使用して作成できます。

var when = new Date(1980, 1, 1);
show(when);

既に見てきた中括弧とコロンの表記法と同様に、new はオブジェクト値を作成する方法です。すべてのプロパティ名と値を指定する代わりに、関数を用いてオブジェクトを構築します。これにより、オブジェクトを作成するためのある種の標準的な手順を定義できます。このような関数は コンストラクターと呼ばれ、第8章 でそれらの記述方法について説明します。

Date コンストラクターは、さまざまな方法で使用できます。

show(new Date());
show(new Date(1980, 1, 1));
show(new Date(2007, 2, 30, 8, 20, 30));

ご覧のとおり、これらのオブジェクトは日付だけでなく時刻も格納できます。引数を指定しないと、現在の日時を表すオブジェクトが作成されます。引数を指定して、特定の日時を要求できます。引数の順序は、年、月、日、時、分、秒、ミリ秒です。これらの最後の4つはオプションであり、指定しないと0になります。

これらのオブジェクトが使用する月の番号は0から11までであり、混乱を招く可能性があります。特に、日の番号は*1から*始まるためです。


Date オブジェクトの内容は、多数の get... メソッドで検査できます。

var today = new Date();
print("Year: ", today.getFullYear(), ", month: ",
      today.getMonth(), ", day: ", today.getDate());
print("Hour: ", today.getHours(), ", minutes: ",
      today.getMinutes(), ", seconds: ", today.getSeconds());
print("Day of week: ", today.getDay());

getDay を除くこれらすべてには、日付オブジェクトの値を変更するために使用できる set... バリアントもあります。

オブジェクト内部では、日付は1970年1月1日からのミリ秒単位の差で表されます。これはかなり大きな数であると想像できます。

var today = new Date();
show(today.getTime());

日付で非常に役立つことは、日付を比較することです。

var wallFall = new Date(1989, 10, 9);
var gulfWarOne = new Date(1990, 6, 2);
show(wallFall < gulfWarOne);
show(wallFall == wallFall);
// but
show(wallFall == new Date(1989, 10, 9));

<><=>= を使用した日付の比較は、期待どおりの動作をします。日付オブジェクトが == を使用してそれ自体と比較されると、結果は true になり、これも良好です。しかし、== を使用して日付オブジェクトを異なるが等しい日付オブジェクトと比較すると、false が返されます。えっ?

前述のように、== は、2つの異なるオブジェクトを比較する場合、同じプロパティが含まれていても false を返します。これは少し厄介でエラーが発生しやすいため、>=== は多かれ少なかれ同様の方法で動作することが期待されます。2つの日付が等しいかどうかをテストするには、次のようにします。

var wallFall1 = new Date(1989, 10, 9),
    wallFall2 = new Date(1989, 10, 9);
show(wallFall1.getTime() == wallFall2.getTime());

日時だけでなく、Date オブジェクトには タイムゾーンに関する情報も含まれています。アムステルダムで1時になると、時期によってはロンドンで正午、ニューヨークで午前7時になる場合があります。このような時間は、タイムゾーンを考慮した場合にのみ比較できます。DategetTimezoneOffset 関数を使用して、GMT(グリニッジ標準時)との差が何分あるかを確認できます。

var now = new Date();
print(now.getTimezoneOffset());

練習問題 4.6
"died 27/04/2006: Black Leclère"

日付部分は常に段落のまったく同じ場所にあります。なんて便利なんでしょう。段落を引数として受け取り、日付を抽出して日付オブジェクトとして返す関数 extractDate を記述してください。

function extractDate(paragraph) {
  function numberAt(start, length) {
    return Number(paragraph.slice(start, start + length));
  }
  return new Date(numberAt(11, 4), numberAt(8, 2) - 1,
                  numberAt(5, 2));
}

show(extractDate("died 27-04-2006: Black Leclère"));

Number を呼び出さなくても機能しますが、前述のように、文字列を数値であるかのように使用したくありません。内部関数は、Numberslice の部分を3回繰り返すことを回避するために導入されました。

月の番号に - 1 があることに注意してください。ほとんどの人と同様に、エミリーおばさんは1か月からカウントするため、Date コンストラクターに渡す前に値を調整する必要があります。(Date オブジェクトは通常の人間の方法で日をカウントするため、日の番号にはこの問題はありません。)

第10章 では、固定構造の文字列から部分を抽出するためのより実用的で堅牢な方法について説明します。


猫の保存方法は、今後異なります。セットに値 true を入れる代わりに、猫に関する情報を含むオブジェクトを保存します。猫が死んだとき、セットから削除するのではなく、オブジェクトにプロパティ death を追加して、生き物が死んだ日付を保存します。

これは、addToSet 関数と removeFromSet 関数が役に立たなくなったことを意味します。似たようなものが必要ですが、生年月日と、後で母親の名前も保存する必要があります。

function catRecord(name, birthdate, mother) {
  return {name: name, birth: birthdate, mother: mother};
}

function addCats(set, names, birthdate, mother) {
  for (var i = 0; i < names.length; i++)
    set[names[i]] = catRecord(names[i], birthdate, mother);
}
function deadCats(set, names, deathdate) {
  for (var i = 0; i < names.length; i++)
    set[names[i]].death = deathdate;
}

catRecord は、これらのストレージオブジェクトを作成するための個別の関数です。Spot のオブジェクトを作成するなど、他の状況で役立つ場合があります。「レコード」とは、限られた数の値をグループ化するために使用されるこのようなオブジェクトによく使用される用語です。


それでは、段落から母猫の名前を抽出してみましょう。

"born 15/11/2003 (mother Spot): White Fang"

これを行う1つの方法は...

function extractMother(paragraph) {
  var start = paragraph.indexOf("(mother ") + "(mother ".length;
  var end = paragraph.indexOf(")");
  return paragraph.slice(start, end);
}

show(extractMother("born 15/11/2003 (mother Spot): White Fang"));

indexOf はパターンの終わりの位置ではなく開始位置を返すため、開始位置を "(mother " の長さに合わせて調整する必要があることに注意してください。


練習問題 4.7

extractMother が行うことは、より一般的な方法で表現できます。3つの引数(すべて文字列)を取る関数 between を記述してください。最初の引数のうち、2番目と3番目の引数で指定されたパターンの間に出現する部分を返します。

したがって、between("born 15/11/2003 (mother Spot): White Fang", "(mother ", ")")"Spot" を返します。

between("bu ] boo [ bah ] gzz", "[ ", " ]")"bah" を返します。

2番目のテストを機能させるには、indexOf に2番目のオプションパラメーターを指定して、検索を開始する場所を指定できることを知っておくと便利です。

function between(string, start, end) {
  var startAt = string.indexOf(start) + start.length;
  var endAt = string.indexOf(end, startAt);
  return string.slice(startAt, endAt);
}
show(between("bu ] boo [ bah ] gzz", "[ ", " ]"));

between を使用すると、extractMother をより簡単な方法で表現できます。

function extractMother(paragraph) {
  return between(paragraph, "(mother ", ")");
}

新しく改良されたcat-algorithm(猫アルゴリズム)は次のようになります。

function findCats() {
  var mailArchive = retrieveMails();
  var cats = {"Spot": catRecord("Spot", new Date(1997, 2, 5),
              "unknown")};

  function handleParagraph(paragraph) {
    if (startsWith(paragraph, "born"))
      addCats(cats, catNames(paragraph), extractDate(paragraph),
              extractMother(paragraph));
    else if (startsWith(paragraph, "died"))
      deadCats(cats, catNames(paragraph), extractDate(paragraph));
  }

  for (var mail = 0; mail < mailArchive.length; mail++) {
    var paragraphs = mailArchive[mail].split("\n");
    for (var i = 0; i < paragraphs.length; i++)
      handleParagraph(paragraphs[i]);
  }
  return cats;
}

var catData = findCats();

この追加データにより、エミリーが話している猫のおばについてついに手がかりを得ることができます。このような関数は役に立つでしょう。

function formatDate(date) {
  return date.getDate() + "/" + (date.getMonth() + 1) +
         "/" + date.getFullYear();
}

function catInfo(data, name) {
  if (!(name in data))
    return "No cat by the name of " + name + " is known.";

  var cat = data[name];
  var message = name + ", born " + formatDate(cat.birth) +
                " from mother " + cat.mother;
  if ("death" in cat)
    message += ", died " + formatDate(cat.death);
  return message + ".";
}

print(catInfo(catData, "Fat Igor"));

catInfo の最初の return 文は、エスケープハッチとして使用されます。指定された猫に関するデータがない場合、関数の残りの部分は意味がないため、すぐに値を返します。これにより、残りのコードが実行されなくなります。

過去には、複数の return 文を含む関数を罪深いと考えるプログラマーグループがありました。これは、どのコードが実行され、どのコードが実行されないかを確認するのが難しくなるという考えに基づいていました。第5章で説明する他の手法により、この考えの背後にある理由は多かれ少なかれ時代遅れになっていますが、「ショートカット」return文の使用を批判する人にまだ出会うかもしれません。


練習問題 4.8

catInfo で使用されている formatDate 関数は、月と日の部分が1桁の場合、その前にゼロを追加しません。これを行う新しいバージョンを作成してください。

function formatDate(date) {
  function pad(number) {
    if (number < 10)
      return "0" + number;
    else
      return number;
  }
  return pad(date.getDate()) + "/" + pad(date.getMonth() + 1) +
             "/" + date.getFullYear();
}
print(formatDate(new Date(2000, 0, 1)));

練習問題 4.9

猫を含むオブジェクトを引数として受け取り、生きている中で最も古い猫の名前を返す oldestCat 関数を作成してください。

function oldestCat(data) {
  var oldest = null;

  for (var name in data) {
    var cat = data[name];
    if (!("death" in cat) &&
        (oldest == null || oldest.birth > cat.birth))
      oldest = cat;
  }

  if (oldest == null)
    return null;
  else
    return oldest.name;
}

print(oldestCat(catData));

if 文の条件は少し難解に見えるかもしれません。これは、「現在の猫が死んでおらず、oldestnull または現在の猫よりも後に生まれた猫の場合にのみ、現在の猫を変数 oldest に格納する」と読むことができます。

data に生きている猫がいない場合、この関数は null を返すことに注意してください。あなたのソリューションはその場合どうなりますか?


配列に慣れてきたので、関連する事柄をお見せしましょう。関数が呼び出されるたびに、arguments という名前の特別な変数が、関数本体が実行される環境に追加されます。この変数は、配列に似たオブジェクトを参照します。最初の引数にはプロパティ 0、2番目の引数には 1 があり、関数が与えられたすべての引数について同様です。また、length プロパティも持ちます。

ただし、このオブジェクトは実際の配列ではありません。push などのメソッドはなく、何かを追加しても length プロパティは自動的に更新されません。なぜそうなのか、私は実際にはわかりませんでしたが、これは注意が必要なことです。

function argumentCounter() {
  print("You gave me ", arguments.length, " arguments.");
}
argumentCounter("Death", "Famine", "Pestilence");

print のように、一部の関数は任意の数の引数を取ることができます。これらは通常、arguments オブジェクト内の値をループして、それらに対して何かを行います。他の関数はオプションの引数を取ることができます。これらの引数は、呼び出し元によって指定されない場合、適切なデフォルト値を取得します。

function add(number, howmuch) {
  if (arguments.length < 2)
    howmuch = 1;
  return number + howmuch;
}

show(add(6));
show(add(6, 4));

練習問題 4.10

練習問題 4.2range 関数を拡張して、2番目のオプションの引数を取るようにします。引数が1つだけ指定された場合、以前のように動作し、0から指定された数値までの範囲を生成します。2つの引数が指定された場合、最初の引数は範囲の開始を示し、2番目の引数は終了を示します。

function range(start, end) {
  if (arguments.length < 2) {
    end = start;
    start = 0;
  }
  var result = [];
  for (var i = start; i <= end; i++)
    result.push(i);
  return result;
}

show(range(4));
show(range(2, 4));

オプションの引数は、上記の add の例のように正確には機能しません。指定されていない場合、最初の引数は end の役割を果たし、start0 になります。


練習問題 4.11

はじめにでこのコード行を覚えているかもしれません

print(sum(range(1, 10)));

これで range ができました。この行を機能させるために必要なのは、sum 関数だけです。この関数は数値の配列を取り、それらの合計を返します。簡単にできるので、作成してください。

function sum(numbers) {
  var total = 0;
  for (var i = 0; i < numbers.length; i++)
    total += numbers[i];
  return total;
}

print(sum(range(1, 10)));

第2章では、Math.max 関数と Math.min 関数について説明しました。これまでの知識があれば、これらは実際にはMath という名前で格納されているオブジェクトのプロパティ maxmin であることに気付くでしょう。これは、オブジェクトが果たすことができるもう1つの役割です。つまり、多数の関連値を保持するウェアハウスです。

Math の中にはかなりの数の値があります。それらがすべてグローバル環境に直接配置されていたら、いわゆる「汚染」が発生します。名前が付けられているほど、誤って一部の変数の値を上書きしてしまう可能性が高くなります。たとえば、何かを max という名前にしたいと思うのは、それほど突飛なことではありません。

ほとんどの言語は、すでに使用されている名前で変数を定義しようとすると、停止するか、少なくとも警告を表示します。JavaScriptはそうではありません。

いずれにせよ、Math 内には数学関数と定数の全装備を見つけることができます。すべての三角関数、つまり cossintanacosasinatan があります。πとeは、すべて大文字(PIE)で記述されます。これは、かつては何かが定数であることを示すための流行の書き方でした。pow は、私たちが書いてきた power 関数の優れた代替手段であり、負の指数と小数の指数も受け入れます。sqrt は平方根を取ります。maxmin は、2つの値の最大値または最小値を与えることができます。roundfloorceil は、それぞれ最も近い整数、それ以下の整数、それ以上の整数に数値を丸めます。

Math には他にも多くの値がありますが、このテキストはリファレンスではなく、入門書です。リファレンスとは、言語に何かが存在するのではないかと疑ったときに、それが何と呼ぶのか、あるいはそれがどのように機能するのかを正確に調べるために参照するものです。残念ながら、JavaScriptの包括的で完全なリファレンスはありません。これは主に、現在の形式が、異なるブラウザが異なる拡張機能を異なる時間に次々と追加するという混沌としたプロセスの結果であるためです。はじめにで述べたECMA標準ドキュメントは、基本言語の確実なドキュメントを提供していますが、多かれ少なかれ読めません。ほとんどの場合、最善の策はMozilla Developer Networkです。


Math オブジェクトで何が利用できるかを見つける方法をすでに考えているかもしれません

for (var name in Math)
  print(name);

しかし、残念ながら何も表示されません。同様に、これを行うと

for (var name in ["Huey", "Dewey", "Loui"])
  print(name);

012 のみが表示され、lengthpushjoin は表示されません。これらは間違いなくそこにあります。どうやら、オブジェクトの一部のプロパティは隠されているようです。これには正当な理由があります。すべてのオブジェクトには、たとえば、オブジェクトを何らかの関連する文字列に変換する toString などのメソッドがいくつかあります。たとえば、オブジェクトに格納した猫を探しているときに、これらのメソッドを表示したくはありません。

Math のプロパティが隠されている理由は私にはわかりません。誰かがそれを神秘的なオブジェクトにしたかったのでしょう。

プログラムがオブジェクトに追加するすべてのプロパティは表示されます。それらを隠す方法はありません。これは残念なことです。なぜなら、第8章でわかるように、for/in ループに表示されることなく、オブジェクトにメソッドを追加できると便利だからです。


一部のプロパティは読み取り専用です。値を取得できますが、変更することはできません。たとえば、文字列値のプロパティはすべて読み取り専用です。

他のプロパティは「アクティブ」になる可能性があります。それらを変更すると、 _何か_ が発生します。たとえば、配列の長さを短くすると、余分な要素が破棄されます

var array = ["Heaven", "Earth", "Man"];
array.length = 2;
show(array);
  1. このアプローチにはいくつかの微妙な問題があります。これらについては、第8章で説明し、解決します。この章では、これで十分に機能します。