こんにちは、Ryohei(@ityryohei)です!

JavaScriptで配列やオブジェクトを扱う際、要素のコピー、結合、特定のプロパティの抽出といった操作は日常茶飯事です。しかし、従来のやり方(concatObject.assign、インデックスを使った代入など)は冗長になりがちで、意図しないデータ破壊(ミューテーション)を引き起こすリスクもありました。

この課題を解決するのが、ES6で導入されたスプレッド構文分割代入(Destructuring Assignment)です。これらを活用すれば、コードは劇的に短く、安全になり、不変性(Immutability)を保ったデータ操作が可能になります。

本記事では、この二つの強力な構文の基本的な原理から、実務で頻出するオブジェクトや配列のコピー、結合、そして関数引数への応用テクニックまでを徹底解説します。

配列やオブジェクトを操作するたびに、冗長なコードになったり、元のデータが意図せず変わってしまったりするんだ。もっとスマートで安全な方法はないかな?

上記の疑問にお答えします。

では、解説していきます。

スプレッド構文と分割代入が解決する問題

従来のデータ操作の課題

スプレッド構文と分割代入が登場する前は、以下のような問題がありました。

  1. 冗長な結合:
    配列を結合する際にArray.prototype.concat()、オブジェクトを結合する際にObject.assign()を使う必要があり、冗長でした。
  2. 不便な抽出:
    オブジェクトから特定のプロパティの値を取り出すために、一行ずつ変数に代入する必要がありました。
  3. シャローコピーの問題:
    配列やオブジェクトを単純に=で代入すると、参照(メモリのアドレス)がコピーされるため、新しい変数で変更を加えると元のデータも変わってしまうという意図しない副作用(ミューテーション)のリスクがありました。

スプレッド構文(...)とRestパラメータの原理的な違い

どちらも三つのドット(...)という同じ表記を用いますが、コード上の「位置」によって役割が完全に異なります。

名称役割(何をするか)コード上の位置
Spread(展開)要素をバラバラに展開する配列リテラル、オブジェクトリテラル、関数呼び出しの右辺
Rest(残余)残りの要素を集めて配列/オブジェクトにする分割代入や関数定義の左辺

この共通の表記は、「多数の要素を扱う」という点では共通しているためですが、その操作は正反対です。Spreadはコンテナを「広げる」操作、Restはコンテナに「まとめる」操作と理解すると分かりやすいでしょう。

基本的な使い方:配列とオブジェクトの活用法

1. 配列への応用(Spread)

Spread構文は配列の要素を簡単に結合したり、コピーしたりするのに使われます。

JavaScript

const fruits = ['apple', 'banana'];
const vegetables = ['carrot', 'potato'];

// 配列の結合
const combined = [...fruits, ...vegetables]; 
// -> ['apple', 'banana', 'carrot', 'potato']

// 配列のシャローコピー
const copiedFruits = [...fruits];
copiedFruits.push('orange');

console.log(fruits);       // -> ['apple', 'banana'] (元のデータは不変)
console.log(copiedFruits); // -> ['apple', 'banana', 'orange']

2. オブジェクトへの応用(Spread)

オブジェクトに対しても、プロパティを簡単に結合したり、上書きしたりできます。

JavaScript

const user = { id: 1, name: 'Alice', age: 30 };
const updates = { age: 31, city: 'Tokyo' };

// 結合と上書き(updatesのageがuserのageを上書き)
const newUser = { ...user, ...updates }; 
// -> { id: 1, name: 'Alice', age: 31, city: 'Tokyo' }

// オブジェクトのシャローコピー
const copiedUser = { ...user };
copiedUser.age = 50;

console.log(user.age);       // -> 30 (元のデータは不変)
console.log(copiedUser.age); // -> 50

実践コード:分割代入によるデータ抽出とRestプロパティ

1. オブジェクトからの抽出とRestプロパティ

オブジェクトの分割代入では、特定のプロパティを抽出した後、残りのすべてのプロパティを新しいオブジェクトに集めるRestプロパティが利用できます。

JavaScript

const userProfile = {
  id: 1,
  firstName: 'Taro',
  lastName: 'Yamada',
  role: 'Admin',
  score: 95
};

// idとroleを抽出し、残りのプロパティを'userData'オブジェクトにまとめる
const { id, role, ...userData } = userProfile; 

console.log(id);    // -> 1
console.log(role);  // -> 'Admin'
console.log(userData); // -> { firstName: 'Taro', lastName: 'Yamada', score: 95 }

