JavaScriptの配列をどう解説するかを考えていて、配列って普段どういう風に使ってるけ?みたいなことを書き出してみました。

皆さんは配列をどう使いますか?

追記: 次のページでこの記事をブラッシュアップした話をhttps://jsprimer.net/で公開しています。

配列の作成

配列の作成には配列リテラル([])を使います。 配列リテラルには初期値も指定できます。

var emptyArray = []; // 空の配列を作成
var array = [1, 2, 3]; // 値をもった配列を作成

Arrayオブジェクトをnew演算子でインスタンス化する方法は基本的には使いません。 こちらは配列リテラルとは異なり、初期値ではなく配列の長さを指定し、疎な配列を作ります。

疎な配列とは、配列の要素が空となっているもので、隙間を持った配列のことを言います。

JavaScriptでは、配列は固定長ではなく可変長のみとなっていることや、 初期値を指定できないため、new Arrayで疎な配列を作る意味は少ないです。

// 10個分のlengthを持った疎の配列
var array = new Array(10);
// 中身が空なので、値を持っていない
console.log(array.hasOwnProperty(0));// => false

バイナリデータを扱うようなパフォーマンスが求められるケースは、 Array(配列)ではなくTypedArray(型付き配列)を利用します。

Array(配列)とTypedArray(型付き配列)は似ていますが、 基本的に異なる目的を持ったものなので、ここではArray(配列)についてを扱います。

配列の要素を削除する

delete演算子で配列の要素を削除することができますが、 注意点としては値を消すだけで、消した値を詰めるような処理は行いません。 つまり、deleteした結果として疎な配列ができあがります。

次のように、length3の配列の1番目の要素を消しても、 length3のままとなります。 delete演算子では、自動的に削除された配列の要素を詰めません。

var array = [1, 2, 3];
console.log(array.length); // => 3
delete array[1];
console.log(array); // => [1, , 3]
console.log(array.length); // => 3

一方、Array#spliceメソッドを利用すると、削除した要素を自動で詰めることができます。 Array#spliceメソッドは、index番目から削除する数だけ要素を取り除き、必要ならば要素を同時に追加できます。

array.splice(index, 削除する数, [追加する要素][, ..., 追加する要素]);

つまり、配列の1番目の要素を削除するには、1番目から1つの要素を削除するという指定をする必要があります。 このとき、削除した要素は自動で詰められるため、疎な配列にはなりません。

var array = [1, 2, 3];
console.log(array.length); // => 3
array.splice(1, 1);
console.log(array); // => [1, 3]
console.log(array.length); // => 2

このArray#spliceメソッドをImmutableにする場合は結構小難しい書き方になります。

function immutableSplice(arr, start, deleteCount, ...items) {
    return [...arr.slice(0, start), ...items, ...arr.slice(start + deleteCount)];
}

Arrayの要素を全削除

配列の要素をすべて削除するには length0を設定する方法があります。

var array = [1, 2, 3];
array.length = 0;
console.log(array[0] === undefined); // => true

仕様的には代入されたlengthからはみ出ている要素をすべて [[Delete]] とするという処理になります。

Array#spliceやそもそもその要素を削除するひつようがないなら、空の配列で変数を上書きするでもよいはずです。

var array = [1, 2, 3];
array = [];
// or
array.length = 0;

次の2つはarrayという変数を参照の値を渡しているものがあるかで意味が異なります。 次のようにlengthを変更した場合は、コールバック関数に渡した配列も影響を受けます。

function doSomething(callbck) {
    var array = [];
    callback(array);
    // array = [];
    // or
    // array.length = 0;
}

doSomething((array) => {
    console.log(array);
});

配列は参照型

配列はプリミティブな値ではなくオブジェクトなので、変数に入れると参照型の値になります。 次にように、配列を参照するaという変数の値をbに代入しても、bには配列の参照が入るだけです。 そのため、aに変更を加えると、bも同じ配列を参照しているため影響を受けます。

var a = [1, 2, 3];
var b = a;
a.push(4);
console.log(b); // => [1, 2, 3, 4]

一方、プリミティブな値である文字列では、baを代入する際にaの値がコピーされます。 つまり、変数aに変更を加えても、コピーされた値をもつ変数bは影響を受けません。

var a = "string";
var b = a;
a = a + "!";
console.log(b); // => "string"

StringやNumberなどのプリミティブな値は、作成後に値そのものの状態は変更できません。 このような特性をもつものをImmutableと呼び、StringなどはImmutableです。

一方、ArrayやObjectなどのプリミティブな値でないものは、作った後も状態を変更できるためMutableと呼ばれます。

詳しくはデータ型とリテラル · JavaScriptの入門書 #jsprimerを参照してください。

Arrayのコピー

配列をshallow copyする流派としてconcatとsliceがあります。

var array = [1, 2, 3];
var copyC = array.concat();
var copyS = array.slice();
console.log(array !== copyC); // => true
console.log(array !== copyS); // => true
console.log(copyC !== copyS); // => true

