LINDORのAdvent Calendar(本物)の20日目を開けたところ。
配列とかおれおれAdvent Calendar2018 – 20日目

... を使うと配列から配列を作るのが簡単です。

const arr1 = [11, 22, 33];
const arr2 = [44, 55];

const arr3 = [...arr1, ...arr2];
console.log(arr3); // => [ 11, 22, 33, 44, 55 ]

const arr4 = [0, ...arr1, 0, ...arr2, 0];
console.log(arr4); // => [ 0, 11, 22, 33, 0, 44, 55, 0 ]

使い方

次の個所で利用可能です。

  • 配列初期化子 []
  • オブジェクト初期化子 {}
  • 関数呼び出し時の引数 ()
  • 分割代入 = 、関数の仮引数 ()

配列初期化子 []

... に続けて反復可能 (iterable) なオブジェクトを置きます。まあ普通は配列ですね。

const arr1 = [22, 33];
const arr2 = [55, 66];
const arr = [11, ...arr1, 44, ...arr2, 77];
console.log(arr);
// [ 11, 22, 33, 44, 55, 66, 77 ]

初期化中の配列要素として ... を伴ったものを見つけると、内部処理 GetIterator() を通して [Symbol.iterator]() メソッドを用いて反復し、要素を追加します。

反復可能オブジェクト

反復子を得られれば動くので、必ずしも配列でなくても構いません。

function* gen () {
  yield 11;
  yield 22;
  yield 33;
}

const it = gen();
const arr = [...it];
console.log(arr);
// [ 11, 22, 33 ]

オブジェクト初期化子 {}

オブジェクトも ... で複製できます。 Object.assign() よりらくちん。

const obj1 = { b: 22, c: 33 };
const obj2 = { e: 55, f: 66 };
const obj = { a: 11, ...obj1, d: 44, ...obj2, g: 77 };
console.log(obj);
// { a: 11, b: 22, c: 33, d: 44, e: 55, f: 66, g: 77 }

内部処理 CopyDataProperties() を用いて ... 右のオブジェクトのプロパティをコピーしていきます。

純粋オブジェクト以外も使えます。

配列

使えます。インデックスがプロパティ名になります。

const arr1 = [11, 22];
const obj = { ...arr1 };
console.log(obj);
// { '0': 11, '1': 22 }

Symbol をプロパティ名に持つオブジェクト

使えます。普通に複製されます。

const obj1 = { [Symbol('hey')]: 33 };
const obj = { ...obj1 };
console.log(obj);
// { [Symbol(hey)]: 33 }

継承してきたプロパティ

は追加されません。

何か new して作ったオブジェクトで使える、 prototype から持ってきてる系メソッドが追加されちゃったりしないわけですね。便利。

const obj1 = Object.create({ inherited: 11 });
obj1.own = 22;
console.log(obj1.inherited); // => 11
console.log(obj1.own); // => 22

const obj = { ...obj1 };
console.log(obj); // => { own: 22 }
console.log(obj.inherited); // => undefined

非オブジェクト

無視されます。

const obj = { ...123 };
console.log(obj);
// {}

undefinednull の場合、内部処理 CopyDataProperties() の過程で単純に無視されます。

それ以外、真偽値、数値、文字列、シンボルの場合、内部処理 ToObject() を通して対応するコンストラクター(例えば String )の新規オブジェクトが作成されるんだけど、新しいオブジェクトは当然自身のプロパティを一切持っていないので、何も追加されません。

(ちなみに内部処理 ToObject()undefinednull を与えると、 TypeError になっちゃう。)

関数呼び出し時の引数

関数を作る際ではなく呼び出す方ね。

const arr = [11, 22, 33];
const max = Math.max(...arr);
console.log(max); // => 33

配列初期化子 []... と同様、内部処理 GetIterator() を用いて反復、引数リストを作成して、関数呼び出しを実行します。

