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

私は二度、「バベッジさん、もし機械に間違った数字を入れたら、正しい答えが出てくるのでしょうか」と尋ねられたことがあります。[...]そのような質問を誘発する可能性のある考え方の混乱を、私は正しく理解することができません。

チャールズ・バベッジ, 哲学者の一生からの抜粋 (1864)
Illustration of a squirrel next to a pile of books and a pair of glasses. A moon and stars are visible in the background.

数値、ブール値、文字列は、データ構造を構築するための原子です。しかし、多くの種類の情報は、複数の原子を必要とします。オブジェクトを使用すると、他のオブジェクトを含む値をグループ化して、より複雑な構造を構築できます。

これまで構築してきたプログラムは、単純なデータ型のみを操作していたという事実に制限されていました。この章でデータ構造の基本を学んだ後、役立つプログラムを書き始めるのに十分な知識を得ることができます。

この章では、多かれ少なかれ現実的なプログラミング例を通して、当面の問題に適用される概念を紹介していきます。例のコードは、多くの場合、本書の前半で紹介した関数とバインディングを基に構築されます。

ウェアスクイレル

時々、通常は午後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]` で取得されます。ゼロベースのカウントは技術で長い伝統があり、ある意味では理にかなっていますが、慣れるのに少し時間がかかります。インデックスは、配列の先頭から数えてスキップする項目の数と考えるとよいでしょう。

プロパティ

過去の章で、`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` プロパティがあります。

メソッド

文字列と配列の値はどちらも、`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つの間の曖昧さはそれほど問題ではありません。この問題が発生する1つのケースは、短縮形の矢印関数からオブジェクトを返したい場合です。波括弧が関数本体として解釈されるため、n => {prop: n}と記述することはできません。代わりに、オブジェクトが式であることを明確にするために、オブジェクトを括弧で囲む必要があります。

存在しないプロパティを読み取ると、値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に設定することと、実際に削除することの違いは、前者の場合、オブジェクトはまだそのプロパティを持っている(ただし、あまり面白い値を持っていない)のに対し、後者の場合、プロパティはもはや存在せず、infalseを返すということです。

オブジェクトが持つプロパティを調べるには、Object.keys関数を使用できます。関数にオブジェクトを渡すと、オブジェクトのプロパティ名である文字列の配列が返されます。

console.log(Object.keys({x: 0, y: 0, z: 2}));
// → ["x", "y", "z"]

あるオブジェクトから別のオブジェクトにすべてのプロパティをコピーする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... */
];

可変性

すぐに実際のプログラミングに取り掛かりますが、その前に、理解しておくべき理論がもう1つあります。

オブジェクトの値は変更できることを確認しました。前の章で説明した数値、文字列、ブール値などの値の型はすべて不変です。これらの型の値を変更することはできません。それらを組み合わせて新しい値を派生させることはできますが、特定の文字列値を取得すると、その値は常に同じままになります。その中のテキストは変更できません。"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

object1object2の結合は、同じオブジェクトを掴んでいます。そのため、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のようにプロパティを宣言する代わりに、プロパティ名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の間の数値です。

ピザを食べるというイベントを取り上げて、各数値が測定でその組み合わせが発生した回数を示すこのような頻度テーブルに入れることができます。

A two-by-two table showing the pizza variable on the horizontal, and the squirrel variable on the vertical axis. Each cell show how many time that combination occurred. In 76 cases, neither happened. In 9 cases, only pizza was true. In 4 cases only squirrel was true. And in one case both occurred.

そのテーブルをnと呼ぶと、次の式を使用してϕを計算できます。

ϕ =
n11n00n10n01
n1•n0•n•1n•0

(この時点で、10年生の数学の授業の恐ろしいフラッシュバックに集中するために本を置いている場合は、ちょっと待ってください!難解な表記が無限に続くページであなたを苦しめるつもりはありません。今はこれ1つの式だけです。そして、この式でも、JavaScriptに変換するだけです。)

表記n01は、最初の変数(リス度)が偽(0)で、2番目の変数(ピザ)が真(1)である測定の数を示します。ピザのテーブルでは、n01は9です。

n1•は、最初の変数が真であるすべての測定値の合計を指し、例のテーブルでは5です。同様に、n•0は、2番目の変数が偽である測定値の合計を指します。

したがって、ピザのテーブルでは、除算線(被除数)の上の部分は1×76−4×9 = 40になり、下の部分(除数)は5×85×10×80、または√340,000の平方根になります。これはϕ ≈ 0.069になります。これはごくわずかです。ピザを食べることは、変形に影響を与えていないようです。

