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

タグ: 配列

もう配列のメソッド(とか)全部説明する。(配列とかおれおれAdvent Calendar2018 – 24日目)

カテゴリー: JavaScript

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

全部やる。

一覧

配列を作成

  • []
  • new Array(), Array()
  • Array.from()
  • Array.of()

新規配列を返却

  • Array.prototype.concat()
  • Array.prototype.filter()
  • Array.prototype.map()
  • Array.prototype.reduce()
  • Array.prototype.reduceRight()
  • Array.prototype.slice()

検索

  • Array.prototype.every()
  • Array.prototype.find()
  • Array.prototype.findIndex()
  • Array.prototype.includes()
  • Array.prototype.indexOf()
  • Array.prototype.lastIndexOf()
  • Array.prototype.some()

配列以外を返却

  • Array.isArray()
  • Array.prototype.join()
  • Array.prototype.toLocaleString()
  • Array.prototype.toString()

破壊的操作

  • Array.prototype.copyWithin()
  • Array.prototype.fill()
  • Array.prototype.pop()
  • Array.prototype.push()
  • Array.prototype.reverse()
  • Array.prototype.shift()
  • Array.prototype.sort()
  • Array.prototype.splice()
  • Array.prototype.unshift()

反復

  • Array.prototype.entries()
  • Array.prototype.forEach()
  • Array.prototype.keys()
  • Array.prototype.values()
  • Array.prototype[Symbol.iterator]()

    (さらに…)

reduce()はArrayにて最強……おぼえておけ。(配列とかおれおれAdvent Calendar2018 – 23日目)

カテゴリー: JavaScript

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

配列界最強のメソッドです。(?)

まじ便利というか万能なので、ぜひご利用頂きたい。

ちょっとわかりづらいんだけど、こんな↓感じです。

const arr = [11, 22, 33];

const obj = arr.reduce((acc, value, index) => {
  acc[`number${index}`] = value;
  return acc;
}, {});
console.log(obj);
// { number0: 11, number1: 22, number2: 33 }

与えたコールバック関数の次に第2引数がある点に注目。

インターフェイスと使い方

result = arr.reduce(callback[, initialValue])
// callback = (accumulator, currentValue[, currentIndex, array]) => accumulator;

引数

コールバック関数と初期値の2つです。

第2引数の初期値に、例えばここに空のオブジェクトを与えたりします。

第1引数のコールバック関数が最初に受け取る第1引数は、その初期値になります。その値を操作して return すると、2回目以降のコールバック呼び出しの第1引数にはその return した値が与えられます。

戻り値

順に繰り返し、コールバック関数が最後に return した値が、 reduce() 全体の戻り値になります。

初期値をそのまま返してみる例

initial がコールバック関数の第1引数 acc として与えられて、毎回それを return するので以降も同じインスタンスを受け取り、最終的に結果もそれになります。

const arr = [11, 22, 33];

const initial = {};
const result = arr.reduce((acc, _, index) => {
  console.log(index, acc === initial);
  return acc;
}, initial);
console.log('result', result === initial);
// 0 true
// 1 true
// 2 true
// result true

初回だけ初期値を受け取ってみる例

return 時に新しいインスタンスを生成してみます。2回目はそれを受け取るので false 、また新しいインスタンスになって3回目も同じく false です。

const arr = [11, 22, 33];

const initial = {};
const result = arr.reduce((acc, _, index) => {
  console.log(index, acc === initial);
  return { ...acc };
}, initial);
console.log('result', result === initial);
// 0 true
// 1 false
// 2 false
// result false

数値を増やしてみる例

繰り返しながら操作する対象はオブジェクトに限らず、このように数値でも可能です。

const arr = [11, 22, 33];

const result = arr.reduce((acc, _, index) => {
  console.log(index, acc);
  return acc + 1;
}, 100);
console.log('result', result);
// 0 100
// 1 101
// 2 102
// result 103

初期値を省略した例

省略すると配列の最初の要素が与えられます。またコールバック関数はその次の要素から呼ばれます。