仮引数を ... で受け取って、それをそのまま他の関数へパスする、みたいな使い方が良さそう?

分割代入、関数の仮引数

分解して与えるんじゃなくて、与えられたものを分解し、かつまとめて変数の値として受け取るもの。

const cols = ['Taro', 'Yamada', 199, 99.9];
const [name, ...rest] = cols;
const exec = (cmd, ...options) => {}
exec('goStraight', 50);
exec('moveTo', 10, 20);

分割代入はまた後日やります。

その他

「スプレッド演算子」ではない

... は特定の書式でしか利用できない構文 (syntax) の一部です。

適当な場所で適当に使うと構文エラーになります。

const foo = [] + ...[];
// SyntaxError: Unexpected token ...

まあおれも去年は「スプレッド演算子」と呼んでたけどね!

あとMDNも前は「スプレッド演算子」言ってたはず。気が付いたら変わってた。

「スプレッド構文」もない

実は ... を用いた各種構文の一部であって、 ... 単体には名前は付いてないみたいです。括弧 () に括弧 (parentheses) 以上の名前がないのと同様。

ただ配列初期化子 [] の構文においては SpreadElement という名前の、えーと何ていうの、個所、で ... が利用されてます。ただこれも ... だけじゃなくて ...AssignmentExpression 全体で SpreadElement になるので、やっぱり ... 自体の名前はないですね。

実は仕様書中にも “spread” という単語はそんなに出てきてないです。

Chromeで仕様書から検索した様子。
“spread” で検索してヒットは37件のみ。

とはいえ、英単語 spread が持つ雰囲気(のひとつ)は「折りたたまれたものを広げる」という感じだそうなので、まあぴったりですね。MDNでも “Spread syntax” だし、他の人たちもそう呼んでるし、これでいいよね。IIFE(即時実行関数)みたいなもんか。そうか?

仕様書中ではあと他に、文法 (Grammar) の章で句読点 (punctuator) のひとつとして紹介されているが。パーザー作るときに必要な知識なのかな、よくわからない。

デフォルトコンストラクター

継承はしたけどコンストラクターを用意していないクラスでは、 ... を使ったこんなコンストラクターが自動的に用意されるようです。

constructor(... args){ super (...args);}

(なんかスペースの置き方独特だな!)

concat() と連結展開可能性

単語 “spread” の数少ない出現個所のひとつに IsConcatSpreadable() という内部処理がある。

配列の concat() からのみ呼ばれる内部処理。

concat() はこの内部処理を利用して、プロパティ [Symbol.isConcatSpreadable] を参照して、 true であれば、強制的に配列とみなして展開、対象配列へ連結するというもの。逆に false であれば強制的に展開なしに連結します。初期値はないので普通は undefined で、その場合は配列かどうかで判断されます。

例として、まずは配列風のオブジェクト。配列ではないので、オブジェクト丸ごとが要素になります。

const obj = {
  0: 11,
  1: 22,
  2: 33,
  length: 3,
};
const arr = [0];
const arr2 = arr.concat(obj);
console.log(arr2);
// [ 0, { '0': 11, '1': 22, '2': 33, length: 3 } ]

続いて [Symbol.isConcatSpreadable] を設定したもの。無事、本物の配列のように連結されました。

const obj = {
  0: 11,
  1: 22,
  2: 33,
  length: 3,
  [Symbol.isConcatSpreadable]: true,
};
const arr = [0];
const arr2 = arr.concat(obj);
console.log(arr2);
// [ 0, 11, 22, 33 ]

逆に普通の配列インスタンスで [Symbol.isConcatSpreadable]false を設定すると、展開されず配列丸ごとが対象配列の要素になります。二重配列。

スプレッド構文関係ないけど、せっかくなのでここで。

おしまい

そこまで ... を頻繁に使うかというとそうでもない気もするんだけど、でもこれがあるとめっちゃ楽になる場面があるので、この仕様作ってくれたひとありがとう、という気持ちです。

関連

参考