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

カテゴリー: JavaScript

スプレッド演算子……じゃなくて、スプレッド構文の使える場所とか使い方とかそういう。(配列とかおれおれ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);
// {}

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 を設定すると、展開されず配列丸ごとが対象配列の要素になります。二重配列。

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

おしまい

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

関連

参考

反復処理の中身、IterableとIteratorについて仕様書を調べてみた。(配列とかおれおれAdvent Calendar2018 – 19日目)

カテゴリー: JavaScript

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

ここまで何度か出てきた反復可能 (iterable) と反復子 (iterator) のお話です。やっと。

先にまとめ

  • Iterableオブジェクトは
    • Iteratorを返すメソッド [Symbol.iterator] を持つ
    • for-of で使える
  • Iteratorオブジェクトは
    • IteratorResultを返すメソッド next() を持つ
  • IteratorResultオブジェクトは
    • プロパティ done, value を持つ
  • 配列は
    • Iterableである
    • Iteratorではない
  • arr.values()
    • Iterableである
    • Iteratorである
  • Generatorオブジェクトは
    • Iterableであり、かつIteratorである
  • Generator関数は
    • function*(){} のやつ
    • Generatorオブジェクトを返す

自作例

// Iteratorはnext()を持つ
class MyIterator {
  constructor () {
    this.index = 0;
    this.values = [11, 22, 33];
  }

  next () {
    const value = this.values[this.index];
    this.index += 1;
    const result = { done: !value, value };
    return result;
  }
}

// Iterableは[Symbol.iterator]()を持つ
class MyIterable {
  [Symbol.iterator] () {
    return new MyIterator();
  }
}

const iterable = new MyIterable();
for (const value of iterable) {
  console.log(value);
}
// 11
// 22
// 33

反復 (iteration)

IterableとIteratorは、実在するコンストラクターではなく概念的な「インターフェイス」として定義されています。仕様だけが示されて、それを満たせば「これはIterableである」とかなんとか言って良いということです。

仕様書のIterationの章は、インターフェイスについての説明から始まります。

An interface is a set of property keys whose associated values match a specific specification. Any object that provides all the properties as described by an interface’s specification conforms to that interface. An interface is not represented by a distinct object. There may be many separately implemented objects that conform to any interface. An individual object may conform to multiple interfaces.

  • インターフェイスとは、関連する値が特定の仕様に合致するプロパティキーの組み合わせである。
  • インターフェイスの仕様で述べられるすべてのプロパティを提供するあらゆるオブジェクトは、そのインターフェイスに準拠している。
  • インターフェイスは個別のオブジェクトとしては存在しない。
  • あるインターフェイスに準拠する、別々に実装されている複数のオブジェクトがあってもよい。
  • 個々のオブジェクトが複数のインターフェイスに準拠してもよい。

(※訳注: 勝手に1文ごとに箇条書きへ変換しました。内容はそのまま。)

ちなみに isIterable()isIterator() のようなものは仕様にはありません。まあメソッド実行して戻り値が正しいことまで確認しないといけないので、JavaScriptで綺麗につくるのはちょっと難しいっすかね。

定義されているインターフェイス

5種あります。

  • 反復可能 Iterable
  • 反復子 Iterator
  • 非同期反復可能 AsyncIterable
  • 非同期反復子 AsyncIterator
  • 反復結果 IteratorResult

反復可能 (Iterable)

[Symbol.iterator]() メソッドが反復子を返すもの。

for-of で使えるやつ。配列とか、 Set とか、配列とか、文字列とか、あと配列とか。

仕様が短い。

Property Value Requirements
@@iterator A function that returns an Iterator object. The returned object must conform to the Iterator interface.

プロパティ 要求
@@iterator Iterator オブジェクトを返す関数。 返却されるオブジェクトは Iterator インターフェイスに準拠しなければならない。

[Symbol.iterator]() メソッド

仕様書では @@iterator という形で表現される名前のメソッドです。特別なものなので、名前が文字列ではなくシンボルになっています。知らんけど。

この [Symbol.iterator]() という名前でIteratorを返すオブジェクトをIterableと呼べるという話だけど、それ以外のメソッドがIteratorを返すのは自由です。例えば配列はこの [Symbol.iterator]() メソッドを持ちますが、他にも values() というIteratorを返すメソッドも持っています。

ちなみに arr[Symbol.iterator] === arr.values です。