Array#concatメソッドとArray#sliceメソッドは意図して配列以外をthisとして指定できるようになっています。 そのため、Array-likeなオブジェクトをthisにして、配列にする方法としても利用されています。

Array.prototype.slice.call(document.querySelectorAll("div"));

しかし、ES2015からはArray.fromメソッドという、Array-likeを配列にする適切なメソッドがあります。 あとで詳しく解説しますが、Array.fromメソッドのほうが直感的なのでこちらを利用して変換した方がよいです。

Array.from(document.querySelectorAll("div"));

配列の末尾に要素を追加

末尾に要素を追加する場合は Array#push が利用できます。

var array = [1, 2, 3];
array.push(4);
console.log(array); // => [1, 2, 3, 4]

Array#pushはmutableな操作なので、immutableにやりたい場合もあります。 Immutableにやりたい場合は、配列のコピーの最後に要素を追加すればよいはずです。

var array = [1, 2, 3];
var newArray = [...array, 4];
console.log(newArray); // => [1, 2, 3, 4]

もちろんArray#sliceなどでコピーした配列にpushするでも問題ありません。

var array = [1, 2, 3];
var newArray = array.slice();
newArray.push(4);
console.log(newArray); // => [1, 2, 3, 4]

先頭に要素を追加する場合も、Array#pushArray#unshiftに変わるだけで同じです。

new Array + fill

new Array(len) で指定したlength疎な配列を作ることができます。 しかし、この配列の要素はundefinedが値として入っているわけではありません。 単純に array[0] にはキーそのものがないため、 undefinedが返ってきています。

var array = new Array(10);
console.log(array.length === 10);// => true
console.log(array[0] === undefined); // => true
// hasOwnPropertyでプロパティを持っているかで確認できる
var a = [undefined];
var b = new Array(1);
console.log(a.hasOwnProperty(0)); // => true
console.log(b.hasOwnProperty(0)); // => false

配列もオブジェクトであるため、疎な配列は次のようなオブジェクトであるといえます。

// new Array(10)
var array = {
    length: 10,
    __proto__: Array.prototype
};
array[0]; // => undefined

これによりnew Arrayでは配列中の値がないのでArray#mapなどが意図した挙動にはなりません Array#mapなどは配列中の値がない添字をスキップします。

var array = new Array(10).map((item, index) => {
    return index;
});
console.log(array[0] === undefined);// => true

そのため、これを回避する場合は明示的に値を入れた配列を使うか、 Array.fromを使うことで疎な配列も扱えます。

明示的に値を埋める、いわゆる0埋めのような操作はArray#fillを使うのが簡単です。

var array = new Array(10).fill(0).map((item, index) => {
    return index;
});
console.log(array[9] === 9);// => true

Array.fromメソッドはArray-likeやiterableなオブジェクトから新しく配列を作る静的メソッドです。

先ほどのnew Arrayで作った疎な配列もlengthは持っているので、for文などで走査することはできます。 Array.fromは、argumentsのようなArray-likeや疎な配列も列挙でき かつ Array#mapのような仕組みを持っています。

var array = Array.from(new Array(10), (item, index) => {
    return index;
});
console.log(array[9] === 9);// => true

他にも、Iterableを配列にできるので、Mapオブジェクトを配列へ変換するときにも利用できます。

var map = new Map([[1, 2], [2, 4], [4, 8]]);
console.log(Array.from(map));// => [[1, 2], [2, 4], [4, 8]]

これは、Spread Operator(...)を使うことでも同様のことが行なえます。 Array.fromは第二引数でマッピング方法を指定できるのでより柔軟な処理が書けるという違いがあります。

var map = new Map([[1, 2], [2, 4], [4, 8]]);
console.log([...map]);// => [[1, 2], [2, 4], [4, 8]]

flatten

配列の入れ子をflattenにしたいというケース。

[[1], [2], [3]] => [1, 2, 3]

concatを使った方法が有名です。

Array#concatを使った方法ではshallowなflattenを行えます。

function flatten(array) {
    return Array.prototype.concat.apply([], array);
}
var array = [[1], [2], [3]];
flatten(array); // => [1, 2, 3]

再帰的にやることでdeepなflattenができます。 もう一つのflattenを行う方法として、... spread operatorで配列を展開してしまう方法です。

function flatten(array) {
    return array.reduce((prev, curr) => {
        Array.isArray(curr) ? [...prev, ...flatten(curr)] : [...prev, curr];
    }, []);
}
var array = [[1], [2], [3]];
flatten(array); // => [1, 2, 3]

Array.prototype.flatMap & Array.prototype.flatten ProposalはStage 1なので、将来Array#flattenメソッドが利用できる可能性もあります。

entriesで何か

オブジェクをループ時に key と value のどちらも必要な場合は、Object.entriesメソッドを利用すると簡単です。

