【JS体操第3問ヒント③】「疎」な配列を「密」にする12の方法

こんにちは!面白プロデュース事業部のおばらです。
今回は JS体操第3問「Zalgo Text の生成」の問題のヒントにもなるかもしれないシリーズ第3弾。

JavaScript で「疎」な配列を(要素数は変えずに)「密」にする12の方法を

  • コードゴルフ(文字数を短くしたい場合)
  • ワンライナ(1行で書きたい場合)

的な視点も交え、まとめてみます。


JS体操第3問「Zalgo Text の生成」 とこれまでのヒント記事については以下をご覧ください。

JS体操第3問「Zalgo Text の生成」
JS体操第3問「Zalgo Text の生成」編集画面 JS体操第3問「Zalgo Text の生成」編集画面(エラー)

hubspot.kayac.com

techblog.kayac.com

techblog.kayac.com


目次


疎な配列とは

配列の要素の値として undefined でも null でもない概念「空(empty)」があります。

「空(empty)」 の「要素(スロット、slot)」を1つ以上持つ配列は、「疎(sparse)」な配列、「疎配列(sparse array)」と呼ばれます。つまり歯抜けの配列、隙間がある配列です。

一方「空(empty)」の要素を1つも持たない配列、つまり「疎(sparse)」ではない配列は「密(dense)」な配列と呼ばれることもあります。つまりぎっしり詰まった配列です。

developer.mozilla.org


疎な配列の作り方

中身がすべて or 一部「空(empty)」な「疎配列」の作り方の例を以下にいくつか示します。実務では意図して「疎」な配列を作りたい機会はあまり無いかもしれませんが、逆に意図せず配列を「疎」にしてしまう可能性に注意しましょう。

なおサンプルコードでは配列の長さ(length)、要素数は 10 とします。


配列リテラル

配列リテラルで要素の値を指定せずコンマのみで。

const arr = [,,,,,,,,,,];

arr; // => [empty × 10]
const arr = [0,,1,,2,,3,,4,,];

arr; // => [0, empty, 1, empty, 2, empty, 3, empty, 4, empty]


length プロパティの指定

配列リテラルで長さ 0 の配列を生成し、後から length を指定。

const arr = [];

arr.length = 10;
arr; // => [empty × 10]
const arr = [0,1,2,3,4];

arr.length = 10;
arr; // => [0, 1, 2, 3, 4, empty × 5]


添字アクセス

配列リテラルで長さ 0 の配列を生成し、後から添字アクセスで指定。

const arr = [];

arr[9] = 0;
arr; // => [empty × 9, 0]
const arr = [0,1,2,3,4];

arr[9] = 0;
arr; // => [0, 1, 2, 3, 4, empty × 4, 0]


delete で要素を削除

密な配列の要素を delete 演算子で削除。

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

delete arr[0];

arr; // => [empty, 1, 2, 3, 4, 5, 6, 7, 8, 9]
const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

delete arr[5];

arr; // => [0, 1, 2, 3, 4, empty, 6, 7, 8, 9]
const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

delete arr[9];

arr; // => [0, 1, 2, 3, 4, 5, 6, 7, 8, empty]

developer.mozilla.org


Array() コンストラクタ

Array() コンストラクタの引数で配列の長さ、要素数を指定。

const arr = new Array(10);

arr; // => [empty × 10]


なお new 演算子は省略できます。

const arr = Array(10);

arr; // => [empty × 10]


developer.mozilla.org


他にも「疎」な配列の作り方、意図せず「疎」になってしまう処理があるので注意しましょう。



疎な配列のデメリット

特にコードゴルフやワンライナにおいては「疎」な配列を「密」な配列にしたいことがよくあります。 例えば何らかの処理を任意の回数だけ繰り返したい場合です。

console.log()w と10回だけ出力したいとします。
長さが 10 の配列と forEach() で楽勝!と思いきや、以下のコードではなぜか1回も出力されません。

const arr = Array(10); // => [empty × 10]

arr.forEach(() => console.log('w')); // => 1回も出力されない

forEach() は「空(empty)」の要素に対して処理を行ってくれない、つまり「空」の要素をスキップしてしまうからです。


10回出力されるためには「空」の要素をすべて何らかの値で埋める必要があります。この場合、埋める値は何でも良いので 0 で埋めてみます。

const arr = Array(10).fill(0); // => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