実装例

Iteratorが何かは次項に譲りつつ、それを返す [Symbol.iterator]() メソッドを持つオブジェクト(のクラス)です。

class MyIterable {
  [Symbol.iterator] () {
    return new MyIterator();
  }
}

反復子 (Iterator)

next() メソッドを持ち、それを使って反復できるもの。

Object.values() とか。for-offor に分解したときに出てくるやつ。

Property Value Requirements
next A function that returns an IteratorResult object. The returned object must conform to the IteratorResult interface. If a previous call to the next method of an Iterator has returned an IteratorResult object whose done property is true, then all subsequent calls to the next method of that object should also return an IteratorResult object whose done property is true. However, this requirement is not enforced.

Note 1

Arguments may be passed to the next function but their interpretation and validity is dependent upon the target Iterator. The for-of statement and other common users of Iterators do not pass any arguments, so Iterator objects that expect to be used in such a manner must be prepared to deal with being called with no arguments.

プロパティ 要求
next IteratorResult オブジェクトを返す関数。 返却されるオブジェクトは IteratorResult インターフェイスに準拠しなければならない。もし Iterator の前回の next メソッド呼び出しが返した IteratorResultdone プロパティが true である場合、以降の next メソッド呼び出しが返す IteratorResult オブジェクトも done プロパティが true になるだろう (should) 。ただし、この要求は強制されない。

ノート1

引数を next 関数へ与えても良いが、それらの解釈や妥当性は対象 Iterator に依る。 for-of 構文やその他の一般的な Iterator の利用者は引数を何も与えないため、そのような作法での利用が想定される Iterator オブジェクトは、引数なしで呼ばれた場合も対処するようになっていなければならない。

実装例

内包する値が固定だけど。

class MyIterator {
  constructor () {
    this.index = 0;
    this.values = [11, 22, 33];
  }

  next () {
    const value = this.values[this.index];
    this.index += 1;
    const result = { done: !value, value };
    return result;
  }
}

任意のプロパティ

next() は必須だけど、他に return()throw() を実装しても良いそうです。

Property Value Requirements
return A function that returns an IteratorResult object. The returned object must conform to the IteratorResult interface. Invoking this method notifies the Iterator object that the caller does not intend to make any more next method calls to the Iterator. The returned IteratorResult object will typically have a done property whose value is true, and a value property with the value passed as the argument of the return method. However, this requirement is not enforced.
throw A function that returns an IteratorResult object. The returned object must conform to the IteratorResult interface. Invoking this method notifies the Iterator object that the caller has detected an error condition. The argument may be used to identify the error condition and typically will be an exception object. A typical response is to throw the value passed as the argument. If the method does not throw, the returned IteratorResult object will typically have a done property whose value is true.

Note 2

Typically callers of these methods should check for their existence before invoking them. Certain ECMAScript language features including for-of, yield*, and array destructuring call these methods after performing an existence check. Most ECMAScript library functions that accept Iterable objects as arguments also conditionally call them.

プロパティ 要求
return IteratorResult オブジェクトを返す関数。 返却されるオブジェクトは IteratorResult インターフェイスに準拠しなければならない。このメソッドの呼び出しは Iterator オブジェクトへ、呼び出し側がこれ以上 next メソッド呼び出しを行う意図がないことを通知する。返却される IteratorResultdone プロパティは、一般に true になり、 value プロパティは return メソッドの引数に与えられた値となる。ただし、この要求は強制されない。
throw IteratorResult オブジェクトを返す関数。 返却されるオブジェクトは IteratorResult インターフェイスに準拠しなければならない。このメソッドの呼び出しは Iterator オブジェクトへ、呼び出し者がエラー状態を検出したことを通知する。引数はエラー状態特定のために使ってもよく、また多くの場合は例外オブジェクトになる。代表的な目的は引数として渡された値を throw することである。このメソッドが throw しない場合、返却される IteratorResultdone プロパティは、一般に true になる。

ノート2

一般的にこれらのメソッドの呼び出し側は、その存在を確認してから呼び出すことになるだろう (should) 。 for-ofyield* 、配列の分解 (destructuring) といった、信頼できるECMAScript言語の機能は存在確認を実行した後にこれらのメソッドを呼び出している。 Iterable オブジェクトを受け付けるほとんどのECMAScriptライブラリー関数もまた、条件次第でこれらを呼ぶようになっている。

