2023年1月24日

配列

オブジェクトでは、キー付きの値のコレクションを保存できます。これは問題ありません。

しかし、多くの場合、1番目、2番目、3番目の要素などを持つ*順序付きコレクション*が必要になります。たとえば、ユーザー、商品、HTML要素などのリストを保存するために必要になります。

ここではオブジェクトを使用するのは不便です。なぜならオブジェクトは要素の順序を管理するためのメソッドを提供しないからです。既存のプロパティの「間」に新しいプロパティを挿入することはできません。オブジェクトはそのような用途向けに設計されていません。

順序付きコレクションを保存するために、Arrayという特別なデータ構造が存在します。

宣言

空の配列を作成するための構文は2つあります。

let arr = new Array();
let arr = [];

ほとんどの場合、2番目の構文が使用されます。角括弧の中に初期要素を指定できます。

let fruits = ["Apple", "Orange", "Plum"];

配列要素には、0から始まる番号が付けられます。

要素は角括弧内の番号で取得できます。

let fruits = ["Apple", "Orange", "Plum"];

alert( fruits[0] ); // Apple
alert( fruits[1] ); // Orange
alert( fruits[2] ); // Plum

要素を置き換えることができます。

fruits[2] = 'Pear'; // now ["Apple", "Orange", "Pear"]

…または、新しい要素を配列に追加します。

fruits[3] = 'Lemon'; // now ["Apple", "Orange", "Pear", "Lemon"]

配列内の要素の合計数はlengthです。

let fruits = ["Apple", "Orange", "Plum"];

alert( fruits.length ); // 3

alertを使って配列全体を表示することもできます。

let fruits = ["Apple", "Orange", "Plum"];

alert( fruits ); // Apple,Orange,Plum

配列には任意の型の要素を格納できます。

例えば

// mix of values
let arr = [ 'Apple', { name: 'John' }, true, function() { alert('hello'); } ];

// get the object at index 1 and then show its name
alert( arr[1].name ); // John

// get the function at index 3 and run it
arr[3](); // hello
末尾のカンマ

配列はオブジェクトと同様に、カンマで終わることができます。

let fruits = [
  "Apple",
  "Orange",
  "Plum",
];

「末尾のカンマ」スタイルは、すべての行が似たようになるため、項目の挿入/削除が簡単になります。

「at」で最後の要素を取得

最近の追加
これは最近言語に追加されたものです。古いブラウザではポリフィルが必要になる場合があります。

配列の最後の要素が必要だとしましょう。

一部のプログラミング言語では、fruits[-1]のように同じ目的で負のインデックスを使用できます。

ただし、JavaScriptでは機能しません。角括弧のインデックスは文字通りに扱われるため、結果はundefinedになります。

最後の要素のインデックスを明示的に計算してアクセスできます。fruits[fruits.length - 1]

let fruits = ["Apple", "Orange", "Plum"];

alert( fruits[fruits.length-1] ); // Plum

少し面倒ではありませんか?変数名を2回記述する必要があります。

幸いなことに、より短い構文があります。fruits.at(-1)

let fruits = ["Apple", "Orange", "Plum"];

// same as fruits[fruits.length-1]
alert( fruits.at(-1) ); // Plum

言い換えれば、arr.at(i)

  • i >= 0の場合、arr[i]とまったく同じです。
  • iの負の値の場合、配列の末尾から後退します。

メソッド pop/push, shift/unshift

キューは配列の最も一般的な用途の1つです。コンピュータサイエンスでは、これは2つの操作をサポートする要素の順序付きコレクションを意味します。

  • push は要素を末尾に追加します。
  • shift は先頭から要素を取得し、キューを前進させ、2番目の要素が1番目になります。

配列は両方の操作をサポートしています。

実際には、これは非常に頻繁に必要になります。たとえば、画面に表示する必要があるメッセージのキューなどです。

配列には別のユースケースがあります。それはスタックという名前のデータ構造です。

それは2つの操作をサポートします。

  • push は要素を末尾に追加します。
  • pop は末尾から要素を取得します。

したがって、新しい要素は常に「末尾」から追加または取得されます。

スタックは通常、トランプのパックとして説明されます。新しいカードは一番上に追加されるか、一番上から取得されます。