var object = {
    key1: "value1",
    key2: "value2",
};
var keyValues = Object.entries(object).map(([key, value]) => {
    return `${key}: ${value}`;
});
console.log(keyValues); // => ["key1:value1", "key2:value2"];

keyだけならObject.keysメソッド、valueだけならObject.valuesメソッドが利用できます。

indexOf => findIndex

配列から指定した要素を見つける場合に indexOf だと===での一致でしか見つけることができません。 そのため、オブジェクトのプロパティを見て探索する場合には利用できません。

var array = [{ id: 1 }, { id: 2}];
var index = array.indexOf({ id: 1});
console.log(index); // => -1

代わりにES2015からはArray#findIndex が利用できます。

var array = [{ id: 1 }, { id: 2}];
var index = array.findIndex(item => item.id === 1);
console.log(index); // => 0

find => some

配列の中に、判定に一致するものを含んでいるかという真偽値が欲しいのなら、Array#someが利用できます。

var array = [{ id: 1 }, { id: 2}];
var isContained = array.some(item => item.id === 1);
console.log(isContained); // => true

indexOf => includes

配列が指定した要素を含んでいるかに array.indexOf(value) !== -1 を使ったイディオムがあります。 先ほども書いたようにindexOf===による比較なので、Array#someで書くと次のような処理になります。

var array = ["a", "b", "c"];
var target = "b";
var containB = array.some(item => {
    return target === item;
});
console.log(containB); // => true

これをArray#indexOfを使えば1行で書くことができます。

var array = ["a", "b", "c"];
var target = "b";
var containB = array.indexOf(target) !== -1;
console.log(containB); // => true

しかし、ES2016からはArray#includesが利用できるので、このイディオムを使う必要はありません。

var array = ["a", "b", "c"];
var containB = array.includes("b");
console.log(containB); // => true

splice => …

Array#spliceはmutableな操作になっています。 そのため、配列から n 番目の要素を削除した配列をImmutableに作るのは結構面倒です。

mubtaleでよいなら、次のように書くことができます。

function deleteItemAtIndex(array, index) {
    array.splice(index, 1);// spliceの返り値は削除した値
    return array;
}
var array = [1, 2, 3];
var result = deleteItemAtIndex(array, 1);
console.log(result); // => [1, 3]

これをImmutableする場合、Spread OperatorとArray#sliceを使うことで次のように書くことができます。

function deleteItemAtIndex(array, index) {
    // 常に新しい配列を返す
    return [
        ...array.slice(0, index),
        ...array.slice(index + 1)
    ];
}
var array = [1, 2, 3];
var result = deleteItemAtIndex(array, 1);
console.log(result);// => [1, 3]

配列から値を取り出す

テストなどで、指定して位置の値を取り出したいときがあります。

function sortByKey(array, key) {
    return array.sort(function(a, b) {
        var x = a[key];
        var y = b[key];
        if (x > y) {
            return 1;
        } else if (x < y) {
            return -1;
        } else {
            return 0;
        }
    });
}

これをテストした時には、返り値の0番目の値を取り出してみたいということが多いです。 この場合に、Destructuringを使い値を取り出すと変数にまとめて取り出せます。

function sortByKey(array, key) {
    return array.slice().sort(function(a, b) {
        var x = a[key];
        var y = b[key];
        if (x > y) {
            return -1;
        } else if (x < y) {
            return 1;
        } else {
            return 0;
        }
    });
}
var array = [{ "key": 2 }, { "key": 1 }];
var sorted = sortByKey(array, "key");
var [first, second] = sorted;
console.log(first === array[0]); // => true

join

配列を文字列にする方法はさまざまな方法があります。 単純な方法としては、Array#joinメソッドを利用することです。

array.join(区切り文字);

Array#joinメソッドでは配列を指定した区切り文字で結合した文字列を作成してくれます。 区切り文字を指定しなかった場合は、デフォルト値として,区切り文字として指定されます。

var array = [1, 2, 3];
var string = array.join();
console.lg(string);// => "1,2,3"

ループと反復処理

Array#mapメソッドなどのループと反復処理についてはループと反復処理 · JavaScriptの入門書 #jsprimerを参照してください。

空の配列を返す

配列を返すAPIは、返す値がないときも空の配列を返すようにします。

function getSomeList(){
	if(返すものがないとき){
		return [];
	}
	// かえすものがあるとき
}

こうすることで、このAPIを利用する側はnullチェックをしなくても良くなります。 nullundefinedを返してしまうと、このAPIを使うたびにnullチェックが必要となります。 nullチェックが不要ならば不要な形にした方が良いはずです。

おまけ

Arrayのメソッドで破壊的なものとそうでないものをまとめたもの

追記: この記事で登場したMutableなメソッドのImmutable版を提供するライブラリを作りました。

azu/immutable-array-prototype: A collection of Immutable Array prototype methods(Per method packages).