反復可能な反復子

ECMAScriptでIteratorとして記述されているものは、いずれも %IteratorPrototype% という秘密のプロトタイプを継承するようになっています。

このプロトタイプは単に this を返すだけの [Symbol.iterator]() を持つとされます。

つまり標準のIteratorは反復可能であり、Iteratorが作るIteratorは自身である (it === it[Symbol.iterator]()) というわけですね。

と、そうわけで配列はもちろんのこと、配列から明示的に生成したIteratorもまた for-of で使えるのです。

const arr = [11, 22, 33];

for (const value of arr) {
  console.log('arr', value);
}
// arr 11
// arr 22
// arr 33

const it = arr.values();
for (const value of it) {
  console.log('it', value);
}
// it 11
// it 22
// it 33

非同期の反復可能、反復子

for-await-of で使えるやつ。

基本的に同期のものと同じっぽい。

違うのは、 next() が返すものが「 IteratorResult オブジェクト」から「 IteratorResult オブジェクトのプロミス」になってる点。

反復結果 (IteratorResult)

いろいろ書いてあるけど、持ってるのは値だけなんで、実際そんなにややこしくはないね。

Property Value Requirements
done Either true or false. This is the result status of an iterator next method call. If the end of the iterator was reached done is true. If the end was not reached done is false and a value is available. If a done property (either own or inherited) does not exist, it is consider to have the value false.
value Any ECMAScript language value. If done is false, this is the current iteration element value. If done is true, this is the return value of the iterator, if it supplied one. If the iterator does not have a return value, value is undefined. In that case, the value property may be absent from the conforming object if it does not inherit an explicit value property.

プロパティ 要求
done truefalse これは iteratornext メソッド呼び出しの結果状態である。iteratorが最後まで到達していれば donetrue になる。iteratorが最後まで到達していなければ donefalse になり、 value は有効になる。 done プロパティ(自身のものでも継承したものでも)が存在しない場合は false 値を持つものとみなす。
value ECMAScript言語の任意の値。 done が false の場合、これは現在の反復要素の値である。 done が true の場合、これは iterator の戻り値である。(あれば。) iterator が戻り値を持たない場合、 valueundefined になる。その場合、確認中のオブジェクトが明確な value プロパティを継承していなければ value プロパティはそのオブジェクトから欠落してもよい (may) 。

ジェネレーター

反復子でありながら反復可能であるもの

A Generator object is an instance of a generator function and conforms to both the Iterator and Iterable interfaces.

Generator instances directly inherit properties from the object that is the value of the prototype property of the Generator function that created the instance. Generator instances indirectly inherit properties from the Generator Prototype intrinsic, %GeneratorPrototype%.

Generatorオブジェクトはgenerator関数のインスタンスであり、 IteratorIterable インターフェイスの両方に準拠するものである。

Generatorインスタンスはそのインスタンスを生成したGenerator関数の prototype プロパティの値であるオブジェクトから直接的にプロパティを継承する。Generatorインスタンスは固有のGeneratorプロトタイプ、%GeneratorPrototype%から非直接的にプロパティを継承する。

Generator関数 (function*(){}) の正体がわかりましたね!

function* f () {
  yield 1;
}
f.prototype.sayHello = () => console.log('Hello!');

const it = f();
it.sayHello();
// Hello!

for-of とIterable

GetIterator() という内部処理があって、こいつで of 右側のオブジェクトから例の [Symbol.iterator]() を通してIteratorを取得してます。取得できなければ TypeError

第3引数に任意の method を渡せるようになってるけど、仕様書見た感じ全部 @@iterator@@asyncIterator みたい。

その他

Object.values() vs arr.values()

Object.values(obj) はただの配列を返して、その中身が obj のプロパティ値になります。

arr.values()arr の項目を順に返すIteratorを返します。

おまけ: 英単語 iteration vs repetition

反復と繰り返し、どちらも同じような意味だけど、どう違うんだろうか。

これの回答がわかりやすい。(合ってるのかどうかは判断しかねるが。)

Iteration uses an iterator to count through a list of (usually different) items:

Today, I must:
1: go shopping
2: clean the house
3: mow the lawn

Repetition does the same thing again, and again, etc…:

One sheep, two sheep, three sheep, …

漫画の単行本を続けて読むのが iterate で、同じ本を何度も読むのが repeat てな感じみたい。