スタックの場合、最後にプッシュされたアイテムが最初に受信されます。これはLIFO(Last-In-First-Out)原則とも呼ばれます。キューの場合、FIFO(First-In-First-Out)があります。

JavaScriptの配列は、キューとスタックの両方として機能できます。要素を先頭または末尾のどちらにも追加/削除できます。

コンピュータサイエンスでは、これを許可するデータ構造は両端キューと呼ばれます。

配列の末尾を操作するメソッド

pop

配列の最後の要素を抽出し、それを返します。

let fruits = ["Apple", "Orange", "Pear"];

alert( fruits.pop() ); // remove "Pear" and alert it

alert( fruits ); // Apple, Orange

fruits.pop()fruits.at(-1) の両方が配列の最後の要素を返しますが、fruits.pop() はそれを削除することで配列を変更もします。

push

配列の末尾に要素を追加します。

let fruits = ["Apple", "Orange"];

fruits.push("Pear");

alert( fruits ); // Apple, Orange, Pear

fruits.push(...) の呼び出しは fruits[fruits.length] = ... と同じです。

配列の先頭を操作するメソッド

shift

配列の最初の要素を抽出し、それを返します。

let fruits = ["Apple", "Orange", "Pear"];

alert( fruits.shift() ); // remove Apple and alert it

alert( fruits ); // Orange, Pear
unshift

配列の先頭に要素を追加します。

let fruits = ["Orange", "Pear"];

fruits.unshift('Apple');

alert( fruits ); // Apple, Orange, Pear

メソッド pushunshift は一度に複数の要素を追加できます。

let fruits = ["Apple"];

fruits.push("Orange", "Peach");
fruits.unshift("Pineapple", "Lemon");

// ["Pineapple", "Lemon", "Apple", "Orange", "Peach"]
alert( fruits );

内部

配列は特別な種類のオブジェクトです。プロパティ arr[0] にアクセスするために使用される角括弧は、実際にはオブジェクトの構文に由来しています。これは基本的に obj[key] と同じであり、arr はオブジェクトであり、数値はキーとして使用されます。

それらは、順序付けられたデータコレクションを操作するための特別なメソッドと、length プロパティを提供することによりオブジェクトを拡張します。しかし、そのコアでは依然としてオブジェクトです。

JavaScriptには8つの基本的なデータ型しかないことを覚えておいてください(詳細についてはデータ型の章を参照してください)。配列はオブジェクトであり、したがってオブジェクトのように動作します。

たとえば、参照でコピーされます。

let fruits = ["Banana"]

let arr = fruits; // copy by reference (two variables reference the same array)

alert( arr === fruits ); // true

arr.push("Pear"); // modify the array by reference

alert( fruits ); // Banana, Pear - 2 items now

…しかし、配列を本当に特別なものにしているのは、その内部表現です。エンジンは、この章の図に描かれているように、要素を連続したメモリ領域に次々と格納しようとします。また、配列を非常に高速に動作させるための他の最適化もあります。

しかし、配列を「順序付きコレクション」として扱うことをやめ、通常のオブジェクトであるかのように扱い始めると、これらはすべて壊れます。

たとえば、技術的にはこれを行うことができます。

let fruits = []; // make an array

fruits[99999] = 5; // assign a property with the index far greater than its length

fruits.age = 25; // create a property with an arbitrary name

これは、配列が基本的にはオブジェクトであるため可能です。任意のプロパティを配列に追加できます。

しかし、エンジンは私たちが配列を通常のオブジェクトとして操作していることを見抜きます。配列固有の最適化はそのようなケースには適していないため、オフになり、そのメリットはなくなります。

配列を誤用する方法

  • arr.test = 5 のように、数値ではないプロパティを追加します。
  • arr[0] を追加してから arr[1000] を追加するなど、穴を作ります(その間に何もありません)。
  • arr[1000]arr[999] のように、配列を逆順に埋めます。

配列は、*順序付けられたデータ*を操作するための特別な構造として考えてください。配列はそれのための特別なメソッドを提供します。配列は、連続した順序付けられたデータを操作するためにJavaScriptエンジン内で慎重に調整されています。このように使用してください。また、任意のキーが必要な場合は、実際には通常のオブジェクト {} が必要になる可能性が高くなります。

パフォーマンス

