【Eloquent JavaScript和訳】 Chapter 4:データ構造: オブジェクトと配列

EloquentJavaScriptJa

公開:

0 なるほど
13,268 VIEW
0 コメント

本章では幾つかのシンプルな問題の解決方法を紹介します。途中、新しいタイプの値、配列とオブジェクト、について説明し、それらに関するテクニックを紹介します。

次のような状況を考えてください: あなたには変わり者の叔母 Emily がいて、彼女には 50 匹以上 (誰も数えたことはありません) の猫を飼っているという噂があります。その叔母が定期的に近況を知らせてきます。通常次のようなものです:

可愛い甥へ、

あなたがスカイダイビングをやっていると聞きましたが、本当ですか? 気をつけて下さいヨ。私の夫がどうなったか憶えているでしょう? 2階から落ちただけですヨ!

こちらはとても刺激的です。Mr. Drake といって素敵な紳士が隣りに越してきました。私は今週ずっと彼の気を引こうとして色々試していました。でも彼は猫が苦手のようです。猫アレルギーでもあるのかしら? 次に合ったら Fat Igor を彼の肩に乗せてみようと思っています。どうなるか興味津々です。

この前話した詐欺事件は思ったよりも好転しています。既に 5 件の '支払い' を済ませて、残るはあと 1 件です。でも少し後悔し始めています。お前の言うとおり、やはりあれは悪いことなのでしょう。

(... 中略 ...)

愛をこめて、叔母 Emily

死亡 27/04/2006: Black Leclère

誕生 05/04/2006: 母 Lady Penelope の子供、Red Lion、Doctor Hobbles 3世、Little Iroquois

あなたはこの老婦人に調子を合わせ、猫の系譜を辿ってみたいと思います。そこでメールの最後に "追伸: Doctor Hobbles 2 世は子供の誕生を喜んだことと思います!" とか、なるべく死んだ猫には触れずに " Lady Penelope はお元気ですか? 彼女はもう 50 歳になるのでしょう?" などと付け加えるのも良いでしょう。あなたは叔母からの古い Eメールをたくさん所有しています。そしてそれらの最後には必ず猫の死と生の情報が同じフォーマットで記されています。

あなたはこれら全てのメールを一通づつ調べたいとは思いません。幸い、我々は例題を必要としていました。そこで我々に代わってこの仕事を行うプログラムを作ってみましょう。差し当たり、最後の Eメールの時点で生存している猫のリストを作るプログラムを書きます。

文通が始まった頃、叔母 Emily は 1 匹の猫 Spot しか飼っていませんでした。 (彼女はその頃はまだ変わり者ではなかったのです。)

プログラムを書き始める前に、そのプログラムにさせることについて何か手掛かりになるものが必要です。次のようなプランで進めましょう:

1. まず "Spot" を含む名前を集めます。
2. 全ての Eメールを年代順に並べます。
3. "誕生" または "死亡" で始まる段落を探します。
4. "誕生" で始まる段落にある名前を集めた名前に加えます。
5. "死亡" で始まる段落にある名前を集めた名前から削除します。

次のようにして段落から名前を抜き取ります:

1. 段落中のコロンを探します。
2. コロンの後の部分を抜き出します。
3. コンマを探し、抜き出した部分を名前に分けます。

叔母 Emily がいつも同じフォーマットを使ったか、名前の綴りに間違いはなかったか、という疑問が残るかもしれません。

まず、属性について話しましょう。JavaScript の値の多くはそれに関連した別の値を持ちます。この関連付けを属性と呼びます。全ての文字列には length と呼ばれる属性があり、文字列の文字数に言及しています。

属性には 2 通りの方法でアクセスすることができます:

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

2 番目の方法は最初の方法の省略表現で、属性名が有効な変数名の時 ― スペースやシンボルを含まず 2 進数字で始まらない時 ― のみ機能します。

数字、ブール、null、および undefined は属性を持ちません。これらの値から属性を読み込もうとするとエラーになります。ブラウザーがどんなエラー・メッセージを出すか知りたければ (ブラウザーによっては暗号のような場合もあります) 次のコードを試してください。

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