この例↓だと最初 arr[0] の 11 がコールバック関数で処理されず、最初から acc へ与えられます。

const arr = [11, 22, 33];

const result = arr.reduce((acc, value, index) => {
  console.log(index, acc, value);
  return value;
});
console.log('result', result);
// 1 11 22
// 2 22 33
// result 33

例

合計

const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

const sum = arr.reduce((acc, value) => acc + value);
console.log(sum); // => 55

最大値

Math.max(...arr) でやれるやつ。

const arr = [94, 39, 63, 87, 52, 3, 10, 95, 5];

const max = arr.reduce((acc, value) => (acc > value ? acc : value));
console.log(max); // => 95

マップ

map() 代わり。

const arr = [11, 22, 33];

const result = arr.reduce((acc, value, index) => {
  acc[index] = 1000 + value;
  return acc;
}, []);
console.log(result);
// [ 1011, 1022, 1033 ]

フィルター

filter() 代わり。

const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

const result = arr.reduce((acc, value) => {
  if (value % 3 === 0) {
    acc.push(value);
  }
  return acc;
}, []);
console.log(result);
// [ 3, 6, 9 ]

平坦化

flat() の代わり。

const arr = [
  11,
  [22, 33],
  44,
  [55, 66, 77],
];

const result = arr.reduce((acc, value) => acc.concat(value), []);
console.log(result);
// [ 11, 22, 33, 44, 55, 66, 77 ]

検索

find() 代わり。

const arr = [
  { id: '101', name: 'Alice' },
  { id: '102', name: 'Bob' },
  { id: '103', name: 'Charlie' },
];

const result = arr.reduce((acc, value) => {
  if (acc) {
    return acc;
  }

  if (value.id === '102') {
    return value;
  }

  return undefined;
}, undefined);
console.log(result);
// { id: '102', name: 'Bob' }

同じようにして some() 代替も作れる。

繰り返し

forEach() 代わり。

const arr = [11, 22, 33];

arr.reduce((_, value, index) => {
  console.log(index, value);
}, undefined);
// 0 11
// 1 22
// 2 33

初期値を省略すると最初の要素が呼ばれない点に注意。

IDでインデックス

識別子を含むオブジェクトの配列から、その識別子でアクセスできるオブジェクトを作成します。

const arr = [
  { id: '101', name: 'Alice' },
  { id: '102', name: 'Bob' },
  { id: '103', name: 'Charlie' },
];

const map = arr.reduce((acc, record) => {
  acc[record.id] = record;
  return acc;
}, {});
console.log(map);
// { '101': { id: '101', name: 'Alice' },
//   '102': { id: '102', name: 'Bob' },
//   '103': { id: '103', name: 'Charlie' } }

オブジェクトを複製

{ ...obj } や Object.assign() の代わり。

const obj = {
  foo: 123,
  bar: {
    hoge: 234,
    fuga: {
      message: 'Yay!',
    },
  },
};

const obj2 = Object.entries(obj).reduce((acc, [key, value]) => {
  acc[key] = value;
  return acc;
}, {});
console.log(obj2);
// { foo: 123, bar: { hoge: 234, fuga: { message: 'Yay!' } } }

reduce() より Object.entries() の方が良い仕事してる気もする。

深いコピーでオブジェクトを複製

const obj = {
  foo: 123,
  bar: {
    hoge: 234,
    fuga: {
      message: 'Yay!',
    },
  },
};

const deepCopy = (obj) => Object.entries(obj).reduce((acc, [key, value]) => {
  if (typeof value === 'object') {
    acc[key] = deepCopy(value);
  } else {
    acc[key] = value;
  }
  return acc;
}, {});

// 浅いコピー
const obj2 = { ...obj };
console.log(obj2);
// { foo: 123, bar: { hoge: 234, fuga: { message: 'Yay!' } } }
console.log(obj2.bar === obj.bar);
// true

