コンテンツにスキップ

JavaScript/配列

出典: フリー教科書『ウィキブックス(Wikibooks)』

JavaScriptには、配列という便利なデータ構造があります。配列は、複数の値を一つのインスタンスに格納することができます。これにより、複数の値を扱う場合に便利です。配列には、文字列や数値などのプリミティブ型の値をはじめ、JavaScriptの全てのオブジェクトを格納することができます。

この章では、配列とその操作について学びます。配列の作成、要素の追加や削除、配列の検索など、配列に対する基本的な操作を学びます。また、配列を扱う上で役立つメソッドについても学びます。

配列はJavaScriptにおいて非常に重要なデータ構造であり、JavaScriptを学ぶ上で欠かせない部分です。この章を通じて、配列について深く理解し、JavaScriptのプログラミング能力を向上させてください。

配列リテラル

[編集]

JavaScriptでは、配列を作成するために配列リテラルと呼ばれる構文があります。配列リテラルは、角括弧( [ ] )で要素を囲んで定義されます。

以下は、配列リテラルを用いた配列の作成の例です。

// 配列リテラルで配列を作成する
const fruits = ['apple', 'banana', 'orange'];

// 配列の中身を表示する
console.log(fruits); // ['apple', 'banana', 'orange']

上記の例では、fruitsという名前の配列を作成しています。配列リテラルの中に'apple''banana''orange'という3つの文字列が要素として含まれています。console.log()を用いて、配列の内容をコンソールに表示しています。

また、配列リテラルを使用することで、空の配列を作成することもできます。

// 空の配列を作成する
const emptyArray = [];

異なる型のオブジェクトを含んだ配列を作ることも出来ます。

const ary = [null, false, true, { a: 0, b: 1 }, 123, 3.14, 5n, [0, 1, 2], undefined];
console.log(ary); // ,false,true,[object Object],123,3.14,5,0,1,2,
console.log(ary[4]); // 123
  1. 配列aryを定義しています。配列の中には、nullfalsetrue、オブジェクト { a: 0, b: 1 }、数値 123、小数 3.14、BigInt 5n、配列 [0, 1, 2]undefined が含まれています。
  2. ary配列を console.log() 関数を用いて表示しています。console.log() 関数は、与えられた値をコンソールに表示します。配列を表示する場合、要素が , で区切られて並べられます。オブジェクト { a: 0, b: 1 }[object Object] と表示されます。
  3. ary配列の4番目の要素を console.log() 関数を用いて表示しています。配列のインデックスは、0から始まるため、4番目の要素は 123 です。console.log() 関数によって、 123 がコンソールに表示されます。

このように、配列リテラルは配列を簡単に作成するための便利な構文です。

添字を使った要素の参照

[編集]

JavaScriptの配列は、複数の値を格納できるデータ構造で、それぞれの値は配列内で一意のインデックス番号によって参照されます。インデックス番号は、0から始まる整数値で、配列内の要素の順序に従って割り当てられます。

配列の要素を参照するには、角括弧 [] 内に要素のインデックス番号を指定します。例えば、以下のように書くことで、配列aryの2番目の要素にアクセスすることができます。

const ary = ["apple", "banana", "cherry"];
const secondElement = ary[1]; // "banana"
const noExistElement = ary[3]; // undefined
  1. このコードでは、文字列の配列 ary を定義しています。配列の各要素には、それぞれインデックス番号が割り当てられ、0から始まる整数値がインデックス番号になります。この場合、ary 配列の最初の要素は "apple" で、インデックス番号は 0 です。次の要素は "banana" で、インデックス番号は 1 です。最後の要素は "cherry" で、インデックス番号は 2 です。
  2. 次に、ary[1] という表記を使用して、ary 配列の 1 番目の要素 "banana" にアクセスしています。この要素は、変数 secondElement に代入され、文字列 "banana" が格納されます。
  3. さらに、ary[3] という表記を使用して、ary 配列の存在しないインデックス番号である 3 番目の要素にアクセスしています。この場合、配列に存在しない要素にアクセスしているため、undefined が返され、変数 noExistElement に代入されます。

C言語の配列のように、要素数を予め決め全ての要素の型が同じオブジェクトに型付き配列があります。

疎な配列

配列の length プロパティを変更したり、大きなインデックスを使って要素の書き換えを行ったらどうなるでしょう。

let ary = [1, 2, 3];

