JavaScript中級 #4 デストラクチャリングと spread/rest 詳細

基礎 #5 オブジェクトと配列でデストラクチャリングと spread を軽く見ました。この記事はそのツールを実戦でよく出会うパターンで深く入っていきます。

オブジェクトデストラクチャリング — 基本 + 応用 #

基本 + 名前変更 + 既定値
const user = { id: 'u1', name: 'カーティス', age: 30 };

// 基本
const { name, age } = user;

// 名前を変えて
const { name: userName } = user;

// 既定値
const { email = 'なし' } = user;

// 名前変更 + 既定値
const { phone: userPhone = '未入力' } = user;

特に最後のパターンが最初は紛らわしいです — phone キーを userPhone で受けて、なければ '未入力'。オプションオブジェクトを受け取る関数によく登場します。

ネストオブジェクトを解く #

ネストデストラクチャリング
const response = {
  data: {
    user: {
      id: 'u1',
      profile: {
        name: 'カーティス',
        age: 30,
      },
    },
  },
};

const {
  data: {
    user: {
      profile: { name, age },
    },
  },
} = response;

console.log(name, age);   // カーティス 30

深いところの値を一行で引っ張ってきます。途中の変数(datauser)は作らずに必要な leaf 値だけを取得します。

ただしあまり深く使うと読みづらいです。ネストが二段階以上深いなら二、三段階に分けて解くか、オプショナルチェーン(#5)を使うほうが普通はより明確です。

配列デストラクチャリング — パターンたち #

基礎復習 + 応用
const arr = [1, 2, 3, 4, 5];

const [a, b] = arr;             // a=1, b=2
const [, , c] = arr;             // c=3 (前二つをスキップ)
const [first, ...rest] = arr;    // first=1, rest=[2,3,4,5]
const [x = 10] = [];             // x=10 (既定値)

配列は順序に意味があるので — デストラクチャリングも順序通り受け取ります。

Swap — 二つの変数の値を交換 #

古典 swap
let a = 1, b = 2;
[a, b] = [b, a];
console.log(a, b);   // 2 1

一時変数なしで一行で終わらせる慣用句。昔は let temp = a; a = b; b = temp; でした。

引数デストラクチャリング — 最もよく使う場面 #

関数の引数でデストラクチャリングを使うのは、モダンJavaScriptの最もよくある慣用句です。

オプションオブジェクトパターン
function createUser({ name, age, email = '未入力' }) {
  console.log(name, age, email);
}

createUser({ name: 'カーティス', age: 30 });
// カーティス 30 未入力

このパターンの利点:

  • 呼び出し側でキー名が見える — 引数の順序を覚える必要がない
  • オプション引数をキーと既定値で表現
  • 新しい引数追加に互換性維持 — 既存の呼び出しが壊れない

Reactコンポーネントの props がまさにこのパターンです。

空オブジェクトで呼び出し可能に #

安全なオプションパターン
function init({ debug = false, retries = 3 } = {}) {
  console.log(debug, retries);
}

init();                       // false 3 — 引数なしでも OK
init({ debug: true });        // true 3

引数自体に = {} を置くと、引数を渡さなくても空オブジェクトで始まってデストラクチャリングが安全になります。オプションがすべて任意のときの中心的なパターンです。

一部は受け取り、残りはまるごと #

rest で残りを集める
function update(user, { id, ...rest }) {
  console.log(id);     // 個別に
  console.log(rest);   // 残りのオブジェクト
  Object.assign(user, rest);
}

update(targetUser, { id: 'u1', name: 'カーティス', age: 30 });
// id: u1
// rest: { name: 'カーティス', age: 30 }

id は別途抽出して検査/ログ/特別処理し、残りはまるごとオブジェクトで受けて spread/assign で流し送るパターン。ライブラリの関数によく使われます。

Spread 応用 — オブジェクト #

1) 深いマージはできない (浅いコピー) #

spread は浅いコピー
const a = { user: { name: 'カーティス', age: 30 } };
const b = { ...a, theme: 'dark' };

b.user.age = 31;
console.log(a.user.age);   // 31  ← a も一緒に変わる

{...a} は1段階のプロパティだけを新しく作ります。中のオブジェクトは依然として同じ参照(基礎 #2)を共有します。深いコピーが必要なら:

深いコピー — structuredClone
const a = { user: { name: 'カーティス', age: 30 } };
const b = structuredClone(a);

b.user.age = 31;
console.log(a.user.age);   // 30

structuredClone は ES2022 で標準化されたビルトイン。ほとんどのデータ構造(オブジェクト、配列、Date、Map、Set など)を深くコピーしてくれます。昔は JSON.parse(JSON.stringify(...)) トリックを使いましたが、それは関数/Date/undefined を扱えませんでした。

2) 条件付きプロパティ — && と spread #