// 深いコピー
const obj3 = deepCopy(obj);
console.log(obj3);
// { foo: 123, bar: { hoge: 234, fuga: { message: 'Yay!' } } }
console.log(obj3.bar === obj.bar);
// false

出現要素数を数える

何でもいいいんだけど、試しにHTML要素の要素名で。

const els = [...document.querySelectorAll('*')];
const counts = els.reduce((acc, el) => {
  const name = el.tagName.toLocaleLowerCase();
  if (!acc[name]) {
    acc[name] = 0;
  }
  acc[name] += 1;
  return acc;
}, {});
console.log(counts);
// Object { html: 1, head: 1, meta: 23, script: 12, title: 6, link: 32, body: 1, ul: 20, li: 186, a: 223, … }

クラス名とかにしてもおもしろそう。

細かいところ

仕様書の説明

珍しく仕様書にある説明が長いので、ちょっと見てみましょう。

Note 1

callbackfn should be a function that takes four arguments. reduce calls the callback, as a function, once for each element after the first element present in the array, in ascending order.

callbackfn is called with four arguments: the previousValue (value from the previous call to callbackfn), the currentValue (value of the current element), the currentIndex, and the object being traversed. The first time that callback is called, the previousValue and currentValue can be one of two values. If an initialValue was supplied in the call to reduce, then previousValue will be equal to initialValue and currentValue will be equal to the first value in the array. If no initialValue was supplied, then previousValue will be equal to the first value in the array and currentValue will be equal to the second. It is a TypeError if the array contains no elements and initialValue is not provided.

reduce does not directly mutate the object on which it is called but the object may be mutated by the calls to callbackfn.

The range of elements processed by reduce is set before the first call to callbackfn. Elements that are appended to the array after the call to reduce begins will not be visited by callbackfn. If existing elements of the array are changed, their value as passed to callbackfn will be the value at the time reduce visits them; elements that are deleted after the call to reduce begins and before being visited are not visited.

ノート 1

callbackfn は4つの引数を取る関数になるだろう (should) 。 reduce はこのコールバックを、関数として、配列に現れる最初の要素以降の各要素ごとに、昇順で呼ぶ。

callbackfn は以下の4の引数とともに呼ばれる: previousValue (前回の callbackfn 呼び出しで得られる値)、 currentValue (現在の要素の値)、 currentIndex 、横断中のオブジェクト。最初にコールバック関数が呼ばれたとき、 previousValue と currentValue は以下のいずれかになる。 reduce に initialValue が与えられた場合、 previousValue は initialValue に等しくなり、 currentValue は配列の最初の値と等しくなる。 initialValue が与えられなかった場合、 previousValue は配列の最初の値と等しくなり、 currentValue は2番目と等しくなる。配列がひとつも要素を持たず、かつ initialValue も与えられなかった場合、 TypeError になる。

reduce は呼ばれたオブジェクトを直接変化させないが、そのオブジェクトは callbackfn 呼び出しによって変化されてもよい (may) 。

reduce に処理される要素の範囲は最初の callbackfn 呼び出しの前に設定される。最初の reduce 呼び出しが開始した後に配列へ追加された要素は、 callbackfn から参照されることはない。配列の既存の要素が変更された場合、 callbackfn へ与えられる値は reduce がそれらを参照した時点での値になる。また、 reduce 呼び出しが始まった後、参照より前に削除された要素は、参照されない。

(訳注: 関数が配列要素を visit することを「参照」としました。)

うっ日本語読みづら。

空の配列でエラー

配列の要素ひとつ以上あるいは初期値を与えないとエラーに。

[].reduce(() => {});
// TypeError: Reduce of empty array with no initial value

途中で追加された要素は呼ばれない

const arr = [11, 22, 33];
arr.reduce((_, value, index, original) => {
  if (index === 0) {
    original.push(99);
  }
  console.log(index, value);
}, 0);
// 0 11
// 1 22
// 2 33

console.log(arr);
// [ 11, 22, 33, 99 ]

途中で変更された要素は変更後の値が利用される