ary.length = 10;
console.log(ary); // "1,2,3,,,,,,,"
console.log(ary[7]); // undefined
ary[6] = 99;
ary[8] = void 0;
ary[100] = "str";
delete ary[1];
ary.forEach((x,i) => console.log(`${i}: ${x}`));
// 0: 1
// 2: 3
// 6: 99
// 8: undefined
// 100: str
  • 3行目: 元々の配列 ary のlength 3 を超える 10 をlengthに代入
  • 4行目: ary を文字列化すると "1,2,3,,,,,,,"
  • 5行目: 拡大された範囲の要素は、 undefined (に見えます、本当?)
  • 6行目: 拡大された範囲の要素に数値を代入
  • 7行目: 拡大された範囲の要素に void 0 を代入(undefinedを得るイディオム)
  • 8行目: 拡大された範囲すら超える遠い位置に文字列を代入
  • 9行目: ary の二番目の要素を delete
  • 10行目: forEach メソッドでそれぞれの要素をインデックスとともに表示すると、削除された2番目の配列要素と代入していない 4,5,7,9..99番目要素は callback 関数の呼び出しそのものが有りません。
    void 0 を代入した8番目の要素は callback 関数が呼び出されています(未代入と削除された要素が undefined に見えるのは表現上の問題です)。

このように初期化されていない要素を持つ配列の要素の事を、疎な配列と言います。数学の疎行列(要素の殆どが 0 な行列)とは異なる概念なので字面に騙されないようにして下さい。

Array

[編集]

JavaScriptにおける配列は、Arrayコンストラクタ関数を使用して作成することができます。Arrayコンストラクタ関数は、以下のように使われます。

const myArray = new Array(); // 空の配列を作成
const myArray2 = new Array(length); // length要素の配列を作成
const myArray3 = new Array(element0, element1, ..., elementN); // element0, element1, ..., elementNを含む配列を作成

上記のコードのlengthは、配列の長さを表します。element0element1、...、elementNは、配列に含める要素を表します。

また、Arrayコンストラクタ関数の代わりに、配列リテラルを使用することもできます。例えば、以下のように書くことができます。

const myArray = []; // 空の配列を作成
const myArray2 = [element0, element1, ..., elementN]; // element0, element1, ..., elementNを含む配列を作成

こちらは、より簡潔で一般的に使用されます。

スプレッド構文

[編集]

JavaScriptにおけるスプレッド構文は、配列の要素を展開するために使用されます。配列の要素を展開することにより、別の配列に追加することができます。スプレッド構文は、...という記号で表されます。

例えば、以下のように配列を作成して、スプレッド構文を使用して配列の要素を展開し、新しい配列に追加することができます。

const myArray = [1, 2, 3];
const myNewArray = [...myArray, 4, 5, 6];
console.log(myNewArray); // [1, 2, 3, 4, 5, 6]

上記の例では、...myArrayを使用して、myArrayの要素を新しい配列に追加しています。[...myArray, 4, 5, 6]は、myArrayの要素に456を追加した新しい配列を作成することになります。

また、スプレッド構文を使用して、配列を結合することもできます。

const array1 = [1, 2, 3];
const array2 = [4, 5, 6];
const array3 = [...array1, ...array2];
console.log(array3); // [1, 2, 3, 4, 5, 6]

上記の例では、スプレッド構文を使用して、array1array2を結合し、新しい配列array3を作成しています。

スプレッド構文と Arrray.prototype.map メソッドを組み合わせたトリック

const ary = [...Array(5)].map((_, n) => n) // [ 0, 1, 2, 3, 4 ]

このコードは、5つの要素を持つ配列を作成し、それをスプレッド演算子 ... を使って展開します。この結果、空で、要素数が5の配列が作成されます。

そして、mapメソッドが配列の各要素に対して繰り返し処理を実行します。ここで、第一引数 _ は無視され、第二引数 n には現在のインデックスが渡されます。このコードでは、繰り返し処理の中で、各要素にインデックス番号を割り当てています。

その結果、最終的に [0, 1, 2, 3, 4] という配列が作成されます。つまり、このコードは、0から始まり、5未満の整数を持つ配列を作成するための一般的な方法の一例です。

配列の反復処理

[編集]