おしまい

ほとんどただの翻訳になってしまった。

まあ仕様書が一番わかりやすいから仕方ない。

関連

参考

配列で重複する項目を取り除くやつ4種。(配列とかおれおれAdvent Calendar2018 – 18日目)

カテゴリー: JavaScript

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

ありそうでないメソッド、それが重複排除です。

Underscore.jsLodashには uniq() てのがあって、こんな感じ↓で使えます。

const arr0 = [11, 22, 11, 22, 33];
const arr1 = _.uniq(arr0);

JavaScriptにはありません。

Set を使う

単純な値ならこれが一番簡単。

Set は重複しない値を格納できるオブジェクトです。 ... で展開して配列オブジェクトへ再変換します。

const arr0 = [11, 22, 11, 22, 33];
const arr1 = [...new Set(arr0)];
console.log(arr1); // => [ 11, 22, 33 ]

includes() で確認する

これが一番素直。

includes() は配列が指定の要素を含むかどうか調べるやつです。 重複してなかったら追加するだけ。

const arr0 = [11, 22, 11, 22, 33];

const arr1 = arr0.reduce((a, v) => {
  if (!a.includes(v)) {
    a.push(v);
  }
  return a;
}, []);

console.log(arr1); // => [ 11, 22, 33 ]

reduce() に自信がなければ forEach() でも。

const arr1 = [];
arr0.forEach((v) => {
  if (!arr1.includes(v)) {
    arr1.push(v);
  }
});

some() でやる

Setincludes() もオブジェクトの場合は完全に同じインスタンスでないと反応しないので、その場合は some() がよろしいかと。

もっと自由度が高い。

const arr0 = [
  { id: '11', num: 1 },
  { id: '22', num: 2 },
  { id: '11', num: 3 },
  { id: '22', num: 4 },
  { id: '33', num: 5 },
];

const arr1 = arr0.reduce((a, v) => {
  if (!a.some((e) => e.id === v.id)) {
    a.push(v);
  }
  return a;
}, []);

console.log(arr1);
// [ { id: '11', num: 1 },
//   { id: '22', num: 2 },
//   { id: '33', num: 5 } ]

重複分は先に出てくるやつが優先です。

Map を使う

これもオブジェクトに対応。初見で「ぎょえー」てなりそう。

const arr0 = [
  { id: '11', num: 1 },
  { id: '22', num: 2 },
  { id: '11', num: 3 },
  { id: '22', num: 4 },
  { id: '33', num: 5 },
];

const arr1 = [...new Map(arr0.map((v) => [v.id, v])).values()];

console.log(arr1);
// [ { id: '11', num: 3 },
//   { id: '22', num: 4 },
//   { id: '33', num: 5 } ]

仕組みは Setsome() の組み合わせです、だいたい。

  1. map() でIDとオブジェクト本体の組み合わせへ変換
  2. 組み合わせを元に Map オブジェクトを作成。ここでID( Map のキー)が重複するものは排除)
  3. values() でオブジェクト( Map の値)のみの反復子を得る
  4. スプレッド構文 ... を伴う配列初期化子 [] で配列に。完成

重複分は後ろにあるやつが優先(上書き)です。

おしまい

なんか良いのある?

参考

だいたいの繰り返しは配列のforEach()でいける。(配列とかおれおれAdvent Calendar2018 – 17日目)

カテゴリー: JavaScript

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

繰り返しにもいろいろあるよ。

お待たせ! 現代的JavaScriptでは主流の、よく for を置き換え使うやつです。

まず for の例

const arr = [11, 22, 33];

for (let i = 0; i < arr.length; i++) {
  const value = arr[i];
  console.log(value);
}

forEach() にした例

const arr = [11, 22, 33];

arr.forEach((value) => {
  console.log(value);
});

かんたーん。

簡単なので、あまり語ることはありません。

仕様

引数は他のこれ系の配列メソッドと同じです。

forEach() は引数に、関数オブジェクトをひとつ受け取ります。 戻り値はありません。 (undefined)

与える関数は、3つの引数が与えられます。

  • value … 配列の要素
  • index … インデックス
  • array … 操作中の配列本体
const arr = [11, 22, 33];

arr.forEach((value, index, array) => {
  console.log(index, ':', value, ' <- ', array[index], array);
});

第2引数

実は forEach() その他にはには第2引数 thisArg があります。