const arr = [11, 22, 33];
arr.reduce((_, value, index, original) => {
  if (index === 0) {
    original[1] = 99;
  }
  console.log(index, value);
}, 0);
// 0 11
// 1 99
// 2 33

途中で削除された要素は呼ばれない

const arr = [11, 22, 33];
arr.reduce((_, value, index, original) => {
  if (index === 0) {
    delete original[1];
  }
  console.log(index, value);
}, 0);
// 0 11
// 2 33

途中じゃなくても、削除されて空き枠になっていれば飛ばされます。

空き枠は飛ばされる

まず空き枠、プロパティとして存在しない要素はコールバックで呼ばれません。これは他の繰り返し系配列メソッドと同様。

加えて、初期値を省略した場合に得られる値も、空き枠を飛ばした最初の要素が呼ばれます。

const arr = [, 22, 33,, 55];

arr.reduce((acc, value, index) => {
  console.log(acc, value, index);
  return acc;
});
// 22 33 2
// 22 55 4

初回コールバック関数呼び出し時の第1引数 acc は普通 arr[0] になるんだけど、それが存在しないので、代わりに最初に出現する arr[1] の 22 になります。第2引数 value はもちろんその次 33 。

また 33 の次、 arr[3] もないので、それを飛ばして2回目のコールバック関数呼び出しは arr[4] の 55 が value に与えられます。

途中で終了できない

例外投げるとかは別だけど、 for の break みたいな機能はないです。

  1. Repeat, while k < len

(存在しないものを飛ばしながら)単に繰り返すだけ。

その他

逆に回す reduceRight()

てのもあります。

おしまい

ややこしいやつだけど、大変便利なのでおぼえておくと何かと役に立ちます。

ちょっと読みづらいのが難点。

参考

空じゃないけど空の配列の話。(配列とかおれおれAdvent Calendar2018 – 22日目)

カテゴリー: JavaScript

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

コンストラクターに要素数を与えると、その「要素数」を持った配列が作られます。しかし、「その数の要素」は持っておらず、配列の中身が undefined すら入っていない「空き枠」の状態になります。

const arr = new Array(3);
console.log(arr.length); // => 3
console.log(arr); // => [ <3 empty items> ]

作り方

コンストラクター

さっきの。

const arr = new Array(3);

大きなインデックスで代入

arr.length を超えるインデックスで値を設定すると配列の大きさが拡張される、と一般には表現されるんだけど、実際は length が更新されるだけで該当インデックスに至る途中の枠は埋められず、空き枠になります。

const arr = [11, 22, 33];
delete arr[2];
console.log(arr.length); // => 3
console.log(arr); // => [ 11, 22, <1 empty item> ]

配列初期化子 []

値為しでカンマ , を置くことで同様に空き枠を伴う配列を作成することができます。

const arr = [,, 1, , 2, ,,];
console.log(arr.length); // => 7
console.log(arr);
// => [ <2 empty items>, 1, <1 empty item>, 2, <2 empty items> ]

delete

普通のオブジェクトプロパティと同様、 delete で配列の要素を削除できます。削除後は空き枠になります。

const arr = [11, 22, 33];
delete arr[1];
console.log(arr.length); // => 3
console.log(arr); // => [ 11, <1 empty item>, 33 ]

※ちなみに普通のオブジェクトの場合。

delete obj.foo;

空き枠にせず、削除して詰めたい場合は splice() を使ってください。

仕組み

インデックスはただのプロパティ名

まずJavaScript (ECMAScript) の配列こと Array インスタンスは、例えばC言語のように実際に連続したメモリー領域として存在するわけではなく、JSの他のオブジェクトと同様の仕組みです。俗にいうハッシュマップ。俗にね。

例えばオブジェクトのプロパティは obj.foo としてアクセスできますが、これは obj['foo'] とも書けます。

逆に言えば arr[0] というのも、 arr.0 と同じようなものなのです。(実際は .0 は使えないけど。)