JavaScriptの配列をイテレーションする方法には、以下のような方法があります。

  1. forループを使う
    forループは、配列のインデックス番号を使用して、配列の各要素を反復処理するための古典的な方法です。以下は、forループを使用して配列をイテレーションする例です。
    const ary = ["apple", "banana", "cherry"];
    for (let i = 0; i < ary.length; i++) {
      console.log(ary[i]);
    }
    
    この場合、forループ内で ary.length プロパティを使用して、配列の長さを取得し、ループ回数を制御しています。
  2. forEachメソッドを使う
    配列には、forEachメソッドが用意されており、このメソッドを使用すると、簡潔に配列の各要素を反復処理することができます。以下は、forEachメソッドを使用して配列をイテレーションする例です。
    const ary = ["apple", "banana", "cherry"];
    ary.forEach((element) => {
      console.log(element);
    });
    
    この場合、forEachメソッドには、コールバック関数が渡されます。コールバック関数は、配列の各要素に対して一度ずつ実行されます。この場合、各要素を element という引数で受け取り、console.log() メソッドで出力しています。
  3. for...ofループを使う
    ES6以降、for...ofループが導入されました。このループは、配列の各要素に対して反復処理を行うための構文です。以下は、for...ofループを使用して配列をイテレーションする例です。
    const ary = ["apple", "banana", "cherry"];
    for (const element of ary) {
      console.log(element);
    }
    
:# この場合、for...ofループにより、配列 ary の各要素が element という変数に順番に代入され、ループが実行されます。ループ本体では、各要素を console.log() メソッドで出力しています。

以上のように、JavaScriptの配列をイテレーションする方法は複数ありますが、どの方法でも同じ結果を得ることができます。

for-in構文の罠

[編集]

JavaScriptには、for...inループという構文があります。この構文は、オブジェクトのプロパティを列挙するためのものですが、配列でも使用することができます。

しかし、配列をfor...inループで列挙する場合、以下のような問題が発生することがあります。

  1. 配列のインデックスは文字列型であるため、文字列型のプロパティも一緒に列挙される。
    const ary = ["apple", "banana", "cherry"];
    ary.foo = "bar";
    
    for (const index in ary) {
      console.log(index);
    }
    
    // Output:
    // 0
    // 1
    // 2
    // foo
    
    この場合、配列に foo というプロパティを追加していますが、for...inループで列挙すると、配列のインデックスとともに、追加されたプロパティも列挙されます。
  2. 配列のプロトタイプチェーンにあるプロパティも列挙される。
    Array.prototype.foo = "bar";
    const ary = ["apple", "banana", "cherry"];
    
    for (const index in ary) {
      console.log(index);
    }
    
    // Output:
    // 0
    // 1
    // 2
    // foo
    
  1. この場合、配列のプロトタイプチェーンにある foo プロパティも列挙されます。

上記のような問題があるため、配列を列挙する場合は、for...inループを使用せず、forループやforEachメソッド、for...ofループなど、適切な方法を使用することが推奨されます。

Array.prototype.reduceメソッド

[編集]

配列の中から最大値を探す

[編集]
const a = []; //巨大配列を乱数で埋め尽くす
for (let i = 0; i < 999999; i++)
  a[i] = Math.random() * 100;

console.log(a.reduce((a, b) => Math.max(a, b), -Infinity)); //reduceは出来る!

console.log(Math.max(...a)); // エラー! RangeError: Maximum call stack size exceeded

このコードは、まず空の配列 a を作成し、ループ内で Math.random() を使用して a 配列に999999個のランダムな数字を詰め込んでいます。

次に、a.reduce((a, b) => Math.max(a, b), -Infinity) を使用して、a 配列の最大値を取得しようとしています。 reduce メソッドは、配列の要素を1つずつ処理し、それらを単一の値にまとめるために使用されます。第1引数の関数は、2つの引数を受け取り、それらの中からどちらが大きいかを返します。ここでは -Infinity が初期値として渡されており、それはすべての値よりも小さいので、reduce メソッドは a 配列の最大値を返します。

Math.max は可変長引数を受け取るため、スプレッド構文を使用して配列 a をそのまま引数として渡すことができます。しかし、可変長引数には引数の数に上限があるため、巨大な配列を引数として渡すと RangeError: Maximum call stack size exceeded エラーが発生します。

一方、 reduce メソッドは引数として渡されるコールバック関数の第1引数に累積値を、第2引数に現在の要素を渡して順次処理を行うため、配列を引数として渡してもスタックオーバーフローは発生しません。

最小値・最大値・合計・総積を一度に求める

[編集]
const a = [];
for (let i = 0; i < 100; i++)
  a[i] = Math.random() * 100;

ans = a.reduce(({min, max, sum, infProd}, n) => {
  min = Math.min(min, n);
  max = Math.max(max, n);
  sum += n;
  infProd *= n;
  return {min, max, sum, infProd};
}, {
  min: Infinity,
  max: -Infinity,
  sum: 0,
  infProd: 1,
});
console.log(`最小値 = ${ans.min}
最大値 = ${ans.max}
算術平均 = ${ans.sum/a.length}
幾何平均 = ${Math.pow(ans.infProd, 1/a.length)}
`);

