第4章データ構造:オブジェクトと配列
私は二度、「もし機械に間違った数字を入力したら、正しい答えが出てきますか?」と尋ねられました。…私は、そのような質問を引き起こすような種類の考えの混乱を正しく理解することができません。
数値、ブール値、文字列は、データ構造を構築するためのレンガです。しかし、1つのレンガだけでは大した家は建てられません。オブジェクトを使用すると、他のオブジェクトを含む値をグループ化し、より複雑な構造を構築できます。
これまで作成してきたプログラムは、単純なデータ型しか操作していなかったため、深刻な制約を受けていました。この章では、データ構造の基本的な理解をあなたのツールキットに追加します。この章の終わりまでに、いくつかの有用なプログラムを書き始めるのに十分な知識が身につきます。
この章では、より現実的なプログラミング例を段階的に解説し、必要に応じて概念を導入します。例コードは、テキストの前の章で紹介された関数や変数を頻繁に利用します。
人狼リス
時折、たいてい午後8時から10時の間に、ジャックは自分がふわふわした尻尾を持つ小さな毛皮の齧歯類に変身していることに気づきます。
一方で、ジャックは自分が古典的な狼男ではないことにかなり満足しています。リスに変身する方が、狼に変身するよりも問題が少ない傾向があります。隣人を誤って食べてしまう心配(それは厄介でしょう)をする代わりに、隣人の猫に食べられてしまうことを心配しています。裸で意識を失った状態でオークの樹冠にある不安定な細い枝の上で目を覚ました2回の後、彼は夜に部屋のドアと窓に鍵をかけ、忙しく過ごすために床にクルミをいくつか置くようになりました。