あ、後述するけどインデックスは数値ではなく文字列です。 arr[0] は arr['0'] へ変換されると。

「在る undefined 」と「無い undefined 」

配列じゃない普通のオブジェクトで、こういうのがあったとします。

const obj = {
  foo: undefined,
};
console.log(obj.foo); // => undefined
console.log(obj.bar); // => undefined

どちらも undefined だけど、前者 obj.foo はその中身が undefined として存在する一方で、後者 obj.bar は存在しないため undefined を得ます。

得られる値は同じだけど動きが違いますね。配列でもこれと同様のことが起きているわけです。

配列風オブジェクトでそれっぽく書きます。

const obj = {
  0: 11,
  2: undefined,
  length: 3,
};

console.log(obj.length);// => 3
console.log(obj[1]);// => undefined
console.log(obj[2]);// => undefined

あるかないか確かめる

オブジェクトがあるプロパティを持つかどうかは、 in 演算子を使って調べることができます。

const obj = {
  foo: undefined,
};
console.log('foo' in obj); // => true
console.log('bar' in obj); // => false

配列でも使える。

const arr = [11, , undefined];
console.log('2' in arr); // => true
console.log('1' in arr); // => false

空き枠で気を付けること

ただ undefined が挿入されている場合と何が違うかというと、配列要素を繰り返す系の多くのメソッドでコールバックが呼ばれません。

undefined が入ってる普通の配列

まず普通のやつ。これは期待通りに動作する。

const arr = [undefined, undefined, undefined];
console.log('length :', arr.length);
// length : 3

arr.forEach((item, i) => {
  console.log(`[${i}] : ${item}`);
});
// [0] : undefined
// [1] : undefined
// [2] : undefined

const arr2 = arr.map((_, i) => i);
console.log(arr2);
// [ 0, 1, 2 ]

数だけあって空の配列

undefined も入っていない空き枠の場合。

const arr = new Array(3);
console.log('length :', arr.length);
// length : 3

arr.forEach((item, i) => {
  console.log(`[${i}] : ${item}`); // <- これが実行されない
});

const arr2 = arr.map((_, i) => i);
console.log(arr2);
// [ <3 empty items> ]

呼ばれません。

呼ばれない機能的理由

例えば forEach() の場合、仕様はこのようになっています。(抜粋)

  1. Let k be 0.
  2. Repeat, while k < len
    1. Let Pk be ! ToString(k).
    2. Let kPresent be ? HasProperty(O, Pk).
    3. If kPresent is true, then
      1. Let kValue be ? Get(O, Pk).
      2. Perform ? Call(callbackfn, T, « kValue, k, O »).
    4. Increase k by 1.

(※強調は引用者)

(引用註: 記述の都合上、リスト項目先頭の数字が変わっています。)

よくある for 文みたいな繰り返し方ですね。

問題はコールバック実行前の条件 “If kPresent is true” です。 HasProperty() を使ってプロパティキーがあるか確認しています。空き枠だとこの条件を満たせません。

呼ばれない思想的理由

わからないです。なんでだろね。

オブジェクトで Object.entries() とかしたら存在しないものは当然追加されないので、それに合わせたのかなあ。

空き枠への対処

自前で for を書く

件の除外条件をわざわざ書かなければ(結果として) undefined を得るだけなので、当然 length 分動きます。

コードはいらないよね。

空き枠を埋める

空き枠だけうまく埋める機能ってないので(ないよね?)、 in を使って自力で埋める必要がありそう。

const fillEmptySlots = (arr, value = undefined) => {
  for (let i = 0; i < arr.length; i++) {
    if (!(i in arr)) {
      arr[i] = value;
    }
  }
};

const arr = [11,, undefined];
fillEmptySlots(arr);
console.log(arr); // => [ 11, undefined, undefined ]

全ての枠を埋める

全てが空き枠の場合、あるいは既存のものを無視しても良い場合は、 fill() というメソッドがあります。

const arr = [11,, undefined];
arr.fill();
console.log(arr); // => [ undefined, undefined, undefined ]

