JavaScript: オブジェクトの分割代入とスプレッド構文を使ってみる

ECMAScript 2015の分割代入は、配列のほかオブジェクトに用いることもできます。また、ECMAScript 2018では、オブジェクトにスプレッド構文が採り入れられました。これらの構文の使い方を、簡単にご説明します。なお、配列の場合については、「[JavaScript: ECMAScript 2015のスプレッド構文・残余引数・分割代入を使ってみる](https://qiita.com/FumioNonaka/items/20e96ca9ede2ae157bcc)」をお読みください。


# スプレッド構文を使う
[スプレッド構文](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/Spread_syntax)(`...`)をオブジェクトに用いると、プロパティと値の組みが展開されて取り出せます。つぎのコードは、オブジェクトを複製する例です。

```js
const rect = {type: 'rectangle', width: 50, height: 20};
const rectClone = {...rect};
console.log(rectClone);  // {type: "rectangle", width: 50, height: 20}
```

ただし、つくられるのは浅いコピーです。値がオブジェクトのときは、参照が渡されることになります[^1]。

```js
const rect = {type: 'rectangle', coords: {width: 50, height: 20}};
const rectClone = {...rect};
rect.coords.height = 25;
console.log(rectClone);  // {type: "rectangle", coords: {width: 50, height: 25}}
```

複数のオブジェクトを展開すれば、ひとつにまとめられます。ただし、同じプロパティがあると、あとの値で上書きされることにご注意ください。この仕様を逆に利用すれば、一部のプロパティ値を書き替えたオブジェクトのコピーがつくれるということです。

```js
const rect = {type: 'rectangle', width: 50, height: 20};
const circle = {type: 'circle', radius: 25};
const union = {...rect, ...circle};
console.log(union);  // {width: 50, height: 20, type: "circle", radius: 25}
```

展開したオブジェクトは、入れ子にしてまとめることもできます。

```js
const type = {type: 'rectangle'};
const coords = {x: 20, y: 10};
const size = {width: 50, height: 20};
const rect = {...type, coords: {...coords, ...size}};
console.log(rect);  // {type: "rectangle", coords: {x: 20, y: 10, width: 50, height: 20}}
```

同じかたちの構文でも、関数の[残余引数](https://qiita.com/FumioNonaka/items/20e96ca9ede2ae157bcc#残余引数)は配列です。任意の数のオブジェクトをまとめようとしたとき、つぎのように定めた関数では意図した結果が得られません。配列インデックスをプロパティとして、オブジェクトにまとめられてしまうからです。

```js
function mergedObjects(...objects) {
  return {...objects};
}
console.log(mergedObjects(type, coords, size));
// {0: {type: "rectangle"}, 1: {x: 20, y: 10}, 2: {width: 50, height: 20}}
```

関数として定めるなら、残余引数の配列からオブジェクトをひとつずつ取り出してまとめなければなりません。つぎのコードは、`Array​.prototype​.reduce()`メソッドを使った例です(「[配列要素の平均を求める](https://qiita.com/FumioNonaka/items/fee07b53fd277b218c97#配列要素の平均を求める)」参照)。

```js
function mergedObjects(...objects) {
  // return {...objects};
  return objects.reduce(
    (objects, object) => ({...objects, ...object})
  , {});
}
console.log(mergedObjects(type, coords, size));
// {type: "rectangle", x: 20, y: 10, width: 50, height: 20}
```


# 分割代入を使う
オブジェクトの[分割代入](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment)は、オブジェクトからプロパティを取り出して対応する変数に代入する構文です。

```js
let {a, b} = {a: 0, b: 1};
console.log(a, b);  // 0 1
```

対応するプロパティがない変数には、`undefined`が入ります。また、すべてのプロパティを取り出す必要はありません。

```js
let {a, b, x} = {a: 0, x: 10, y: 20};
console.log(a, b, x);  // 0 undefined 10
```

取り出したプロパティ値を、名前の異なる変数に割り当てることもできます。また、変数にデフォルト値を与えれば、対応するプロパティがない場合にその値が用いられます。

```js
let {x, a: y, z = 0} = {a: 1, x: 10, y: 20};
console.log(x, y, z);  // 10 1 0
```

オブジェクトから必要なプロパティを抜き出したり、まとめたりするときに使うと便利です。

```js
document.addEventListener('mousedown', ({ clientX, clientY }) => {
	const mousePoint = { clientX, clientY };
	console.log(mousePoint);  // {clientX: x座標値, clientY: y座標値}
});
```

また、オブジェクトの残余プロバティ([Rest Properties](https://github.com/tc39/proposal-object-rest-spread#user-content-rest-properties))が、ECMAScript提案のステージ4の段階になりました。関数の残余引数とは異なり、オブジェクトにまとまります。

```js
let {a, b, ...rest} = {a: 0, b: 1, x: 10, y: 20};
console.log(a, b, rest);  // 0 1 {x: 10, y: 20}
```

つぎのコードは、関数の引数にオブジェクトの分割代入を用いた例です。入れ子のオブジェクトから、プロパティを取り出すこともできます。

```js
function getRectArea({type, coords: {width, height}}) {
  if (type === 'rectangle') {
    return width * height;
  }
}
console.log(getRectArea({
  type: 'rectangle',
  coords: {x: 20, y: 10, width: 50, height: 20}
}));  // 1000
```

ただし、入れ子の親に対応するプロパティがないとエラーになります。

```js
console.log(getRectArea({type: 'rectangle'}));
// TypeError: Right side of assignment cannot be destructured
```

つぎの関数は、このエラーを避けるため、引数とプロパティにデフォルト値を用いた例です。

```js
// function getRectArea({type, coords: {width, height}}) {
function getRectArea({type, coords = {}}) {
  if (type === 'rectangle') {
    const {width = 0, height = 0} = coords;
    return width * height;
  }
}
console.log(getRectArea({
  type: 'rectangle',
  coords: {x: 20, y: 10, width: 50}
}));  // 0
console.log(getRectArea({type: 'rectangle'}));  // 0
```

つぎの関数は引数にオブジェクトの分割代入を用い、さらに残余プロパティも使ってみました。

```js
function getArea({type, ...coords}) {
  switch (type) {
    case 'rectangle':
      const {width, height} = coords;
      return width * height;
    case 'circle':
      const {radius} = coords;
      return radius ** 2 * Math.PI;
  }
}
console.log(getArea({
  type: 'rectangle',
  width: 50,
  height: 20
}));  // 1000
console.log(getArea({
  type: 'circle',
  radius: 10
}));  // 314.1592653589793
```

オブジェクトの種類(`type`)をもうひとつ増やしてみましょう。ここで注意したいのは、`case`はブロックをつくらず、`switch`文がひとつのブロックなので、その中で変数の重複が許されないことです。プロパティ名がかぶってしまうときは、変数名を変えることが考えられます。

```js
function getArea({type, ...coords}) {
  switch (type) {
    case 'rectangle':
      const {width, height} = coords;
      return width * height;
    case 'triangle':
      const {base, height: _height} = coords;  // 変数名を変える
      return base * _height / 2;
    case 'circle':
      const {radius} = coords;
      return radius ** 2 * Math.PI;
  }
}
console.log(getArea({
  type: 'triangle',
  base: 50,
  height: 20
}));  // 500
```

あるいは、`switch`文の外で変数宣言する手もあります。このときは、また別の注意をしなければなりません。分割代入を変数宣言で始めないときは、左辺がブロック`{}`とみなされ、[代入演算子`=`が続けられない](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Assignment_without_declaration)ことです。そのため、代入文はかっこ`()`でくくらなければなりません。

```js
function getArea({type, ...coords}) {
  let width, height, radius;
  switch (type) {
    case 'rectangle':
      ({width, height} = coords);
      return width * height;
    case 'triangle':
      ({base, height} = coords);
      return base * height / 2;
    case 'circle':
      ({radius} = coords);
      return radius ** 2 * Math.PI;
  }
}
```

オブジェクトを展開してまとめるスプレッド構文と、プロパティが簡単に取り出せる分割代入、少し注意すべき点はあるものの、コードがすっきり書けます。いろいろと工夫できそうです。

##### [追記: 2020/06/27]
TwitterでAdam Argyle(@argyleink)氏が、つぎのようなテクニックを紹介されていました。JavaScriptの組み込み済みクラス(`Array`)のインスタンスにも、オブジェクトの分割代入が使えるということです。

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">TIL I can use destructuring assignment as a destructuring key, gives me this immutable one liner that extracts the first and last element from an array<a href="https://twitter.com/hashtag/es6?src=hash&amp;ref_src=twsrc%5Etfw">#es6</a> <a href="https://t.co/99Uxa9ccy3">pic.twitter.com/99Uxa9ccy3</a></p>&mdash; Adam Argyle (@argyleink) <a href="https://twitter.com/argyleink/status/1139415654132436993?ref_src=twsrc%5Etfw">June 14, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

配列インデックスもプロパティとして扱われます。変数名を変えたいときはコロン(`:`)のあとに添え、そのままでよければコロン(`:`)以下を省けばよいのです。さらに、プロパティ名を式で表す[計算されたプロパティ名](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/Object_initializer#Computed_property_names)も用いました。

```js
const array = [0, 1, 2, 3, 4];
const { 0: first, length, [length - 1]: last} = array;
console.log({ first, length, last });  // {first: 0, length: 5, last: 4}
```

##### [追記: 2022/01/18]
関数の引数に入れ子でオブジェクトの分割代入を用いると便利なことが少なくありません。たとえば、`<input>`要素の`change`イベントハンドラです。要素の`value`プロパティを取り出そうとするとき、分割代入を使わなければこのような手順でしょう。

```js
const changeHandler = (event) => {
	const inputElement = event.target;
	console.log(inputElement.value);
};
```

分割代入で取り出せば、つぎのように引数から直に`value`が得られます。

```js
const changeHandler = ({target: {value}}) => {
	console.log(value);
};
```


[^1]: 深いコピーをつくるには、つぎのようなやり方が考えられます。<pre><code>const rectClone = JSON.parse(JSON.stringify(rect));</code></pre>