これは第1引数の関数実行時に this へ束縛されるオブジェクトなんだけど、 近年は this が変わらないアロー関数を用いるのが主流なので、 使う場面は少ないかなと思います。

const obj = {
  arr: [11, 22, 33],

  logAll: function () {
    this.arr.forEach(function (value) {
      this.output(value);
    }, this);
  },

  output: function (msg) {
    console.log('Message: ', msg);
  },
};

obj.logAll();

再利用

これも他の配列メソッドと同様なんだけど、配列以外のオブジェクトへ適用しても適切に動作するよう設計されています。

基本的に forEach() でできて for でできないことってないんじゃないかな。

普通に繰り返す

const arr = [11, 22, 33];
arr.forEach((value) => {
  console.log(value);
});

配列内の他の要素を参照する

えーと例えば毎年の人口とか、そういう数値情報が与えられて、その増減を見ていきたい場合。

まずは for 文でやる例。

const arr = [100, 110, 115, 103, 110, 90];

for (let i = 1; i < arr.length; i++) {
  const item1 = arr[i - 1];
  const item2 = arr[i];
  const diff = item2 - item1;
  const sign = diff < 0 ? '' : '+';
  console.log(`${item1} -> ${item2} (${sign}${diff})`);
}

// 100 -> 110 (+10)
// 110 -> 115 (+5)
// 115 -> 103 (-12)
// 103 -> 110 (+7)
// 110 -> 90 (-20)

まず最初に i = 1 から始めるのはできないので、繰り返しの中で飛ばします。(slice() とかするとインデックスがずれちゃうので注意。そっちの方が良いかもだけど。)

また配列全体は与える関数の第3引数にもらえるので、これを利用します。

const arr = [100, 110, 115, 103, 110, 90];

arr.forEach((item2, i, all) => {
  if (i < 1) { return; }
  const item1 = all[i - 1];
  const diff = item2 - item1;
  const sign = diff < 0 ? '' : '+';
  console.log(`${item1} -> ${item2} (${sign}${diff})`);
});

ここで all は外側の arr と同じなので、そっちでも良いです。

結果を配列にしたいなら reduce() も有用。

同じ処理を繰り返す

配列記法から直接メソッド実行できるので、匿名関数の即時実行みたいなノリで、匿名配列の即時利用てな感じでも使えます。

[
  '.target1',
  '.target2',
  '.target3',
].forEach((selector) => {
  const el = document.querySelector(selector);
  el.classList.add('targeted');
});

同じ処理をまとめるってなら関数化が正解だと思うんだけど、手軽に書きたいときとか。

セミコロンを行末に置かないスタイルの場合はご注意ください。配列記法 [] が前の行と繋がってしまうので。

似た計算をする

縦横両方向の計算とか、プロパティ名は異なるが算出方法は同じ、みたいな場面で。

// 抜粋

[
  ['clientWidth', 'left'],
  ['clientHeight', 'top'],
].forEach(([sizeName, axisName]) => {
  const pos = (elWrapper[sizeName] - elItem[sizeName]) / 2;
  elItem.style[axisName] = `${pos}px`;
});

純粋な計算と副作用を分けたい場合は map() でも。

forEach() でできないこと

と、他と組み合わせてのやり方。

forEach() は「先頭から末尾まで繰り返す」ものなので、それ以外のパターンで繰り返したい場合はコードこねこねしてやる必要があります。

先頭以外から、末尾以外までの繰り返し

あんまりやらない気もするけど、途中からとか途中までとかはできません。

split() と組み合わせます。

const arr = ['A', 'B', 'C'];

// 先頭から1個までを飛ばす
// => B, C
arr.slice(1).forEach((item, i) => {
  console.log(`${i}: ${item}`);
});

// 末尾から1個までを飛ばす
// => A, B
arr.slice(0, -1).forEach((item, i) => {
  console.log(`${i}: ${item}`);
});

index を見てコールバック関数で return するのでもアリ。

飛ばして回す

一つ飛ばしとかはできません。

できないので、繰り返しのコールバック関数で都度 return して、何もしないようにします。

const arr = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ'];

// 3つごと
// => A, D, G, J, ...
arr.forEach((c, i) => {
  if (i % 3 !== 0) { return; }
  console.log(`${i}: ${c}`);
});

あるいは事前に弾いておく。こっちの方が見た目はきれいっぽい。