arr.forEach(() => console.log('w')); // => ちゃんと10回出力される!


このように、特にコードゴルフやワンライナでは「疎」な配列を「密」にしたい、 つまり値はなんでも良いからとにかく「空(empty)」の要素を「空」以外の何かにしたいケースはよくあります。


配列のメソッドにおける「空(empty)」要素の扱いの違い

配列のインスタンスメソッドはメソッドによって「空(empty)」の扱いに違いがあります。

以下の(古めの)メソッドは配列の「空」の要素をスキップしたり(undefined ではなく)「空」のままにしたりします。 (MDNより)


以下の(新しめの)メソッドは「空(empty)」の要素を undefined と同様に扱います。 (MDNより)
join()toLocaleString() などは新しめのメソッドではないですが)


配列のメソッドによって 「空(empty)」の要素の扱いが異なることに注意しましょう。

developer.mozilla.org


「疎」な配列を「密」にする12の方法

さていよいよ本題。コードゴルフにも役立つかもしれない、「疎」な配列を(要素数を変えずに)「密」にする12の方法をご紹介します。

① for ループ

まずは一番シンプルな方法から。

const arr = new Array(10);

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

arr; // => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

for 文は式、値として扱えないのでコードゴルフでは使いづらいですね。

developer.mozilla.org


② fill()

すでにサンプルコードでも登場した fill()。 その名の通り配列の要素を任意の値で埋めるメソッドです。

const arr = new Array(10).fill(0);

arr; // => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

この方法はよく使われており、コードの意図もわかりやすいです。


fill() の引数を省略すると undefined を指定したことになります。

const arr = new Array(10).fill();

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]


developer.mozilla.org


③ スプレッド構文で展開(コピー)

配列は反復可能(iterable)です。 具体的に言うと Array.prototype[Symbol.iterator] が定義・実装されています。

粗配列をスプレッド構文で展開すると「空(empty)」の要素が undefined になります。

const arr = [...Array(10)];

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]

配列リテラルとスプレッド構文の組み合わせは、配列を(手軽に)コピーする方法としてもよく使われます。


配列自体が反復可能ですが、あえて「イテレータ(Iterator)」を返すメソッドを挟んでみるとこうなります。

const arr = [...Array(10).keys()];

arr; // => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
const arr = [...Array(10).values()];

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]
const arr = [...Array(10).entries()];

arr; // => [Array(2), Array(2), Array(2), Array(2), Array(2), Array(2), Array(2), Array(2), Array(2), Array(2)]


ちなみにスプレッド構文が無かった時代によく用いられていた配列のコピー方法、 concat()slice() を使う方法では「疎」の配列を「密」にすることはできません。 concat()slice() は「空(empty)」を undefined と同等に扱わない(古い)メソッドだからです。

const arr = [].concat(Array(10));

arr; // => [empty × 10]
const arr = [].slice.apply(Array(10));

arr; // => [empty × 10]


developer.mozilla.org

developer.mozilla.org


④ Array() + Array()

Array() を二重に使ってみます。

const arr = Array(...Array(10));

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]

developer.mozilla.org


⑤ Array.apply()

const arr = Array.apply(null, Array(10));

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]

④ とやっていることはほぼ同じですね。


ちなみに apply() の兄弟、call() を無理やり使うとしたらこう。

const arr = Array.call(null, ...Array(10));

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]

apply()call() はクラス記法が登場する前はサブクラスのコンストラクタ関数内でスーパークラスのコンストラクタ関数を実行するときによく使っていました。


developer.mozilla.org

developer.mozilla.org


⑥ Array.of() + スプレッド構文

const arr = Array.of(...Array(10)); 

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]

developer.mozilla.org


⑦ Array.from()

Array.from() は「反復可能(iterable)なオブジェクト」 or「array-like なオブジェクト」を(本物の)配列にしてくれるクラスメソッドです。 この場合の array-like とは length プロパティを持つオブジェクトのことです。

Array(10) は反復可能で且つ array-like なオブジェクトです。(本物の配列を array-like と呼ぶもおかしいですが)

const arr = Array.from(Array(10));

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]

同時に値も指定したい場合は第2引数に渡す関数で。

const arr = Array.from(Array(10), () => 0);

arr; // => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
const arr = Array.from(Array(10), (_, i) => i);

arr; // => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]


イテレータからでも。