条件付きでプロパティ追加
const includeEmail = true;
const profile = {
  name: 'カーティス',
  age: 30,
  ...(includeEmail && { email: 'me@example.com' }),
};

includeEmailtrue なら { email: ... } が spread、false なら空オブジェクトが spread (何も起こらない)。

オプションオブジェクトを動的に作るときによく使うパターンです。

3) 関数引数に spread #

関数引数を展開
const args = [1, 2, 3];
Math.max(...args);   // 3

function logAll(a, b, c) {
  console.log(a, b, c);
}
logAll(...args);     // 1 2 3

apply のモダンな代替。昔は Math.max.apply(null, args) でした。

Spread vs Rest — 同じ ... 別の意味 #

同じ ... 構文が場面によって意味が変わります。

場面名前意味
関数呼び出しの引数spread配列を引数群へ展開
配列/オブジェクトリテラルの中spread中の要素/プロパティを展開して入れる
関数の引数rest引数を配列に集める
デストラクチャリングの場面rest残りを集める
場面別比較
const arr = [1, 2, 3];

Math.max(...arr);            // spread — 展開
const copy = [...arr];        // spread — 展開
function f(...args) {}        // rest — 集める
const [head, ...tail] = arr;  // rest — 集める

左辺で集めれば rest、右辺で展開すれば spread。 これが最も短い覚え方です。

よく出会う実戦パターン群 #

1) immutable update — 一つのフィールドだけ変えて新しいオブジェクト #

フィールド更新
const user = { id: 'u1', name: 'カーティス', age: 30 };

const updated = { ...user, age: 31 };
// 元は変わらず、age だけ変わった新しいオブジェクト

React の setState のような場面で必須です。

2) 配列 — インデックスで一つの要素だけ更新 #

配列の一つの要素を更新
const items = ['a', 'b', 'c', 'd'];
const i = 2;
const newValue = 'C';

const updated = [
  ...items.slice(0, i),
  newValue,
  ...items.slice(i + 1),
];
// ['a', 'b', 'C', 'd']

// または ES2023 の toSpliced
const updated2 = items.toSpliced(i, 1, newValue);

toSpliced のほうが短いです。モダンJavaScriptがどんどんイミュータブルメソッドを増やしていく流れです。

3) キーを合わせる — 二つのオブジェクトをマージ #

オブジェクトマージ
const defaults = { theme: 'light', lang: 'ko' };
const userPrefs = { theme: 'dark' };

const merged = { ...defaults, ...userPrefs };
// { theme: 'dark', lang: 'ko' }

後ろに書いたオブジェクトが同じキーを上書きします(優先)。Object.assign({}, defaults, userPrefs) と同じ効果。

4) JSX/React props 伝達 #

props をまるごと伝達
function Wrapper(props) {
  return <Child {...props} />;
}

React コードで本当によく見ます。親が受け取った props を子にまるごと流し送る場面。

落とし穴 — オブジェクト spread 時の prototype 情報の喪失 #

{...obj}obj の直接プロパティだけを取ってきます。プロトタイプチェーンは追いません。

prototype 喪失
class User {
  constructor(name) {
    this.name = name;
  }
  greet() {
    console.log(`こんにちは、${this.name}`);
  }
}

const u = new User('カーティス');
const copy = { ...u };

u.greet();        // こんにちは、カーティス
copy.greet();     // ✗ TypeError — copy はただのオブジェクト、greet なし

クラスインスタンスを spread でコピーすると一般的なオブジェクトになります。メソッドはプロトタイプにあるので消えます。クラスインスタンスを本当にコピーするには Object.assign も同じ限界があり、structuredClone はクラスインスタンスをコピーできません(plain オブジェクトに変換)。クラスインスタンスをコピーするにはクラスに clone() メソッドを直接作るほうが一般的です。

まとめ #

この記事で整理した内容:

  • オブジェクトデストラクチャリング — 名前変更、既定値、二つの組み合わせ、ネスト
  • 配列デストラクチャリング — swap 慣用句、rest で頭/尻尾を分離
  • 引数デストラクチャリング — オプションオブジェクトパターンが最も多い
  • = {} で引数なしの呼び出しを安全に
  • spread は浅いコピー — 深いコピーは structuredClone
  • 条件付きプロパティ — ...(cond && { ... })
  • spread vs rest — 左辺 rest / 右辺 spread
  • immutable update パターンたち
  • クラスインスタンスは spread でコピーできない

次の記事(#5 オプショナルチェーンと nullish 合体)では、深いオブジェクトに安全にアクセスする ?.、そして falsy と nullish の違いを解いてくれる ?? 演算子を扱います。

X