※スマホ対応はしてません。

タグ: スプレッド構文

最初から分割代入すると書くのが楽。(配列とかおれおれAdvent Calendar2018 – 21日目)

カテゴリー: JavaScript

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

配列を分解しながら要素ごとに変数へ代入することができます。

const n = arr[0];
const m = arr[1];

↓

const [n, m] = arr;

使える場所

  • 変数の初期化(let, const 宣言)、代入
  • 関数引数

できること

  • 必要なものだけ受け取って初期化、代入
  • 残余 ...
  • 初期値 =

使い方

配列を分解して変数を作る

const arr = [11, 22, 33, 44, 55];
const [a, b, c, d, e] = arr;
console.log(a, b, c, d, e);

要素の省略

先頭や途中のものを飛ばす場合、変数を省略してカンマ , だけ置きます。

const [ , b, , d, e] = arr;

末尾から飛ばす場合は何も書かなくて良い。

const [ , b, ,] = arr;

なんなら全部飛ばして [,,,] = arr みたいにも文法的には書けます。書くことないだろうけど。

入れ子

2次元配列の場合、受け取り側も2次元配列で表現することで、子配列の要素を直接得られます。

const positions = [
  [11, 22],
  [33, 44],
];

const [
  [x1, y1],
  [x2, y2]
] = positions;
console.log(x1, y1, x2, y2);
// 11 22 33 44

もちろん2次元に限らず、N次元でいくらでも入れ子にできます。

オブジェクトの記法と組み合わせることも可能。

残余

... を使って「残り全部」を受け取れます。

const [a, ...rest] = arr;

これも入れ子にしたり、オブジェクトの記法と組み合わせたりすることができます。

const arr = [11, 22, 33];
const [a, ...{ length }] = arr;
console.log(a, length);
// 11 2

変数を使う方の場面では逆にばらばらの値へ展開することができます。

初期値

得られた値が undefined のとき、 = で指定した初期値が変数に格納されます。基準は値の有無ではなく undefined かどうかです。

const arr = [11, undefined];
const [a = -11, b = -22, c = -33] = arr;
console.log(a, b, c);
// 11 -22 -33

入れ子と組み合わせるのもあり。

利用可能なもの

配列以外でも、反復可能なものなら何でもいける。

const set = new Set([11, 22]);
const [a, b] = set;
console.log(a, b);
// 11 22
function* gen () {
  yield 11;
  yield 22;
  yield 33;
}

const [a, b, c] = gen();
console.log(a, b, c);
// 11 22 33

配列風オブジェクトはだめ

反復可能ではないので。

const obj = {
  0: 11,
  1: 22,
  2: 33,
  length: 3,
};
const [a, b, c] = obj;
console.log(a, b, c);

TypeError: obj is not iterable

関数引数も分解や残余を使える

[a, b, c] の代わりに (a, b, c) 的な雰囲気で、同じように ... で「残り」を得たり、 [] で分解して受け取ることができます。

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

const f = ([a, ...rest]) => console.log(a, rest);
f(gen());
// 11 [ 22, 33 ]

arguments オブジェクトがあるし、 (a, b, ...rest) を [a, b, ...rest] = arguments みたいに考えれば妥当だよね。

利用例

正規表現でURLを分解

// 実務でご利用の際は `match()` の結果が `null` になり得る点をお忘れなく

const url = 'https://ginpei.info/path/to/file';

const matched = url.match(/(https?:)\/\/([\w.-]+)(.*)/);
const [ , protocol, host, path] = matched;
console.log('protocol:', protocol);
console.log('host:', host);
console.log('path:', path);
// protocol: https:
// host: ginpei.info
// path: /path/to/file

URLのパラメーターを分解

const search = '?id=123&mode=fine'; // location.searchみたいな
const params = search
  .slice(1) // 冒頭 "?" を飛ばす
  .split('&')
  .reduce((map, pair) => {
    const [name, value] = pair.split('=');
    map[name] = value;
    return map;
  }, {});
console.log(params);
// => { id: '123', mode: 'fine' }

実際は items[]=a&items[]=b みたいな重複したものにも対応が必要かもね。

さらっと reduce() 使ったけど、何でもできる配列の最強メソッドです。

Object.entries() で

key-value組を分解するのにも便利。

const colors = {
  apple: 'red',
  banana: 'yellow',
  orange: 'orange',
};

Object.entries(colors).forEach(([name, color]) => {
  console.log(`${name} is ${color}`);
});

for-of でも

of の左側でも使えます。

const colors = new Map();
colors.set('apple', 'red');
colors.set('banana', 'yellow');
colors.set('orange', 'orange');

for (const [name, color] of colors) {
  console.log(`${name} is ${color}`);
}

Promise.all() の結果で

複数のAPIを並列に fetch() した結果とか。

const [a, b] = await Promise.all([
  new Promise((resolve) => resolve('Hello')),
  new Promise((resolve) => resolve('World!')),
]);
console.log(a, b); // => Hello World!

コマンドを受け付ける

jQuery UIのやつみたいに、第1引数にコマンド名、以下コマンドに応じて0個以上のパラメーター、みたいな。

function exec (command, ...params) {
  switch (command) {
    // なんかする
  }
}

exec('start');
exec('say', 'Hello!');
exec('move', 10, 22);

残余引数で受けるより、 params をオブジェクトでまるっともらう方が良さそうな気もする。

その他

残余引数と関数引数の数

関数オブジェクトは length プロパティを持っていて、引数の数が格納されてます。残余引数の場合はそれに数えられません。

先のこの例↓だと、 f.length は 0 になります。

const f = (...args) => console.log(args);

