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
オブジェクトから必要なプロパティを抜き出したり、まとめたりするときに使うと便利です。
document.addEventListener('mousedown', ({ clientX, clientY }) => {
const mousePoint = { clientX, clientY };
console.log(mousePoint); // {clientX: x座標値, clientY: y座標値}
});
また、オブジェクトの残余プロバティ(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;
}
}
オブジェクトを展開してまとめるスプレッド構文と、プロパティが簡単に取り出せる分割代入、少し注意すべき点はあるものの、コードがすっきり書けます。いろいろと工夫できそうです。
[追記: 2020/06/27]
TwitterでAdam Argyle(@argyleink)氏が、つぎのようなテクニックを紹介されていました。JavaScriptの組み込み済みクラス(Array
)のインスタンスにも、オブジェクトの分割代入が使えるということです。
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#es6 pic.twitter.com/99Uxa9ccy3
— Adam Argyle (@argyleink) June 14, 2019
配列インデックスもプロパティとして扱われます。変数名を変えたいときはコロン(:
)のあとに添え、そのままでよければコロン(:
)以下を省けばよいのです。さらに、プロパティ名を式で表す計算されたプロパティ名も用いました。
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
プロパティを取り出そうとするとき、分割代入を使わなければこのような手順でしょう。
const changeHandler = (event) => {
const inputElement = event.target;
console.log(inputElement.value);
};
分割代入で取り出せば、つぎのように引数から直にvalue
が得られます。
const changeHandler = ({target: {value}}) => {
console.log(value);
};
-
深いコピーをつくるには、つぎのようなやり方が考えられます。
↩const rectClone = JSON.parse(JSON.stringify(rect));