メソッド push/pop は高速に実行されますが、shift/unshift は低速です。

配列の先頭よりも末尾を操作する方が速いのはなぜですか?実行中に何が起こるかを見てみましょう。

fruits.shift(); // take 1 element from the start

インデックス 0 を持つ要素を取得して削除するだけでは十分ではありません。他の要素も番号を振り直す必要があります。

shift 操作は3つのことを行う必要があります。

  1. インデックス 0 を持つ要素を削除します。
  2. すべての要素を左に移動し、インデックス 1 から 0 へ、2 から 1 へというように番号を振り直します。
  3. length プロパティを更新します。

配列内の要素が多いほど、移動に時間がかかり、メモリ内の操作が増えます。

同様のことが unshift でも起こります。配列の先頭に要素を追加するには、最初に既存の要素を右に移動し、インデックスを増やす必要があります。

では、push/pop はどうでしょうか?それらは何も移動する必要はありません。末尾から要素を抽出するために、pop メソッドはインデックスをクリアし、length を短縮します。

pop 操作のアクション

fruits.pop(); // take 1 element from the end

pop メソッドは他の要素がインデックスを保持するため、何も移動する必要はありません。そのため、非常に高速です。

push メソッドでも同様です。

ループ

配列項目をループ処理する最も古い方法の1つは、インデックスに対する for ループです。

let arr = ["Apple", "Orange", "Pear"];

for (let i = 0; i < arr.length; i++) {
  alert( arr[i] );
}

しかし、配列には別の形式のループ、for..of があります。

let fruits = ["Apple", "Orange", "Plum"];

// iterates over array elements
for (let fruit of fruits) {
  alert( fruit );
}

for..of は現在の要素の番号へのアクセスを提供しませんが、ほとんどの場合、それで十分です。そして、より短いです。

技術的には、配列はオブジェクトであるため、for..in を使用することも可能です。

let arr = ["Apple", "Orange", "Pear"];

for (let key in arr) {
  alert( arr[key] ); // Apple, Orange, Pear
}

しかし、それは実際には悪い考えです。それには潜在的な問題があります。

  1. for..in ループは、数値プロパティだけでなく、すべてのプロパティを反復処理します。

    ブラウザやその他の環境には、いわゆる「配列のような」オブジェクトがあり、配列のように見えます。つまり、length とインデックスプロパティを持っていますが、通常は必要としない数値以外のプロパティやメソッドも持つ場合があります。for..in ループはそれらも列挙してしまいます。したがって、配列のようなオブジェクトを扱う必要がある場合、これらの「余分な」プロパティが問題になる可能性があります。

  2. for..in ループは配列ではなく汎用オブジェクト向けに最適化されているため、10〜100倍遅くなります。もちろん、それでも非常に高速です。高速化が重要になるのはボトルネックの場合だけです。それでも、この違いを認識しておく必要があります。

一般的に、配列には for..in を使用すべきではありません。

「length」について

length プロパティは、配列を変更すると自動的に更新されます。正確に言うと、配列内の値の数ではなく、最大の数値インデックスに 1 を加えたものです。

たとえば、大きなインデックスを持つ単一の要素は、大きな長さになります。

let fruits = [];
fruits[123] = "Apple";

alert( fruits.length ); // 124

通常、このような配列は使用しないことに注意してください。

length プロパティのもう 1 つの興味深い点は、書き込み可能であるということです。

手動で増やすと、特に何も起こりません。しかし、減らすと、配列は切り詰められます。このプロセスは不可逆的です。以下に例を示します。

let arr = [1, 2, 3, 4, 5];

arr.length = 2; // truncate to 2 elements
alert( arr ); // [1, 2]

arr.length = 5; // return length back
alert( arr[3] ); // undefined: the values do not return

したがって、配列をクリアする最も簡単な方法は、arr.length = 0; です。

new Array()

配列を作成するもう 1 つの構文があります。

let arr = new Array("Apple", "Pear", "etc");

角括弧 [] の方が短いため、めったに使用されません。また、これには注意が必要な機能があります。

new Array が数値である単一の引数で呼び出されると、項目なしで、指定された長さを持つ配列が作成されます。

どのようにして自滅してしまうかを見てみましょう。

let arr = new Array(2); // will it create an array of [2] ?