ちなみに手元のEdge (EdgeHTML 17) だと 1 になりました。へえ。

残余引数の分解

結合してからの分解。なんだこれ。

const f = (...[a, b]) => console.log(a, b);

前述の通り ... を使うと関数引数の数 f.length に反映されないので、こっそり受け付けたいときにこの組み合わせが便利ですね。嘘です、たぶんそんなことする理由ない。

なお手元のEdge (EdgeHTML 17) だと動きませんでした。(関数引数じゃなくて変数の方は動く。)

Object doesn't support property or method 'Symbol.iterator'

オブジェクトの分割代入

同じようなもんです。 { ...rest } もある。

おしまい

ないならないでも書けるんだけど、使えると短くかけてすごく便利。

関連

参考

スプレッド演算子……じゃなくて、スプレッド構文の使える場所とか使い方とかそういう。(配列とかおれおれAdvent Calendar2018 – 20日目)

カテゴリー: JavaScript

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);
// {}

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

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

(ちなみに内部処理 ToObject() へ undefined か null を与えると、 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 を設定すると、展開されず配列丸ごとが対象配列の要素になります。二重配列。

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

おしまい

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

関連

参考

配列を「開いて」使うスプレッド演算子。(現代的JavaScriptおれおれアドベントカレンダー2017 – 18日目)

カテゴリー: JavaScript

現代的JavaScriptおれおれアドベントカレンダー2017 – 18日目

概要

配列をばらばらの配列要素だったことにして使えるやつです。

const arr = [1, 2, 3];

const arr2 = [0, ...arr];  // => [0, 1, 2, 3]

使い方

カンマ , 区切りでいろいろ書くところへ、 ...arr のようにすると、その配列の中身をばらばらに書いたかのような状態に展開してくれます。

配列とか、

const arr0 = [1, 2, 3];

const arr1 = [0, ...arr];  // => [0, 1, 2, 3]

関数とか。

function foo(a, b, c) {
  console.log(a, b, c);
}

const arr = [1, 2, 3];
foo(...arr);  // 1, 2, 3

途中でも、いくつでも

残余引数とかは末尾でしか使えなかったんですが、展開する分には十分明瞭なので、どこでもいくつでも使えます。

const arr1 = [1, 2, 3];
const arr2 = [4, 5];
const arr3 = [];

const all = ['|', ...arr1, '|', ...arr2, '|', ...arr3, '|'];  // => [ '|', 1, 2, 3, '|', 4, 5, '|', '|' ]

配列の中の配列

... を付けないとただの入れ子になります。そりゃそうじゃ。

const arr0 = [1, 2, 3];

const arr1 = [0, arr0];  // => [0, [1, 2, 3]]
const arr2 = [0, ...arr0];  // => [0, 1, 2, 3]

配列の複製

残余代入の方でも同じようなことやったけど、変数に格納する必要がない分こっちの方が良いかも。

const arr1 = [1, 2, 3];

function checkCloned(arr2) {
  console.log(arr1);
  console.log(arr2);
  console.log(arr1 === arr2);
}

checkCloned([...arr1]);
[ 1, 2, 3 ]
[ 1, 2, 3 ]
false

配列の結合

Array#conat() の代わりに使えそうです。

const arr1 = [1, 2, 3];
const arr2 = [4, 5];

const all1 = arr1.concat(arr2);  // => [1, 2, 3, 4, 5]
const all2 = [...arr1, ...arr2];  // => [1, 2, 3, 4, 5]

先頭項目を入れ替える

const arr = [1,2,3,4,5];

const [, ...secondAndLater] = arr;
const arr2 = [99, ...secondAndLater];  // => [99, 2, 3, 4, 5];

書いてはみたけど何に使えるかな。

オブジェクトのスプレッド

オブジェクトの残余代入と同様、ES2017までにはまだ存在していない未来仕様です。ES2018に入りそう。

const obj1 = { a: 1 };
const obj2 = { ...obj1 };

執筆時点でChrome、Firefoxとあと手元のNode.js v8.5で動いてます。

一方babel-preset-envのv1.6.1では対応してないっぽい。

SyntaxError: /path/to/project/main.js: Unexpected token (2:15)
  1 | const obj1 = { a: 1 };
> 2 | const obj2 = { ...obj1 };
    |                ^

(細かいバージョンは調べてません。)

Reactで使う

JSXの仕様なのでESのものではないんだけど、似たようなものなので紹介しておきます。あ、これオブジェクトの展開してるね。

you can use ... as a “spread” operator

const Button = props => {
  const { kind, ...other } = props;
  const className = kind === "primary" ? "PrimaryButton" : "SecondaryButton";
  return <button className={className} {...other} />;
};

Reactは普段使いしてないです。なんか違ってたらごめんなさい、教えてください。

その他

名前

みんな「スプレッド演算子 (spread operator)」と呼んでるけど、仕様書にそういう表現ないね。

あと配列の方は “SpreadElement” という名前が出てきてるんだけど、関数引数の方は特に名前が付いてもいない。

参考

  • ECMAScript® 2017 Language Specification
    • 12.2.5 Array Initializer … “SpreadElement” 配列のスプレッド演算子の記法
    • 12.2.5.2 Runtime Semantics: ArrayAccumulation … スプレッド演算子の処理
    • 12.3 Left-Hand-Side Expressions … 関数呼び出しの ArgumentList
    • 12.3.4 Function Calls
    • 12.3.6.1 Runtime Semantics: ArgumentListEvaluation … “…AssignmentExpression” とかの解釈
    • 9.4.4 Arguments Exotic Objects … arguments
  • スプレッド演算子 – JavaScript | MDN