KDE BLOG

不器用ですから

【ES2015】スプレッド演算子の基礎まとめ

ES2015から使えるスプレッド演算子Spread Operator)について理解がまとまっていないので、きちんとまとめておきたいと思います。

構文

...(ドット3つ)をつけることで、配列や文字列などの iterable なオブジェクトをその場で展開します。
iterableiterator などについては今後理解を深めた後に記事にまとめる予定です。 ざっくりいうと、for...of文で展開できるオブジェクトのことのようです)

関数呼び出しでは 0 個以上の引数として、Array リテラルでは 0 個以上の要素として、Object リテラルでは 0 個以上の key-value のペアとして展開されます。

▼関数呼び出し

myFunc(...params);

▼配列リテラル

[1, 2, ...ary];

▼オブジェクトリテラル (ES2018からの新機能: Object Rest/Spread Properties です)

const objClone = { ...obj };

関数呼び出しでの例

/**
 * 引数をそのままconsole出力
 */
function test(param1, param2, param3) {
  console.log(param1, param2, param3);
}

const ary = [1, 2, 3];

test(ary);    // [1, 2, 3] undefined undefined
test(...ary); // 1, 2, 3

test(...ary) では[1, 2, 3] の配列が1, 2, 3 と個別の値に展開されて、 test(1, 2, 3) という形になっているのが分かります。

ちなみに、スプレッド演算子を使わずに、配列要素を引数に関数を呼び出すには Function.prototype.apply() を使います。

test.apply(null, ary); // 1, 2, 3

配列リテラルでの例

const ary = [3, 4, 5];
console.log([1, 2, ...ary]); //  [1, 2, 3, 4, 5]

上記のように、配列の要素として、個別の値に展開できます。
ちなみに空の配列を展開した場合は何も起きません。

const ary = [3, 4, 5];
const emptyAry = [];
console.log([1, 2, ...ary, ...emptyAry]); //  [1, 2, 3, 4, 5]

配列のコピー

const ary = [1, 2];
const copyAry = [...ary];
ary.push(3);

console.log([...ary, ...copyAry]); // [1, 2, 3, 1, 2]
// ary への変更が copyAry に影響されていないことが分かる

通常、配列のコピーは参照渡しを防ぐために Array.prototype.slice() を使いますが、スプレッド演算子を使えば const copyAry = [...ary]; のように簡単に作れます。
[] を書いた時点で新しい配列が作成されるので、その中に同じ値の配列を展開しても参照が異なるので期待通りの挙動になるわけですね。

ただ、これは 1段階の深さでコピーされていることに注意 が必要です。
2段階以降に関してはシャロ―コピーとなっており元のオブジェクトの変更に影響を受けます。(これはObject.assign() でも同様のようです)

const ary = [1, 2, [3]];
const aryCopied= [...ary];
ary[2][0] = 0;

console.log([...ary, ...aryCopied]); // [1, 2, [0], 1, 2, [0]]

Objectリテラルでの例

const obj = { a: 1 };
console.log({ ...obj }); // { a: 1 }

ES2018から正式に使えるようになった機能です。
配列リテラルのように複数回 ... を使うことが可能で、その場合 Object.assign() のようにマージされます。
プロパティ名が被る場合は後から展開した方のオブジェクトのプロパティが上書きされます。

const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const objMerged = { ...obj1, ...obj2 };

console.log(objMerged); // {a: 1, b: 3, c: 4}

なお、展開・コピーされるプロパティは列挙可能(enumerable: true)なプロパティになります。
下記はそれを確認しているサンプルです。

const obj = { a: 1 };

Object.defineProperties(obj, {
  b: {
    value: 2,
    enumerable: true
  },
  c: {
    value: 3,
    enumerable: false // デフォルトがfalseなので本来は指定不要
  }
});

console.log({ ...obj }); // {a: 1, b: 2}  プロパティ「c」は列挙不可のため展開されない

ちなみに当然ながら配列と同じく1段階の深さのコピーのため、元のオブジェクトの2段階以降の深さのオブジェクトの変更に影響を受けます。