alert( arr[0] ); // undefined! no elements.

alert( arr.length ); // length 2

このような驚きを避けるために、本当に何をしているのかを理解している場合を除き、通常は角括弧を使用します。

多次元配列

配列は、それ自体が配列である項目を持つことができます。たとえば、行列を格納するために多次元配列に使用できます。

let matrix = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
];

alert( matrix[1][1] ); // 5, the central element

toString

配列には、要素のカンマ区切りのリストを返す独自の toString メソッドの実装があります。

例えば

let arr = [1, 2, 3];

alert( arr ); // 1,2,3
alert( String(arr) === '1,2,3' ); // true

また、これを試してみましょう。

alert( [] + 1 ); // "1"
alert( [1] + 1 ); // "11"
alert( [1,2] + 1 ); // "1,21"

配列には Symbol.toPrimitive も、有効な valueOf もありません。toString 変換のみを実装しているため、ここで [] は空文字列になり、[1]"1" に、[1,2]"1,2" になります。

二項プラス "+" 演算子が何らかのものを文字列に追加する場合、それも文字列に変換するため、次のステップは次のようになります。

alert( "" + 1 ); // "1"
alert( "1" + 1 ); // "11"
alert( "1,2" + 1 ); // "1,21"

== で配列を比較しない

JavaScript の配列は、他のいくつかのプログラミング言語とは異なり、演算子 == で比較すべきではありません。

この演算子には配列に対する特別な処理はなく、他のオブジェクトと同様に機能します。

ルールを思い出してみましょう。

  • 2 つのオブジェクトが == で等しいのは、同じオブジェクトへの参照である場合のみです。
  • == の引数の 1 つがオブジェクトで、もう 1 つがプリミティブの場合、オブジェクトは「オブジェクトのプリミティブ変換」の章で説明したように、プリミティブに変換されます。
  • …互いに == で等しく、他には何も等しくない nullundefined を除いて。

厳密な比較 === は、型を変換しないため、さらに単純です。

したがって、配列を == で比較する場合、まったく同じ配列を参照する 2 つの変数を比較しない限り、それらは決して同じではありません。

例を示します。

alert( [] == [] ); // false
alert( [0] == [0] ); // false

これらの配列は、技術的には異なるオブジェクトです。したがって、それらは等しくありません。== 演算子は、項目ごとの比較を行いません。

プリミティブとの比較も、一見奇妙な結果になる可能性があります。

alert( 0 == [] ); // true

alert('0' == [] ); // false

ここで、両方の場合において、プリミティブを配列オブジェクトと比較します。したがって、配列 [] は比較の目的でプリミティブに変換され、空文字列 '' になります。

次に、「型変換」の章で説明したように、比較プロセスはプリミティブで続行されます。

// after [] was converted to ''
alert( 0 == '' ); // true, as '' becomes converted to number 0

alert('0' == '' ); // false, no type conversion, different strings

では、どのように配列を比較すればよいのでしょうか?

それは簡単です。== 演算子を使用しないでください。代わりに、ループ内または次の章で説明する反復メソッドを使用して、項目ごとに比較します。

まとめ

配列は、順序付けられたデータ項目を格納および管理するのに適した特殊な種類のオブジェクトです。

宣言

// square brackets (usual)
let arr = [item1, item2...];

// new Array (exceptionally rare)
let arr = new Array(item1, item2...);

new Array(number) の呼び出しは、指定された長さを持つが、要素のない配列を作成します。

  • length プロパティは、配列の長さ、または正確に言うと、最後の数値インデックスに 1 を加えたものです。これは、配列メソッドによって自動調整されます。
  • 手動で length を短縮すると、配列は切り詰められます。

要素の取得

  • arr[0] のように、インデックスで要素を取得できます。
  • また、負のインデックスを許可する at(i) メソッドを使用することもできます。i の値が負の場合、配列の末尾から戻ります。i >= 0 の場合、arr[i] と同じように動作します。

次の操作で配列をデキューとして使用できます。

  • push(...items) は、末尾に items を追加します。
  • pop() は、末尾から要素を削除し、それを返します。
  • shift() は、先頭から要素を削除し、それを返します。
  • unshift(...items) は、先頭に items を追加します。