対象の配列を破壊的に更新されてますね。

「全てが空き枠の場合」としたが、引数に開始位置、終了位置を受け付けるので、空き枠の範囲が明確な場合でも利用可能。

そもそも空き枠を作らない

はいそうですね。

配列インデックス

インデックスは数値ではなく文字列

ちらと触れたけど、インデックスは文字列です。もっというと「232 – 1未満の正の整数を文字列にしたもの」です。

Properties are identified using key values. A property key value is either an ECMAScript String value or a Symbol value. All String and Symbol values, including the empty string, are valid as property keys. A property name is a property key that is a String value.

An integer index is a String-valued property key that is a canonical numeric String (see 7.1.16) and whose numeric value is either +0 or a positive integer ≤ 253-1. An array index is an integer index whose numeric value i is in the range +0 ≤ i < 232-1.

プロパティはキー値を用いて特定される。プロパティのキー値はECMAScriptのString値かSymbol値のいずれかである。あらゆるString及びSymbol値(空文字列を含む)はプロパティキーとして妥当 (valid) である。 property name と言う場合はString値のキーを指す。

integer index は正規の数値文字列 (a canonical numeric String) であるString値プロパティキーであり(7.1.16を見よ)、その数的な値 (numeric value) は +0 ないし正の整数 ≤ 253-1 である。 array index は、その数的な値 i が +0 ≤ i < 232-1 の範囲内となる integer index である。

i < 232-1

へーちょっと試してみよう。

const arr = [];
arr[2 ** 32 - 2] = 0;
console.log(arr); // => [ <4294967294 empty items>, 0 ]
console.log(arr.length); // => 4294967295

arr[2 ** 32 - 1] = 0;
console.log(arr); // => [ <4294967294 empty items>, 0, '4294967295': 0 ]
console.log(arr.length); // => 4294967295

なるほど。

プロパティアクセス

いわゆる「配列の添え字アクセス」も、実際は全て文字列へ変換され、普通のプロパティアクセスになります。

ちなみに foo.bar も foo['bar'] へ変換されます。

(抜粋)

The dot notation is explained by the following syntactic conversion:

MemberExpression . IdentifierName

is identical in its behaviour to

MemberExpression [ <identifier-name-string> ]

その他

名称

FirefoxのコンソールやMDNの一部で “empty slots” という表現を見かけてそれ採用してたんだけど、一般的なんだろうか。

(Note: this implies an array of 7 empty slots, not slots with actual undefined values)

仕様中にそういう表現は見つからない。

日本語の「ある」と「ない」

「有無」なんて熟語にもなるくらい対として扱われるこいつらだけど、「ある」は動詞なのに「ない」は形容詞なんだよね。なんで動詞にならなかったんだろう? ないものはないので動けないから?

英語も「ある」が “exist” だけど「ない」に該当する語はなさそう。文脈によっては “absent” も使えるが形容詞だ。 “lack” は動詞だけど、「欠ける」だとなんか違うよね。

おまけ: 各ブラウザーでの空き枠の表現

次のコードをコンソールで実行した結果。

Array(3)

Chrome 71

(3) [empty × 3]

Firefox 64

Array(3) [ <3 empty slots> ]

Safari 12

[] (3) = $

最初の [] 以外は文字色が薄くなってました。

Node.js v10

[ <3 empty items> ]

Edge (EdgeHTML 17)

[object Array]: [, , ]

IE 11

!!

[object Array][undefined, undefined, undefined]

forEach() のコールバックはちゃんと呼ばれませんでした。良かった。

おしまい

ちなみにこれで困ったことないです。でもいつか引っかかりそう。

関連

参考

更新履歴

  • 2018-12-22 参考リンクをいくつか追加
  • 2018-12-22 「プロパティアクセス」の説明があいまいだったのを修正

最初から分割代入すると書くのが楽。(配列とかおれおれ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 を設定すると、展開されず配列丸ごとが対象配列の要素になります。二重配列。

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

おしまい

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

関連

参考