こんにちは!面白プロデュース事業部のおばらです。
今回は JS体操第3問「Zalgo Text の生成」の問題のヒントにもなるかもしれないシリーズ第3弾。
JavaScript で「疎」な配列を(要素数は変えずに)「密」にする12の方法を
- コードゴルフ(文字数を短くしたい場合)
- ワンライナ(1行で書きたい場合)
的な視点も交え、まとめてみます。
JS体操第3問「Zalgo Text の生成」 とこれまでのヒント記事については以下をご覧ください。
目次
- 疎な配列とは
- 疎な配列の作り方
- 疎な配列のデメリット
- 配列のメソッドにおける「空(empty)」要素の扱いの違い
- 「疎」な配列を「密」にする12の方法
- おまけ:「粗」な配列の「空(empty)」の要素のみを取り除く方法
- まとめ
疎な配列とは
配列の要素の値として undefined
でも null
でもない概念「空(empty)」があります。
「空(empty)」 の「要素(スロット、slot)」を1つ以上持つ配列は、「疎(sparse)」な配列、「疎配列(sparse array)」と呼ばれます。つまり歯抜けの配列、隙間がある配列です。
一方「空(empty)」の要素を1つも持たない配列、つまり「疎(sparse)」ではない配列は「密(dense)」な配列と呼ばれることもあります。つまりぎっしり詰まった配列です。
疎な配列の作り方
中身がすべて 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]
Array() コンストラクタ
Array()
コンストラクタの引数で配列の長さ、要素数を指定。
const arr = new Array(10); arr; // => [empty × 10]
なお new
演算子は省略できます。
const arr = Array(10); arr; // => [empty × 10]
他にも「疎」な配列の作り方、意図せず「疎」になってしまう処理があるので注意しましょう。
疎な配列のデメリット
特にコードゴルフやワンライナにおいては「疎」な配列を「密」な配列にしたいことがよくあります。 例えば何らかの処理を任意の回数だけ繰り返したい場合です。
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より)
concat()
copyWithin()
every()
filter()
flat()
flatMap()
forEach()
indexOf()
lastIndexOf()
map()
reduce()
reduceRight()
reverse()
slice()
some()
sort()
splice()
以下の(新しめの)メソッドは「空(empty)」の要素を undefined
と同様に扱います。
(MDNより)
(join()
や toLocaleString()
などは新しめのメソッドではないですが)
entries()
fill()
find()
findIndex()
findLast()
findLastIndex()
includes()
join()
keys()
toLocaleString()
toReversed()
toSorted()
toSpliced()
values()
with()
配列のメソッドによって 「空(empty)」の要素の扱いが異なることに注意しましょう。
「疎」な配列を「密」にする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
文は式、値として扱えないのでコードゴルフでは使いづらいですね。
② 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]
③ スプレッド構文で展開(コピー)
配列は反復可能(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]
④ Array() + Array()
Array()
を二重に使ってみます。
const arr = Array(...Array(10)); arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]
⑤ 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()
はクラス記法が登場する前はサブクラスのコンストラクタ関数内でスーパークラスのコンストラクタ関数を実行するときによく使っていました。
⑥ Array.of() + スプレッド構文
const arr = Array.of(...Array(10)); arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]
⑦ 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); // => ['反復可能が優先']
⑧ toSorted()
sort()
の非破壊版、toSorted()
も使えます。
toSorted()
は「空(empty)」を undefined
と同等に扱ってくれます。
const arr = Array(10).toSorted(); arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]
⑨ toReversed()
reverse()
の非破壊版、toReversed()
も。
const arr = Array(10).toReversed(); arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]
⑩ toSpliced()
splice()
の非破壊版、toSpliced()
も。
const arr = Array(10).toSpliced(); arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]
⑪ with()
const arr = Array(10).with(); arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]
⑫ JSON.parse() + JSON.stringify()
ちょっと強引ですがこんな方法も。
const arr = JSON.parse(JSON.stringify(Array(10))); arr; // => [null, null, null, null, null, null, null, null, null, null]
JSON.parse
は eval
や Function
で代替しても良いですね。
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]
flat()
const arr = [0,,1,,2,,3].flat(); arr; // => [0, 1, 2, 3]
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文字です)
みなさまのご参加をお待ちしております!