配列の要素を反復処理するには

  • for (let i=0; i<arr.length; i++) – 最速で動作し、古いブラウザとも互換性があります。
  • for (let item of arr) – 項目のみを対象とした最新の構文
  • for (let i in arr) – 絶対に使用しないでください。

配列を比較するには、== 演算子 (および >, < など) を使用しないでください。これらは配列に対する特別な処理を行いません。それらは他のオブジェクトとして処理し、それは通常望ましい動作ではありません。

代わりに、for..of ループを使用して、配列を項目ごとに比較できます。

次の章「配列メソッド」で、配列を操作するためのメソッドについてさらに詳しく学びます。

課題

重要度: 3

このコードは何を表示しますか?

let fruits = ["Apples", "Pear", "Orange"];

// push a new value into the "copy"
let shoppingCart = fruits;
shoppingCart.push("Banana");

// what's in fruits?
alert( fruits.length ); // ?

結果は 4 です。

let fruits = ["Apples", "Pear", "Orange"];

let shoppingCart = fruits;

shoppingCart.push("Banana");

alert( fruits.length ); // 4

これは、配列がオブジェクトであるためです。したがって、shoppingCartfruits の両方が同じ配列への参照です。

重要度: 5

5 つの配列操作を試してみましょう。

  1. 項目「Jazz」と「Blues」を持つ配列 styles を作成します。
  2. 末尾に「Rock-n-Roll」を追加します。
  3. 中央の値を「Classics」に置き換えます。中央の値を見つけるためのコードは、奇数長の任意の配列で機能する必要があります。
  4. 配列の最初の値を削除し、表示します。
  5. 配列の先頭に RapReggae を追加します。

処理中の配列

Jazz, Blues
Jazz, Blues, Rock-n-Roll
Jazz, Classics, Rock-n-Roll
Classics, Rock-n-Roll
Rap, Reggae, Classics, Rock-n-Roll
let styles = ["Jazz", "Blues"];
styles.push("Rock-n-Roll");
styles[Math.floor((styles.length - 1) / 2)] = "Classics";
alert( styles.shift() );
styles.unshift("Rap", "Reggae");
重要度: 5

結果は何ですか?理由は?

let arr = ["a", "b"];

arr.push(function() {
  alert( this );
});

arr[2](); // ?

呼び出し arr[2]() は、構文的に昔ながらの obj[method]() であり、obj の役割には arr があり、method の役割には 2 があります。

したがって、オブジェクトメソッドとして関数 arr[2] を呼び出します。当然、オブジェクト arr を参照する this を受け取り、配列を出力します。

let arr = ["a", "b"];

arr.push(function() {
  alert( this );
})

arr[2](); // a,b,function(){...}

配列には 3 つの値があります。最初は 2 つあり、それに加えて関数です。

重要度: 4

次の処理を行う関数 sumInput() を作成します。

  • prompt を使用してユーザーに値を尋ね、値を配列に格納します。
  • ユーザーが数値以外の値、空の文字列を入力するか、「キャンセル」を押すと、質問を終了します。
  • 配列項目の合計を計算して返します。

P.S. ゼロ 0 は有効な数値です。ゼロで入力を停止しないでください。

デモを実行

このソリューションの微妙でありながら重要な点に注意してください。value = +value の後で、空文字列 (停止記号) をゼロ (有効な数値) から区別できなくなるため、prompt の直後に value を数値に変換しません。代わりに、後で実行します。

function sumInput() {

  let numbers = [];

  while (true) {

    let value = prompt("A number please?", 0);

    // should we cancel?
    if (value === "" || value === null || !isFinite(value)) break;

    numbers.push(+value);
  }

  let sum = 0;
  for (let number of numbers) {
    sum += number;
  }
  return sum;
}

alert( sumInput() );
重要度: 2

入力は数値の配列です。例:arr = [1, -2, 3, 4, -9, 6]

タスクは、項目の合計が最大になる arr の連続部分配列を見つけることです。

その合計を返す関数 getMaxSubSum(arr) を作成します。

例えば

getMaxSubSum([-1, 2, 3, -9]) == 5 (the sum of highlighted items)
getMaxSubSum([2, -1, 2, 3, -9]) == 6
getMaxSubSum([-1, 2, 3, -9, 11]) == 11
getMaxSubSum([-2, -1, 1, 2]) == 3
getMaxSubSum([100, -9, 2, -3, 5]) == 100
getMaxSubSum([1, 2, 3]) == 6 (take all)

