Help us understand the problem. What is going on with this article?

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

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

スプレッド構文を使う

スプレッド構文(...)をオブジェクトに用いると、プロパティと値の組みが展開されて取り出せます。つぎのコードは、オブジェクトを複製する例です。

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

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

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}}

複数のオブジェクトを展開すれば、ひとつにまとめられます。ただし、同じプロパティがあると、あとの値で上書きされることにご注意ください。

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}

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

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}}

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

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()メソッドを使った例です(「配列要素の平均を求める」参照)。

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}

分割代入を使う

オブジェクトの分割代入は、オブジェクトからプロパティを取り出して対応する変数に代入する構文です。

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

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

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

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

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

また、オブジェクトの残余プロバティ(Rest Properties)が、ECMAScript提案のステージ4の段階になりました。関数の残余引数とは異なり、オブジェクトにまとまります。

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

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

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

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

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

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

// 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

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

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文がひとつのブロックなので、その中で変数の重複が許されないことです。プロパティ名がかぶってしまうときは、変数名を変えることが考えられます。

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文の外で変数宣言する手もあります。このときは、また別の注意をしなければなりません。分割代入を変数宣言で始めないときは、左辺がブロック{}とみなされ、代入演算子=が続けられないことです。そのため、代入文はかっこ()でくくらなければなりません。

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;
  }
}

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


  1. 深いコピーをつくるには、つぎのようなやり方が考えられます。

    const rectClone = JSON.parse(JSON.stringify(rect));
     
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away