const arr = Array.from(Array(10).keys());

arr; // => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]


型付き配列からでも。

const arr = Array.from(new Int8Array(10));

arr; // => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]


ためしに自作の array-like オブジェクト、つまり length プロパティを持つオブジェクトを渡してみます。

const arr = Array.from({ length: 10 });

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]
const arr = Array.from({ length: Number(10) });

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]
const arr = Array.from({ length: '10' });

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]
const arr = Array.from({ length: '    10    ' });

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]


ちなみに、Array.from() が「反復可能(iterable)であること」と「array-like であること」のどちらを優先するかは、例えば以下のコードでチェックすることができます。

// 反復可能なオブジェクト
const obj = {
  [Symbol.iterator]() {
    return ['反復可能'][Symbol.iterator]();
  },
};

Array.from(obj); // => ['反復可能']
// array-like なオブジェクト
const obj = {
  0: 'array-like',
  length: 1,
};

Array.from(obj); // => ['array-like']
// 反復可能で且つ array-like なオブジェクト
const obj = {
  0: 'array-like が優先',
  length: 1,
  [Symbol.iterator]() {
    return ['反復可能が優先'][Symbol.iterator]();
  },
};

Array.from(obj); // => ['反復可能が優先']


developer.mozilla.org

tc39.es


⑧ toSorted()

sort() の非破壊版、toSorted() も使えます。 toSorted() は「空(empty)」を undefined と同等に扱ってくれます。

const arr = Array(10).toSorted();

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]

developer.mozilla.org


⑨ toReversed()

reverse() の非破壊版、toReversed() も。

const arr = Array(10).toReversed();

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]

developer.mozilla.org


⑩ toSpliced()

splice() の非破壊版、toSpliced() も。

const arr = Array(10).toSpliced();

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]

developer.mozilla.org


⑪ with()

const arr = Array(10).with();

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]

developer.mozilla.org


⑫ JSON.parse() + JSON.stringify()

ちょっと強引ですがこんな方法も。

const arr = JSON.parse(JSON.stringify(Array(10))); 

arr; // => [null, null, null, null, null, null, null, null, null, null]


JSON.parseevalFunction で代替しても良いですね。

const arr = eval(JSON.stringify(Array(10))); 

arr; // => [null, null, null, null, null, null, null, null, null, null]
const arr = Function(`return${JSON.stringify(Array(10))}`)();

arr; // => [null, null, null, null, null, null, null, null, null, null]

developer.mozilla.org developer.mozilla.org


本記事で紹介する方法は以上です。まだまだあるかもしれません。まだまだあるでしょう。
他にもこんな面白い方法がある!という方はぜひ教えてください。



おまけ:「粗」な配列の「空(empty)」の要素のみを取り除く方法

filter()

const arr = [0,,1,,2,,3].filter(() => true);

arr; // => [0, 1, 2, 3]

developer.mozilla.org


flat()

const arr = [0,,1,,2,,3].flat();

arr; // => [0, 1, 2, 3]

developer.mozilla.org


flatMap();

const arr = [0,,1,,2,,3].flatMap(item => item);

arr; // => [0, 1, 2, 3]


なお map() では NG です。

const arr = [0,,1,,2,,3].map(item => item);

arr; // => [0, empty, 1, empty, 2, empty, 3]


developer.mozilla.org developer.mozilla.org



まとめ

最後まで読んでいただき、ありがとうございます。

今回は「疎」な配列を「密」にする12の方法をまとめてみました。 まだまだあると思うので、他にもこんな面白い方法がある!という方はぜひ教えてください。

本記事の内容は、もしかしたら現在開催中のJS体操第3問「Zalgo Textの生成」のヒントにもなるかもしれません。

『JS体操』とは面白法人カヤックが主催する JavaScript のコードゴルフ大会。
その第3問は任意の文字列を「Zalgo Text」に変換する JavaScript の長〜いコードをできるだけ短くする!というお題です。 元々2222文字もあるコードを、なんと115文字以下まで減らせます。(現状の最短記録は114文字です)

みなさまのご参加をお待ちしております!

JS体操第3問「Zalgo Text の生成」

JS体操第3問「Zalgo Text の生成」編集画面 JS体操第3問「Zalgo Text の生成」編集画面(エラー)

hubspot.kayac.com

techblog.kayac.com

techblog.kayac.com