このコードは、配列aの最小値、最大値、算術平均、幾何平均を求めるものです。配列aは、Math.random()を用いて0以上100未満の数値が100個ランダムに生成されます。

reduceメソッドを使用して、配列aの要素を1つずつ処理していき、その過程で最小値、最大値、総和、無限積を計算しています。reduceメソッドは、第1引数にコールバック関数を取り、第2引数に初期値を取ります。このコードでは、初期値として、最小値には正の無限大、最大値には負の無限大、総和には0、無限積には1を指定しています。コールバック関数の第1引数には、前の処理結果のオブジェクトが、第2引数には現在処理している要素の値が渡されます。コールバック関数では、最小値、最大値、総和、無限積を更新した後、オブジェクトとして返します。

最後に、各値を求めたオブジェクトから取り出して、console.logで出力しています。算術平均は、ans.sumを要素数で割ったもので求められます。幾何平均は、無限積を要素数乗根で割ったもので求められます。

精度改善版
const a = [];
for (let i = 0; i < 100; i++)
  a[i] = Math.random() * 100;

const ans = a.reduce(({min, max, sum, geoMean, c}, n) => {
  // Kahan Summation Algorithm を使用して合計を更新
  const y = n - c;
  const t = sum + y;
  c = (t - sum) - y;
  sum = t;

  // 他の統計的な値も更新
  min = Math.min(min, n);
  max = Math.max(max, n);

  // 幾何平均の計算(Math.hypot()を使用)
  if (isNaN(geoMean)) {
    geoMean = n;
  } else {
    geoMean = Math.hypot(geoMean, n);
  }

  return {min, max, sum, geoMean, c};
}, {
  min: Infinity,
  max: -Infinity,
  sum: 0,
  geoMean: NaN,
  c: 0,
});

// 結果を出力
console.log(`最小値 = ${ans.min}
最大値 = ${ans.max}
算術平均 = ${ans.sum / a.length}
幾何平均 = ${ans.geoMean}
`);
  • Kahan Summation Algorithmを使用しているため、数値の精度が向上しています。
  • 幾何平均の計算に Math.hypot()を使用しており、数値のオーバーフローに対処しています。
  • 初期状態で geoMean をNaNにしておくことで、非数値の場合にも適切に処理できます。

配列の配列

[編集]

JavaScriptでは、配列の要素として、他の配列を含めることができます。このように、配列の中に配列が含まれる構造を「配列の配列」と呼びます。

以下は、配列の配列の例です。

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

この場合、matrix は3つの要素を持ち、それぞれが配列です。各配列の要素は、[行][列] のようにアクセスすることができます。たとえば、行番号が 1、列番号が 2 の要素にアクセスするには、以下のようにします。

console.log(matrix[1][2]); // Output: 6

また、配列の配列に対して、forループやforEachメソッド、for...ofループなどのイテレーションを行うこともできます。ただし、配列の配列を列挙する際には、二重のループが必要になるため、処理が複雑になることがあります。

at()メソッドと仕様バグ

[編集]
ECMA2022から at() メソッドが追加されましたが、引数を整数に変換するときに値の妥当性をチェックしない仕様なので、整数以外がわたらないことをプログラマが保証する必要があり、自明なリテラルを渡す以外の使い方は避けるべきです[1]

at()の機能は、配列に対して .at(-2) のようにメソッド指定すると、末尾から2番目の値を返す配列の要素参照です(ただし[ ]のように左辺値化はできません)。つまり、マイナスのインデックス値を指定すると、末尾から数えた位置にある値を返します。

一方、atメソッドで0以上のインデックスを指定した場合は、通常の配列の数え方と同様に、前から数えた数え方をします。

コード例
const b = [15 ,3 ,7, 41];

console.log(b.at(-2) );
console.log(b.at(0) );

console.log(b.at("foo"));
console.log(b.at(NaN));
console.log(b.at(1.5));

console.log(b["foo"]);
console.log(b[NaN]);
console.log(b[1.5]);
実行結果
7
15
15
15
3
undefined
undefined
undefined
at()の引数に整数以外を与えた時の挙動が異常です。
これは、負のインデックスに対応するために一度引数を整数に強制変換していることが原因です。
”foo” ⇒ 0, NaN ⇒ 0, 1.5 ⇒ 1
この様な挙動をするので、at()を使う場合は、プログラマの責任で整数であることを保証する必要があります。
Relative Indexing Method
「Relative Indexing Method」は、配列の中で負のインデックスを使って末尾からのインデックス指定ができるようにする提案です。