相関の計算

JavaScriptでは、2x2のテーブルを4つの要素を持つ配列([76, 9, 4, 1])で表現できます。他にも、2つの要素を持つ配列を2つ含む配列([[76, 9], [4, 1]])や、"11""01"のようなプロパティ名を持つオブジェクトなど、さまざまな表現方法がありますが、フラットな配列はシンプルで、テーブルにアクセスする式を短く記述できます。配列のインデックスは2ビットのバイナリ数として解釈します。ここで、左端(最上位)の桁はリスの変数を示し、右端(最下位)の桁はイベント変数を示します。たとえば、バイナリ数10は、ジャックがリスに変身したが、イベント(例えば、「ピザ」)が発生しなかったケースを指します。これは4回発生しました。そして、バイナリの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バインディングに格納されています。また、ダウンロード可能なファイルにも格納されています。

日記から特定のイベントの2x2テーブルを抽出するには、すべてのエントリをループ処理し、リスの変身に関連してイベントが発生した回数を集計する必要があります。

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を追加します。

これで、個々の相関関係を計算するために必要なツールが揃いました。残りのステップは、記録されたすべてのタイプのイベントの相関関係を見つけ、何か目立つものがあるかどうかを確認することだけです。

配列のループ

tableFor関数には、次のようなループがあります。

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.`);
}

forループが変数定義の後にofという単語を使用すると、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

これは強力な結果です。この現象は、ジャックがピーナッツを食べ、歯を磨かないときに正確に発生します。彼が歯の衛生にだらしなくさえなければ、自分の苦悩に気づくことはなかったでしょう。

これを踏まえて、ジャックはピーナッツを完全に食べるのをやめ、変身が止まったことに気づきます。

しかし、この完全に人間的な生活様式から何かが欠けていることに気づくのにほんの数ヶ月しかかかりません。野生の冒険がなければ、ジャックはまったく生きていると感じません。彼はむしろフルタイムで野生動物になることに決めます。森の中に美しい小さなツリーハウスを建て、ピーナッツバターディスペンサーと10年分のピーナッツバターを備え付けた後、彼は最後に一度変身し、リスの短く活気に満ちた生活を送ります。

配列学のさらなる探求

この章を終える前に、オブジェクト関連の概念をいくつか紹介したいと思います。まず、一般的に役立つ配列メソッドから始めます。

配列の最後に要素を追加および削除するpushpopを、この章の先ほど見ました。配列の先頭に要素を追加および削除するための対応するメソッドは、unshiftshiftと呼ばれます。

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

indexOflastIndexOfの両方には、検索を開始する場所を示すオプションの第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メソッドを使用して配列を連結して新しい配列を作成できます。これは、+演算子が文字列に対して行う操作と似ています。

次の例は、concatsliceの両方が実際にどのように機能するかを示しています。配列とインデックスを受け取り、指定されたインデックスの要素が削除された元の配列のコピーである新しい配列を返します。

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要素の配列であるかのように新しい配列に追加されます。

文字列とそのプロパティ

lengthtoUpperCaseのようなプロパティは、文字列値から読み取ることができます。ただし、新しいプロパティを追加しようとしても、それは保持されません。

let kim = "Kim";
kim.age = 88;
console.log(kim.age);
// → undefined

文字列、数値、およびブール型の値はオブジェクトではなく、新しいプロパティを設定しようとしても言語はエラーを発生させませんが、実際にはそれらのプロパティは保存されません。前述のように、このような値は不変であり、変更することはできません。

ただし、これらの型には組み込みのプロパティがあります。すべての文字列値には、多くのメソッドがあります。非常に便利なものとしては、同じ名前の配列メソッドに似たsliceindexOfがあります。

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のように、それが唯一のパラメータである場合、すべての引数を保持します。

同様の三点リーダー表記を使用して、引数の配列で関数を呼び出すことができます。

let numbers = [5, 1, 7];
console.log(max(...numbers));
// → 7

これは配列を関数呼び出しに「展開」し、その要素を個別の引数として渡します。max(9, ...numbers, 2)のように、そのような配列を他の引数と一緒に含めることが可能です。

同様に、角括弧の配列表記では、三点リーダー演算子を使用して別の配列を新しい配列に展開できます。

let words = ["never", "fully"];
console.log(["will", ...words, "understand"]);
// → ["will", "never", "fully", "understand"]

これは波括弧オブジェクトでも機能し、別のオブジェクトからのすべてのプロパティを追加します。プロパティが複数回追加された場合、最後に追加された値が優先されます。

let coordinates = {x: 10, y: 0};
console.log({...coordinates, y: 5, z: 1});
// → {x: 10, y: 5, z: 1}

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(タンジェント)、およびそれぞれの逆関数であるacosasinatanが含まれています。数値π(パイ)—または少なくとも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もあります。つまり、負の値を否定しますが、正の値はそのままにします。

分割代入

少しの間、phi関数に戻りましょう。

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

これは、letvar、またはconstで作成されたバインディングでも機能します。バインドしている値が配列であることがわかっている場合は、角括弧を使用して値の「内側を覗き込み」、そのコンテンツをバインドできます。

同様のトリックは、角括弧の代わりに波括弧を使用するオブジェクトにも機能します。

let {name} = {name: "Faraji", age: 23};
console.log(name);
// → Faraji

nullまたはundefinedを分割代入しようとすると、それらの値のプロパティに直接アクセスしようとした場合と同様に、エラーが発生することに注意してください。

オプションのプロパティアクセス

特定の値がオブジェクトを生成するかどうか確信がないが、オブジェクトを生成する場合はそこからプロパティを読み取りたい場合は、ドット表記のバリアントを使用できます:object?.property

function city(object) {
  return object.address?.city;
}
console.log(city({address: {city: "Toronto"}}));
// → Toronto
console.log(city({name: "Vera"}));
// → undefined

a?.bは、aがnullまたはundefinedでない場合はa.bと同じ意味です。nullまたはundefinedの場合は、undefinedと評価されます。例のように、特定のプロパティが存在するかどうか、または変数がundefined値を保持しているかどうか確信がない場合に便利です。

同様の表記は、角括弧アクセス、さらには関数呼び出しでも使用できます。括弧または角括弧の前に?.を付けることで使用できます。

console.log("string".notAMethod?.());
// → undefined
console.log({}.arrayProp?.[0]);
// → undefined

JSON

プロパティは値を保持するのではなく、値をつかむため、オブジェクトと配列は、内容のアドレス—メモリ内の場所—を保持するビットのシーケンスとしてコンピューターのメモリに格納されます。内部に別の配列を持つ配列は、(少なくとも)内部配列用の1つのメモリ領域と、内部配列のアドレスを表す数値を含む、外部配列用の別のメモリ領域で構成されます。

データを後で使用するためにファイルに保存したり、ネットワーク経由で別のコンピューターに送信したりする場合は、これらのメモリのアドレスのもつれを、保存または送信できる記述に何らかの方法で変換する必要があります。興味のある値のアドレスとともにコンピューターのメモリ全体を送信することもできますが、それは最良のアプローチではないようです。

できることは、データをシリアル化することです。つまり、フラットな記述に変換されます。一般的なシリアル化形式は、JavaScript Object Notationの略であるJSON(「ジェイソン」と発音)と呼ばれます。JavaScript以外の言語でも、Web上のデータストレージおよび通信形式として広く使用されています。

JSONは、JavaScriptの配列とオブジェクトの記述方法と似ていますが、いくつかの制限があります。すべてのプロパティ名は二重引用符で囲む必要があり、単純なデータ式のみが許可されます—関数呼び出し、バインディング、または実際の計算を伴うものは許可されません。JSONではコメントは許可されていません。

ジャーナルエントリは、JSONデータとして表現すると次のようになります。

{
  "squirrel": false,
  "events": ["work", "touched tree", "pizza", "running"]
}

JavaScriptには、この形式との間でデータを変換するための関数JSON.stringifyJSON.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のほとんどの値にはプロパティがあり、例外はnullundefinedです。プロパティにはvalue.propまたはvalue["prop"]を使用してアクセスします。オブジェクトはプロパティに名前を使用し、多かれ少なかれ固定されたセットを格納する傾向があります。一方、配列は通常、概念的に同一の値の量が異なり、プロパティの名前として(0から始まる)数値を使用します。

配列には、lengthや多数のメソッドなど、いくつかの名前付きプロパティがあります。メソッドはプロパティ内に存在し、(通常は)プロパティである値に対して動作する関数です。

配列は、特別な種類のforループ:for (let element of array)を使用して反復処理できます。

演習

範囲の合計

この本の紹介で、次のことを数値範囲の合計を計算するのに適した方法として言及しました。

console.log(sum(range(1, 10)));

2つの引数startendを取り、startから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 になるオプションのパラメータにすることができます。

range が負の step 値を理解できるようにするには、2 つの別々のループ(カウントアップ用とカウントダウン用)を書くのがおそらく最善です。これは、カウントダウン時にはループが終了したかどうかをチェックする比較が <= ではなく >= になる必要があるためです。

また、範囲の終了が開始よりも小さい場合は、別のデフォルトの step、つまり -1 を使用するのも良いでしょう。そうすれば、range(5, 2) は無限ループに陥るのではなく、意味のあるものを返します。パラメータのデフォルト値で、前のパラメータを参照することが可能です。

配列の反転

配列には、要素が表示される順序を反転させることで配列を変更する reverse メソッドがあります。この演習では、reverseArrayreverseArrayInPlace の 2 つの関数を作成します。最初の reverseArray は、引数として配列を受け取り、要素が逆順になった新しい配列を生成する必要があります。2 番目の reverseArrayInPlace は、reverse メソッドと同じ動作をする必要があります。つまり、引数として与えられた配列の要素を反転することで、配列を変更します。どちらも標準の reverse メソッドを使用してはいけません。

前の章の副作用と純粋関数に関するメモを振り返ると、どちらのバリアントがより多くの状況で役立つと予想されますか?どちらの方が速く実行されますか?

// Your code here.

let myArray = ["A", "B", "C"];
console.log(reverseArray(myArray));
// → ["C", "B", "A"];
console.log(myArray);
// → ["A", "B", "C"];
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.length - 1; i >= 0; i--) のような(やや扱いにくい)for 仕様が必要です。

配列をインプレースで反転するのはより困難です。後で必要になる要素を上書きしないように注意する必要があります。reverseArray を使用するか、配列全体をコピーする(array.slice() は配列をコピーするのに良い方法です)と動作しますが、ズルです。

コツは、最初と最後の要素を入れ替え、次に 2 番目と最後から 2 番目の要素を入れ替え、というように続けることです。これを行うには、配列の長さの半分をループします(Math.floor を使用して切り捨てます。奇数の要素を持つ配列の中央の要素に触れる必要はありません)。位置 i の要素と位置 array.length - 1 - i の要素を入れ替えます。ローカルバインディングを使用して要素の 1 つを一時的に保持し、その要素をその鏡像で上書きし、ローカルバインディングからの値を鏡像が以前あった場所に配置することができます。

リスト

値の汎用的な塊として、オブジェクトはあらゆる種類のデータ構造を構築するために使用できます。一般的なデータ構造はリスト(配列と混同しないでください)です。リストはオブジェクトのネストされた集合であり、最初のオブジェクトは 2 番目のオブジェクトへの参照を持ち、2 番目のオブジェクトは 3 番目のオブジェクトへの参照を持つというように続きます。

let list = {
  value: 1,
  rest: {
    value: 2,
    rest: {
      value: 3,
      rest: null
    }
  }
};

結果として得られるオブジェクトは、次の図に示すようにチェーンを形成します。

A diagram showing the memory structure of a linked list. There are 3 cells, each with a value field holding a number, and a 'rest' field with an arrow to the rest of the list. The first cell's arrow points at the second cell, the second cell's arrow at the last cell, and the last cell's 'rest' field holds null.

リストの優れた点は、構造の一部を共有できることです。たとえば、2 つの新しい値 {value: 0, rest: list}{value: -1, rest: list}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 (let node = list; node; node = node.rest) {}

それがどのように機能するか理解できますか?ループのすべての反復で、node は現在のサブリストを指し、本文はその value プロパティを読み取って現在の要素を取得できます。反復の最後に、node は次のサブリストに移動します。それが null の場合、リストの最後に到達しており、ループは終了します。

nth の再帰バージョンも同様に、リストの「テール」のますます小さな部分を調べ、同時にインデックスがゼロに達するまでカウントダウンします。この時点で、調べているノードの 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 つの方法は、両方のオブジェクトが同じ数のプロパティを持っていること(プロパティリストの長さが同じであること)を確認することです。次に、オブジェクトのプロパティの 1 つをループして比較するときは、常に、他のオブジェクトが実際にその名前のプロパティを持っていることを最初に確認します。同じ数のプロパティを持ち、一方のすべてのプロパティが他方にも存在する場合、同じプロパティ名のセットを持っていることになります。

関数から正しい値を返すには、不一致が見つかったときにすぐに false を返し、関数の最後に true を返すのが最善です。