すべての項目が負の場合、何も取らない (部分配列が空) という意味になり、合計はゼロになります。

getMaxSubSum([-1, -2, -3]) = 0

高速なソリューションを考えてみてください。可能な場合は、O(n2) または O(n) で。

テスト付きのサンドボックスを開きます。

低速なソリューション

可能なすべての部分合計を計算できます。

最も簡単な方法は、すべての要素を取得し、そこから始まるすべての部分配列の合計を計算することです。

たとえば、[-1, 2, 3, -9, 11] の場合

// Starting from -1:
-1
-1 + 2
-1 + 2 + 3
-1 + 2 + 3 + (-9)
-1 + 2 + 3 + (-9) + 11

// Starting from 2:
2
2 + 3
2 + 3 + (-9)
2 + 3 + (-9) + 11

// Starting from 3:
3
3 + (-9)
3 + (-9) + 11

// Starting from -9
-9
-9 + 11

// Starting from 11
11

コードは実際にはネストされたループです。外側のループは配列要素を反復処理し、内側のループは現在の要素から始まる部分合計をカウントします。

function getMaxSubSum(arr) {
  let maxSum = 0; // if we take no elements, zero will be returned

  for (let i = 0; i < arr.length; i++) {
    let sumFixedStart = 0;
    for (let j = i; j < arr.length; j++) {
      sumFixedStart += arr[j];
      maxSum = Math.max(maxSum, sumFixedStart);
    }
  }

  return maxSum;
}

alert( getMaxSubSum([-1, 2, 3, -9]) ); // 5
alert( getMaxSubSum([-1, 2, 3, -9, 11]) ); // 11
alert( getMaxSubSum([-2, -1, 1, 2]) ); // 3
alert( getMaxSubSum([1, 2, 3]) ); // 6
alert( getMaxSubSum([100, -9, 2, -3, 5]) ); // 100

このソリューションの時間計算量は、O(n2) です。言い換えれば、配列サイズを 2 倍にすると、アルゴリズムの動作時間は 4 倍になります。

大きな配列 (1000、10000 個以上の項目) の場合、このようなアルゴリズムは深刻な動作の遅さを招く可能性があります。

高速なソリューション

配列を反復処理し、変数 s に要素の現在の部分合計を保持しましょう。s がある時点で負になった場合は、s=0 を割り当てます。そのようなすべての s の最大値が答えになります。

説明があいまいすぎる場合は、コードを見てください。十分に短いです。

function getMaxSubSum(arr) {
  let maxSum = 0;
  let partialSum = 0;

  for (let item of arr) { // for each item of arr
    partialSum += item; // add it to partialSum
    maxSum = Math.max(maxSum, partialSum); // remember the maximum
    if (partialSum < 0) partialSum = 0; // zero if negative
  }

  return maxSum;
}

alert( getMaxSubSum([-1, 2, 3, -9]) ); // 5
alert( getMaxSubSum([-1, 2, 3, -9, 11]) ); // 11
alert( getMaxSubSum([-2, -1, 1, 2]) ); // 3
alert( getMaxSubSum([100, -9, 2, -3, 5]) ); // 100
alert( getMaxSubSum([1, 2, 3]) ); // 6
alert( getMaxSubSum([-1, -2, -3]) ); // 0

アルゴリズムには、正確に 1 回の配列パスが必要なので、時間計算量は O(n) です。

アルゴリズムの詳細については、こちらをご覧ください: 最大部分配列問題。それでも、なぜ機能するのかが不明な場合は、上記の例でアルゴリズムをトレースし、どのように機能するかを確認してください。それはどんな言葉よりも優れています。

テスト付きのソリューションをサンドボックスで開きます。

チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善すべき点について提案がある場合は、コメントする代わりに GitHub issue を送信するか、プルリクエストを送信してください。
  • 記事の中で何かを理解できない場合は、詳しく説明してください。
  • コードを数語挿入するには、<code> タグを使用し、複数行の場合は <pre> タグで囲み、10 行を超える場合はサンドボックスを使用してください(plnkrjsbincodepen…)