例えば、配列の末尾から2番目の要素を取得する場合、通常はary[ary.length - 2]と記述する必要がありますが、この提案が採用されるとary.at(-2)と記述できるようになります。

このIssueでは、提案に関連するat()メソッドの仕様について議論されています。具体的には、以下のような問題が報告されています。

  1. at()メソッドに渡される引数が数値型ではない場合、エラーが発生するかどうかについての仕様が不明瞭である。
  2. at()メソッドに渡される引数がNaNである場合、エラーが発生するべきかどうかについての仕様が不明瞭である。
  3. at()メソッドに渡される引数が小数である場合、どのように振る舞うべきかについての仕様が不明瞭である。
このような問題が提起されたため、仕様の改善が求められています。

附録

[編集]

チートシート

[編集]
// 配列の作成
const ary = [1, 2, 3];
const emptyArr = [];

// 配列の要素数
const length = ary.length;

// 配列の要素の取得
const firstElement = ary[0];
const lastElement = ary[ary.length - 1];
const secondToFourthElements = ary.slice(1, 4); // [2, 3]
const lastTwoElements = ary.slice(-2); // [2, 3]

// 配列の要素の変更
ary[0] = 0;
ary.push(4);
ary.pop();
ary.unshift(-1);
ary.shift();

// 配列の要素の追加・削除
ary.push(4);
ary.splice(1, 1); // [0, 2, 3, 4]
ary.splice(1, 0, 1.5); // [0, 1.5, 2, 3, 4]

// 配列の要素の検索
const index = ary.indexOf(2);
const lastIndex = ary.lastIndexOf(2);
const includes2 = ary.includes(2);

// 配列の要素のフィルタリング・マッピング
const filtered = ary.filter(num => num % 2 === 0);
const mapped = ary.map(num => num * 2);

// 配列の要素の結合
const ary1 = [1, 2];
const ary2 = [3, 4];
const concatArr = ary1.concat(ary2); // [1, 2, 3, 4]
const spreadArr = [...ary1, ...ary2]; // [1, 2, 3, 4]

// 配列の要素のソート
const sorted = ary.sort((a, b) => a - b);
const reversed = ary.reverse();

// 配列のイテレーション
ary.forEach(num => console.log(num));
const result = ary.reduce((total, num) => total + num, 0);
const mappedArr = ary.map(num => num * 2);
const filteredArr = ary.filter(num => num % 2 === 0);
const every = ary.every(num => num >= 0);
const some = ary.some(num => num === 2);
const found = ary.find(num => num > 2);
const foundIndex = ary.findIndex(num => num > 2);

// 配列の空要素の作成
const emptyArray = Array(5); // [empty × 5]
const filledArray = Array(5).fill(0); // [0, 0, 0, 0, 0]

用語集

[編集]
  1. 配列 (Array): 複数の値を一つの変数に格納するデータ構造。配列内の各要素にはインデックス番号が割り当てられており、各要素はそのインデックス番号で参照される。
  2. 要素 (Element): 配列内の各値のこと。インデックス番号によってアクセスされる。
  3. インデックス (Index): 配列内の各要素に割り当てられた番号。通常、0から始まる整数値である。
  4. 配列リテラル (Array Literal): 配列を作成するための簡潔な構文。[ ] の中に、カンマで区切った値を列挙する。
  5. 配列要素の追加 (Push): 配列の末尾に新しい要素を追加するメソッド。
  6. 配列要素の削除 (Pop): 配列の末尾の要素を削除するメソッド。
  7. 配列要素の取得 (Get): 配列内の要素にアクセスするための方法。添字演算子 [ ] を使用し、インデックス番号を指定する。
  8. 配列要素の変更 (Set): 配列内の要素を変更するための方法。添字演算子 [ ] を使用し、インデックス番号を指定し、新しい値を代入する。
  9. 配列の長さ (Length): 配列内の要素数を取得するプロパティ。配列の末尾のインデックス番号に 1 を加えたものと等しい。
  10. 配列の反復処理 (Iteration): 配列内の要素を順番に処理する方法。forループ、for...ofループ、forEachメソッドなどがある。
  11. スプレッド構文 (Spread Syntax): 配列を展開して、個々の要素を別々の引数や別の配列に挿入するための構文。
  12. 分割代入 (Destructuring Assignment): 配列内の要素を個別の変数に割り当てるための構文。配列の各要素が変数に一致するように、[ ] 内に変数を列挙する。
  1. ^ Should it really be Math.trunc(n) || 0 #49