これにより、猫とオークの問題は解決しました。しかし、ジャックはまだその状態に苦しんでいます。変身の不規則な発生は、何かによって引き起こされている可能性があるのではないかと彼を疑わせます。しばらくの間、彼は木に触れた日だけそれが起こると信じていました。そこで彼は完全に木に触るのをやめ、近づくことさえ避けました。しかし、問題は続きました。
より科学的なアプローチに切り替えて、ジャックは自分がその日に行ったことすべてと、自分が変身したかどうかを毎日記録することにしました。このデータを使用して、変身をトリガーする条件を絞り込むことを期待しています。
彼が最初にやったことは、この情報を保存するためのデータ構造を設計することでした。
データセット
デジタルデータの一塊を操作するには、まずそれをマシンのメモリに表現する方法を見つける必要があります。簡単な例として、2、3、5、7、11という数値の集合を表したいとしましょう。
文字列を使って工夫することもできます—結局のところ、文字列は任意の長さにすることができるので、多くのデータを格納できます—そして"2 3 5 7 11"
を表現として使用できます。しかし、これは扱いにくいです。数字を取り出して数値に戻す必要があります。
幸いなことに、JavaScriptは値のシーケンスを格納するためのデータ型を提供しています。これは配列と呼ばれ、角括弧で囲まれた値のリストとして記述されます。値はカンマで区切られます。
var listOfNumbers = [2, 3, 5, 7, 11]; console.log(listOfNumbers[2]); // → 5 console.log(listOfNumbers[2 - 1]); // → 3
配列内の要素を取得するための表記法も角括弧を使用します。式に続く角括弧のペアで、その中に別の式が含まれている場合、左側の式の要素で、括弧内の式で指定されたインデックスに対応する要素が検索されます。
配列の最初のインデックスは1ではなく0です。そのため、最初の要素はlistOfNumbers[0]
で読み取ることができます。プログラミングのバックグラウンドがない場合、この規則に慣れるのに時間がかかるかもしれません。しかし、0から始まるカウントはテクノロジーにおいて長い伝統があり、この規則が常に(JavaScriptではそうであるように)一貫して守られている限り、うまく機能します。
プロパティ
これまでの例では、myString.length
(文字列の長さを取得するため)やMath.max
(最大値関数)のような、疑わしいと思われる式をいくつか見てきました。これらは、ある値のプロパティにアクセスする式です。最初のケースでは、myString
の値のlength
プロパティにアクセスします。2番目のケースでは、Math
オブジェクト(数学関連の値と関数の集合)の名前付きmax
プロパティにアクセスします。
ほとんどすべてのJavaScript値にはプロパティがあります。例外はnull
とundefined
です。これらの非値のプロパティにアクセスしようとすると、エラーが発生します。
null.length; // → TypeError: Cannot read property 'length' of null
JavaScriptでプロパティにアクセスする最も一般的な2つの方法は、ドットと角括弧を使用することです。value.x
とvalue[x]
の両方ともvalue
のプロパティにアクセスしますが、必ずしも同じプロパティではありません。違いはx
の解釈方法にあります。ドットを使用する場合、ドットの後の部分は有効な変数名でなければならず、プロパティの名前を直接指定します。角括弧を使用する場合、括弧内の式が評価されてプロパティ名が取得されます。value.x
は「x」という名前のvalue
のプロパティを取得するのに対し、value[x]
は式x
を評価して、その結果をプロパティ名として使用します。
したがって、興味のあるプロパティの名前が「length」であることがわかっている場合は、value.length
と言います。変数i
に保持されている値によって名前が付けられたプロパティを抽出したい場合は、value[i]
と言います。そして、プロパティ名は任意の文字列であるため、「2」や「John Doe」という名前のプロパティにアクセスしたい場合は、角括弧を使用する必要があります:value[2]
またはvalue["John Doe"]
。これは、事前にプロパティの正確な名前がわかっている場合でも当てはまります。「2」も「John Doe」も有効な変数名ではないため、ドット表記ではアクセスできないためです。
配列の要素はプロパティに格納されます。これらのプロパティの名前は数値であり、多くの場合、変数から名前を取得する必要があるため、それらにアクセスするには角括弧構文を使用する必要があります。配列のlength
プロパティは、その配列に含まれる要素数を示します。このプロパティ名は有効な変数名であり、その名前を事前に知っているので、配列の長さを探すには、通常、array["length"]
よりも書きやすいarray.length
と書きます。
メソッド
文字列オブジェクトと配列オブジェクトの両方には、length
プロパティに加えて、関数値を参照する多数のプロパティが含まれています。
var doh = "Doh"; console.log(typeof doh.toUpperCase); // → function console.log(doh.toUpperCase()); // → DOH
すべての文字列にはtoUpperCase
プロパティがあります。呼び出されると、すべての文字が大文字に変換された文字列のコピーが返されます。toLowerCase
もあります。それが何をするかは推測できるでしょう。
興味深いことに、toUpperCase
の呼び出しは引数を渡していませんが、関数は何らかの方法で、プロパティを呼び出した値である文字列"Doh"
にアクセスできます。これがどのように機能するかは、第6章で説明されています。
関数を格納するプロパティは、一般的に、それらが属する値のメソッドと呼ばれます。「toUpperCase
は文字列のメソッドである」というように。
この例は、配列オブジェクトが持ついくつかのメソッドを示しています。
var mack = []; mack.push("Mack"); mack.push("the", "Knife"); console.log(mack); // → ["Mack", "the", "Knife"] console.log(mack.join(" ")); // → Mack the Knife console.log(mack.pop()); // → Knife console.log(mack); // → ["Mack", "the"]
push
メソッドを使用して、配列の最後に値を追加できます。pop
メソッドは逆を行います。配列の最後の値を削除して返します。文字列の配列は、join
メソッドを使用して単一の文字列にフラット化できます。join
に与えられた引数は、配列の要素間に貼り付けられるテキストを決定します。
オブジェクト
人狼リスに戻りましょう。一連の日々のログエントリは配列として表すことができます。しかし、エントリは単なる数値や文字列で構成されていません—各エントリはアクティビティのリストと、ジャックがリスに変身したかどうかを示すブール値を格納する必要があります。理想的には、これらの値を単一の値にグループ化し、これらのグループ化された値をログエントリの配列に入れることができれば理想的です。
オブジェクト型の値は、プロパティの任意の集合であり、これらのプロパティは自由に追加または削除できます。オブジェクトを作成する1つの方法は、波括弧表記を使用することです。
var day1 = { squirrel: false, events: ["work", "touched tree", "pizza", "running", "television"] }; console.log(day1.squirrel); // → false console.log(day1.wolf); // → undefined day1.wolf = false; console.log(day1.wolf); // → false
波括弧の中に、カンマで区切られたプロパティのリストを記述できます。各プロパティは、名前、コロン、プロパティの値を提供する式として記述されます。スペースと改行は重要ではありません。オブジェクトが複数行にわたる場合、前の例のようにインデントすると、可読性が向上します。名前が有効な変数名または有効な数値ではないプロパティは、引用符で囲む必要があります。
var descriptions = { work: "Went to work", "touched tree": "Touched a tree" };
これは、波括弧がJavaScriptで2つの意味を持つことを意味します。ステートメントの先頭では、ステートメントのブロックを開始します。その他の位置では、オブジェクトを表します。幸いなことに、ステートメントを波括弧オブジェクトで始めることはほとんど役に立たず、一般的なプログラムでは、これらの2つの使用法の間には曖昧さがありません。
存在しないプロパティを読み取ると、値undefined
が生成されます。これは、前の例でwolf
プロパティを初めて読み取ろうとしたときに発生します。
=
演算子を使用して、プロパティ式に値を代入することができます。これにより、プロパティが既に存在する場合はその値が置き換えられ、存在しない場合はオブジェクトに新しいプロパティが作成されます。
変数バインディングの触手モデルに戻って簡単に説明すると、プロパティバインディングも同様です。それらは値をつかむのですが、他の変数やプロパティも同じ値を保持している可能性があります。オブジェクトは、それぞれに名前が刻まれた無数の触手を持つタコのように考えることができます。

delete
演算子は、そのようなタコから触手を切り取ります。これは単項演算子であり、プロパティアクセス式に適用すると、オブジェクトから名前付きプロパティが削除されます。これはよくあることではありませんが、可能です。
var anObject = {left: 1, right: 2}; console.log(anObject.left); // → 1 delete anObject.left; console.log(anObject.left); // → undefined console.log("left" in anObject); // → false console.log("right" in anObject); // → true
2項in
演算子は、文字列とオブジェクトに適用すると、そのオブジェクトがそのプロパティを持っているかどうかを示すブール値を返します。プロパティをundefined
に設定することと実際に削除することの違いは、前者の場合、オブジェクトはまだプロパティを持っている(単にあまり興味深い値を持っていないだけ)のに対し、後者の場合、プロパティは存在しなくなり、in
はfalse
を返すことです。
したがって、配列は、物事のシーケンスを格納するために特化した一種のオブジェクトです。typeof [1, 2]
を評価すると"object"
が生成されます。それらを、すべての腕が整然と並んだ、数字でラベル付けされた長い平たいタコとして考えることができます。

