焦らず学ぶJavaScript(ES2022版)
本書をサポートしていただけますと幸いです:購入 または 寄付
(広告、ブロックしないでください。)

31章 配列(Array



31.1章 クイックリファレンス:配列

JavaScriptの配列は非常に柔軟なデータ構造であり、リスト、スタック、キュー、タプル(例:ペア)などとして使用されます。

配列関連操作の中には、配列を破壊的に変更するものと、元のコンテンツのコピーに変更を適用して新しい配列を非破壊的に生成するものがあります。

31.1.1節 配列の使い方

配列の作成、要素の読み取りと書き込み

// Creating an Array
const arr = ['a', 'b', 'c']; // Array literal
assert.deepEqual(
  arr,
  [ // Array literal
    'a',
    'b',
    'c', // trailing commas are ignored
  ]
);

// Reading elements
assert.equal(
  arr[0], 'a' // negative indices don’t work
);
assert.equal(
  arr.at(-1), 'c' // negative indices work
);

// Writing an element
arr[0] = 'x';
assert.deepEqual(
  arr, ['x', 'b', 'c']
);

配列の長さ

const arr = ['a', 'b', 'c'];
assert.equal(
  arr.length, 3 // number of elements
);
arr.length = 1; // removing elements
assert.deepEqual(
  arr, ['a']
);
arr[arr.length] = 'b'; // adding an element
assert.deepEqual(
  arr, ['a', 'b']
);

.push()による破壊的な要素の追加

const arr = ['a', 'b'];

arr.push('c'); // adding an element
assert.deepEqual(
  arr, ['a', 'b', 'c']
);

// Pushing Arrays (used as arguments via spreading (...)):
arr.push(...['d', 'e']);
assert.deepEqual(
  arr, ['a', 'b', 'c', 'd', 'e']
);

スプレッド構文(...)による非破壊的な要素の追加

const arr1 = ['a', 'b'];
const arr2 = ['c'];
assert.deepEqual(
  [...arr1, ...arr2, 'd', 'e'],
  ['a', 'b', 'c', 'd', 'e']
);

配列のクリア(すべての要素の削除)

// Destructive – affects everyone referring to the Array:
const arr1 = ['a', 'b', 'c'];
arr1.length = 0;
assert.deepEqual(
  arr1, []
);

// Non-destructive – does not affect others referring to the Array:
let arr2 = ['a', 'b', 'c'];
arr2 = [];
assert.deepEqual(
  arr2, []
);

要素のループ処理

const arr = ['a', 'b', 'c'];
for (const value of arr) {
  console.log(value);
}

// Output:
// 'a'
// 'b'
// 'c'

インデックスと値のペアのループ処理

const arr = ['a', 'b', 'c'];
for (const [index, value] of arr.entries()) {
  console.log(index, value);
}

// Output:
// 0, 'a'
// 1, 'b'
// 2, 'c'

配列リテラルを使用できない場合(例:事前に長さがわからない場合、または大きすぎる場合)の配列の作成と初期化

const four = 4;

// Empty Array that we’ll fill later
assert.deepEqual(
  new Array(four),
  [ , , , ,] // four holes; last comma is ignored
);

// An Array filled with a primitive value
assert.deepEqual(
  new Array(four).fill(0),
  [0, 0, 0, 0]
);

// An Array filled with objects
// Why not .fill()? We’d get single object, shared multiple times.
assert.deepEqual(
  Array.from({length: four}, () => ({})),
  [{}, {}, {}, {}]
);

// A range of integers
assert.deepEqual(
  Array.from({length: four}, (_, i) => i),
  [0, 1, 2, 3]
);

31.1.2節 配列メソッド

この節では、Array APIの概要を簡単に説明します。この章の最後には、より包括的なクイックリファレンスがあります。

既存の配列から新しい配列を導出する

> ['■','●','▲'].slice(1, 3)
['●','▲']
> ['■','●','■'].filter(x => x==='■') 
['■','■']

> ['▲','●'].map(x => x+x)
['▲▲','●●']
> ['▲','●'].flatMap(x => [x,x])
['▲','▲','●','●']

指定されたインデックスにある配列要素を削除する

// .filter(): remove non-destructively
const arr1 = ['■','●','▲'];
assert.deepEqual(
  arr1.filter((_, index) => index !== 1),
  ['■','▲']
);
assert.deepEqual(
  arr1, ['■','●','▲'] // unchanged
);

// .splice(): remove destructively
const arr2 = ['■','●','▲'];
arr2.splice(1, 1); // start at 1, delete 1 element
assert.deepEqual(
  arr2, ['■','▲'] // changed
);

配列のサマリーを計算する

> ['■','●','▲'].some(x => x==='●')
true
> ['■','●','▲'].every(x => x==='●')
false

> ['■','●','▲'].join('-')
'■-●-▲'

> ['■','▲'].reduce((result,x) => result+x, '●')
'●■▲'
> ['■','▲'].reduceRight((result,x) => result+x, '●')
'●▲■'

反転と初期化

// .reverse() changes and returns `arr`
const arr = ['■','●','▲'];
assert.deepEqual(
  arr.reverse(), arr
);
// `arr` was changed:
assert.deepEqual(
  arr, ['▲','●','■']
);

// .fill() works the same way:
assert.deepEqual(
  ['■','●','▲'].fill('●'),
  ['●','●','●']
);

.sort()も配列を変更して返します

// By default, string representations of the Array elements
// are sorted lexicographically:
assert.deepEqual(
  [200, 3, 10].sort(),
  [10, 200, 3]
);

// Sorting can be customized via a callback:
assert.deepEqual(
  [200, 3, 10].sort((a,b) => a - b), // sort numerically
  [ 3, 10, 200 ]
);

配列要素の検索

> ['■','●','■'].includes('■')
true
> ['■','●','■'].indexOf('■')
0
> ['■','●','■'].lastIndexOf('■')
2
> ['■','●','■'].find(x => x==='■')
'■'
> ['■','●','■'].findIndex(x => x==='■')
0

先頭または末尾への要素の追加または削除

// Adding and removing at the start
const arr1 = ['■','●'];
arr1.unshift('▲');
assert.deepEqual(
  arr1, ['▲','■','●']
);
arr1.shift();
assert.deepEqual(
  arr1, ['■','●']
);

// Adding and removing at the end
const arr2 = ['■','●'];
arr2.push('▲');
assert.deepEqual(
  arr2, ['■','●','▲']
);
arr2.pop();
assert.deepEqual(
  arr2, ['■','●']
);

31.2章 JavaScriptにおける配列の2つの使用方法

JavaScriptでは、配列を2つの方法で使用できます。

実際には、これらの2つの方法はよく混在して使用されます。

特に、シーケンス配列は非常に柔軟なため、(従来の)配列、スタック、キューとして使用できます。その方法は後で説明します。

31.3章 基本的な配列操作

31.3.1節 配列の作成、読み取り、書き込み

配列を作成する最良の方法は、配列リテラルを使用することです。

const arr = ['a', 'b', 'c'];

配列リテラルは角括弧[]で始まり、終わります。3つの要素'a''b''c'を持つ配列を作成します。

末尾のカンマは配列リテラルでは許可され、無視されます。

const arr = [
  'a',
  'b',
  'c',
];

配列要素を読み取るには、角括弧の中にインデックスを入れます(インデックスは0から始まります)。

const arr = ['a', 'b', 'c'];
assert.equal(arr[0], 'a');

配列要素を変更するには、インデックス付きの配列に代入します。

const arr = ['a', 'b', 'c'];
arr[0] = 'x';
assert.deepEqual(arr, ['x', 'b', 'c']);

配列インデックスの範囲は32ビット(最大長を除く)です:[0, 232−1)

31.3.2節 配列の.length

すべての配列には.lengthプロパティがあり、これを使用して配列の要素数を取得および変更(!)することができます。

配列の長さは常に最大のインデックスに1を加えた値になります。

> const arr = ['a', 'b'];
> arr.length
2

長さのインデックスの配列に書き込むと、要素が追加されます。

> arr[arr.length] = 'c';
> arr
[ 'a', 'b', 'c' ]
> arr.length
3

要素を(破壊的に)追加する別の方法は、.push()配列メソッドを使用することです。

> arr.push('d');
> arr
[ 'a', 'b', 'c', 'd' ]

.lengthを設定すると、要素を削除して配列が切り詰められます。

> arr.length = 1;
> arr
[ 'a' ]

  練習問題:.push()を使用した空行の削除

exercises/arrays/remove_empty_lines_push_test.mjs

31.3.3節 負のインデックスによる要素への参照

いくつかの配列メソッドは負のインデックスをサポートしています。インデックスが負の場合、配列の長さに加算されて使用可能なインデックスが生成されます。したがって、次の2つの.slice()の呼び出しは同等です。どちらも最後の要素から始まるarrのコピーを作成します。

> const arr = ['a', 'b', 'c'];
> arr.slice(-1)
[ 'c' ]
> arr.slice(arr.length - 1)
[ 'c' ]
31.3.3.1節 .at():単一要素の読み取り(負のインデックスをサポート)[ES2022]

配列メソッド.at()は、指定されたインデックスの要素を返します。正のインデックスと負のインデックスの両方をサポートします(-1は最後の要素、-2は最後から2番目の要素などを指します)。

> ['a', 'b', 'c'].at(0)
'a'
> ['a', 'b', 'c'].at(-1)
'c'

一方、角括弧演算子[]は負のインデックスをサポートしません(既存のコードを壊すため変更できません)。それらは非要素プロパティのキーとして解釈されます。

const arr = ['a', 'b', 'c'];

arr[-1] = 'non-element property';
// The Array elements didn’t change:
assert.deepEqual(
  Array.from(arr), // copy just the Array elements
  ['a', 'b', 'c']
);

assert.equal(
  arr[-1], 'non-element property'
);

31.3.4節 配列のクリア

配列をクリア(空にする)するには、.lengthを0に設定します。

const arr = ['a', 'b', 'c'];
arr.length = 0;
assert.deepEqual(arr, []);

または、空の新しい配列を配列を格納している変数に代入します。

let arr = ['a', 'b', 'c'];
arr = [];
assert.deepEqual(arr, []);

後者の方法は、同じ配列を指している他の場所に影響を与えないという利点があります。ただし、共有配列を全員のためにリセットする必要がある場合は、前者の方法を使用する必要があります。

31.3.5節 スプレッド構文による配列リテラルへの展開 [ES6]

配列リテラル内では、スプレッド要素は3つのドット(...)とそれに続く式で構成されます。式が評価され、反復処理されます。反復処理された各値は、追加の配列要素になります。例:

> const iterable = ['b', 'c'];
> ['a', ...iterable, 'd']
[ 'a', 'b', 'c', 'd' ]

つまり、スプレッド構文を使用して配列のコピーを作成し、イテラブルを配列に変換できます。

const original = ['a', 'b', 'c'];

const copy = [...original];

const iterable = original.keys();
assert.deepEqual(
  [...iterable], [0, 1, 2]
);

ただし、上記の2つのユースケースについては、Array.from()の方が自己記述的で、個人的には好んでいます。

const copy2 = Array.from(original);

assert.deepEqual(
  Array.from(original.keys()), [0, 1, 2]
);

スプレッド構文は、配列(およびその他のイテラブル)を配列に連結する場合にも便利です。

const arr1 = ['a', 'b'];
const arr2 = ['c', 'd'];

const concatenated = [...arr1, ...arr2, 'e'];
assert.deepEqual(
  concatenated,
  ['a', 'b', 'c', 'd', 'e']);

スプレッド構文は反復処理を使用するため、値がイテラブルである場合にのみ機能します。

> [...'abc'] // strings are iterable
[ 'a', 'b', 'c' ]
> [...123]
TypeError: 123 is not iterable
> [...undefined]
TypeError: undefined is not iterable

  スプレッド構文とArray.from()は浅いコピーを作成します

スプレッド構文またはArray.from()を使用して配列をコピーすると、浅いコピーが作成されます。新しい配列に新しいエントリが作成されますが、値は元の配列と共有されます。浅いコピーの結果については、§28.4「オブジェクトリテラルへのスプレッド構文(...)[ES2018]」で説明しています。

31.3.6節 配列:インデックスとエントリの列挙 [ES6]

.keys()メソッドは配列のインデックスを列挙します。

const arr = ['a', 'b'];
assert.deepEqual(
  Array.from(arr.keys()), // (A)
  [0, 1]);

.keys()はイテラブルを返します。A行では、そのイテラブルを配列に変換しています。

配列インデックスの列挙は、プロパティの列挙とは異なります。前者は数値を生成し、後者は数値の文字列表現(インデックス以外のプロパティキーに加えて)を生成します。

const arr = ['a', 'b'];
arr.prop = true;

assert.deepEqual(
  Object.keys(arr),
  ['0', '1', 'prop']);

.entries()メソッドは、配列の内容を[インデックス、要素]ペアとして列挙します。

const arr = ['a', 'b'];
assert.deepEqual(
  Array.from(arr.entries()),
  [[0, 'a'], [1, 'b']]);

31.3.7節 値が配列かどうかを判定する

値が配列かどうかを確認する方法は2つあります。

> [] instanceof Array
true
> Array.isArray([])
true

instanceofは通常問題ありません。値が別のレルムから来る可能性がある場合は、Array.isArray()が必要です。簡単に言うと、レルムはJavaScriptのグローバルスコープのインスタンスです。一部のレルムは互いに分離されています(例:ブラウザのWeb Worker)。しかし、データの移動ができるレルムもあります。例えば、ブラウザの同一オリジンのiframeなどです。x instanceof Arrayxのプロトタイプチェーンをチェックするため、xが別のレルムの配列である場合はfalseを返します。

typeofは配列をオブジェクトとして分類します。

> typeof []
'object'

31.4章 for-ofループと配列 [ES6]

本書では既にfor-ofループについて触れています。このセクションでは、配列に対する使用方法を簡単に要約します。

31.4.1 for-of:要素の反復処理

以下のfor-ofループは、配列の要素を反復処理します。

for (const element of ['a', 'b']) {
  console.log(element);
}
// Output:
// 'a'
// 'b'

31.4.2 for-of:インデックスの反復処理

このfor-ofループは、配列のインデックスを反復処理します。

for (const element of ['a', 'b'].keys()) {
  console.log(element);
}
// Output:
// 0
// 1

31.4.3 for-of:[インデックス、要素]ペアの反復処理

以下のfor-ofループは、[インデックス、要素]ペアを反復処理します。後述するデストラクチャリングにより、for-ofの先頭でindexelementを設定するための便利な構文が得られます。

for (const [index, element] of ['a', 'b'].entries()) {
  console.log(index, element);
}
// Output:
// 0, 'a'
// 1, 'b'

31.5 配列のようなオブジェクト

配列で動作する操作の中には、最小限の要件しか必要としないものがあります。値は配列のようなオブジェクトである必要があります。配列のような値とは、以下のプロパティを持つオブジェクトです。

例えば、Array.from()は配列のようなオブジェクトを受け入れ、配列に変換します。

// If we omit .length, it is interpreted as 0
assert.deepEqual(
  Array.from({}),
  []);

assert.deepEqual(
  Array.from({length:2, 0:'a', 1:'b'}),
  [ 'a', 'b' ]);

配列のようなオブジェクトのTypeScriptインターフェースは次のとおりです。

interface ArrayLike<T> {
  length: number;
  [n: number]: T;
}

  現代のJavaScriptでは、配列のようなオブジェクトは比較的まれです

配列のようなオブジェクトはES6以前は一般的でしたが、今ではあまり見かけません。

31.6 反復可能オブジェクトと配列のような値の配列への変換

反復可能オブジェクトと配列のような値を配列に変換する一般的な方法は2つあります。

後者の方が、より分かりやすいので好みです。

31.6.1 スプレッド構文(...)による反復可能オブジェクトの配列への変換

配列リテラル内では、...によるスプレッド構文は、任意の反復可能オブジェクトを配列要素のシーケンスに変換します。例えば、

// Get an Array-like collection from a web browser’s DOM
const domCollection = document.querySelectorAll('a');

// Alas, the collection is missing many Array methods
assert.equal('map' in domCollection, false);

// Solution: convert it to an Array
const arr = [...domCollection];
assert.deepEqual(
  arr.map(x => x.href),
  ['https://2ality.com', 'https://exploringjs.dokyumento.jp']);

DOMコレクションは反復可能であるため、変換が機能します。

31.6.2 Array.from()による反復可能オブジェクトと配列のようなオブジェクトの配列への変換

Array.from()は2つのモードで使用できます。

31.6.2.1 Array.from()のモード1:変換

最初のモードは、次の型シグネチャを持ちます。

.from<T>(iterable: Iterable<T> | ArrayLike<T>): T[]

Iterableインターフェースは同期反復処理に関する章でArrayLikeインターフェースは本章の以前のセクションで説明されています。

パラメータを1つだけ指定した場合、Array.from()は反復可能オブジェクトまたは配列のようなオブジェクトを配列に変換します。

> Array.from(new Set(['a', 'b']))
[ 'a', 'b' ]
> Array.from({length: 2, 0:'a', 1:'b'})
[ 'a', 'b' ]
31.6.2.2 Array.from()のモード2:変換とマッピング

Array.from()の2番目のモードには、2つのパラメータが含まれます。

.from<T, U>(
  iterable: Iterable<T> | ArrayLike<T>,
  mapFunc: (v: T, i: number) => U,
  thisArg?: any)
  : U[]

このモードでは、Array.from()はいくつかの処理を行います。

つまり、型Tの要素を持つ反復可能オブジェクトから、型Uの要素を持つ配列に変換されます。

これは一例です。

> Array.from(new Set(['a', 'b']), x => x + x)
[ 'aa', 'bb' ]

31.7 任意の長さの配列の作成と埋め込み

配列を作成する最良の方法は、配列リテラルを使用することです。しかし、常にそれが可能なわけではありません。配列が大きすぎる場合、開発中に長さが分からない場合、または長さを柔軟に保ちたい場合があります。そのような場合、配列の作成と場合によっては埋め込みに、以下の手法をお勧めします。

31.7.1 後で完全に埋める空の配列を作成する必要があるか?

> new Array(3)
[ , , ,]

結果は3つの穴(空のスロット)を持っています。配列リテラルの最後のコンマは常に無視されます。

31.7.2 プリミティブ値で埋めた配列を作成する必要があるか?

> new Array(3).fill(0)
[0, 0, 0]

注意:オブジェクトで.fill()を使用する場合、各配列要素は(共有して)このオブジェクトを参照します。

const arr = new Array(3).fill({});
arr[0].prop = true;
assert.deepEqual(
  arr, [
    {prop: true},
    {prop: true},
    {prop: true},
  ]);

次の小節では、この問題の解決策を説明します。

31.7.3 オブジェクトで埋めた配列を作成する必要があるか?

> new Array(3).fill(0)
[0, 0, 0]

大きなサイズの場合、一時的な配列はかなりのメモリを消費する可能性があります。次のアプローチにはこの欠点はありませんが、自己説明的な記述が少ないです。

> Array.from({length: 3}, () => ({}))
[{}, {}, {}]

一時的な配列の代わりに、一時的な配列のようなオブジェクトを使用しています。

31.7.4 整数の範囲を作成する必要があるか?

function createRange(start, end) {
  return Array.from({length: end-start}, (_, i) => i+start);
}
assert.deepEqual(
  createRange(2, 5),
  [2, 3, 4]);

0から始まる整数の範囲を作成するための、少し技巧的な代替手法を次に示します。

/** Returns an iterable */
function createRange(end) {
  return new Array(end).keys();
}
assert.deepEqual(
  Array.from(createRange(4)),
  [0, 1, 2, 3]);

これは、.keys()undefined要素として扱い、そのインデックスをリストアップするため機能します。

31.7.5 要素がすべて整数またはすべて浮動小数点数の場合、型付き配列を使用する

整数または浮動小数点数からなる配列を扱う場合、この目的のために作成された型付き配列を検討する必要があります。

31.8 多次元配列

JavaScriptには真の多次元配列がありません。要素が配列である配列に頼る必要があります。

function initMultiArray(...dimensions) {
  function initMultiArrayRec(dimIndex) {
    if (dimIndex >= dimensions.length) {
      return 0;
    } else {
      const dim = dimensions[dimIndex];
      const arr = [];
      for (let i=0; i<dim; i++) {
        arr.push(initMultiArrayRec(dimIndex+1));
      }
      return arr;
    }
  }
  return initMultiArrayRec(0);
}

const arr = initMultiArray(4, 3, 2);
arr[3][2][1] = 'X'; // last in each dimension
assert.deepEqual(arr, [
  [ [ 0, 0 ], [ 0, 0 ], [ 0, 0 ] ],
  [ [ 0, 0 ], [ 0, 0 ], [ 0, 0 ] ],
  [ [ 0, 0 ], [ 0, 0 ], [ 0, 0 ] ],
  [ [ 0, 0 ], [ 0, 0 ], [ 0, 'X' ] ],
]);

31.9 その他の配列機能(高度な内容)

このセクションでは、配列の操作で頻繁には遭遇しない現象を見ていきます。

31.9.1 配列インデックスは(やや特殊な)プロパティキーです

配列要素は数値でアクセスするため特殊だと思うかもしれません。しかし、そのために行う角括弧演算子[]は、プロパティへのアクセスにも使用される同じ演算子です。これはシンボル以外の任意の値を文字列に変換します。したがって、配列要素は(ほぼ)通常のプロパティ(A行)であり、インデックスとして数値または文字列を使用しても問題ありません(B行とC行)。

const arr = ['a', 'b'];
arr.prop = 123;
assert.deepEqual(
  Object.keys(arr),
  ['0', '1', 'prop']); // (A)

assert.equal(arr[0], 'a');  // (B)
assert.equal(arr['0'], 'a'); // (C)

さらに混乱を招くことに、これは言語仕様でのみ定義されている方法です(JavaScriptの理論と言えるでしょう)。ほとんどのJavaScriptエンジンは内部で最適化を行い、実際に整数を用いて配列要素にアクセスしています(JavaScriptの実践と言えるでしょう)。

配列要素に使用されるプロパティキー(文字列!)はインデックスと呼ばれます。文字列strがインデックスであるとは、それを32ビット符号なし整数に変換して元に戻した結果が元の値と同じであることを意味します。数式で書くと

ToString(ToUint32(str)) === str
31.9.1.1 インデックスのリスト化

プロパティキーをリストアップする場合、インデックスは特別に扱われます。常に最初に表示され、数値のようにソートされます('2''10'の前に来ます)。

const arr = [];
arr.prop = true;
arr[1] = 'b';
arr[0] = 'a';

assert.deepEqual(
  Object.keys(arr),
  ['0', '1', 'prop']);

.length.entries().keys()は配列インデックスを数値として扱い、インデックス以外のプロパティは無視することに注意してください。

assert.equal(arr.length, 2);
assert.deepEqual(
  Array.from(arr.keys()), [0, 1]);
assert.deepEqual(
  Array.from(arr.entries()), [[0, 'a'], [1, 'b']]);

.keys().entries()によって返される反復可能オブジェクトを配列に変換するために、Array.from()を使用しました。

31.9.2 配列は辞書であり、穴を持つことができます

JavaScriptでは2種類の配列を区別します。

配列は実際にはインデックスから値への辞書であるため、JavaScriptでは疎型になる可能性があります。

  推奨事項:穴を避ける

これまで、密集型配列しか見ていませんが、穴を避けることをお勧めします。穴はコードを複雑にし、配列メソッドによって一貫して処理されません。さらに、JavaScriptエンジンは密集型配列を最適化するため、高速になります。

31.9.2.1 穴の作成

要素を代入するときにインデックスをスキップすることで、穴を作成できます。

const arr = [];
arr[0] = 'a';
arr[2] = 'c';

assert.deepEqual(Object.keys(arr), ['0', '2']); // (A)

assert.equal(0 in arr, true); // element
assert.equal(1 in arr, false); // hole

A行では、arr.keys()は穴をundefined要素として扱い、それらを表示しないため、Object.keys()を使用しています。

穴を作成するもう1つの方法は、配列リテラルで要素をスキップすることです。

const arr = ['a', , 'c'];

assert.deepEqual(Object.keys(arr), ['0', '2']);

配列要素を削除することもできます。

const arr = ['a', 'b', 'c'];
assert.deepEqual(Object.keys(arr), ['0', '1', '2']);
delete arr[1];
assert.deepEqual(Object.keys(arr), ['0', '2']);
31.9.2.2 配列操作は穴をどのように処理するか?

残念ながら、配列操作が穴を処理する方法はたくさんあります。

いくつかの配列操作は穴を削除します。

> ['a',,'b'].filter(x => true)
[ 'a', 'b' ]

いくつかの配列操作は穴を無視します。

> ['a', ,'a'].every(x => x === 'a')
true

いくつかの配列操作は穴を無視しますが、保持します。

> ['a',,'b'].map(x => 'c')
[ 'c', , 'c' ]

いくつかの配列操作は穴をundefined要素として扱います。

> Array.from(['a',,'b'], x => x)
[ 'a', undefined, 'b' ]
> Array.from(['a',,'b'].entries())
[[0, 'a'], [1, undefined], [2, 'b']]

Object.keys().keys()とは異なる動作をします(文字列と数値、穴にはキーがありません)。

> Array.from(['a',,'b'].keys())
[ 0, 1, 2 ]
> Object.keys(['a',,'b'])
[ '0', '2' ]

ここで覚えておくべきルールはありません。配列操作が穴をどのように処理するかが問題になる場合は、コンソールで迅速にテストするのが最善の方法です。

31.10 要素の追加と削除(破壊的および非破壊的)

JavaScriptのArrayは非常に柔軟で、配列、スタック、キューの組み合わせのようです。このセクションでは、配列要素の追加と削除の方法を調べます。ほとんどの操作は、破壊的(配列の変更)と非破壊的(変更されたコピーの作成)の両方で実行できます。

31.10.1 要素と配列の先頭への追加

以下のコードでは、単一の要素をarr1に、配列をarr2に破壊的に追加しています。

const arr1 = ['a', 'b'];
arr1.unshift('x', 'y'); // prepend single elements
assert.deepEqual(arr1, ['x', 'y', 'a', 'b']);

const arr2 = ['a', 'b'];
arr2.unshift(...['x', 'y']); // prepend Array
assert.deepEqual(arr2, ['x', 'y', 'a', 'b']);

スプレッド構文を使用すると、配列をarr2にunshiftできます。

非破壊的な先頭への追加は、スプレッド要素を使用して行われます。

const arr1 = ['a', 'b'];
assert.deepEqual(
  ['x', 'y', ...arr1], // prepend single elements
  ['x', 'y', 'a', 'b']);
assert.deepEqual(arr1, ['a', 'b']); // unchanged!

const arr2 = ['a', 'b'];
assert.deepEqual(
  [...['x', 'y'], ...arr2], // prepend Array
  ['x', 'y', 'a', 'b']);
assert.deepEqual(arr2, ['a', 'b']); // unchanged!

31.10.2 要素と配列の末尾への追加

以下のコードでは、単一の要素をarr1に、配列をarr2に破壊的に追加しています。

const arr1 = ['a', 'b'];
arr1.push('x', 'y'); // append single elements
assert.deepEqual(arr1, ['a', 'b', 'x', 'y']);

const arr2 = ['a', 'b'];
arr2.push(...['x', 'y']); // (A) append Array
assert.deepEqual(arr2, ['a', 'b', 'x', 'y']);

スプレッド構文(...)を使用すると、配列をarr2にpushできます(A行)。

非破壊的な末尾への追加は、スプレッド要素を使用して行われます。

const arr1 = ['a', 'b'];
assert.deepEqual(
  [...arr1, 'x', 'y'], // append single elements
  ['a', 'b', 'x', 'y']);
assert.deepEqual(arr1, ['a', 'b']); // unchanged!

const arr2 = ['a', 'b'];
assert.deepEqual(
  [...arr2, ...['x', 'y']], // append Array
  ['a', 'b', 'x', 'y']);
assert.deepEqual(arr2, ['a', 'b']); // unchanged!

31.10.3 要素の削除

配列要素を削除する3つの破壊的な方法を次に示します。

// Destructively remove first element:
const arr1 = ['a', 'b', 'c'];
assert.equal(arr1.shift(), 'a');
assert.deepEqual(arr1, ['b', 'c']);

// Destructively remove last element:
const arr2 = ['a', 'b', 'c'];
assert.equal(arr2.pop(), 'c');
assert.deepEqual(arr2, ['a', 'b']);

// Remove one or more elements anywhere:
const arr3 = ['a', 'b', 'c', 'd'];
assert.deepEqual(arr3.splice(1, 2), ['b', 'c']);
assert.deepEqual(arr3, ['a', 'd']);

.splice()については、本章の最後にあるクイックリファレンスで詳しく説明します。

rest要素によるデストラクチャリングにより、配列の先頭から要素を非破壊的に削除できます(デストラクチャリングについては後述します)。

const arr1 = ['a', 'b', 'c'];
// Ignore first element, extract remaining elements
const [, ...arr2] = arr1;

assert.deepEqual(arr2, ['b', 'c']);
assert.deepEqual(arr1, ['a', 'b', 'c']); // unchanged!

しかし、rest要素は配列の最後に来る必要があります。したがって、接尾辞を抽出するためだけに使用できます。

  演習:配列を使用したキューの実装

exercises/arrays/queue_via_array_test.mjs

31.11 メソッド:反復処理と変換(.find().map().filter()など)

このセクションでは、配列の反復処理と変換を行う配列メソッドについて見ていきます。

31.11.1 反復処理と変換メソッドのコールバック

すべての反復処理と変換メソッドはコールバックを使用します。前者はすべての反復値をコールバックに供給し、後者はコールバックに配列の変換方法を尋ねます。

これらのコールバックは、次の型シグネチャを持ちます。

callback: (value: T, index: number, array: Array<T>) => boolean

つまり、コールバックは3つのパラメータを取得します(いずれかのパラメータを無視しても構いません)。

コールバックが返す内容はそのメソッドによって異なります。可能性としては以下があります。

これらのメソッドの詳細は、後で詳しく説明します。

31.11.2 要素の検索: .find().findIndex()

.find() は、コールバック関数が真偽値(truthy value)を返す最初の要素を返し、何も見つからない場合は undefined を返します。

> [6, -5, 8].find(x => x < 0)
-5
> [6, 5, 8].find(x => x < 0)
undefined

.findIndex() は、コールバック関数が真偽値を返す最初の要素のインデックスを返し、何も見つからない場合は -1 を返します。

> [6, -5, 8].findIndex(x => x < 0)
1
> [6, 5, 8].findIndex(x => x < 0)
-1

.findIndex() は、次のように実装できます。

function findIndex(arr, callback) {
  for (const [i, x] of arr.entries()) {
    if (callback(x, i, arr)) {
      return i;
    }
  }
  return -1;
}

31.11.3 .map(): 要素に新しい値を与えながらコピーする

.map() は、レシーバ(元の配列)の修正済みコピーを返します。コピーの要素は、map のコールバック関数をレシーバの要素に適用した結果です。

これらすべては、例を通して理解するのが容易です。

> [1, 2, 3].map(x => x * 3)
[ 3, 6, 9 ]
> ['how', 'are', 'you'].map(str => str.toUpperCase())
[ 'HOW', 'ARE', 'YOU' ]
> [true, true, true].map((_x, index) => index)
[ 0, 1, 2 ]

.map() は、次のように実装できます。

function map(arr, mapFunc) {
  const result = [];
  for (const [i, x] of arr.entries()) {
    result.push(mapFunc(x, i, arr));
  }
  return result;
}

  練習問題: .map() を使用した行番号付け

exercises/arrays/number_lines_test.mjs

31.11.4 .flatMap(): 0個以上の値へのマッピング

Array<T>.prototype.flatMap() の型シグネチャは次のとおりです。

.flatMap<U>(
  callback: (value: T, index: number, array: T[]) => U|Array<U>,
  thisValue?: any
): U[]

.map().flatMap() はどちらも、入力配列を出力配列に変換する方法を制御する関数 callback をパラメータとして受け取ります。

これが .flatMap() の動作です。

> ['a', 'b', 'c'].flatMap(x => [x,x])
[ 'a', 'a', 'b', 'b', 'c', 'c' ]
> ['a', 'b', 'c'].flatMap(x => [x])
[ 'a', 'b', 'c' ]
> ['a', 'b', 'c'].flatMap(x => [])
[]

このメソッドの実装方法を検討する前に、ユースケースを検討します。

31.11.4.1 ユースケース: フィルタリングとマッピングの同時実行

配列メソッド .map() の結果は、常に呼び出された配列と同じ長さになります。つまり、そのコールバック関数は、関心のない配列要素をスキップできません。.flatMap() がこれを行う機能は、次の例で役立ちます。

次の関数 processArray() を使用して配列を作成し、その後 .flatMap() を使用してフィルタリングおよびマッピングします。

function processArray(arr, callback) {
  return arr.map(x => {
    try {
      return { value: callback(x) };
    } catch (e) {
      return { error: e };
    }
  });
}

次に、processArray() を使用して配列 results を作成します。

const results = processArray([1, -5, 6], throwIfNegative);
assert.deepEqual(results, [
  { value: 1 },
  { error: new Error('Illegal value: -5') },
  { value: 6 },
]);

function throwIfNegative(value) {
  if (value < 0) {
    throw new Error('Illegal value: '+value);
  }
  return value;
}

これで、.flatMap() を使用して、results から値のみ、またはエラーのみを抽出できます。

const values = results.flatMap(
  result => result.value ? [result.value] : []);
assert.deepEqual(values, [1, 6]);
  
const errors = results.flatMap(
  result => result.error ? [result.error] : []);
assert.deepEqual(errors, [new Error('Illegal value: -5')]);
31.11.4.2 ユースケース: 単一入力値を複数の出力値にマッピングする

配列メソッド .map() は、各入力配列要素を1つの出力要素にマッピングします。しかし、複数の出力要素にマッピングしたい場合はどうすればよいでしょうか?

次の例で必要になります。

> stringsToCodePoints(['many', 'a', 'moon'])
['m', 'a', 'n', 'y', 'a', 'm', 'o', 'o', 'n']

文字列の配列をUnicode文字(コードポイント)の配列に変換します。次の関数は、.flatMap() を使用してこれを実現します。

function stringsToCodePoints(strs) {
  return strs.flatMap(str => Array.from(str));
}
31.11.4.3 簡単な実装

.flatMap() は次のように実装できます。注: この実装は、組み込みバージョンよりもシンプルです。組み込みバージョンは、たとえば、より多くのチェックを実行します。

function flatMap(arr, mapFunc) {
  const result = [];
  for (const [index, elem] of arr.entries()) {
    const x = mapFunc(elem, index, arr);
    // We allow mapFunc() to return non-Arrays
    if (Array.isArray(x)) {
      result.push(...x);
    } else {
      result.push(x);
    }
  }
  return result;
}

  練習問題: .flatMap()

31.11.5 .filter(): 要素の一部のみを保持する

配列メソッド .filter() は、コールバック関数が真偽値を返すすべての要素を収集した配列を返します。

例:

> [-1, 2, 5, -7, 6].filter(x => x >= 0)
[ 2, 5, 6 ]
> ['a', 'b', 'c', 'd'].filter((_x,i) => (i%2)===0)
[ 'a', 'c' ]

.filter() は、次のように実装できます。

function filter(arr, filterFunc) {
  const result = [];
  for (const [i, x] of arr.entries()) {
    if (filterFunc(x, i, arr)) {
      result.push(x);
    }
  }
  return result;
}

  練習問題: .filter() を使用した空行の削除

exercises/arrays/remove_empty_lines_filter_test.mjs

31.11.6 .reduce(): 配列からの値の導出(上級)

メソッド .reduce() は、配列 arr の「サマリー」を計算するための強力なツールです。サマリーはあらゆる種類の値にすることができます。

reduce は、関数型プログラミングでは foldl (「fold left」)としても知られており、そこで人気があります。ただし、コードを理解しにくくする可能性があるという注意点があります。

.reduce() の型シグネチャは次のとおりです(Array<T> 内)。

.reduce<U>(
  callback: (accumulator: U, element: T, index: number, array: T[]) => U,
  init?: U)
  : U

T は配列要素の型、U はサマリーの型です。両者は異なる場合もあれば異なる場合もあります。accumulator は「サマリー」の別の名前です。

配列 arr のサマリーを計算するために、.reduce() はすべての配列要素をコールバック関数に一度に1つずつ渡します。

const accumulator_0 = callback(init, arr[0]);
const accumulator_1 = callback(accumulator_0, arr[1]);
const accumulator_2 = callback(accumulator_1, arr[2]);
// Etc.

callback は、以前に計算されたサマリー(パラメータ accumulator に格納)と現在の配列要素を組み合わせ、次の accumulator を返します。.reduce() の結果は、最終的な accumulator です。つまり、すべての要素を処理した後の callback の最後の結果です。

言い換えれば: callback が大部分の作業を行い、.reduce() はそれを便利な方法で呼び出すだけです。

コールバック関数が配列要素をアキュムレータに折り畳むと言うことができます。そのため、この操作は関数型プログラミングでは「fold」と呼ばれています。

31.11.6.1 最初の例

.reduce() の動作の例を見てみましょう。関数 addAll() は、配列 arr のすべての数値の合計を計算します。

function addAll(arr) {
  const startSum = 0;
  const callback = (sum, element) => sum + element;
  return arr.reduce(callback, startSum);
}
assert.equal(addAll([1,  2, 3]), 6); // (A)
assert.equal(addAll([7, -4, 2]), 5);

この場合、アキュムレータには、callback が既に処理したすべての配列要素の合計が格納されます。

行Aの配列からどのようにして結果6が導き出されたのでしょうか? callback の次の呼び出しによって行われます。

callback(0, 1) --> 1
callback(1, 2) --> 3
callback(3, 3) --> 6

注記

あるいは、for-of ループを使用して addAll() を実装することもできます。

function addAll(arr) {
  let sum = 0;
  for (const element of arr) {
    sum = sum + element;
  }
  return sum;
}

どちらの実装が「優れている」か断言するのは難しいです。.reduce() ベースの実装の方が少し簡潔ですが、for-of ベースの実装の方が、特に関数型プログラミングに慣れていない人にとっては少し理解しやすいでしょう。

31.11.6.2 例: .reduce() を使用したインデックスの検索

次の関数は、配列メソッド .indexOf() の実装です。指定された searchValue が配列 arr 内に出現する最初のインデックスを返します。

const NOT_FOUND = -1;
function indexOf(arr, searchValue) {
  return arr.reduce(
    (result, elem, index) => {
      if (result !== NOT_FOUND) {
        // We have already found something: don’t change anything
        return result;
      } else if (elem === searchValue) {
        return index;
      } else {
        return NOT_FOUND;
      }
    },
    NOT_FOUND);
}
assert.equal(indexOf(['a', 'b', 'c'], 'b'), 1);
assert.equal(indexOf(['a', 'b', 'c'], 'x'), -1);

.reduce() の1つの制限は、早期に終了できないことです(for-of ループでは、break を使用できます)。ここでは、見つけたらすぐに結果を返します。

31.11.6.3 例: 配列要素の倍増

関数 double(arr) は、要素がすべて2倍された inArr のコピーを返します。

function double(inArr) {
  return inArr.reduce(
    (outArr, element) => {
      outArr.push(element * 2);
      return outArr;
    },
    []);
}
assert.deepEqual(
  double([1, 2, 3]),
  [2, 4, 6]);

初期値 [] を変更して要素を追加します。破壊的でない、より関数的な double() のバージョンを以下に示します。

function double(inArr) {
  return inArr.reduce(
    // Don’t change `outArr`, return a fresh Array
    (outArr, element) => [...outArr, element * 2],
    []);
}
assert.deepEqual(
  double([1, 2, 3]),
  [2, 4, 6]);

このバージョンはよりエレガントですが、速度が遅く、メモリ消費量も多くなります。

  練習問題: .reduce()

31.12 .sort(): 配列のソート

.sort() の型定義は次のとおりです。

sort(compareFunc?: (a: T, b: T) => number): this

デフォルトでは、.sort() は要素の文字列表現をソートします。これらの表現は < を使用して比較されます。この演算子は、辞書順で比較します(最初の文字が最も重要です)。数値をソートする場合に確認できます。

> [200, 3, 10].sort()
[ 10, 200, 3 ]

自然言語の文字列をソートする場合、コードユニット値(文字コード)に従って比較されることに注意する必要があります。

> ['pie', 'cookie', 'éclair', 'Pie', 'Cookie', 'Éclair'].sort()
[ 'Cookie', 'Pie', 'cookie', 'pie', 'Éclair', 'éclair' ]

アクセントのないすべての大文字は、アクセントのないすべての子音の前に、アクセント付きの文字の前にきます。適切な自然言語ソートが必要な場合は、JavaScript国際化APIIntl を使用できます。

.sort() は、**インプレース**でソートします。レシーバ(元の配列)を変更して返します。

> const arr = ['a', 'c', 'b'];
> arr.sort() === arr
true
> arr
[ 'a', 'b', 'c' ]

31.12.1 ソート順のカスタマイズ

パラメータ compareFunc を使用してソート順をカスタマイズできます。これは、次の数値を返す必要があります。

  これらの規則を覚えるためのヒント

負の数はゼロより小さいです(など)。

31.12.2 数値のソート

このヘルパー関数を使用して数値をソートできます。

function compareNumbers(a, b) {
  if (a < b) {
    return -1;
  } else if (a === b) {
    return 0;
  } else {
    return 1;
  }
}
assert.deepEqual(
  [200, 3, 10].sort(compareNumbers),
  [3, 10, 200]);

以下は、迅速で簡単な代替手段です。

> [200, 3, 10].sort((a,b) => a - b)
[ 3, 10, 200 ]

このアプローチの欠点は次のとおりです。

31.12.3 オブジェクトのソート

オブジェクトをソートする場合も、比較関数を使用する必要があります。例として、次のコードは、年齢でオブジェクトをソートする方法を示しています。

const arr = [ {age: 200}, {age: 3}, {age: 10} ];
assert.deepEqual(
  arr.sort((obj1, obj2) => obj1.age - obj2.age),
  [{ age: 3 }, { age: 10 }, { age: 200 }] );

  練習問題: 名前でオブジェクトをソートする

exercises/arrays/sort_objects_test.mjs

31.13 クイックリファレンス: Array

凡例

31.13.1 new Array()

new Array(n) は、n 個の穴を含む長さ n の配列を作成します。

// Trailing commas are always ignored.
// Therefore: number of commas = number of holes
assert.deepEqual(new Array(3), [,,,]);

new Array() は空の配列を作成します。ただし、代わりに常に [] を使用することをお勧めします。

31.13.2 Array の静的メソッド

31.13.3 Array.prototype のメソッド

31.13.4 出典

  クイズ

クイズアプリを参照してください。