第4章データ構造:オブジェクトと配列
私は二度、「もし機械に間違った数字を入力したら、正しい答えが出てきますか?」と聞かれました。[...] そんな質問をするような種類の思考の混乱を、私は正しく理解できません。

数値、ブール値、文字列は、データ構造の構成要素となる原子です。しかし、多くの種類の情報は、一つの原子だけでは不十分です。オブジェクトを使用すると、他のオブジェクトを含む値をグループ化して、より複雑な構造を構築できます。
これまで作成してきたプログラムは、単純なデータ型しか扱っていなかったため、その機能が制限されていました。本章では、基本的なデータ構造を紹介します。本章を読み終える頃には、実用的なプログラムを書き始めるのに十分な知識が身につきます。
本章では、現実的なプログラミング例を段階的に解説し、問題に合わせて概念を導入していきます。例題のコードは、多くの場合、本文の早い段階で紹介された関数やバインディングを基に構築されます。
人狼リス
時々、通常は午後8時から10時の間に、ジャックは自分がふわふわした尻尾のある小さなげっ歯類に変身していることに気づきます。
一方、ジャックは、典型的な狼男ではないことにかなり満足しています。リスに変身する方が、狼に変身するよりも問題が少ないからです。隣人を誤って食べてしまう心配(それはひどい事態でしょう)をする代わりに、隣人の猫に食べられないか心配しています。裸で意識を失った状態で、オークの樹冠にある不安定な細い枝の上で目を覚ました二回の後、彼は夜には部屋のドアと窓を施錠し、自分を忙しくさせるために床にクルミをいくつか置くようになりました。
これで猫と木の問題は解決しました。しかし、ジャックは自分の状態を完全に解消したいと思っています。変身の不規則な発生は、何かが引き金になっているのではないかと疑っています。しばらくの間、彼はオークの木の近くにいた日にだけそれが起こると信じていました。しかし、オークの木を避けても問題は解決しませんでした。
より科学的なアプローチに切り替えたジャックは、ある日に行ったことすべてと、変身したかどうかを毎日記録し始めました。このデータを使って、変身を誘発する条件を絞り込みたいと考えています。
データセット
デジタルデータのかたまりを扱うには、まずマシンのメモリにそれを表現する方法を見つける必要があります。例えば、2、3、5、7、11という数字の集合を表したいとします。
文字列を使って工夫することもできます—文字列は任意の長さを持つことができるため、多くのデータを格納できます—そして"2 3 5 7 11"
を表現として使用できます。しかし、これは扱いにくいです。アクセスするには、何らかの方法で数字を抽出して数値に変換する必要があります。
幸い、JavaScriptは値のシーケンスを格納するためのデータ型を提供しています。それは配列と呼ばれ、角括弧で囲まれた値のリストとして記述され、コンマで区切られています。
let listOfNumbers = [2, 3, 5, 7, 11]; console.log(listOfNumbers[2]); // → 5 console.log(listOfNumbers[0]); // → 2 console.log(listOfNumbers[2 - 1]); // → 3
配列内の要素にアクセスするための表記も角括弧を使用します。式に続けて角括弧を付け、その中に別の式を入れると、左側の式の要素のうち、括弧内の式で指定されたインデックスに対応する要素が参照されます。
配列の最初のインデックスは1ではなく0です。したがって、最初の要素はlistOfNumbers[0]
で取得されます。0から始まるカウントは、技術の世界では長い伝統があり、ある意味では非常に理にかなっていますが、慣れるまでには時間がかかります。インデックスは、配列の先頭から数えてスキップする要素の数と考えてください。
プロパティ
これまでの章で、myString.length
(文字列の長さを取得するため)やMath.max
(最大値関数)のような、少し怪しげに見える式をいくつか見てきました。これらは、ある値のプロパティにアクセスする式です。最初のケースでは、myString
の値のlength
プロパティにアクセスします。2番目のケースでは、Math
オブジェクト(数学関連の定数と関数の集合)にあるmax
という名前のプロパティにアクセスします。
ほとんどすべてのJavaScriptの値にはプロパティがあります。例外はnull
とundefined
です。これらの非値のプロパティにアクセスしようとすると、エラーが発生します。
null.length; // → TypeError: null has no properties
JavaScriptでプロパティにアクセスする主な方法は、ドットと角括弧の2つです。value.x
とvalue[x]
の両方ともvalue
のプロパティにアクセスしますが、必ずしも同じプロパティではありません。違いは、x
の解釈方法にあります。ドットを使用する場合、ドットの後の単語はプロパティの文字通りの名前です。角括弧を使用する場合、括弧内の式が評価されてプロパティ名が取得されます。value.x
は「x」という名前のvalue
のプロパティを取得するのに対し、value[x]
は式x
を評価しようと試み、その結果を文字列に変換してプロパティ名として使用します。
したがって、目的のプロパティがcolorという名前であることがわかっている場合は、value.color
と言います。バインディングi
が保持する値で指定された名前のプロパティを抽出する場合は、value[i]
と言います。プロパティ名は文字列です。任意の文字列を使用できますが、ドット表記は有効なバインディング名のように見える名前の場合にのみ機能します。したがって、2またはJohn Doeという名前のプロパティにアクセスする場合は、角括弧を使用する必要があります:value[2]
またはvalue["John Doe"]
。
配列の要素は、数値をプロパティ名として使用して、配列のプロパティとして格納されます。数値にはドット表記を使用できず、通常はインデックスを保持するバインディングを使用したいので、要素にアクセスするには角括弧表記を使用する必要があります。
配列のlength
プロパティは、要素の数を示します。このプロパティ名は有効なバインディング名であり、その名前を事前に知っているため、配列の長さを知るには、通常array["length"]
よりも書きやすいarray.length
と書きます。
メソッド
文字列と配列の値には、length
プロパティに加えて、関数値を保持する多くのプロパティが含まれています。
let doh = "Doh"; console.log(typeof doh.toUpperCase); // → function console.log(doh.toUpperCase()); // → DOH
すべての文字列にはtoUpperCase
プロパティがあります。呼び出されると、すべての文字が大文字に変換された文字列のコピーが返されます。その逆を行うtoLowerCase
もあります。
興味深いことに、toUpperCase
の呼び出しは引数を渡していませんが、関数は何らかの方法で文字列"Doh"
(プロパティを呼び出した値)にアクセスできます。その仕組みについては、第6章で説明します。
関数を保持するプロパティは、一般的に、属する値のメソッドと呼ばれます。「toUpperCase
は文字列のメソッドです」というように。
この例では、配列を操作するために使用できる2つのメソッドを示しています。
let sequence = [1, 2, 3]; sequence.push(4); sequence.push(5); console.log(sequence); // → [1, 2, 3, 4, 5] console.log(sequence.pop()); // → 5 console.log(sequence); // → [1, 2, 3, 4]
push
メソッドは配列の最後に値を追加し、pop
メソッドは逆の動作を行い、配列の最後の値を削除して返します。
これらのややばかげた名前は、スタックに対する操作の伝統的な用語です。プログラミングにおけるスタックとは、値をプッシュしてポップできるデータ構造であり、最後に追加されたものが最初に削除される逆順で処理されます。これらはプログラミングでは一般的です—前の章の関数呼び出しスタックを思い出してください。これは同じ考え方の例です。
オブジェクト
ウェアリスに戻りましょう。一連の日誌のログエントリは配列として表現できます。しかし、エントリは数値や文字列だけではありません。各エントリには、アクティビティのリストと、ジャックがリスに変身したかどうかを示すブール値を格納する必要があります。理想的には、これらを単一の値にグループ化し、それらのグループ化された値をログエントリの配列に入れることが望ましいです。
オブジェクト型の値は、プロパティの任意の集合です。オブジェクトを作成する1つの方法は、式として中括弧を使用することです。
let day1 = { squirrel: false, events: ["work", "touched tree", "pizza", "running"] }; console.log(day1.squirrel); // → false console.log(day1.wolf); // → undefined day1.wolf = false; console.log(day1.wolf); // → false
中括弧内には、コンマで区切られたプロパティのリストがあります。各プロパティには、コロンに続く名前と値があります。オブジェクトを複数行にわたって記述する場合、例のようにインデントすると可読性が向上します。名前が有効なバインディング名または有効な数値でないプロパティは、引用符で囲む必要があります。
let descriptions = { work: "Went to work", "touched tree": "Touched a tree" };
これは、JavaScriptにおいて中括弧が2つの意味を持つことを意味します。文の先頭では、文のブロックを開始します。その他の位置では、オブジェクトを表します。幸いなことに、中括弧でオブジェクトで文を開始することはめったに役立たないので、これら2つのあいまいさはそれほど問題になりません。
存在しないプロパティを読み取ると、値undefined
が返されます。
=
演算子を使用して、プロパティ式に値を代入できます。これにより、プロパティが既に存在する場合はその値が置き換えられ、存在しない場合はオブジェクトに新しいプロパティが作成されます。
バインディングの触手モデルに戻ると、プロパティバインディングも同様です。それらは値をつかみますが、他のバインディングやプロパティもそれらの同じ値を保持している可能性があります。オブジェクトは、それぞれに名前のタトゥーが刻まれた無数の触手を持つタコのように考えることができます。
delete
演算子は、そのようなタコから触手を切り取ります。これは単項演算子であり、オブジェクトプロパティに適用されると、オブジェクトから名前付きプロパティが削除されます。これはよくあることではありませんが、可能です。
let 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
を返します。
オブジェクトがどのようなプロパティを持っているかを知るには、Object.keys
関数を使用できます。オブジェクトを渡すと、文字列の配列(オブジェクトのプロパティ名)が返されます。
console.log(Object.keys({x: 0, y: 0, z: 2})); // → ["x", "y", "z"]
1つのオブジェクトから別のオブジェクトにすべてのプロパティをコピーするObject.assign
関数があります。
let objectA = {a: 1, b: 2}; Object.assign(objectA, {b: 3, c: 4}); console.log(objectA); // → {a: 1, b: 3, c: 4}
したがって、配列は単に物事のシーケンスを格納するために特化したオブジェクトの一種です。typeof []
を評価すると、"object"
が生成されます。それらを、すべての触手がきれいに並んだ、番号でラベル付けされた長い平たいタコとして考えることができます。
ジャックが保持するジャーナルは、オブジェクトの配列で表現します。
let 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つの異なるオブジェクトを持つことの間には違いがあります。次のコードを考えてみましょう。
let object1 = {value: 10}; let object2 = object1; let 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
と同じプロパティを含む異なるオブジェクトを指しますが、別個の寿命を持ちます。
バインディングも変更可能または定数にすることができますが、これは値の動作とは別です。数値の値は変化しませんが、let
バインディングを使用して、バインディングが指す値を変更することで変化する数値を追跡できます。同様に、オブジェクトへのconst
バインディング自体は変更できず、同じオブジェクトを指し続けますが、そのオブジェクトの内容は変更される可能性があります。
const score = {visitors: 0, home: 0}; // This is okay score.visitors = 1; // This isn't allowed score = {visitors: 1, home: 1};
JavaScriptの==
演算子でオブジェクトを比較すると、同一性によって比較されます。つまり、両方のオブジェクトが正確に同じ値の場合にのみtrue
を生成します。異なるオブジェクトを比較すると、プロパティが同一であってもfalse
が返されます。オブジェクトの内容によってオブジェクトを比較する「深い」比較演算はJavaScriptに組み込まれていませんが、自分で記述することは可能です(この章の最後にある演習の1つです)。
獣人のログ
そこで、ジャックはJavaScriptインタープリターを起動し、ジャーナルを維持するために必要な環境をセットアップします。
let journal = []; function addEntry(events, squirrel) { journal.push({events, squirrel}); }
ジャーナルに追加されたオブジェクトはやや奇妙に見えます。events: events
のようにプロパティを宣言する代わりに、プロパティ名だけを指定しています。これは同じ意味の省略記法です。中括弧表記のプロパティ名に値が続かない場合、その値は同じ名前のバインディングから取得されます。
それから、毎晩午後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);
十分なデータポイントが得られたら、統計を使用して、これらのイベントのどれがリス化に関連している可能性があるかを調べようとしています。
相関は、統計変数間の依存性の尺度です。統計変数は、プログラミング変数とまったく同じではありません。統計では、通常、測定値のセットがあり、各変数はすべての測定値について測定されます。変数間の相関は、通常、-1から1の範囲の値として表されます。相関がゼロであることは、変数が関連していないことを意味します。相関が1であることは、2つの変数が完全に関連していることを示しています。一方を知っていれば、もう一方も知ることができます。-1も、変数が完全に関連しているが、反対であることを意味します。一方が真である場合、もう一方は偽です。
2つのブール変数間の相関の尺度を計算するには、ファイ係数(ϕ)を使用できます。これは、変数の異なる組み合わせが観測された回数を示す度数表を入力とする式です。式の出力は、相関を表す-1から1の数値です。
ピザを食べるというイベントを取り上げ、次のような度数表に示すことができます。各数値は、測定値においてその組み合わせが発生した回数を示します。
ϕ = |
n11n00 − n10n01
√ n1•n0•n•1n•0
|
(この時点で、高校の数学の授業のひどいフラッシュバックに集中するために本を置いている場合—待ってください!私はあなたが暗号のような記号でいっぱいのページで拷問するつもりはありません—今はこの1つの式だけです。そして、この1つの式についても、JavaScriptに変換するだけです。)
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の表は、4要素の配列([76, 9, 4, 1]
)でJavaScriptで表現できます。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 tableFor(event, journal) { let table = [0, 0, 0, 0]; for (let i = 0; i < journal.length; i++) { let entry = journal[i], index = 0; if (entry.events.includes(event)) index += 1; if (entry.squirrel) index += 2; table[index] += 1; } return table; } console.log(tableFor("pizza", JOURNAL)); // → [76, 9, 4, 1]
配列には、配列に特定の値が存在するかどうかをチェックするincludes
メソッドがあります。この関数は、それを用いて、関心のあるイベント名が、特定の日のイベントリストの一部であるかどうかを判断します。
tableFor
のループ本体は、エントリに関心のある特定のイベントが含まれているかどうか、およびイベントがリスの出来事と同時に発生するかどうかをチェックすることにより、各ジャーナルエントリが表のどのボックスに分類されるかを判断します。その後、ループは表の正しいボックスに1を加算します。
これで、個々の相関関係を計算するために必要なツールが揃いました。残りの手順は、記録されたすべての種類のイベントについて相関関係を見つけ、何か目立つものがあるかどうかを確認することだけです。
配列ループ
for (let i = 0; i < JOURNAL.length; i++) { let entry = JOURNAL[i]; // Do something with entry }
この種のループは、古典的なJavaScriptでは一般的です。配列を一度に1つの要素ずつ処理することはよくあることで、そのためには配列の長さに対してカウンタを実行し、各要素を順番に選択します。
現代のJavaScriptでは、このようなループをより簡単に記述する方法があります。
for (let entry of JOURNAL) { console.log(`${entry.events.length} events.`); }
変数定義の後にof
という単語があるfor
ループの場合、of
の後に与えられた値の要素をループ処理します。これは、配列だけでなく、文字列やその他のいくつかのデータ構造にも適用されます。その仕組みについては、第6章で説明します。
最終分析
データセットに発生するすべての種類のイベントについて、相関関係を計算する必要があります。そのためには、まずすべての種類のイベントを見つける必要があります。
function journalEvents(journal) { let events = []; for (let entry of journal) { for (let event of entry.events) { if (!events.includes(event)) { events.push(event); } } } return events; } console.log(journalEvents(JOURNAL)); // → ["carrot", "exercise", "weekend", "bread", …]
すべてのイベントを処理し、まだそこにないものをevents
配列に追加することにより、この関数はすべての種類のイベントを収集します。
for (let event of journalEvents(JOURNAL)) { console.log(event + ":", phi(tableFor(event, JOURNAL))); } // → carrot: 0.0140970969 // → exercise: 0.0685994341 // → weekend: 0.1371988681 // → bread: -0.0757554019 // → pudding: -0.0648203724 // and so on...
ほとんどの相関関係はゼロに近いようです。ニンジン、パン、またはプディングを食べることは、明らかにリス人間に変身を引き起こしません。週末の方がやや頻繁に発生するようです。結果をフィルタリングして、0.1より大きく、または-0.1より小さい相関関係のみを表示しましょう。
for (let event of journalEvents(JOURNAL)) { let correlation = phi(tableFor(event, JOURNAL)); 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 (let entry of JOURNAL) { if (entry.events.includes("peanuts") && !entry.events.includes("brushed teeth")) { entry.events.push("peanut teeth"); } } console.log(phi(tableFor("peanut teeth", JOURNAL))); // → 1
これは強い結果です。この現象は、ジャックがピーナッツを食べ、歯を磨かなかった場合に正確に発生します。もし彼が歯の衛生に関してそれほどずぼらでなければ、彼は自分の病気に気づかなかったでしょう。
これを理解したジャックは、ピーナッツを完全に食べるのをやめ、変身が戻ってこないことに気づきます。
数年もの間、ジャックは順調に過ごします。しかし、ある時点で彼は職を失います。彼は仕事がないということは医療サービスがないことを意味する不快な国に住んでいるため、彼はサーカスでインクレディブル・スクイレルマンとして働くことを余儀なくされ、毎公演前にピーナッツバターを口に詰め込みます。
ある日、このみじめな存在にうんざりしたジャックは、人間の形に戻ることができず、サーカスのテントの隙間をすり抜け、森の中に消えていきます。彼は二度と見られませんでした。
さらなる配列論
章を終える前に、オブジェクト関連の概念をいくつかさらに紹介したいと思います。まず、一般的に役立ついくつかの配列メソッドを紹介します。
配列の最後に要素を追加および削除するpush
とpop
を、この章の前の方で説明しました。配列の先頭で要素を追加および削除する対応するメソッドは、unshift
とshift
と呼ばれます。
let todoList = []; function remember(task) { todoList.push(task); } function getTask() { return todoList.shift(); } function rememberUrgently(task) { todoList.unshift(task); }
そのプログラムはタスクのキューを管理します。remember("groceries")
を呼び出すことでキューの最後にタスクを追加し、何かを実行する準備ができたら、getTask()
を呼び出してキューの先頭のアイテムを取得(および削除)します。rememberUrgently
関数はタスクを追加しますが、キューの後ろではなく前に追加します。
特定の値を検索するために、配列はindexOf
メソッドを提供します。このメソッドは配列の先頭から最後まで検索し、要求された値が見つかったインデックス(見つからなかった場合は-1)を返します。先頭ではなく最後から検索するには、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
は開始インデックスの後のすべての要素を取ります。開始インデックスを省略して、配列全体をコピーすることもできます。
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"]
concat
に配列ではない引数を渡した場合、その値は1要素の配列であるかのように新しい配列に追加されます。
文字列とそのプロパティ
length
やtoUpperCase
のようなプロパティを文字列値から読み取ることができます。しかし、新しいプロパティを追加しようとすると、保持されません。
let kim = "Kim"; kim.age = 88; console.log(kim.age); // → 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
前の章のzeroPad
関数もメソッドとして存在します。これはpadStart
と呼ばれ、必要な長さやパディング文字を引数として取ります。
console.log(String(6).padStart(3, "0")); // → 006
split
を使用して別の文字列のすべての出現箇所で文字列を分割し、join
で再び結合できます。
let sentence = "Secretarybirds specialize in stomping"; let words = sentence.split(" "); console.log(words); // → ["Secretarybirds", "specialize", "in", "stomping"] console.log(words.join(". ")); // → Secretarybirds. specialize. in. stomping
repeat
メソッドを使用して文字列を繰り返すことができます。これは、元の文字列の複数のコピーを連結して作成された新しい文字列を作成します。
console.log("LA".repeat(3)); // → LALALA
文字列型のlength
プロパティは既に見てきました。文字列の個々の文字へのアクセスは、配列要素へのアクセスに似ています(第5章で説明する注意点があります)。
let string = "abc"; console.log(string.length); // → 3 console.log(string[1]); // → b
レストパラメータ
関数が任意個数の引数を受け入れるようにすることは、有用な場合があります。例えば、`Math.max` は、渡された全ての引数の最大値を計算します。
そのような関数を記述するには、関数最後のパラメータの前に3つのドットを置きます。次のようになります。
function max(numbers) { let result = -Infinity; for (let number of numbers) { if (number > result) result = number; } return result; } console.log(max(4, 1, 9, -2)); // → 9
このような関数が呼び出されると、レストパラメータは、それ以降の全ての引数を含む配列にバインドされます。それより前に他のパラメータがある場合、それらの値はその配列に含まれません。`max` のように、それが唯一のパラメータである場合、全ての引数が格納されます。
同様の3つのドット表記を使用して、引数の配列で関数を呼び出すことができます。
let numbers = [5, 1, 7]; console.log(max(numbers)); // → 7
これは配列を関数呼び出しに「展開」し、その要素を個別の引数として渡します。`max(9, .
同様に、角括弧による配列表記では、3つのドット演算子を使用して別の配列を新しい配列に展開できます。
let words = ["never", "fully"]; console.log(["will", words, "understand"]); // → ["will", "never", "fully", "understand"]
Math オブジェクト
これまで見てきたように、`Math` は `Math.max`(最大値)、`Math.min`(最小値)、`Math.sqrt`(平方根)など、数値関連のユーティリティ関数の詰め合わせです。
`Math` オブジェクトは、一連の関連機能をグループ化するコンテナとして使用されます。`Math` オブジェクトは1つしかなく、値としてはほとんど役に立ちません。むしろ、これらの関数と値を全てグローバルバインディングにする必要がないように、名前空間を提供します。
グローバルバインディングが多すぎると、名前空間が「汚染」されます。使用されている名前が多ければ多いほど、既存のバインディングの値を誤って上書きする可能性が高くなります。例えば、プログラムの1つで何かを `max` と名付けたいと思うことは珍しくありません。JavaScript の組み込み `max` 関数は `Math` オブジェクトの中に安全に格納されているため、上書きする心配はありません。
多くの言語では、既に使用されている名前でバインディングを定義しようとすると、停止するか、少なくとも警告します。JavaScript では、`let` または `const` で宣言したバインディングに対してはこれを行います が、—奇妙なことに—標準のバインディングや `var` または `function` で宣言されたバインディングに対しては行いません。
`Math` オブジェクトに戻りましょう。三角関数を行う必要がある場合、`Math` が役立ちます。`cos`(コサイン)、`sin`(サイン)、`tan`(タンジェント)、およびそれらの逆関数である `acos`、`asin`、`atan` を含んでいます。π(パイ)の数値、つまり少なくとも JavaScript の数値に収まる最も近い近似値は、`Math.PI` として利用できます。定数値の名前を全て大文字で書くという古いプログラミングの伝統があります。
function randomPointOnCircle(radius) { let 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}
サインとコサインに慣れていない場合は、心配しないでください。本書でそれらが使用される場合(第14章)、説明します。
前の例では `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`(最も近い整数に丸める)、`Math.abs`(数値の絶対値を取り、負の値を反転させるが正の値はそのままにする)という関数もあります。
デストラクチャリング
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])); }
この関数が読みづらい理由の1つは、配列を指すバインディングがあるのに、配列の要素のバインディング(つまり、`let n00 = table[0]` など)の方がはるかに好ましいことです。幸いなことに、JavaScript ではこれを行う簡潔な方法があります。
function phi([n00, n01, n10, n11]) { return (n11 * n00 - n10 * n01) / Math.sqrt((n10 + n11) * (n00 + n01) * (n01 + n11) * (n00 + n10)); }
これは `let`、`var`、`const` で作成されたバインディングにも機能します。バインドしている値が配列であることがわかっている場合は、角括弧を使用して値の「内部を見る」ことができ、その内容をバインドできます。
同様のトリックは、角括弧の代わりに波括弧を使用してオブジェクトにも機能します。
let {name} = {name: "Faraji", age: 23}; console.log(name); // → Faraji
`null` または `undefined` のデストラクチャリングを試みると、それらの値のプロパティに直接アクセスしようとした場合と同様に、エラーが発生することに注意してください。
JSON
プロパティは値を把握するだけで、それを含むわけではないため、オブジェクトと配列はコンピュータのメモリに、内容のアドレス(メモリ内の場所)を保持するビットのシーケンスとして格納されます。したがって、内部に別の配列を含む配列は、(少なくとも)内部配列のメモリ領域と、外部配列のメモリ領域(内部配列の位置を表す二進数など)で構成されます。
後でデータを実行ファイルに保存したり、ネットワーク経由で別のコンピュータに送信したりする場合は、これらのメモリアドレスの絡み合いを、保存または送信できる記述に変換する必要があります。コンピュータのメモリ全体と、対象の値のアドレスを送信することもできますが、これは最良の方法とは考えられません。
行うことができるのは、データのシリアライズです。つまり、フラットな記述に変換されます。一般的なシリアライズ形式は、JavaScript Object Notation を意味するJSON(「ジェイソン」と発音)と呼ばれます。これは、JavaScript 以外の言語でも、Web 上のデータストレージおよび通信形式として広く使用されています。
JSON は、JavaScript の配列とオブジェクトの書き方と似ていますが、いくつかの制限があります。プロパティ名は全て二重引用符で囲む必要があり、単純なデータ式のみが許可されます。関数呼び出し、バインディング、実際の計算が含まれるものは許可されません。JSON にはコメントは許可されません。
ジャーナルエントリは、JSON データとして表されると次のようになります。
{ "squirrel": false, "events": ["work", "touched tree", "pizza", "running"] }
JavaScript は、データとこの形式との間を変換するための `JSON.stringify` と `JSON.parse` という関数を提供します。最初の関数は JavaScript の値を受け取り、JSON エンコードされた文字列を返します。2番目の関数は、そのような文字列を受け取り、エンコードされた値に変換します。
let string = JSON.stringify({squirrel: false, events: ["weekend"]}); console.log(string); // → {"squirrel":false,"events":["weekend"]} console.log(JSON.parse(string).events); // → ["weekend"]
まとめ
オブジェクトと配列(特定の種類のオブジェクト)は、いくつかの値を単一の値にグループ化する手段を提供します。概念的には、これにより、個々の物全てに腕を巻きつけて別々に保持しようとする代わりに、関連する物をまとめてバッグに入れて持ち歩くことができます。
JavaScript のほとんどの値にはプロパティがありますが、例外として `null` と `undefined` があります。プロパティは `value.prop` または `value["prop"]` を使用してアクセスします。オブジェクトは通常、プロパティに名前を使用し、多かれ少なかれ固定されたセットを格納します。一方、配列は通常、概念的に同一の値を可変量含み、プロパティ名として数値(0から始まる)を使用します。
配列には `length` や多数のメソッドなど、名前付きプロパティもあります。メソッドはプロパティに存在する関数であり、(通常は)それらがプロパティである値に対して作用します。
特別な種類の `for` ループ — `for (let element of array)` — を使用して配列を反復処理できます。
練習問題
範囲の合計
本書の導入では、数値の範囲の合計を計算する優れた方法として、次のようなものが示唆されています。
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` メソッドを繰り返し呼び出して値を追加することで最も簡単に実行できます。関数の最後に配列を返すことを忘れないでください。
終端境界は包含的であるため、ループの終了条件を確認するには、<
ではなく<=
演算子を使用する必要があります。
stepパラメータは省略可能なパラメータで、デフォルト値は(=
演算子を使用して)1です。
負のstep値をrange
で扱うには、おそらく、増加用と減少用の2つの別々のループを作成するのが最適です。これは、ループの終了条件を確認する比較が、減少カウントの場合は<=
ではなく>=
になる必要があるためです。
範囲の終端が開始よりも小さい場合、デフォルトのstep値を-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"]; let arrayValue = [1, 2, 3, 4, 5]; reverseArrayInPlace(arrayValue); console.log(arrayValue); // → [5, 4, 3, 2, 1]
reverseArray
を実装するには、2つの明らかな方法があります。1つ目は、入力配列の先頭から末尾まで順に処理し、新しい配列の先頭に各要素を挿入するためにunshift
メソッドを使用する方法です。2つ目は、入力配列を後ろからループし、push
メソッドを使用する方法です。配列を後ろから反復処理するには、(let i = array.
のような(やや扱いにくい)for
指定が必要です。
配列をその場で反転させるのは困難です。後で必要になる要素を上書きしないように注意する必要があります。reverseArray
を使用するか、配列全体をコピーする(array.slice(0)
は配列をコピーする良い方法です)方法はありますが、これは不正行為です。
コツは、先頭と末尾の要素を交換し、次に2番目と最後から2番目の要素を交換する、といった具合に交換することです。これは、配列の長さの半分をループ処理し(Math.floor
を使用して切り捨てます。奇数個の要素を持つ配列では、中央の要素に触れる必要はありません)、位置i
の要素と位置array.
の要素を交換することで行えます。ローカルバインディングを使用して、一方の要素を一時的に保持し、それを鏡像で上書きしてから、ローカルバインディングの値を鏡像があった場所に配置できます。
リスト
オブジェクトは、汎用的な値の塊として、あらゆる種類のデータ構造を構築するために使用できます。一般的なデータ構造の1つにリスト(配列と混同しないでください)があります。リストは、入れ子になったオブジェクトの集合で、最初のオブジェクトは2番目への参照を保持し、2番目は3番目への参照を保持する、といった具合です。
let list = { value: 1, rest: { value: 2, rest: { value: 3, rest: null } } };
結果として得られるオブジェクトは、次のようなチェーンを形成します。
リストの良い点の1つは、構造の一部を共有できることです。たとえば、{value: 0, rest: list}
と{value: -1, rest: list}
という2つの新しい値を作成した場合(list
は前に定義されたバインディングを参照しています)、どちらも独立したリストですが、最後の3つの要素を構成する構造を共有しています。元のリストも、依然として有効な3要素のリストです。
引数として[1, 2, 3]
が与えられた場合に、上記のようなリスト構造を構築するarrayToList
関数を記述してください。また、リストから配列を生成するlistToArray
関数も記述してください。次に、要素とリストを受け取り、要素を入力リストの先頭に追加する新しいリストを作成するヘルパー関数prepend
と、リストと数値を受け取り、リスト内の指定された位置の要素を返す(0は最初の要素を参照)関数nth
を追加し、そのような要素が存在しない場合はundefined
を返します。
まだ作成していない場合は、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 (let node = list; node; node = node.rest) {}
それがどのように機能するかわかりますか?ループの各反復で、node
は現在のサブリストを指し、本体はそのvalue
プロパティを読み取って現在の要素を取得できます。反復処理の最後に、node
は次のサブリストに移動します。それがnullの場合、リストの終わりに達し、ループは終了します。
nth
の再帰バージョンは、同様に、リストの「テール」のますます小さな部分と同時にインデックスをカウントダウンし、0に達した時点で、見ているノードのvalue
プロパティを返すことができます。リストの0番目の要素を取得するには、単にそのヘッドノードのvalue
プロパティを取得します。N + 1番目の要素を取得するには、このリストのrest
プロパティにあるリストのN番目の要素を取得します。
深い比較
==
演算子は、オブジェクトを同一性によって比較します。しかし、実際のプロパティの値を比較したい場合があります。
2つの値を受け取り、それらが同じ値であるか、またはdeepEqual
への再帰呼び出しで比較した場合にプロパティの値が等しい同じプロパティを持つオブジェクトである場合にのみtrueを返すdeepEqual
関数を記述します。
値を直接比較する必要があるか(その場合は===
演算子を使用します)、それともプロパティを比較する必要があるかを確認するには、typeof
演算子を使用できます。両方の値に対して"object"
を生成する場合は、深い比較を行う必要があります。ただし、1つのばかげた例外を考慮に入れる必要があります。歴史的な偶然により、typeof null
も"object"
を生成します。
オブジェクトのプロパティを反復処理する必要がある場合は、Object.keys
関数が役立ちます。
// Your code here. let 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
のようなものを使用します。両方の引数がオブジェクトである場合にのみプロパティを比較するように注意してください。その他の場合、===
を適用した結果をすぐに返すことができます。
Object.keys
を使用してプロパティを反復処理します。両方のオブジェクトが同じプロパティ名セットを持ち、それらのプロパティが同一の値を持っているかどうかをテストする必要があります。これを行う1つの方法は、両方のオブジェクトが同じ数のプロパティを持っている(プロパティリストの長さが同じである)ことを確認することです。そして、一方のオブジェクトのプロパティをループして比較する場合、常に最初に、他方が実際にその名前のプロパティを持っていることを確認します。同じ数のプロパティがあり、一方のすべてのプロパティが他方にも存在する場合は、同じプロパティ名セットを持っています。