これは、オブジェクトから特定のプロパティ(例えばパスワードなど)を除外して、残りのプロパティだけを別の関数に渡したい場合に非常に役立ちます。

2. 配列からの抽出とRest Element

配列の分割代入におけるRest Elementも同様に、残りの要素を新しい配列にまとめます。

JavaScript

const userScores = [100, 90, 80, 70, 60];

// 最初の2つの要素を変数に代入し、残りを'rest'配列にまとめる
const [highest, secondHighest, ...rest] = userScores;

console.log(highest);        // -> 100
console.log(secondHighest);  // -> 90
console.log(rest);           // -> [80, 70, 60]

応用活用:Restパラメータによる安全な可変長引数

Restパラメータとargumentsオブジェクトの比較

関数定義におけるRestパラメータ(...argsなど)は、関数に渡された全ての引数を純粋な配列としてまとめてくれます。これは、従来の可変長引数を受け取るためのargumentsオブジェクトよりも安全かつ強力です。

特徴Restパラメータ (...args)argumentsオブジェクト
Arrayのインスタンス(map, filterなどがそのまま使える)Arrayライクなオブジェクト(利用には変換が必要)
明確性定義された引数以降の引数のみを扱う関数に渡された全ての引数を扱う
可読性明示的で分かりやすい非推奨であり、非厳格モードでは引数に影響を及ぼす

JavaScript

// Restパラメータを使用
function logScores(name, ...scores) {
  // scoresは純粋な配列なので、すぐにreduceが使える
  const total = scores.reduce((sum, score) => sum + score, 0);
  console.log(`${name}の合計点: ${total}`);
}

logScores('Kenji', 90, 85, 70, 95); // -> Kenjiの合計点: 340

補足:Immutabilityとディープコピーの必要性

なぜシャローコピーでは不十分なのか

スプレッド構文によるコピー({...obj}[...arr])は、あくまでシャローコピー(浅いコピー)です。

実務で頻繁に使われるReactやVueなどの状態管理(State Management)において、データがネストされている場合、シャローコピーでは**不変性(Immutability)**が保証されません。

JavaScript

const originalState = { 
  user: 'Alice', 
  profile: { count: 10 } 
};

// シャローコピー
const newState = { ...originalState }; 

// ネストされたオブジェクトの内部を変更
newState.profile.count = 20;

console.log(originalState.profile.count); // -> 20 (元のStateも変更され、バグの原因となる!)

状態管理システムは、元のStateと新しいStateが「異なる参照」を持つことで変更を検知します。シャローコピーでは内部のオブジェクトの参照が同じままのため、システムが変更を検知できず、UIが更新されないなどのバグが発生します。

ディープコピー(深いコピー)の手法

ネストされたオブジェクトも完全にコピーし、不変性を保証したい場合は、以下のディープコピー手法を使用する必要があります。

  1. JSON.parse(JSON.stringify(original)): 最も簡単な方法ですが、関数やDateオブジェクトなどは正しくコピーできません。
  2. structuredClone(): 最新のブラウザおよびNode.jsで利用可能な標準APIです。関数などを除き、ほとんどのデータ型を安全かつ効率的にディープコピーできます。(最も推奨)
  3. ライブラリ(Lodashなど): _.cloneDeep()など、複雑なオブジェクト構造に対応した強力な機能を提供します。

最後に

スプレッド構文と分割代入は、現代のJavaScript開発において、簡潔で安全なデータ操作を実現するための必須テクニックです。

  • 構造的な役割:
    ...は位置によってSpread(展開)とRest(集約)という正反対の役割を持ちます。
  • 安全性の向上:
    RestプロパティやRestパラメータを使うことで、冗長なコードを排除し、必要なデータのみを扱うことができます。
  • Immutability:
    シャローコピーの限界を理解し、ネストされたデータにはstructuredClone()などのディープコピー手法を適切に使い分けることが、堅牢な状態管理の鍵となります。

これらの構文を日常的に活用することで、あなたのJavaScriptコードはよりクリーンで、堅牢なものとなるでしょう。

以上、JavaScriptのスプレッド構文と分割代入についての解説でした!

この記事を書いた人

Ryohei

Webエンジニア / ブロガー

福岡のWeb制作会社に務めるWebエンジニアです。エンジニア歴は10年程で、好きな言語はPHPとJavaScriptです。本サイトは私がインプットしたWebに関する知識を整理し、共有することを目的に2015年から運営しています。Webに関するご相談があれば気軽にお問い合わせください。