したがって、ジャックの日記をオブジェクトの配列として表現できます。
var journal = [ {events: ["work", "touched tree", "pizza", "running", "television"], squirrel: false}, {events: ["work", "ice cream", "cauliflower", "lasagna", "touched tree", "brushed teeth"], squirrel: false}, {events: ["weekend", "cycling", "break", "peanuts", "beer"], squirrel: true}, /* and so on... */ ];
可変性
すぐに実際のプログラミングに入ります。しかし、まず、理解しておくべき理論がもう一つあります。
オブジェクトの値は変更できることがわかりました。以前の章で説明した数値、文字列、ブール値などの値の型はすべて不変です。つまり、これらの型の既存の値を変更することは不可能です。それらを組み合わせたり、それらから新しい値を導き出したりすることはできますが、特定の文字列値を取得した場合、その値は常に同じままです。その中のテキストは変更できません。"cat"
を含む文字列を参照している場合、他のコードがその文字列の文字を変更して"rat"
にすることはできません。
一方、オブジェクトでは、値の内容はプロパティを変更することで変更できます。
2つの数値、120と120がある場合、それらが同じ物理的なビットを参照しているかどうかに関係なく、それらをまったく同じ数値と考えることができます。しかし、オブジェクトの場合、同じオブジェクトへの2つの参照を持つことと、同じプロパティを含む2つの異なるオブジェクトを持つことの間には違いがあります。次のコードを考えてください。
var object1 = {value: 10}; var object2 = object1; var object3 = {value: 10}; console.log(object1 == object2); // → true console.log(object1 == object3); // → false object1.value = 15; console.log(object2.value); // → 15 console.log(object3.value); // → 10
object1
とobject2
変数は同じオブジェクトを保持しているため、object1
を変更するとobject2
の値も変更されます。object3
変数は、最初はobject1
と同じプロパティを含む異なるオブジェクトを指しますが、別々のライフサイクルを持ちます。
JavaScriptの==
演算子は、オブジェクトを比較する場合、両方のオブジェクトが正確に同じ値である場合にのみtrue
を返します。内容が同じでも、異なるオブジェクトを比較するとfalse
が返されます。オブジェクトの内容を確認する「深い」比較演算はJavaScriptには組み込まれていませんが、自分で記述することは可能です(これは、この章の最後にある演習の1つになります)。
人狼のログ
そこでジャックはJavaScriptインタプリタを起動し、日記を付けるために必要な環境を設定します。
var journal = []; function addEntry(events, didITurnIntoASquirrel) { journal.push({ events: events, squirrel: didITurnIntoASquirrel }); }
そして、毎晩10時—あるいは時には書棚の一番上の棚から降りてきた次の朝—彼はその日を記録します。
addEntry(["work", "touched tree", "pizza", "running", "television"], false); addEntry(["work", "ice cream", "cauliflower", "lasagna", "touched tree", "brushed teeth"], false); addEntry(["weekend", "cycling", "break", "peanuts", "beer"], true);
十分なデータポイントが得られたら、彼はリス化と一日の出来事それぞれの相関関係を計算し、理想的にはそれらの相関関係から何か有益なことを学びたいと考えています。
相関とは、変数間の依存性の尺度です(統計的な意味での「変数」であり、JavaScriptの意味での変数ではありません)。通常、-1から1の範囲の係数として表されます。相関がゼロということは、変数に関連性がないことを意味し、相関が1ということは、2つの変数が完全に関連していることを意味します—一方を知っていれば、もう一方もわかります。-1も、変数が完全に関連しているが、反対であることを意味します—一方が真であれば、もう一方は偽です。
2値(ブール)変数の場合、ファイ係数(ϕ)は相関性の良い尺度を提供し、比較的簡単に計算できます。ϕを計算するには、2つの変数のさまざまな組み合わせが観測された回数を格納した表nが必要です。たとえば、ピザを食べたという出来事を次のような表に入れることができます。
ϕ = |
n11n00 - n10n01
√ n1•n0•n•1n•0
|
表記n01は、最初の変数(リス化)が偽(0)で、2番目の変数(ピザ)が真(1)である測定値の数を示します。この例では、n01は9です。
値n1•は、最初の変数が真であるすべての測定値の合計を表し、この例では5です。同様に、n•0は、2番目の変数が偽である測定値の合計を表します。
したがって、ピザの表の場合、除算記号の上の部分(被除数)は1×76 - 4×9 = 40となり、下部の部分(除数)は5×85×10×80の平方根、つまり√340000となります。これはϕ ≈ 0.069となり、非常に小さいです。ピザを食べることは、変身には影響を与えていないようです。
相関の計算
2×2の表は、JavaScriptでは4要素の配列([76, 9, 4, 1]
)で表すことができます。2つの2要素の配列を含む配列([[76, 9], [4, 1]]
)や、"11"
や"01"
のようなプロパティ名を持つオブジェクトなど、他の表現方法も使用できますが、フラットな配列はシンプルで、表にアクセスする式を短くすることができます。配列へのインデックスは2ビットの2進数として解釈し、最上位ビット(最上位桁)はリス変数、最下位ビット(最下位桁)はイベント変数を表します。たとえば、2進数10
は、ジャックがリスに変身しましたが、イベント(たとえば、「ピザ」)は発生しなかった場合を表します。これは4回発生しました。そして、2進数10
は10進数で2であるため、この数値は配列のインデックス2に格納されます。
function phi(table) { return (table[3] * table[0] - table[2] * table[1]) / Math.sqrt((table[2] + table[3]) * (table[0] + table[1]) * (table[1] + table[3]) * (table[0] + table[2])); } console.log(phi([76, 9, 4, 1])); // → 0.068599434
これは、ϕ式をJavaScriptに直接翻訳したものです。Math.sqrt
は、標準的なJavaScript環境のMath
オブジェクトで提供される平方根関数です。行または列の合計はデータ構造に直接格納されていないため、n1•のようなフィールドを取得するには、表から2つのフィールドを合計する必要があります。
ジャックは3ヶ月間日記をつけました。その結果得られたデータセットは、この章のコーディングサンドボックスでJOURNAL
変数として保存されており、ダウンロード可能なファイルでも入手できます。
この日記から特定のイベントの2×2の表を抽出するには、すべてのエントリをループ処理して、リスの変身との関連でイベントが発生した回数を集計する必要があります。
function hasEvent(event, entry) { return entry.events.indexOf(event) != -1; } function tableFor(event, journal) { var table = [0, 0, 0, 0]; for (var i = 0; i < journal.length; i++) { var entry = journal[i], index = 0; if (hasEvent(event, entry)) index += 1; if (entry.squirrel) index += 2; table[index] += 1; } return table; } console.log(tableFor("pizza", JOURNAL)); // → [76, 9, 4, 1]
hasEvent
関数は、エントリに特定のイベントが含まれているかどうかをテストします。配列には、配列内で特定の値(この場合はイベント名)を見つけようとし、見つかった場合はそのインデックスを返し、見つからなかった場合は-1を返すindexOf
メソッドがあります。したがって、indexOf
の呼び出しが-1を返さない場合は、イベントがエントリで見つかったことがわかります。
tableFor
のループ本体は、エントリにその関心のある特定のイベントが含まれているかどうか、そしてイベントがリスの発生とともに発生するかどうかを確認することで、各日記エントリが表のどのボックスに該当するかを調べます。次に、ループはその表のボックスに対応する配列の数に1を加えます。
これで、個々の相関関係を計算するために必要なツールが揃いました。残りの手順は、記録されたすべての種類のイベントの相関関係を見つけて、何か目立つものがあるかどうかを確認することだけです。しかし、これらの相関関係は計算後にどのように保存する必要がありますか?
オブジェクトをマップとして使用する
考えられる方法の1つは、name
とvalue
プロパティを持つオブジェクトを使用して、すべての相関関係を配列に格納することです。しかし、これでは、特定のイベントの相関関係を調べるのが少々面倒になります。正しいname
を持つオブジェクトを見つけるために、配列全体をループする必要があります。このルックアッププロセスを関数でラップすることもできますが、それでもコードの記述量が増え、コンピュータは必要以上に多くの作業を行うことになります。
より良い方法は、イベントタイプにちなんで名付けられたオブジェクトのプロパティを使用することです。角括弧を使ったアクセス表記を使用してプロパティを作成および読み取り、in
演算子を使用して特定のプロパティが存在するかどうかをテストできます。
var map = {}; function storePhi(event, phi) { map[event] = phi; } storePhi("pizza", 0.069); storePhi("touched tree", -0.081); console.log("pizza" in map); // → true console.log(map["touched tree"]); // → -0.081
マップは、あるドメイン(この場合はイベント名)の値から別のドメイン(この場合はϕ係数)の対応する値への変換方法です。
このようにオブジェクトを使用することにはいくつかの潜在的な問題がありますが、これについては第6章で説明します。今のところは、それらのことは心配しません。
係数を格納したすべてのイベントを見つける必要があるとしたらどうでしょうか。プロパティは配列のように予測可能なシーケンスを形成していないため、通常のfor
ループを使用することはできません。JavaScriptは、オブジェクトのプロパティをループ処理するためのループ構造を特に提供しています。通常のfor
ループと少し似ていますが、in
という単語の使用によって区別されます。
for (var event in map) console.log("The correlation for '" + event + "' is " + map[event]); // → The correlation for 'pizza' is 0.069 // → The correlation for 'touched tree' is -0.081
最終分析
データセットに存在するすべてのイベントの種類を見つけるには、各エントリを順番に処理し、そのエントリ内のイベントをループ処理するだけです。これまでに確認したすべてのイベントタイプについて、相関係数を保持するオブジェクトphis
を保持します。まだphis
オブジェクトにないタイプに遭遇するたびに、その相関を計算してオブジェクトに追加します。
function gatherCorrelations(journal) { var phis = {}; for (var entry = 0; entry < journal.length; entry++) { var events = journal[entry].events; for (var i = 0; i < events.length; i++) { var event = events[i]; if (!(event in phis)) phis[event] = phi(tableFor(event, journal)); } } return phis; } var correlations = gatherCorrelations(JOURNAL); console.log(correlations.pizza); // → 0.068599434
for (var event in correlations) console.log(event + ": " + correlations[event]); // → carrot: 0.0140970969 // → exercise: 0.0685994341 // → weekend: 0.1371988681 // → bread: -0.0757554019 // → pudding: -0.0648203724 // and so on...
ほとんどの相関はゼロに近いようです。ニンジン、パン、プディングを食べても、リス人間に変身することは明らかにありません。ただし、週末にやや頻繁に発生するようです。相関が0.1より大きい、または-0.1より小さいものだけを表示するように結果をフィルタリングしましょう。
for (var event in correlations) { var correlation = correlations[event]; if (correlation > 0.1 || correlation < -0.1) console.log(event + ": " + correlation); } // → weekend: 0.1371988681 // → brushed teeth: -0.3805211953 // → candy: 0.1296407447 // → work: -0.1371988681 // → spaghetti: 0.2425356250 // → reading: 0.1106828054 // → peanuts: 0.5902679812
ありました!他のものよりも明らかに強い相関を持つ2つの要因があります。ピーナッツを食べることは、リスに変身する可能性に強い正の効果があり、一方、歯を磨くことは有意な負の効果があります。
for (var i = 0; i < JOURNAL.length; i++) { var entry = JOURNAL[i]; if (hasEvent("peanuts", entry) && !hasEvent("brushed teeth", entry)) entry.events.push("peanut teeth"); } console.log(phi(tableFor("peanut teeth", JOURNAL))); // → 1
これは間違いありません!この現象は、ジャックがピーナッツを食べ、歯を磨かないときに正確に発生します。もし彼が歯の衛生に関してそれほどずさんな人間でなければ、彼の病気にも気づかなかったでしょう。
これを理解したジャックは、ピーナッツを食べるのを完全に止め、それが彼の変身を完全に止めることを発見しました。
しばらくの間、ジャックは平穏な日々を送ります。しかし、数年後、彼は職を失い、最終的にサーカスで働くことを余儀なくされます。そこで彼は、毎公演前にピーナッツバターを口に詰め込んで「インクレディブル・スクイレルマン」として出演します。ある日、この哀れな存在にうんざりしたジャックは、人間に戻ることができず、サーカスのテントの隙間から飛び出し、森の中に消えていきます。彼は二度と見られませんでした。
配列に関するさらなる解説
この章を終える前に、オブジェクト関連の概念をいくつかご紹介したいと思います。一般的に便利な配列メソッドから始めましょう。
配列の最後に要素を追加および削除するpush
とpop
については、この章の前で説明しました。配列の先頭に要素を追加および削除する対応するメソッドは、unshift
とshift
と呼ばれます。
var todoList = []; function rememberTo(task) { todoList.push(task); } function whatIsNext() { return todoList.shift(); } function urgentlyRememberTo(task) { todoList.unshift(task); }
前のプログラムはタスクのリストを管理します。rememberTo("eat")
を呼び出すことでタスクをリストの最後に追加し、何かを実行する準備ができたら、whatIsNext()
を呼び出してリストの先頭項目を取得(および削除)します。urgentlyRememberTo
関数もタスクを追加しますが、リストの後ろではなく、前に追加します。
indexOf
メソッドには、lastIndexOf
と呼ばれる兄弟メソッドがあります。これは、先頭ではなく配列の末尾から指定された要素の検索を開始します。
console.log([1, 2, 3, 2, 1].indexOf(2)); // → 1 console.log([1, 2, 3, 2, 1].lastIndexOf(2)); // → 3
indexOf
とlastIndexOf
はどちらも、どこから検索を開始するかを示すオプションの第2引数を取ります。
もう1つの基本的なメソッドはslice
です。これは開始インデックスと終了インデックスを取り、それらのインデックス間の要素のみを含む配列を返します。開始インデックスは包含的、終了インデックスは排他的です。
console.log([0, 1, 2, 3, 4].slice(2, 4)); // → [2, 3] console.log([0, 1, 2, 3, 4].slice(2)); // → [2, 3, 4]
終了インデックスが指定されていない場合、slice
は開始インデックス以降のすべての要素を取得します。文字列にもslice
メソッドがあり、同様の効果があります。
concat
メソッドは、文字列に対して+
演算子が行う操作と同様に、配列を結合するために使用できます。次の例では、concat
とslice
の両方が動作しています。配列とインデックスを取り、指定されたインデックスにある要素が削除された元の配列のコピーである新しい配列を返します。
function remove(array, index) { return array.slice(0, index) .concat(array.slice(index + 1)); } console.log(remove(["a", "b", "c", "d", "e"], 2)); // → ["a", "b", "d", "e"]
文字列とそのプロパティ
length
やtoUpperCase
などのプロパティを文字列値から読み取ることができます。しかし、新しいプロパティを追加しようとすると、保持されません。
var myString = "Fido"; myString.myProperty = "value"; console.log(myString.myProperty); // → undefined
string、number、Boolean型の値はオブジェクトではなく、新しいプロパティを設定しようとすると言語が文句を言うことはありませんが、実際にはこれらのプロパティは保存されません。値は不変であり、変更できません。
しかし、これらの型にはいくつかの組み込みプロパティがあります。すべての文字列値には多くのメソッドがあります。おそらく最も便利なものは、slice
とindexOf
で、名前が同じ配列メソッドに似ています。
console.log("coconuts".slice(4, 7)); // → nut console.log("coconut".indexOf("u")); // → 5
1つの違いは、文字列のindexOf
は複数の文字を含む文字列を取ることができるのに対し、対応する配列メソッドは単一の要素のみを検索することです。
console.log("one two three".indexOf("ee")); // → 11
trim
メソッドは、文字列の先頭と末尾から空白(スペース、改行、タブ、および同様の文字)を削除します。
console.log(" okay \n ".trim()); // → okay
文字列型のlength
プロパティは既に見てきました。文字列内の個々の文字へのアクセスは、charAt
メソッドを使用することでも、配列のように数値プロパティを読み取ることで行うこともできます。
var string = "abc"; console.log(string.length); // → 3 console.log(string.charAt(0)); // → a console.log(string[1]); // → b
argumentsオブジェクト
関数が呼び出されるたびに、arguments
という名前の特別な変数が、関数本体が実行される環境に追加されます。この変数は、関数に渡されたすべての引数を保持するオブジェクトを参照します。JavaScriptでは、関数自体が宣言するパラメータの数よりも多く(または少なく)引数を関数に渡すことができます。
function noArguments() {} noArguments(1, 2, 3); // This is okay function threeArguments(a, b, c) {} threeArguments(); // And so is this
arguments
オブジェクトには、関数に実際に渡された引数の数を示すlength
プロパティがあります。また、0、1、2などという名前の各引数用のプロパティもあります。
それが配列によく似ているように聞こえるなら、あなたは正しいです。それは実際、配列によく似ています。しかし、残念ながらこのオブジェクトには配列メソッド(slice
やindexOf
など)がないため、実際の配列よりも使いにくいものです。
function argumentCounter() { console.log("You gave me", arguments.length, "arguments."); } argumentCounter("Straw man", "Tautology", "Ad hominem"); // → You gave me 3 arguments.
console.log
など、任意の数の引数を取ることができる関数があります。これらは通常、そのarguments
オブジェクト内の値をループ処理します。これらは非常に快適なインターフェースを作成するために使用できます。たとえば、ジャックのジャーナルのエントリを作成した方法を思い出してください。
addEntry(["work", "touched tree", "pizza", "running", "television"], false);
彼はこの関数を何度も呼び出すことになるため、呼び出しやすい代替案を作成できます。
function addEntry(squirrel) { var entry = {events: [], squirrel: squirrel}; for (var i = 1; i < arguments.length; i++) entry.events.push(arguments[i]); journal.push(entry); } addEntry(true, "work", "touched tree", "pizza", "running", "television");
このバージョンは、最初の引数(squirrel
)を通常の方法で読み取り、残りの引数(ループはインデックス1から始まり、最初の引数をスキップします)を反復処理して配列に収集します。
Mathオブジェクト
これまで見てきたように、Math
はMath.max
(最大値)、Math.min
(最小値)、Math.sqrt
(平方根)など、数値関連のユーティリティ関数の寄せ集めです。
Math
オブジェクトは、関連する機能のグループ化のためのコンテナとして単純に使用されます。Math
オブジェクトは1つだけであり、値としてはほとんど役に立ちません。むしろ、これらの関数や値をグローバル変数にする必要がないように、名前空間を提供します。
グローバル変数が多すぎると、名前空間が「汚染」されます。使用済みの名前が多ければ多いほど、誤って変数の値を上書きする可能性が高くなります。たとえば、プログラムの1つで何かをmax
と名付けることは珍しくありません。JavaScriptの組み込みmax
関数はMath
オブジェクトの中に安全に収納されているため、上書きについて心配する必要はありません。
多くの言語では、既に使用されている名前で変数を定義するときに、停止するか、少なくとも警告します。JavaScriptはどちらも行わないため、注意してください。
Math
オブジェクトに戻りましょう。三角関数を行う必要がある場合は、Math
が役立ちます。cos
(コサイン)、sin
(サイン)、tan
(タンジェント)と、それらの逆関数acos
、asin
、atan
が含まれています。π(パイ)の数、つまり少なくともJavaScriptの数に収まる最も近い近似値は、Math.PI
として利用できます。(定数値の名前をすべて大文字で書くという古いプログラミングの伝統があります。)
function randomPointOnCircle(radius) { var angle = Math.random() * 2 * Math.PI; return {x: radius * Math.cos(angle), y: radius * Math.sin(angle)}; } console.log(randomPointOnCircle(2)); // → {x: 0.3667, y: 1.966}
サインとコサインにあまり慣れていない場合は、心配しないでください。本書でそれらが使用されている場合、第13章で説明します。
前の例ではMath.random
を使用しています。これは、呼び出すたびに0(包含的)と1(排他的)の間の新しい擬似乱数を返す関数です。
console.log(Math.random()); // → 0.36993729369714856 console.log(Math.random()); // → 0.727367032552138 console.log(Math.random()); // → 0.40180766698904335
コンピュータは決定論的なマシン、つまり同じ入力に対して常に同じように反応するマシンですが、ランダムに見える数値を生成させることは可能です。これを行うために、マシンは内部状態に数値(または数値のグループ)を保持します。次に、乱数が要求されるたびに、この内部状態に対して複雑な決定論的計算を実行し、その計算結果の一部を返します。マシンはまた、その結果を使用して独自の内部状態を変更し、生成される次の「ランダム」数値が異なるようにします。
小数ではなく整数型のランダムな数値が必要な場合は、Math.random
の結果にMath.floor
(最も近い整数に切り捨てる)を使用できます。
console.log(Math.floor(Math.random() * 10)); // → 2
乱数を10倍すると、0以上10未満の数になります。Math.floor
は切り捨てるため、この式は0から9までの任意の数を同等の確率で生成します。
また、Math.ceil
(「天井」、整数に切り上げる)とMath.round
(最も近い整数に丸める)という関数もあります。
グローバルオブジェクト
グローバルスコープ、グローバル変数が存在する空間は、JavaScriptではオブジェクトとしても扱うことができます。各グローバル変数は、このオブジェクトのプロパティとして存在します。ブラウザでは、グローバルスコープオブジェクトはwindow
変数に格納されます。
var myVar = 10; console.log("myVar" in window); // → true console.log(window.myVar); // → 10
要約
オブジェクトと配列(特定の種類のオブジェクト)は、複数の値を単一の値にグループ化する手段を提供します。概念的には、これにより、個々のものすべてに腕を回して個別に保持しようとする代わりに、関連するものをまとめてバッグに入れ、そのバッグを持って走り回ることができます。
JavaScript の値のほとんどはプロパティを持ちますが、例外として `null` と `undefined` はプロパティを持ちません。プロパティは `value.propName` または `value["propName"]` を使用してアクセスします。オブジェクトは通常、プロパティ名を使用し、多かれ少なかれ固定された数のプロパティを保持します。一方、配列は通常、概念的に同一の値を可変数含み、プロパティ名として数値(0 から始まる)を使用します。
配列には `length` やいくつかのメソッドなど、名前付きのプロパティが存在します。メソッドはプロパティに存在する関数であり、(通常は)そのプロパティが属する値に対して作用します。
オブジェクトは、値と名前を関連付けるマップとしても機能します。`in` 演算子を使用して、オブジェクトが特定の名前のプロパティを含むかどうかを確認できます。同じキーワードは `for` ループ(`for (var name in object)`)でも使用して、オブジェクトのプロパティをループ処理できます。
練習問題
範囲の合計
本書のはじめにでは、数値範囲の合計を計算する良い方法として、以下が示唆されました。
console.log(sum(range(1, 10)));
`start` と `end` の2つの引数を取り、`start` から `end` まで(`end` を含む)のすべての数値を含む配列を返す `range` 関数を作成してください。
次に、数値の配列を取り、これらの数値の合計を返す `sum` 関数を作成します。前のプログラムを実行して、実際に 55 を返すかどうかを確認してください。
ボーナス課題として、配列を作成するために使用される「ステップ」値を示すオプションの第3引数を `range` 関数に追加してください。ステップが指定されていない場合、配列要素は1ずつ増加し、以前の動作と同じになります。`range(1, 10, 2)` という関数呼び出しは `[1, 3, 5, 7, 9]` を返す必要があります。負のステップ値でも機能するようにし、`range(5, 2, -1)` が `[5, 4, 3, 2]` を生成するようにしてください。
// Your code here. console.log(range(1, 10)); // → [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] console.log(range(5, 2, -1)); // → [5, 4, 3, 2] console.log(sum(range(1, 10))); // → 55
配列の作成は、最初に変数を `[]`(新しい空の配列)に初期化し、その `push` メソッドを繰り返し呼び出して値を追加するのが最も簡単です。関数の最後に配列を返すことを忘れないでください。
終了境界は包含的であるため、ループの終了をチェックするには `<` ではなく `<=` 演算子を使用する必要があります。
オプションのステップ引数が指定されているかどうかを確認するには、`arguments.length` をチェックするか、引数の値を `undefined` と比較します。指定されていない場合は、関数の先頭でデフォルト値 (1) に設定します。
`range` が負のステップ値を理解するようにするには、ループの終了をチェックする比較が、カウントダウン時には `<=` ではなく `>=` である必要があるため、おそらく2つの別々のループ(1つはカウントアップ用、もう1つはカウントダウン用)を作成するのが最適です。
範囲の終わりが開始よりも小さい場合、別のデフォルトステップ、つまり -1 を使用することも検討できます。そうすることで、`range(5, 2)` は無限ループに陥るのではなく、意味のある値を返します。
配列の反転
配列には `reverse` メソッドがあり、要素の表示順序を反転させることで配列を変更します。この練習問題では、`reverseArray` と `reverseArrayInPlace` の2つの関数を作成します。最初の `reverseArray` は配列を引数として受け取り、同じ要素を逆順に持つ新しい配列を生成します。2番目の `reverseArrayInPlace` は `reverse` メソッドと同じことを行います。つまり、引数として与えられた配列を変更して要素を反転させます。どちらも標準の `reverse` メソッドを使用することはできません。
前の章の副作用と純粋関数に関するメモを考えると、どちらのバリアントがより多くの状況で役立つと予想されますか?どちらがより効率的ですか?
// Your code here. console.log(reverseArray(["A", "B", "C"])); // → ["C", "B", "A"]; var arrayValue = [1, 2, 3, 4, 5]; reverseArrayInPlace(arrayValue); console.log(arrayValue); // → [5, 4, 3, 2, 1]
`reverseArray` を実装する2つの明白な方法があります。1つ目は、入力配列を前から後ろに処理し、新しい配列の `unshift` メソッドを使用して各要素を先頭に挿入することです。2つ目は、入力配列を後ろからループ処理し、`push` メソッドを使用することです。配列を後ろから反復処理するには、`for` 指定(ややぎこちない)` (var i = array.length - 1; i >= 0; i--) ` が必要です。
配列をその場で反転させるのはより困難です。後で必要になる要素を上書きしないように注意する必要があります。`reverseArray` を使用するか、配列全体をコピーする(`array.slice(0)` は配列をコピーする良い方法です)ことは機能しますが、不正行為です。
コツは、先頭と末尾の要素を入れ替え、次に2番目と末尾から2番目の要素を入れ替えるなどすることです。これは、配列の長さの半分をループ処理し(切り捨てるには `Math.floor` を使用します。奇数長の配列の中央要素に触れる必要はありません)、位置 `i` の要素と位置 `array.length - 1 - i` の要素を入れ替えることで実行できます。要素の1つを一時的に保持するローカル変数を使用し、その要素を鏡像で上書きしてから、ローカル変数の値を鏡像があった場所に配置できます。
リスト
オブジェクトは、値の一般的な塊として、あらゆる種類のデータ構造を構築するために使用できます。一般的なデータ構造の1つにリスト(配列と混同しないでください)があります。リストはオブジェクトのネストされた集合であり、最初のオブジェクトは2番目のオブジェクトへの参照を保持し、2番目のオブジェクトは3番目のオブジェクトへの参照を保持する、といった具合です。
var list = { value: 1, rest: { value: 2, rest: { value: 3, rest: null } } };
結果として得られるオブジェクトは、次のようなチェーンを形成します。
リストの良い点は、構造の一部を共有できることです。たとえば、`{value: 0, rest: list}` と `{value: -1, rest: list}` の2つの新しい値を作成した場合(`list` は前に定義された変数を指します)、これらはどちらも独立したリストですが、最後の3つの要素を構成する構造を共有します。さらに、元のリストも有効な3要素リストのままです。
`[1, 2, 3]` を引数として与えられた場合に、前のもののようなデータ構造を構築する `arrayToList` 関数と、リストから配列を生成する `listToArray` 関数を作成してください。また、要素とリストを受け取り、要素を入力リストの先頭に追加する新しいリストを作成するヘルパー関数 `prepend` と、リストと数値を受け取り、リストの指定された位置にある要素を返し、そのような要素がない場合は `undefined` を返す `nth` も作成してください。
まだ作成していない場合は、`nth` の再帰バージョンも作成してください。
// Your code here. console.log(arrayToList([10, 20])); // → {value: 10, rest: {value: 20, rest: null}} console.log(listToArray(arrayToList([10, 20, 30]))); // → [10, 20, 30] console.log(prepend(10, prepend(20, null))); // → {value: 10, rest: {value: 20, rest: null}} console.log(nth(arrayToList([10, 20, 30]), 1)); // → 20
リストの構築は、後ろから前に行うのが最適です。そのため、`arrayToList` は配列を後ろから反復処理し(前の練習問題を参照)、各要素に対して、リストにオブジェクトを追加できます。構築済みのリストの部分を保持するローカル変数を使用し、`list = {value: X, rest: list}` のようなパターンを使用して要素を追加できます。
リストをループ処理するには(`listToArray` と `nth` で)、次のような `for` ループ指定を使用できます。
for (var node = list; node; node = node.rest) {}
それがどのように機能するか分かりますか?ループの各反復で、`node` は現在のサブリストを指し、本文は `value` プロパティを読み取って現在の要素を取得できます。反復の最後に、`node` は次のサブリストに移動します。それが `null` の場合、リストの終わりに達し、ループは終了します。
`nth` の再帰バージョンは、同様に、リストの「末尾」のより小さな部分を調べ、同時にインデックスをカウントダウンして0に達すると、見ているノードの `value` プロパティを返すことができます。リストの0番目の要素を取得するには、単にヘッドノードの `value` プロパティを取得します。要素 *N* + 1 を取得するには、このリストの `rest` プロパティにあるリストの *N* 番目の要素を取得します。
深層比較
`==` 演算子は、同一性によってオブジェクトを比較します。しかし、場合によっては、実際のプロパティの値を比較する方が望ましい場合があります。
2つの値を受け取り、それらが同じ値であるか、または `deepEqual` への再帰呼び出しで比較した場合にも値が等しい同じプロパティを持つオブジェクトである場合にのみ `true` を返す関数 `deepEqual` を作成してください。
2つのものを同一性で比較するかどうか(それには `===` 演算子を使用します)、またはプロパティを見ることで比較するかどうかを判断するには、`typeof` 演算子を使用できます。両方の値に対して ` "object" ` を生成する場合は、深層比較を行う必要があります。ただし、1つのばかげた例外を考慮に入れる必要があります。歴史的な偶然により、`typeof null` も ` "object" ` を生成します。
// Your code here. var obj = {here: {is: "an"}, object: 2}; console.log(deepEqual(obj, obj)); // → true console.log(deepEqual(obj, {here: 1, object: 2})); // → false console.log(deepEqual(obj, {here: {is: "an"}, object: 2})); // → true
実際のオブジェクトを処理しているかどうかをテストする方法は、`typeof x == "object" && x != null` のようになります。両方の引数がオブジェクトである場合にのみプロパティを比較するように注意してください。それ以外のすべての場合は、すぐに `===` を適用した結果を返すことができます。
`for` / `in` ループを使用してプロパティをループ処理します。両方のオブジェクトが同じプロパティ名セットを持ち、それらのプロパティが同一の値を持っているかどうかをテストする必要があります。最初のテストは、両方のオブジェクトのプロパティをカウントし、プロパティの数に違いがある場合は `false` を返すことで実行できます。同じであれば、1つのオブジェクトのプロパティをループ処理し、それぞれについて、もう1つのオブジェクトにもそのプロパティがあることを確認します。プロパティの値は、`deepEqual` への再帰呼び出しによって比較されます。
関数から正しい値を返すのは、不一致が検出された場合にすぐに `false` を返し、関数の最後に `true` を返すのが最適です。