const arr = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ'];

// 3つごと
// => A, D, G, J, ...
arr
  .filter((_, i) => i % 3 === 0)
  .forEach((c, i) => console.log(`${i}: ${c}`));

逆順に繰り返し

意外とこれができません。常に先頭からです。

配列を逆順にする reverse() というのもあるんだけど、こいつは対象の配列自体を逆順にする破壊的な副作用があるので、利用可能な場面が限定的です。

const arr = ['A', 'B', 'C'];

arr.reverse().forEach((item, i) => {
  console.log(`${i}: ${item}`);
});
// => C, B, A

// 元の配列が変わっちゃう!
console.log(arr); // => [ 'C', 'B', 'A' ]

事前に複製してから reverse() すればいいんだけど。

[...arr].reverse()

うーん、 reduceRight() が良いかな。

const arr = ['A', 'B', 'C'];

arr.reduceRight((_, item, i) => {
  console.log(`${i}: ${item}`);
}, 0);

これはこれで第2引数に何か与えるのと、コールバック関数の第1引数も value でないものが与えられるのを忘れないように気を付けないといけない。

回ってくる値を無視する?

const arr = ['A', 'B', 'C'];

arr.forEach((_, i) => {
  const item = arr[arr.length - 1 - i];
  console.log(`${i}: ${item}`);
});

sort() しちゃうのが意味が明瞭で一番良いかもしれない。

配列以外の forEach()

普通のオブジェクト

配列へ変換してやります。

プロパティ名だけ得る例。

const obj = {
  a: 11,
  b: 22,
  c: 33,
};

Object.keys(obj).forEach((key) => {
  console.log(key);
});
// => a, b, c

他に値だけを得る Object.values() と、両方を得る Object.entries() があります。

for-of でやったやつら。

Map, Set

こいつらは forEach() を持ってます。だいたい同じ動き。

Map はインデックスの代わりにキーが得られます。 ですよねー。

順序ははキーを追加した順。

const map = new Map([
  ['foo', 11],
  ['bar', 22],
  ['boo', 33],
]);
map.forEach((value, key) => {
  console.log(key, value);
});

Set の場合、インデックスやキーとなる部分にも値が与えられます。 わお。

こちらも順序は追加順です。

const set = new Set([11, 22, 33]);
set.forEach((value, v2) => {
  console.log(value, v2, value === v2); // 11, 11, true 等
});

DOM系の配列風オブジェクト

でも使えたりします。

const els = document.querySelectorAll('.target');
console.log(els instanceof Array); // => false
els.forEach((el, index) => {
  console.log(index, el);
});

forEach() 以外の、例えば map() とかはないです。

中身は完全に配列のそれと同じ。そこら辺のもうちょい詳しい話を別稿に用意しました。

jQuery

には each() というのがあったり。

const $els = $('.target');
$els.each((index, el) => {
  console.log(index, el);
});

引数の順序が違う点に注意。 (今時使うのかはわからないけど。)

jQueryオブジェクトは反復可能なので、 [...$els].forEach(fn) も使えます。

for と速度面の比較

まず結論ですが、高速化のために forEach() を避けて for を採用する必要はありません

可読性や記述の用意さから考えても forEach() の方が良いように思います。(個人の感想です。)

遅いかと言われれば、もちろん遅いんだけど、そこの遅さが問題になる状況は普通、既に破綻してますから。それよりも前に気にするべき個所があるはずです。計算量( O(n2) とかそういうやつ)を考えて、ファイルアクセスやら画面再描画やらの遅いAPIに気を付けて。

2012年の実験があります。0.001ミリ秒以下の差です。0.001秒じゃないよ。

よっぽど極まった場面では別だけど、まあ微妙な速度性能差ではなく機能差で選ぼうね。

あ、でも

何か探すとかで最後まで繰り返す必要がない場合は forEach() じゃなくて find() とか some() とか、そういう適切なものを使おう。意味が明瞭になって可読性も向上するし。

おしまい

繰り返しシリーズおしまい。はー長かった。

forEach() は便利で大変よろしい。

関連

参考

非同期に繰り返すならfor-await-of構文が使える、けど使わない方が良いかも。(配列とかおれおれAdvent Calendar2018 – 16日目)

カテゴリー: JavaScript

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

繰り返しにもいろいろあるよ。

