分割代入は「形」に対するパターンマッチ
JavaScriptの分割代入(destructuring)を使うと、オブジェクトや配列の「形」をそのままなぞるようなパターンを書くだけで、名前や位置を指定して値を一気に取り出せます。プロパティを1つずつ地道に取り出す代わりに、「こういう形で欲しい」と書けば、JavaScriptがそのとおりに渡してくれるわけです。
分割代入には、大きく分けて2つのスタイルがあります。
波かっこ {} を使えばオブジェクトをプロパティ名で取り出せますし、角かっこ [] を使えば配列を位置(インデックス)で取り出せます。どちらの場合も、左側に書く変数は 新しく宣言している変数 です。値にインデックスでアクセスしているのではなく、「こういうパターンで中身を取り出したい」というかたちを記述しているイメージですね。
ちなみに、分割代入がなかった時代は、同じ処理を次のように書いていました。
const name = user.name;
const age = user.age;
const first = scores[0];
const second = scores[1];
致命的というほどではありませんが、関数を書くたびにこれをやるとなると、さすがに面倒に感じてきます。
オブジェクトの分割代入はプロパティ名で取り出す
オブジェクトの場合、波かっこの中に書く名前は、元のオブジェクトのプロパティ名と一致している必要があります。並び順は関係ありません。JavaScript はキーをもとに値を探してくれるからです。
オブジェクトに該当するプロパティがなければ、変数は undefined になります。エラーにはなりません。
この「こっそりundefinedになる」挙動は頭の片隅に置いておきましょう。プロパティ名のタイポに気づけず、深夜2時に頭を抱える……なんてことになりがちです。
分割代入で変数名をリネームする
オブジェクトのプロパティ名をそのまま変数名として使いたくない場面もあります。スコープ内の別の変数と名前がかぶってしまったり、APIの命名規則が好みじゃなかったり、理由はさまざまです。そんなときは 元の名前: 新しい名前 の形で別名を付けられます。
読み方としては「id プロパティを取り出して、userId という名前で受け取る」という感じです。コロンの前が元のプロパティ名で、後ろが新しい変数名になります。他の言語の型注釈っぽく見えますが、まったくの別物です。
デフォルト値を指定する
プロパティが存在しないかもしれないときは、分割代入のパターンの中で直接デフォルト値を指定できます。
デフォルト値が適用されるのは、値が undefined のときだけです。null や 0、"" はそのまま通ってきて、デフォルト値には置き換わりません。
これ、意外とハマりやすいポイントです。null のときもデフォルト値を効かせたいなら、明示的にチェックするか、分割代入したあとに Null 合体演算子 ?? を使いましょう。
デフォルト値とリネームは組み合わせて使えます:
配列の分割代入は位置で対応する
配列には名前がなくインデックスしか持たないので、分割代入のパターンは「位置」で対応します。興味のない要素はカンマで飛ばせます。
先頭の2つのカンマは「インデックス0と1はスキップ」という意味です。見た目はちょっとごちゃっとしますが、真ん中の要素だけ取り出したいときに便利なテクニックです。
配列の分割代入は、タプル的な値を返す関数と組み合わせると一気に映えます。
React の Hooks でもおなじみのパターンですよね。const [count, setCount] = useState(0) みたいに、あちこちで見かけます。
rest パターン:残りをまとめて受け取る
分割代入のパターンの中で ...name と書くと、名前や位置でマッチしなかった要素をまとめて新しい変数に入れてくれます。
配列の場合、rest は末尾の要素をごっそり拾います。
オブジェクトの場合、rest は明示的に指定されなかったプロパティをまとめて受け取ります。
これは、元のオブジェクトを変更せずに特定のフィールドだけを取り除く定番のイディオムです。rest には id 以外のすべてのプロパティが入った新しいオブジェクトが作られます。
rest 要素は必ずパターンの最後に置く必要があります。const [...init, last] のように書くと構文エラーになります。
ネストした分割代入
分割代入のパターンはネストできます。プロパティがさらにオブジェクトや配列になっている場合、同じ式の中でそのまま分割代入で取り出せます。
便利ではありますが、使いどころは慎重に見極めましょう。ネストした波かっこが3階層にもなると、もはやパズルのような読みにくさになってきます。そんなときは途中の値を別の変数に取り出してあげるのが無難です。賢く書くより、読みやすさを優先しましょう。
ひとつ注意点があります。パターン側の data: { user } は、data という変数を作るわけではありません。これは「data の中に潜って、さらに分割代入を続けろ」という指示にすぎないのです。data 自体も欲しい場合は、別途書き足す必要があります。
関数の引数で分割代入を使う
分割代入が一番よく使われる場面、それは関数の引数リストです。「オプションオブジェクトを渡す」スタイルが、そのままシグネチャで引数の中身を語ってくれるようになります。
呼び出し側から見れば、普通のオプションオブジェクトを渡す関数と何も変わりません。ただ関数の中では、デフォルト値付きの名前付き変数として受け取れるので、options.name || "default" みたいな定型コードを書かなくて済みます。
ひとつ注意点があります。この関数を引数なしで呼び出すとエラーになります。undefined は分割代入できないからです。これを防ぐには、引数全体にもデフォルト値を指定しておきましょう。
パターンのうしろにある = {} は「引数が渡されなかったら空オブジェクトが来たことにしてね」という意味です。そのうえで、中のデフォルト値が効いてくれます。
変数の入れ替えと再代入
分割代入は、すでに宣言済みの変数にも使えます。ただしオブジェクトパターンの場合はカッコで囲む必要があります(そうしないと JavaScript が { をブロックの開始と誤解してしまうからです)。
配列の入れ替えはスッキリ書けますね。オブジェクトの再代入はあまり使う機会はありませんが、カッコで囲むテクニックは、いざというときのために覚えておくと便利です。
実践的な使用例
ここまでの内容をひと通り組み合わせて、APIレスポンスを処理する関数を書いてみましょう。
分割代入のレイヤーごとにデフォルト値を用意しておけば、データが一部欠けていても if でガードを並べずに関数がそのまま動きます。これこそが本当のうまみで、厳密でありながら柔軟なシグネチャが書けるわけです。
次は Object Spread
分割代入はオブジェクトから値を取り出す操作でした。一方の spread は値を入れる操作で、コピー・マージ・上書きに使います。モダンな JavaScript ではこの2つはいつもセットで登場します。次は spread を見ていきましょう。
よくある質問
分割代入(destructuring)とは?
分割代入は、オブジェクトや配列から値を取り出して変数に割り当てる処理を一行で書ける構文です。たとえば const { name } = user; なら user の name プロパティを取り出せますし、const [first, second] = list; なら配列の先頭2つを受け取れます。要するに = の左側に、右辺の「形」と同じパターンを書くイメージですね。
分割代入で変数名を変える(リネーム)にはどう書く?
{ 元のプロパティ名: 新しい変数名 } の形で書きます。例えば const { name: username } = user; と書くと、user.name の値を username という変数に入れられます。TypeScriptの型注釈と見間違えやすいので注意。コロンの前が元のプロパティ名、後ろが新しい変数名、という順番です。
分割代入でデフォルト値は設定できる?
できます。const { timeout = 5000 } = options; と書けば、options.timeout が undefined のときだけ 5000 が使われます。ここがハマりやすいポイントで、デフォルトが効くのは undefined のときだけ。null や 0、空文字 '' はそのまま通ります。リネームとの合わせ技もOKで、const { t: timeout = 5000 } = options; のように書けます。
分割代入とスプレッド構文の違いは?
見た目は似ていますが、やっていることは真逆です。分割代入(const { a, b } = obj)は構造から値を「取り出す」操作、スプレッド(const copy = { ...obj })は新しい構造に値を「入れる」操作です。ちなみに分割代入のパターン内で使う ... はrestパターンと呼ばれ、残りの要素をまとめて別の変数に入れる役割をします。