TypeScriptアドベントカレンダーの12/5のエントリーです。昨日は@nanasi-1さんの【TypeScript】ジェネレーターによる遅延評価でフィボナッチ数列を生成するでした。
イマドキのJavaScriptの書き方2018というのを以前書いたのだけど、配列周りはかなり変わっているな、というのを思ったので、そこの部分だけアップデートするつもりで書いてみました。
実環境で使えるECMAScriptバージョン
今時のブラウザは常に最新に更新されるはずなのでECMAScript 2024の機能もフルに使えるはずですが、おそらくNode.jsのLTSが一番古いJavaScriptエンジンということになるのかな、と思います。本記事執筆時点でサポート中のバージョンは以下の4つです。軽くメソッドを調べたりした感じ、こんな感じかと。202x年の11月ぐらいになると、ES202xがLTSバージョンで使えるようになって・・・というサイクルのようですね。DenoとBunはよくわかりません。
Nodeバージョン | ESバージョン | アクティブ | メンテナンス |
---|---|---|---|
18.20.5(Maintenance) | 2022相当 | 終了 | 2025/4/30まで |
20.18.1(Maintenance) | 2023相当 | 終了 | 2026/4/30まで |
22.11.0(LTS) | 2024相当 | 2025/10/21まで | 2027/4/30まで |
23.3.0(Current) | 2024相当 | 2025/3/31まで | 2025/6/1まで |
VSCodeでコードを修正する場合、ES2023とか2024のメソッドを使おうとすると lib
を上げろと言ってきますが、実際に使えるECMAScriptバージョンにあわせて、tsconfig.jsonのtargetを上げてしまう方が良いかと思います。
npx -p typescript tsc --init |
{ |
MDNを見ると、Widely Availableは2.5年前のバージョン基準ということで、今だとECMAScript 2022以前、という感じですかね。
配列の作成
新規で作る
Array.of()
というクラスメソッドはありますが、指定した要素を持つ配列はリテラルで作れば良いかなと思います。
// リテラルで配列を作成 |
既存のデータ構造からコピーして作る
旧来の書き方だとslice()
を使って行っていたと思いますが、旧来のメソッドはインプレースの変更なのか新規作成なのかが分かりにくいため、見た目で新しいオブジェクトを作成していることが明確なスプレッド構文が良いかと思います。
// 旧: slice利用 |
// 新: 浅いコピーはスプレッド構文 |
ECMAScriptではないのですが、Node.jsでも使える structuredClone()
関数を使うと、深いコピーができます。サポートが遅れていたSafariでも2022/10には使えるようになったので、現時点で全ての環境で使えると判断しても良いでしょう。
// 新: 深いコピーはstructuredClone |
プログラマブルに配列を作る
プログラマブルに「全部0で初期化された配列を作る」ということを行いたい、ということがあるかと思います。
長さ指定で配列を作る場合は Array(n)
で作れますが、これだと配列の中身が undefined
になります。その後初期化が必要なため、疎な配列を作りたい場合以外は使わない方が良いでしょう。
// 旧: Array(n)を利用。作成後にループで初期化、mapで初期化 |
ECMAScript 2015のArray.from()
はイテレータ対応のオブジェクト以外にlength
を持つオブジェクトが使えて、2つ目にmap()
と同じ変換関数を持てるので、これを使って任意の数を設定できます。map()
と同じなので2つめにインデックスが渡されるのでインデックスごとに値を変えたい場合も対応可能です。
// 新: Array(n)を利用。作成後にループで初期化、mapで初期化 |
ループ
ループの書き方は3種類あります。一番使うことになるのが for ... of
ループです。3つ紹介するなかでは最後発で、2015年のECMAScript6で導入されました。この構文はArray
、Set
、Map
、String
などの繰り返し可能(iterable)オブジェクトに対してループします。配列の場合で、インデックス値が欲しい場合は、entries()
メソッドを使います。
const iterable = ["小金井", "小淵沢", "小矢部"]; |
こちらは関数呼び出しを伴わないフラットなコードなので、async/await
とも一緒に使えます。配列の要素を引数にして、1つずつawait
したい場合などです。
const iterable = [10, 20, 30]; |
2つ目に紹介するのはループ変数が必要となる、最初期からあるC言語由来のループです。順番に配列の最後までループする用途であれば、(2重ループとかで)変数名を間違えて意図せずループが終わったり、無限ループになるリスクがあるため、前述の for ... of
がおすすめです。
このループが活躍するのは、アルゴリズムの実装で、ループ変数の開始位置や終了位置が先頭や末尾以外にする必要があるケースなどです。次のコードは、重複を効率よく探すために、 i
と j
の組が重複しないようにループ範囲を調整しています。
const iterable = ["中野駅", "中村屋", "中尊寺", "中央線", "中野駅"]; |
2009年のECMAScript5で追加されたforEach()
もあります。関数型主義的なスタイルで統一するために、for
を禁止してforEach()
のみを使うというコーディング標準を規定している会社(Airbnb)も10年ぐらい前に話題になりましたが、積極的に使う理由はないでしょう。
const iterable = ["大判焼", "大納言", "大所帯"]; |
速度の面で言えば、旧来の for
ループが最速です。 for ... of
や forEach()
は、ループ1周ごとに関数呼び出しが挟まるため、実行コストが多少上乗せされますが、ゲームの座標計算で1フレームごとに数万要素のループを回さなければならない、といったケース以外ではほぼ気にする必要はないでしょうし、特に for ... of
の場合は処理系が最適化を行ってくれているため、パフォーマンス低下は少ないです。
軽く検証したところ、最速のfor
と比べてChromeでfor...of
の処理時間が20%増し、forEach()
が135%増しでした。Safariだと、Chromeでfor...of
が110%増し、forEach()
が460%増しぐらい。forEach()
はもう忘れましょう。
配列のデータ取得
1要素の取得
データの取得方法も選択肢がいくつか増えています。前からn番目の要素取得に関しては変わりませんが、末尾からの取得については、ECMAScriptで2022で追加された at()
メソッドが便利です。正の値を使う場合は動作はインデックスアクセスと変わりませんが、負の数を与えると末尾からのインデックスで値が取得できます。
// 共: 1要素の取得(前からのインデックス) |
// 旧: 末尾の要素の取得 |
// 新: 末尾の要素の取得 |
複数要素の取得
複数の要素を取り出す場合は今までは1つずつ取り出していたと思います。また、残りの部分をまとめて取り出すときはslice()
で取り出します。
もし配列が短い場合にデフォルト値を与えたい場合はORの演算子 ||
を使うことが多いでしょう。ただし、0とかfalsyな値に真面目に対応しようとするともうちょっとがんばらないといけないのですが。
// 旧: 末尾の要素の取得 |
ECMAScript 2015から導入された分割代入では複数要素をまとめて取り出せます。ECMAScript 2018から導入された残りプロパティ(...
)を使うと、残りを全てまとめて取得、というのもできます。
// 新: 要素をまとめて取得。デフォルト値も指定可能。 |
ただし、まとめて取得する場合のスタート位置が状況によって変わる場合は以前の通り、slice()
を使うことになるでしょう。
配列の変更
ウェブフロントエンドの状態管理だと、配列の変更を行う場合には、インプレースで値を変更するのではなく、新しいオブジェクトを作って返す、ということが行われます。Reactが差分検知でそれを期待しており、Reduxとかもそれに従っていました。Redux Toolkitの裏で使われているimmer.jsみたいに、インプレースの変更をもとにイミュータブルに新しいオブジェクトを作ってくれるライブラリもあるにはありますが、本体にもイミュータブルスタイルのメソッド追加が頻繁に行われています。
どちらが新か旧かというのはなく、用途次第だと思うので、破壊的な方法と、イミュータブルの方法を並べて紹介します。
先頭・末尾の要素の追加
末尾に追加するのはpush()
メソッドが昔からある方法です。
const a = [1, 2, 3, 4, 5]; |
スプレッド構文を使うと、メソッド名と役割を覚えずに、リテラル上の位置のまま挿入できて、見た目もわかりやすいでしょう。
const a = [1, 2, 3, 4, 5]; |
先頭・末尾の要素の削除
先頭・末尾の要素の削除はpop()
とshift()
を使います。
const a = [1, 2, 3, 4, 5]; |
先頭の要素の削除はスプレッド構文を使えばできますが、末尾の要素の削除はスプレッド構文ではできません。slice()
を使って、現在よりも1小さい長さを指定してコピーする方法しかないでしょう。
const a = [1, 2, 3, 4, 5]; |
要素の変更
これは一番シンプルな最初に覚える書き方ですね。
const a = [1, 2, 3, 4, 5]; |
こちらはECMAScript 2023の新顔のwith()
メソッドです。
const newA = a.with(1, 4); |
配列の結合
push()
は複数要素対応できるため、スプレッド構文を使えば破壊的な変更は難しくないでしょう。一昔前はb.push.apply(b, c)
みたいなコードを書いていた気がしますが、それは忘れましょう。
const a = [1, 2, 3]; |
昔からあるconcat()
メソッドは新しい配列も作りますが、スプレッド構文1つで配列結合もできるのでこちらだけ覚えておけば良いでしょう。昔のメソッドは破壊的なのかそうじゃないのかが名前だけでは分からない欠点があります。
const a = [1, 2, 3]; |
途中への要素/配列の挿入・削除
昔のJavaScriptには「これが使いこなせれば初級脱出」というsplice()
というメソッドがありました。1つめの引数に処理を開始したい要素のインデックス、2つ目に削除したい要素の数、3つ目以降に挿入したい要素(複数可)というものです。これを使うと、任意の箇所に要素を挿入できます。
2つめの引数を1以上にすると削除します。
const a = [1, 2, 3]; |
これはスプレッド構文だけではやや難しかったのですが、ECMAScript 2023ではslice()
のイミュータブルバージョンが追加されました。
const a = [1, 2, 3]; |
ソート・反転
ミュータブルなソートや反転はおなじみのsort()
やreverse()
を使います。
const arr = [1, 5, 8, 3, 6, 2]; |
splice()
に対するtoSpliced()
みたいに、ECMAScript 2023でsort()
に対するtoSorted()
とreverse()
に対するtoReversed()
が追加されました。
const arr = [1, 5, 8, 3, 6, 2]; |
Pythonは昔からsorted()
とかreversed()
という関数を提供していましたが、「ソート済みかどうか」の判定関数にも見えてしまう名前なのでこちらの方が良いですね。
ネストを解消
flat()
は配列の中に配列がある場合に、それを平坦にならします。デフォルトでは配列の中の配列までが対象ですが、パラメータの数値で、何段まで探索するかが変わります。これはイミュータブルなメソッドです。
const arr = [0, 1, [2, [3, 4, 5]]]; // 3層の配列 |
リスト処理
フロントエンドで関数型スタイルのコーディング技法として広まっているが関数型由来のリスト処理です。昔からあって一番有名なのがmap
/filter
/reduce
ですが、他にもいくつかあります。
1つだけだとfor...of
を使うのとあまり変わりませんが、メソッドチェーンで複数接続すると、小さい関数で期待する結果を得る、関数型スタイルの処理になります。
残念ながら、これらはリストのメソッドであり、DOMのAPIにいくつかある「リストっぽいけどリストじゃない」オブジェクトでは使えなかったりします。今ステージ2のパイプライン演算子が入ることになれば、これらのメソッドも純粋な関数に再定義されて、すべてのイテレーション対応オブジェクトで使えるようになるんじゃないかと期待しています。このリスト処理だと同期処理しか対応できないのですが、それもこのパイプライン演算子が入れば解決の見込みです。
const result = input |
リスト→リスト変換
リストからリストを返すメソッド群が以下の3つです。
map(変換処理)
: ループの要素ごとに変換処理の関数を呼び出し、その結果で新しい配列を作って返すfilter(判定処理)
: ループの要素ごとに判定処理の関数を呼び出し、真の値を返すもののみの新しい配列を作って返すflatMap(変換関数)
: ループの要素ごとに変換処理の関数を呼び出し、その結果をflat()
した新しい配列を作って返す
これらを結合すると柔軟な処理が可能ですが、可能であればfilter()
で要素を減らす処理を先に行うと、トータルのループの回数が減るため、処理が高速化されます。
Reactの仮想DOM構築では、通常はすべて1つの式の中でDOMを作ります。そうなるとif/forが使えません。ifの代わりに三項演算子、for
の代わりにmap
がよく使われます。
const items = [ |
flatMap()
はmap()
とflat()
をくっつけたものです。ただし、配列を返すとそれを展開する点が異なります。くっつけたことにより、map()
よりも柔軟です。
次のサンプルのように、定数を返すだけならmap()
互換ですが、空配列を返すとその要素は消滅します。また、配列を返すことで、1要素から複数の要素を生み出せます。ただし、配列そのものを要素にしたい場合は2重配列にする必要があります。
const a = [1, 2, 3, 4]; |
1つの要素を返す
reduce(結合処理[, 初期値])
: 要素の配列ごとに結合処理を行い、最終的に1つの値にして返すsome(判定処理)
: 要素の1つでも判定処理関数が真の値を返せばtrue
を返すevery(判定処理)
: すべての要素に対して判定処理関数が真の値を返せばtrue
を返す
const items = [ |
reduce()
の結合処理は前の処理結果が1つめの引数に渡され2つ目に配列の要素が入ります。これを要素数分繰り返します。初期値を省略すると2つ目の要素から処理され、最初の要素は前の処理の結果として渡されます。上記の処理は4回呼ばれます。最後の結果が関数の結果となります。
(0, { price: 100 })
=> 100(100, { price: 50 })
=> 150(150, { price: 80 })
=> 230(230, { price: 150 })
=> 380
some()
は条件似合うものが1つでもあればすぐにループを終えるので、every()
はすべての要素を探索するので、not条件を検証したい場合は、some()
で代替できないかは検討してみると良いでしょう。
まとめ
以前の配列は破壊的操作と、イミュータブルな操作が混ざっていてわかりにくかったのですが、関数型スタイルブームに合わせてか、イミュータブルな処理が増えました。リファレンスを見ても、その辺りの区別がつきにくいと思われるため整理のためにまとめてみました。
リスト処理、toXXXed()
メソッドの追加とwith()
メソッド、flat()
メソッド、スプレッド構文を中心に使えばイミュータブルなデータ加工はだいぶ思いのままにできそうだな、ということがわかりました。