for-of の亜種で、非同期に繰り返すやつに対応します。

const sleep = (ms) => new Promise((f) => setTimeout(f, ms));

async function* foo () {
  yield 11;
  await sleep(1000);
  yield 22;
  await sleep(1000);
  yield 33;
}

(async () => {
  for await (const value of foo()) {
    console.log(value);
  }
})();

// 11
// 22
// 33

仕様

for-in, for-of と同じ章で説明されます。

基本的な処理もそれらと一緒。なので過去記事にも目を通しておいて頂きたいです。なにとぞ。

async 内でのみ利用可能

await なので。

// OK
(async () => {
  for await (const v of obj) {
    // …
  }
});

// SyntaxError: Unexpected reserved word
(() => {
  for await (const v of obj) {
    // …
  }
});

非同期に反復可能なオブジェクト

for-of では「反復可能」なオブジェクトが利用可能で、それは適切な [Symbol.iterator]() メソッドが設定されているもの、でした。

for-await-of で利用可能なオブジェクトは「非同期反復可能」なもので、それは適切な [Symbol.asyncIterator]() メソッドが設定されているもの、です。

for-of のときと同じく、そういうオブジェクトを自作することができます。

const sleep = (ms) => new Promise((f) => setTimeout(f, ms));

const obj = {
  async* [Symbol.asyncIterator] () {
    yield 11;
    await sleep(1000);
    yield 22;
    await sleep(1000);
    yield 33;
  },
};

(async () => {
  for await (const value of obj) {
    console.log(value);
  }
})();

1000ミリ秒止まりながら繰り返す様子。

非同期のおさらい

軽く await のお話をします。知ってる人は飛ばして次へ。

async とは

関数を Promise 化するやつです。

// Promise版
const f = () => {
  const promise = new Promise((resolve, reject) => {
    resolve(123);
  });
  return promise;
};
// async版
const f = async () => 123;

const p = f();
console.log(p instanceof Promise); // => true
p.then((result) => {
  console.log(result); // => 123
});

return すると resolve()throwreject() です。

今回の例だと非同期関数の中で何もしてないけど、もちろん普通は Promise なりまた await なりで非同期に処理をします。

await とは

Promisethen() の代わりです。

// promise-then版
p.then((result) => {
  console.log(result);
});
// async-await版
const result = await p;
console.log(result);

インデントが深くならないところが素敵。

async な関数内でのみ利用可能です。 Chrome DevToolsのコンソールだと await 動くけど、あれは特別。 外だとエラーに。

SyntaxError: await is only valid in async function

catch() の代わりは構文の方の try-catch です。

fetch() の例

例えば、指定のパスのHTMLを取得し解析、 <title> に設定されている文字列を得るやつ。

const fetchTitle = (path) => fetch(path)
  .then((res) => res.text()) // text()はPromiseを返す
  .then((html) => html.match(/<title>(.*)<\/title>/i)[1]);

これ↑を、こう↓書けます。

const fetchTitle = async (path) => {
  const res = await fetch(path);
  const html = await res.text();
  return html.match(/<title>(.*)<\/title>/i)[1];
};

でもって async が付いてる fetchTitle()Promise オブジェクトを返すので、こう使います。(もちろんこいつらも await でも良い。)

// 現在ページのタイトルを取得
fetchTitle(location.href)
  .then((title) => console.log('Title:', title));

// トップページのタイトルを取得
fetchTitle('/')
  .then((title) => console.log('Title:', title));

使い方

話を戻して for-await-of は、非同期反復子 (AsyncIterator) が返す結果を await しながら反復します。

こんな非同期反復子を返す関数があったとします。

async function* foo () {
  yield 11;
  await sleep(1000);
  yield 22;
  await sleep(1000);
  yield 33;
}

繰り返さない例

まずはここから。

// 普通の反復子
const it = foo();
const result = it.next();
console.log(result);
// 非同期反復子
const ait = foo();
const result = await ait.next();
console.log(result);

普通の for で繰り返す例

next() 呼び出すところで await してますね。 for 文で同じようにして、値を非同期に得ながら反復することができます。

const ait = foo();
for (let cur = await ait.next(); !cur.done; cur = await ait.next()) {
  const { value } = cur;
  console.log('for', value);
}

for-await-of で書く例

長い for 文になってしまったけれど、大丈夫、僕らには for-await-of 構文があります。