const obj = { 
  a: 1,
  hoge: {
    fuga: 10
  }
};
const objCopied = { ...obj };

// 元のオブジェクトの2段階以降の変更を加える
obj.hoge.fuga = 20;

console.log(objCopied); // { a: 1, hoge: { fuga: 20 } }

レスト構文

これまで紹介してきた書き方は スプレッド構文 と呼ばれるもので、値を展開 することを行ってきました。
それとは逆に 複数の要素を集約して 1 つのオブジェクトにまとめる ことができるがレスト構文Rest parameters、残余引数)です。
その名の通りレスト(残り)の要素をまとめることができます。

レスト構文は主に、分割代入や、関数での不特定多数の引数を配列として受け取る際に使われます。
(分割代入についての詳細はこちら → 【ES2015】分割代入の基本と便利な使い方 - KDE BLOG

分割代入でプロパティをまとめる

const obj = { a: 1, b: 2, c: 3 };
const { ...objProps } = obj;

console.log(objProps); // { a: 1, b: 2, c: 3 }

分割代入で配列を分割する

const [a, b, ...c] = [1, 2, 3, 4, 5];
console.log(c); // [3, 4, 5]

可変長引数の関数を作る

function test (a, ...args) {
  console.log(args);
}

test(1, 2, 3); // [2, 3]

関数の引数はarguments オブジェクトでも取得できますが、残余引数と比べると Array-like なオブジェクトのため mapreduce などの配列メソッドが使えなかったり、引数の一部ではなく全てを取得していたり使いにくいところがありました。

// rest parameters の登場以前は、以下のように記述していた
function test (a, b) {
  const args = Array.prototype.slice.call(arguments, test.length);
  // …
}

// これは以下と等価

function test (a, b, ...args) {
  // ...
}

さらに、分割代入を使えば個々の値に分けることができる上に、デフォルト引数を指定することも可能です。

function test (...[a = 1, b = 2, ...c]) {
  console.log(a, b, c);
}

test();               // 1  2  []
test(10);             // 10 2  []
test(10, 11, 12);     // 10 11 [12]
test(10, 11, 12, 13); // 10 11 [12, 13]

その他の使い方

文字列を配列に分割

const text = 'abcdefg';
const textAry = [...text];
console.log(textAry); // ["a", "b", "c", "d", "e", "f", "g"]

// 以前のやり方
const textAry2 = text.split('');

文字列も iterable なオブジェクトになるためスプレッド演算子が使用可能です。
ちなみに「以前のやり方」として紹介しているArray.prototype.split() での分割ですが、サロゲートペアを含んだ文字列では期待する挙動にならないため注意が必要です。詳しくは下記を参照ください。
文字列 · JavaScriptの入門書 #jsprimer

NodeListやHtmlCollectionを配列化

// HTMLCollection
const divs = document.getElementsByTagName('div');
console.log(Object.prototype.toString.call(divs)) // [object HTMLCollection]

const divsAry = [...divs];
console.log(Object.prototype.toString.call(divsAry)) // [object Array]

// NodeList
const spans = document.querySelectorAll('span');
console.log(Object.prototype.toString.call(spans)) // [object NodeList]

const spansAry = [...spans];
console.log(Object.prototype.toString.call(spansAry)) // [object Array]

配列にすることで filter などのメソッドが使えるようになります。

まとめ

スプレッド演算子といっても、値を展開するスプレッド構文であったり、値をまとめるレスト構文だったりと使う箇所で挙動が大きく変わるのが、慣れないと少し大変かもしれません。
しかし、配列やオブジェクトのコピーやマージ、可変長引数の関数の作成など実用性の高いものを簡潔に書くことが可能になります。
特に分割代入との併用は、React.jsなどのState管理においてはほぼ必須となってきます。

これまで「慣れ」で使ってきたスプレッド演算子ですが、改めて調べてみることで、展開されるプロパティは、列挙可能なものだけということを知れたりできてよかったです。

参考