文字列値の属性を変えることはできません。これから length 以外にもたくさん出て来ますが、追加も削除も一切許されません。

これらは type オブジェクトの値とは異なります。type オブジェクトの値の主な役割は他の値を保持することです。言ってみれば、属性という形で触手を持っているようなものです。この場合、修正も追加も削除も自由です。

オブジェクトは次のように書くことができます:

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);

コンソールにオブジェクトの値が表示されると、それをクリックして属性を調べることができます。クリックすると出力ウィンドウが 'inspect' ウィンドウに変わります。右上隅の小文字の 'x' は出力ウィンドウに戻る時に使います。左矢印は前の属性に戻る時に使います。

show(chineseBox);

Ex. 4.1

猫の問題に対する解決策は名前の 'セット' にあります。このセットは値の集まりで、どの値もただ 1 度だけ現れます。これらの名前が文字列とするなら、名前のセットを表すオブジェクトを使えると思いますか?

名前をこのセットに追加する方法、削除する方法、セットの中に名前があるかをチェックする方法を考えてください。

これはセットのコンテンツをオブジェクトの属性として保存することで可能になります。名前を追加する時は、その名前の属性を値にセットします。どんな値でもかまいません。名前を削除する時は、この属性を削除します。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 つの数字 120 と 120 がある時、それらは事実上同一と考えられます。オブジェクトの関しては、同じオブジェクトに対する 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);

object1 と object2 は 同じ 値を持つ変数です。実際のオブジェクトは 1 つだけで、これが object1 を変えると object2 の値も変わる理由です。別のオブジェクトを指す変数 object3 は、最初は object1 と同じ属性を持ちますが、異なる経過を辿ります。

JavaScript の == 演算子はオブジェクトを比較し、値が全く同じ場合に true を返すにすぎません。同じコンテンツを持っていてもオブジェクトが異なれば false を返します。これは有益な場合もありますが、実用的ではありません。

オブジェクトの値は色々な役割を果たします。セットのような役割を果たすのもその一つです。本章ではその役割の幾つかを示し、8 章 でオブジェクトの重要な使い方を説明します。

猫の問題を解決するプランで ― これから述べることを考えると、プランではなく アルゴリズム を呼んだほうが良いでしょう ― アルゴリズムの中で、アーカイブの全ての Eメール調べる、と述べました。このアーカイブとは一体どんなものでしょう? 何処からやって来たのでしょう?

ここでは 2 番目の質問は無視してください。14 章 でプログラムに重要なデータを取り込む方法を説明します。とりあえず、マジックか何かで Eメールが存在することにしましょう。マジックもコンピューターの中では簡単に実現することもあります。

しかしアーカイブがどのように保存されるかは興味深い疑問です。そこには複数の Eメール が保存されています。Eメールが文字列であることは明らかに可能です。アーカイブ全体を大きな文字列に置くことは可能ですが、実用的ではありません。我々が欲しいのはバラバラの文字列の集合です。

集合はオブジェクトが対象とするものです。次のようにオブジェクトを作ることができます:

var mailArchive = {"the first Eメール": "Dear nephew, ...",
"the second Eメール": "..."
/* 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メール #", current, ": ", mailArchive[current]);

幸運にも、このような目的に使える特別なオブジェクトがあります。配列と呼ばれるもので、配列中に複数の値を含む length 属性や、この種の集合に対して有益な処理をいくつも提供します。

配列を各括弧 ([ と ]) を使って新たに作成することができます:

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

for (var current = 0; current < mailArchive.length; current++)
print("Processing Eメール #", current, ": ", mailArchive[current]);

この例で、エレメントの数は明示的に指定されていません。最初のエレメントは 0、次のエレメントは 1、という具合にカウントされます。

何故 0 から始めるのでしょう?人はカウントは 1 から始めるものと思っています。直感的に理にかなっていないように思えますが、集合内のエレメントに 0 から番号を振ることは実用的な場合が多いのです。次第に分かってくると思いますので、このまま続けましょう。

エレメント 0 から始めることは、集合内に X 個のエレメントがあり、最後のエレメントは X - 1 の場所にあることも意味します。これが例題の for ループが current <mailArchive.length をチェックする理由です。mailArchive.length の場所にはエレメントが無いため、current がその値になるとループを止めます。

Ex. 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));

今までのようにループ変数に counter または current という名前を付ける代わりに、ここでは簡単に i を使います。ループ変数に i、j、または k のような 1 文字を使うことはプログラマーの間ではよく行われていることです。元を辿れば怠惰がその理由ですが、タイプするには 7 文字よりは 1 文字のほうが簡単だし、counter や current が引数の意味をはっきりさせる名前だとも思えません。

プログラムで頻繁に 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 を使って (マジックのように) この配列を手に入れます。Eメールを一つ一つ調べるのもロケット科学ほど難しくなくなりました:

var mailArchive = retrieveMails();

for (var i = 0; i < mailArchive.length; i++) {
var email = mailArchive[i];
print("Processing Eメール #", i);
// Do more things...
}

生存している猫のセットを表す方法も決まりました。次の問題は、Eメール中の "誕生 (born)" または "死亡 (died)" で終わる段落を見つけることです。

まず、段落とは一体何かという疑問が浮かんできます。この場合、文字列の値は役に立ちません: JavaScript でのテキストの概念は '文字の並び' 以上に深く入り込んでいきません。そこで段落を定義する必要があります。

前に改行文字のようなものがあることを知りました。段落を分割するのに使われます。そこで、段落を、改行文字でコンテンツが始まり次の改行文字でコンテンツが終わる部分を、Eメールの一部と考えます。

文字列を段落に分割するアルゴリズムを自分で書く必要もありません。文字列には既に split をいうメソッドがあり、配列の join メソッドと (ほとんど) 逆の機能を持っています。自身のアルゴリズムで何処で分けるかを判断し、与えられた文字列を配列に分割します。

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

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

Ex. 4.3

split と join は互いに全く逆のものとは言えません。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 (誕生)" で始まるかをチェックします:

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");

しかしこれでは手際が良くありません ― 1 つの単語をチェックするのに 10 文字も調べなければなりません。ここで知っておくべきは、1 行が異常に長いときは複数行に分けることが可能だということです。行頭を元の行の最初のエレメントで並べると読み易くなります。

文字列には slice と呼ばれるメソッドもあります。最初の引数で与える場所の文字で始まり、次の引数で与える場所の前の場所の文字で終わる文字列の一部がコピーされます。これによってチェックは次のように短く書くことができます。

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

Ex. 4.4

共に文字列である 2 つの引数を取る関数 startsWith を書いてください。最初の引数が次の引数の文字で始まる場合は true を返し、そうでない場合は false を返します。

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 が返されます。そしてこれは間違っていません。

異常な (しかし正しい) プログラムへの入力を考慮することは無駄ではありません。窮地の問題 (corner cases) と呼ばれるもので、'正常な' 入力で完璧に処理をこなすプログラムでも窮地の問題に出合って大失敗するというのはよくあることです。

猫の問題で未解決なのは段落から名前を抽出することだけになりました。アルゴリズムは次のとおりでした:

1. 段落中のコロンを探します。
2. コロンの後の部分を抜き出します。
3. コンマを探し、抜き出した部分を名前に分けます。

これは "died" (死亡) で始まる段落にも "born" (誕生) で始まる段落にも適用する必要があります。これを関数にし、これら異なる段落を扱う 2 つのコードが共にこの関数を使えるようにすると良いでしょう。

Ex. 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 への引数にはコンマとスペースが含まれます。

この関数はチェックを行いません。ここでは入力が常に正しいこととします。

断片を繋ぎ合わせます。次のようにします:

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);

オブジェクト内の属性の名前を見直すループです。これにより、セット内の全ての名前を一覧表にすることができます。

コードには足を踏み込めないジャングルのような部分が幾つかあります。猫の問題の回答例でもこれが悩みの種です。このジャングルに光を当てるには、意図的に空の行を追加する方法もあります。これで見易くなりますが、真の解決にはなりません。

コードをバラバラにする必要があります。我々は既に助っ人となる関数 startsWith と catNames を書きました。これらは共に問題の小さく分かりやすい部分の対処策になります。これで行きましょう。

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]];
}

これらの関数はセット中の名前の追加と削除を処理します。これで内部ループのほとんどが解決します:

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]));
}
}

我ながらかなり改良されました。

addToSet と removeFromSet は、変数 livingCats を直接使うこともできるのに、何故セットを引数とするのでしょう? 理由は、こうすることによって addToSet と removeFromSet が 我々の現在の問題に完全には束縛されなくて済むからです。もし addToSet が直接 livingCats を変更したら、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.");

アルゴリズム全体が 1 つの関数によってカプセル化されました。これで実行後の乱雑さもなくなります: livingCats は最上位変数ではなく関数内のローカル変数となり、関数が実行中の間だけ存在します。このセットを必要とするコードは findLivingCats を呼び出すことができ、返される値を使います。

handleParagraph を別の関数にすることによって明確さも増しました。しかしこれは猫のアルゴリズムに直結しているため、他の状況では意味を持ちません。さらに、livingCats 変数にアクセスする必要があるため、完全に 関数内関数 の候補です。findLivingCats 内にいる時だけ意味を持ち、親関数の変数にアクセスします。

この解決策は前のものよりも 大きく なってしまいました。しかし前の解決策よりは整然としているし、読み易さも向上したと思います。

プログラムはまだ Eメール内の他の多くの情報を無視しています。誕生日、死亡日、母親の名前等です。

日付から始めます: 日付を保存する良い方法として、オブジェクトに 3 つの属性 year、month、および day を作り、それらに数字を保存することができます。

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));

<、>、<=、>= で望みどおりの比較を行うことができます。1 つの日付オブジェクトを == で自身と比較すると結果は true です。当然です。しかし、== で同じ日付を持つ異なる日付オブジェクトと比較したら? false が返されますよネ?

前に述べたように、== で 2 つの異なるオブジェクトを比較すると、それらの属性が同じでも false が返されます。人は >= も == も大して変わらないと思っているので、これでは少々不器用で間違いの元になります。2 つの日付が等しいかをテストするには次のように行います:

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

日付と時間に加え、Date オブジェクトには timezone の情報も含まれます。Amsterdam で 1時 のとき、時期によって異なりますが、London では正午、New York では朝の 7時 です。このような時間はタイムゾーンを考慮に入れてはじめて比較可能になります。Date の getTimezoneOffset 関数で GMT (Greenwich Mean Time: グリニッジ標準時) からの差を求めることができます。

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

Ex. 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 を呼び出さなくてもかまわないでしょうが、前にも述べたように、私は文字列を数字のように使うことを好みません。内部関数は Number と slice の部分を 3 回繰り返すことを避けるために導入しました。

月に - 1 を使っていることに注意してください。多くの人がそうであるように、叔母 Emily も月を 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 のためのオブジェクトを作成するような時に役に立ちます。'Record' はこのようなオブジェクトに良く使われる用語で、限られた数の値をグループ化するのに使われます。

では段落から母猫の名前を抽出しましょう。

"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"));

開始場所を "(mother " の長さのために調節しなければならないことに注意してください。indexOf が終了場所ではなく開始場所を返すからです。

Ex. 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 ", ")");
}

改良した新しい猫アルゴリズムは次のようになります:

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();

追加情報によって叔母 Emily の猫についての手掛かりをついに掴むことができます。このような関数は便利です:

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 のような 'ショートカット' ステートメントの使用を批判する人がいるかもしれません。

Ex. 4.8

catInfo で使われる formatDate 関数は、1桁の数字の場合は月部分と日部分の前にゼロを追加しません。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)));

Ex. 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 ステートメント中の条件は少々威圧的に思えますが、'猫が死んでおらず、oldest が null または現在の猫以後に誕生した場合、現在の猫を変数 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));

Ex. 4.10

exercise 4.2 の range 関数を任意に 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 として働き、start は 0 になります。

Ex. 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)));

前の章で関数 Math.max と Math.min が出てきました。これらが実際には Math という名前で保存したオブジェクトの属性 max と min であることにお気付きでしょう。これがオブジェクトのもう一つの役割、複数の関連値を保存する倉庫、です。

Math 内には実にたくさんの値があり、それらを全て地球環境に直接置いたら、それこそ汚染そのものです。名前の数が多ければ、その分だけ変数の値が書き換えられてしまう危険性も増えます。例えば max のような名前はどこでも使われる可能性があります。

既に使っている名前を変数に定義しようとすると、多くの言語はそれを阻止、または少なくとも警告します。しかし JavaScript は違います。

Math には数学関数や数学定数が一式揃っています。全ての三角関数 ― cos、sin、tan、acos、asin、atan ― があります。大文字 (PI や E) で書かれる π や e は定数を示す方法として一時流行しました。これまで我々も power 関数として使ってきた pow は、負数も分数指数も受け付けます。sqrt は平方根を求めるのに使います。max と min は 2 つの値の最大値または最小値を出します。round、floor、および ceil はそれぞれ数値を丸めて近似の整数、それよりも小さい整数、それよりも大きい整数を求めます。

Math には他にも多くの値がありますが、本ドキュメントは参考資料ではなく入門書です。参考資料とは何かを確かめる時に探すもので、それがその言語でどう呼ばれているかとかその働きを調べるものではありません。残念ながら、JavaScript には総合的かつ完全な参考資料は存在しません。これは現在の形が、その時々で色々な拡張を重ねて無秩序状態の過程で異なるブラウザーが生まれた結果ともいえます。「はじめに」で触れた ECMA 標準ドキュメントが基本言語のドキュメントとして信頼できるできるものですが、かなり読みづらいのが難点です。Math オブジェクトやその他の基本機能に関しては ここ をご覧ください。Sun のウェブサイト にまだ残っている Netscape の古いドキュメントも役に立ちますが、時代遅れで完全に正しいとは言えません。

Math オブジェクトで使えるものを探す方法を既に思いつかれたかもしれません:

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

悲しいかな、何も出てきません。次のようにしても同じです:

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

0 や 1 や 2 は見えるのに、確実にそこにあるはずの length も push も join も見えません。明らかにオブジェクトの属性の中には隠されているものがあります。これには訳があります。全てのオブジェクトには、例えばオブジェクトを適当な文字列に変換する toString のように実行中はその属性を見たくないメソッドが 2~3 あります。本章で言えば、オブジェクトに保存した猫を検索中にそれらの属性を見たいとは思いません。

Math の属性が何故隠されているのか、私には分かりません。誰かオブジェクトを謎めいたものにしたかったのかもしれません。

プログラムがオブジェクトに追加する属性は全て可視的で、それを隠すことはできません。8 章 で見るように、for/in ループで属性が見えないようにしてメソッドをオブジェクトに追加できたら良いのですが、残念です。

属性の中には読み取り専用のものがあり、値は入手できますが変更することはできません。例えば、文字列値の属性は全て読み取り専用です。

他の属性は '表示 (watch)' することができます。属性を変えれば 結果 も変わります。例えば、配列の長さを短く指定すれば、余分なエレメントは切り捨てられます:

var array = ["Heaven", "Earth", "Man"];
array.length = 2;
show(array);

ブラウザーによってはオブジェクトに watch メソッドがあり、これを使って自身の属性に watcher を追加することができます。Internet Explorer はこれをサポートしていないため、書いているプログラムが全ての '有名な' ブラウザーで実行されなければならない場合はあまり役に立ちません。

var winston = {mind: "compliant"};
function watcher(propertyName, from, to) {
if (to == "compliant")
print("Doubleplusgood.");
else
print("Transmitting information to Thought Police...");
}
winston.watch("mind", watcher);

winston.mind = "rebellious";

翻訳元

Chapter 3: Data structures: Objects and Arrays
 Chapter 3: Data structures: Objects and Arrays
eloquentjavascript.net  

最終更新:

コメント(0

あなたもコメントしてみませんか?

すでにアカウントをお持ちの方はログイン