for await (const value of foo()) {
  console.log(value);
}

はいできあがり。

ついでに、普通の for 文で普通に await する例

ここまでやってきた for-await-of の特徴は反復子が非同期、というところなので、何でもないところで非同期にやるなら普通に await するだけです。

for (const url of urls) {
  const res = await fetch(url);
  console.log(res);
}

可能なら Promise.all()

というわけで色々書いてきたんだけど、非同期に繰り返すのは良くないです。

だってせっかく非同期に処理できるのだから、順番ではなく一度にまとめて実行して、その後に結果を順に処理していく方が高効率です。

例えば非同期処理がひとつ500msかかるとして、3つ順にやれば合計1500msの待機時間が必要になります。これらを Promise.all() で並列に実行してやれば、待ち時間は結局500msのままです。

直列実行は右へ、並列実行は下へ延びる。後者の方が横軸(時間)は短い。

繰り返し中の await を禁じるESLintの設定

ESLintにもそういう設定があります。

Performing an operation on each element of an iterable is a common task. However, performing an await as part of each operation is an indication that the program is not taking full advantage of the parallelization benefits of async/await.

Usually, the code should be refactored to create all the promises at once, then get access to the results using Promise.all(). Otherwise, each successive operation will not start until the previous one has completed.

反復の各要素に対して操作を行うことは一般的な作業です。しかしながら、各段階の操作で await を実行すると、そのプログラムが async/await による並列化の恩恵を十分に享受できないことになります。

一般にこのようなコードは、一度に全てのプロミスを作成しそのうえで Promise.all() を用いて結果を得るようリファクターされるべきです。そうでないと連続的な処理 (successive operation) が前の処理が完了するまで始まりません。

for-await-of を禁じるESLintの設定

ただし前項のものは普通の for 内で await 書いたときに引っかける用で、 for-await-of は引っかからないです。

for-await-of を( for-of は許容しつつ)弾く設定はこんな感じ↓です。

module.exports = {
  "rules": {
    'no-restricted-syntax': [
      'error',
      'ForOfStatement[await=true]',
    ],
  },
};

for-await-of はエラー、 for-of はOKになる。

普通の for 文で普通に await しない例

上の方で書いた繰り返しの途中で await する方の例を、 Promise.all() を使って並列に実行して、その全体を await で待つようにした例です。

const responses = await Promise.all(
  urls.map((url) => {
    console.log(url);
    return fetch(url);
  })
);

for (const res of responses) {
  console.log(res);
}

複数の fetch() が同時に走るので、こっちの方が速いです。

えーと、非同期に繰り返す例かあ……。

同意を促すメッセージ

ごめん思いつかなかった。

// 抜粋

async function* waitForAgreeing (interval = 2000) {
  let index = 0;
  while (true) {
    if (elAgree.checked) {
      return;
    }

    await sleep(interval);
    yield messages[index];
    index = (index + 1) % messages.length;
  }
}

const main = async () => {
  for await (const message of waitForAgreeing()) {
    elMessage.textContent = message;
  }
  elMessage.textContent = 'さんきゅー';
};

2秒ごとに同意を促す文言が表示され続ける例。
同意するまでメッセージが更新され続ける。

その他

for-of 自体があまり良くない?

for-await-of を弾く設定を上の方で書いたけど、そもそも for-of を弾くべき?

AirbnbのESLintの設定をよく使ってるんだけど、そこでは for-of の利用自体が禁止されています。(別途 for-in も禁止。)

理由は設定に書いてある。

iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations.

イテレーター、ジェネレーターは再生成ランタイム (regenerator-runtime) が必要で、それは本ガイドの許容範囲からするとあまりに重すぎます。またそれとは別に、ループの利用は回避し、配列の反復を利用してください。

arr.forEach() を使えってさ。 Object.keys() とかもだめなのかな。どれくらい遅いんだろ。

ちなみに for と比べて forEach() が遅すぎるってことはないです。

おまけ: for-await-in

for await (const key in obj) {
  console.log(key);
}

SyntaxError: Unexpected token in

そんなものはない。

おしまい

あんまり使いどころがなあ。 for でやれることだしせっかくだから for-of でもできるようにしておこう、とかそういう感じなのかなあ。

ああでも配列以外のジェネレーター自体の良い使い道がまだわかってないから、そこら辺わかったら非同期に繰り返したくなるかも。

関連

参考