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

非同期に繰り返すなら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() 、 throw は reject() です。

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

await とは

Promise の then() の代わりです。

// 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 でもできるようにしておこう、とかそういう感じなのかなあ。

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

関連

参考

for-ofで配列も普通のオブジェクトも反復しよう。(配列とかおれおれAdvent Calendar2018 – 15日目)

カテゴリー: JavaScript

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

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

for-in じゃない方です。 こっち使おう。

for-of 文

for-in と違って値の方を持ってきてくれます。

const arr = [11, 22, 33];

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

// 11
// 22
// 33

for に置き換え

反復可能なオブジェクトを対象とする for-of を普通の for 文に置き換えることもできます。

const arr = [11, 22, 33];

const it = arr.values();
for (let cur = it.next(); !cur.done; cur = it.next()) {
  const { value } = cur;
  console.log('for', value);
}

仕様

for-in, for-of, for-await-of は全部まとめて定義されてる。

ので、 for-in の方もみといてください。

for-in は列挙可能なプロパティ名を得ましたが、 for-of の方では反復子 (iterator) を用いた繰り返しを行います。

利用可能なオブジェクトと反復子 (iterator)

for-of は「反復可能」なオブジェクトでのみ利用可能です。

この「反復可能 iterable」であるとは、まあ本稿は for-of が主眼ですのでざっくり申し上げると、 [Symbol.iterator] メソッドがその 反復子 iterator オブジェクトを生成するよう適切に用意されている状態を言います。

自作もできます。

const obj = {
  * [Symbol.iterator] () {
    yield 11;
    yield 22;
    yield 33;
  },
};

詳しくは別稿(予定)をご覧ください。

で、配列は反復可能なオブジェクトなので使えます。

console.log(Symbol.iterator in arr); // => true

他に Set 、 Map それから String オブジェクトも反復可能です。 (文字列 "" 自体はだめ。オブジェクトでないのでメソッドもない。呼び出せるけど。)

未対応のオブジェクトを繰り返す

配列で使う分には簡単だったけど、普通のオブジェクトはそのままでは使えません。ひと手間必要。

const obj = {};
for (const value of obj) {
}
// TypeError: obj is not iterable

次の三通りのメソッドで、何の変哲もないただのオブジェクトから配列を生成します。 前項の通り、配列なら反復可能。

  • Object.values()
  • Object.keys()
  • Object.entries()

反復可能でない通常のオブジェクトはこれら通して反復可能なオブジェクトを得、それを for-of へ与えることができる、という算段です。

以下、こういうオブジェクトがある前提でコード例を提示します。

const obj = {
  foo: 11,
  bar: 22,
  boo: 33,
};

プロパティ値を繰り返す

for (const value of Object.values(obj)) {
  console.log(value);
}

// 11
// 22
// 33

プロパティ名を繰り返す

for (const key of Object.keys(obj)) {
  console.log(key);
}

// foo
// bar
// boo

名前と値を繰り返す

両方か!? key と value の両方ほしいのか? 両方……イヤしんぼめ!!

はい、そんな欲張りさんのための機能もあります。ちょっとわかりづらいんだけど、戻り値としてその2つだけを格納した配列の配列を返します。

for (const [key, value] of Object.entries(obj)) {
  console.log(key, ':', value);
}

// foo : 11
// bar : 22
// boo : 33

得られる順序

3つとも内部処理は同じこれ↓で、得られる順序は以下の通り。

  1. 整数インデックス昇順(を文字列にしたもの)
  2. 文字列キー追加順
const obj = {};

obj[2] = '#1';
obj.z = '#2';
obj[Symbol(1)] = '#3';
obj[10] = '#4';
obj.a = '#5';

console.log(obj);
// => { '2': '#1', '10': '#4', z: '#2', a: '#5', [Symbol(1)]: '#3' }

for (const [key, value] of Object.entries(obj)) {
  console.log(key, ':', value);
}

// 2 : #1
// 10 : #4
// z : #2
// a : #5

仕様はこちら。

ちなみに OrdinaryOwnPropertyKeys() ではキーが Symbol のものも追加順に取得しているのだけれど、その後 EnumerableOwnPropertyNames() の処理で文字列でないものは捨てられます。

先の例↑で console.log() の方では Symbol がキーになってるものも取れているのは、その処理が OrdinaryOwnPropertyKeys() を使ってるからなんでしょう。知らんけど、コンソールの仕様は。

任意の順序に

したい場合、別途 sort() してやります。

文字列順とか localeCompare() が便利っぽい。 (ちなみに local ではなく locale です。)

// プロパティ名昇順
const it = Object.entries(obj)
  .sort(([key1], [key2]) => key1.localeCompare(key2));
for (const [key, value] of it) {
  console.log(key, ':', value);
}

// bar : 22
// boo : 33
// foo : 11

sort() に与えた比較関数は実は省略可能なんですが、罠があったりするので、常に省略しないのが良いやり方かと思っております。

おしまい

強力。

関連

参考

for-inの仕様も見てみたよ。使う機会なさそうだけど。(配列とかおれおれAdvent Calendar2018 – 14日目)

カテゴリー: JavaScript

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

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

for-in は現代ではたぶんもう使う機会ないんじゃないかなと思う。

for-in 文

列挙可能 (enumerable) なオブジェクトプロパティを繰り返します。

const arr = [11, 22, 33];
for (const index in arr) {
  const value = arr[index];
  console.log(index, value);
}
// 0 11
// 1 22
// 2 33

配列じゃなくても使えます、というか配列じゃないので使う場面の方が多いかなと思います。

const obj = { a: 11, b: 22, c: 33 };
for (const prop in obj) {
  const value = obj[prop];
  console.log(prop, value);
}
// a 11
// b 22
// c 33

index は文字列

プロパティ名が文字列なのはわかりやすいと思うんだけど、配列の場合でも数値 0, 1, 2, … ではなく、文字列で '0', '1', '2', … が得られます。

仕様的にも、配列の各要素は文字列をキーに格納されているし、添え字アクセスは文字列へ変換してから行われます。

っていう豆知識です。

仕様

for-in, for-of, for-await-of は全部まとめて定義されてる。

in 左側

for と同様、以下の三種類。

  • ただの式(宣言なしで i = 0 とか)
  • var 宣言
  • let, const 宣言

いったん for の方を見てください。

for-in の場合は for と違って繰り返し全体のレキシカル環境は生成されないっぽい。

繰り返し毎回のレキシカル環境は for と同様、毎回引き継ぎながら用意される様子。 変数の値を書き換えることなく毎回引き継いで新たに作り直すため、 const が使えます。

ただよくわからないところがあって。

繰り返しの前に一度 in の左側の変数を登録するレキシカル環境を作ってるんだけど、登録終わったら元のレキシカル環境へ戻してます。なんだろこれ。事前に検証してる感じ?

というか TDZ て何の頭文字? ”Tsunami Disaster Zone”? 仕様書中ここにしか出てこないんだけど。

これ↓? ( var と違って巻き上げないよって話。) やっぱり「本番はまだだけど事前にちょっとアレしとこうかなー」みたいな感じ?

in 右側

繰り返し前に評価して、 for-in の場合、繰り返し用にプロパティ名を列挙します。

対象オブジェクトからプロトタイプチェインを順々に辿ってゆき、全てのプロパティを列挙する。ただし、同じ名前のものは一度しか呼ばれない。という感じ。

もしJSで実装するならこういう↓処理らしいよ。

function* EnumerateObjectProperties(obj) {
  const visited = new Set();
  for (const key of Reflect.ownKeys(obj)) {
    if (typeof key === "symbol") continue;
    const desc = Reflect.getOwnPropertyDescriptor(obj, key);
    if (desc) {
      visited.add(key);
      if (desc.enumerable) yield key;
    }
  }
  const proto = Reflect.getPrototypeOf(obj);
  if (proto === null) return;
  for (const protoKey of EnumerateObjectProperties(proto)) {
    if (!visited.has(protoKey)) yield protoKey;
  }
}

列挙可能性

この for-in で出てくるかどうかを「列挙可能性 (enumerability)」と呼んでいるようです。

Object.defineProperty() で作るときに enumerable: false にすると、プロパティはあるけど列挙されなくなります。

const obj = { a: 11, b: 22 };
Object.defineProperty(obj, 'c', {
  enumerable: false, // ←これこれ
  value: 33,
});

console.log(obj.c); // => 33

for (const prop in obj) {
  const value = obj[prop];
  console.log(prop, value);
}
// a 11
// b 22

Object.getOwnPropertyDescriptor() を用いると、どういう設定になっているのか調べられます。

…

const descriptorA = Object.getOwnPropertyDescriptor(obj, 'a');
console.log(descriptorA);
// { value: 11,
//   writable: true,
//   enumerable: true,
//   configurable: true }

const descriptorC = Object.getOwnPropertyDescriptor(obj, 'c');
console.log(descriptorC);
// { value: 33,
//   writable: false,
//   enumerable: false,
//   configurable: false }

ちなみにgetter/setterもこいつらで扱えます。

Own property問題

クラスが導入される以前、ES 5時代のJavaScriptでは関数を使ってコンストラクターを用意していました。その場合 for-in でちょっと困ったことがあります。

prototype に設定したものまで出てきちゃうんです。

function MyClass (options) {
  this.name = options.name;
}
MyClass.prototype.sayHello = function () {
  console.log('Hi my name is ' + this.name + '!');
};

const obj = new MyClass({ name: 'Alice' });
obj.sayHello(); // Hi my name is Alice!

obj.boo = 33;
for (const prop in obj) {
  const value = obj[prop];
  console.log(prop, ':', value);
}
// name : Alice
// boo : 33
// sayHello : function () {
//   console.log('Hi my name is ' + this.name + '!');
// }

prototype へ指定している sayHello が出てますね。これは嬉しくない。

そのインスタンスが自分で持っているプロパティなのか、それとも prototype (やその他)から来ているのかを判断する必要があります。

解決

hasOwnProperty() を使います。

これはオブジェクト自身が指定のプロパティを持っていれば true を返すものです。 obj.hasOwnProperty('sayHello') は false になります。

// ...

for (const prop in obj) {
  // prototypeに設定されたもの等は無視
  if (!obj.hasOwnProperty(prop)) {
    continue;
  }

  const value = obj[prop];
  console.log(prop, ':', value);
}

今なら class で大丈夫

です。うん、こっちにしよ。

class MyClass {
  constructor (options) {
    this.name = options.name;
  }

  sayHello () {
    console.log(`Hi my name is ${this.name}!`);
  }
}

const obj = new MyClass({ name: 'Alice' });
obj.boo = 123;

for (const prop in obj) {
  const value = obj[prop];
  console.log(prop, ':', value);
}
// name : Alice
// boo : 33

const desc = Object.getOwnPropertyDescriptor(MyClass.prototype, 'sayHello');
console.log(desc.enumerable); // => false

繰り返し中の操作

削除された場合

呼ばれず無視されます。

A property that is deleted before it is processed by the iterator’s next method is ignored.

反復子 (iterator) の next メソッドにより処理される前に削除されたプロパティは無視される。

追加された場合

えーと「保証されない not guaranteed」てのは、未定義ってことで良いかな。

If new properties are added to the target object during enumeration, the newly added properties are not guaranteed to be processed in the active enumeration.

新しいプロパティが列挙 (enumeration) の途中で対象オブジェクトへ追加された場合、新しく追加されたプロパティが実行中の (active) 列挙内で処理されることは保証されない。

このコード↓で試したところ、繰り返し中には呼ばれませんでした。(手元のChrome, Firefox, Edgeで確認。) もちろん繰り返しの後で確認したら追加されている。

その他

in の右側はゆるい

null とかでもエラーにならない。

for (const i in null) ;

for-of ではエラーです。

順不同

for-in で得られるプロパティ名の順序は未定義 (not specified) です。

The mechanics and order of enumerating the properties is not specified

おしまい

参考

for文を仕様からじっくり見てみる。あとwhileとか。(配列とかおれおれAdvent Calendar2018 – 13日目)

カテゴリー: JavaScript

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

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

  • for, while
  • for-in → 別稿
  • for-of → 別稿
  • for-await-of → 別稿
  • forEach() → 別稿

for 文

古より伝わる技法。JavaScriptに限らずだいたいの言語で利用できます。

const arr = [11, 22, 33];

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

なお普通に先頭から末尾まで繰り返す場合は forEach() を使う方が現代では多いかと。

さて for 文、実行順序としては、

  1. 初期化 let i = 0
  2. 検証 i < arr.length
    • false なら終了
  3. 繰り返し処理 console.log(item)
  4. 繰り返し終端の処理 i++
  5. 「2. 検証」へ戻る

というやつですね。誰向けの説明だこれ。

まあせっかくなんでじっくり考えてみてほしいんですけど、セミコロンで区切って3種類の式を記述しまして、順に呼ばれるわけです。

条件を書く2番目は true/false の判定があるのでアレですが、他は何を書いても良いことになります。 例えば初期化で let を書かなくても良いし、終端式でカウンターを変化させる必要もありません。 というか2番目も省略可能で、その場合常に true 扱いになります。

いっそ省略して for (;;) でもよろしい。

あ、「終端式」とかは

おれが勝手に呼んでるだけです。

MDNでは以下のように紹介されている。

for ([initialization]; [condition]; [final-expression]) statement

ECMAScript的には基本的にどれもただの「式 expression」で、特に名前は決まっていないように思う。あえて探して言うなら……2番目の式が「試験 test」、3番目は「増進 increment」か。1番目は、うーん、そういう定義の仕方してないしなあ。

const は(だいたい)だめ

だめじゃないんだけど、終端式のところで i++ とあるように、変数に格納される値を変えながら繰り返すのが普通の for 文です。 値を変えるので、 const ではなく let である必要があります。

逆に終端式で変数値を変更しない場合、例えば副作用で内部情報が変わるだけとか、いっそ何もしないとか、そういう場合は const でも構いません。

for 文の仕様、第1式と本文

for 文なんて皆知ってておもしろくないと思うので、仕様の方もあたってみました。

第1式、 let i = 0 とか書く部分なんだけど、ここは仕様的には以下の3種類に分類されている。

  • ただの式(宣言なしで i = 0 とか)(省略も含む)
  • var 宣言
  • let, const 宣言

いずれかのパターンで初期処理を終えたのち、本文の評価へと移ります。

ただの式(宣言なしで i = 0 とか)(省略も含む)

普通です。

第1式が与えられた場合、式を評価して結果を取得します。 取得するけど、特に使いません。 まあgetterが実行されるぞと。

与えられない場合、何もしません。

var 宣言

普通です。

まず for 文に限らず var で宣言された変数は事前に準備されています。( for 文においては仕様書13.7.4.5 VarDeclaredNamesと13.7.4.6 VarScopedDeclarationsの部分。(だと思う。))

その後 for 文実行時に var xxx の xxx の部分を評価します。

let, const 宣言

ややこしくて長いです。スコープがいくつも出てきます。

簡単なコード例とスコープの範囲。外側から関数(赤)、forヘッダー(青)、for本文(紫)、for本文ブロック(緑)
「外側(赤)」「繰り返し全体(青)」「繰り返し毎回(紫)」「本文ブロック(緑)」の4つのレキシカル環境。

for 文実行時、まず現在の実行コンテキストの、えーとなにレキシカル環境? (LexicalEnvironment) を元に、繰り返し全体用のレキシカル環境を用意します。 そこに let なり const なりの変数を作成、宣言の内容を評価します。

本文ブロック {…} 実行時はまた新しいレキシカル環境が用意されるので、都合「外側(赤)」「繰り返し全体(青)」「本文ブロック(緑)」の3つのレキシカル環境が生まれることになります。へーそうなんだ。

加えて、 let の場合は繰り返し全体とブロックの間にもうひとつ、「繰り返し毎回(紫)」を作成します。次項。

あ、これらの命名はおれです。

また本文の評価終了後、途中でエラーが発生していたとしても、レキシカル環境を元に戻します。途中でコケると const が露出するとか嫌だもんね。

let 用のお役立ち追加レキシカル環境

第1式の評価が終わったので本文の評価へ移るところですが、その前にもう一点やることがあります。

ヘッダー (…) で let を利用している場合、繰り返しの度に新しいレキシカル環境を作り、それらの変数を移すという特殊な動きがあります。

さっきの例だと初回は「繰り返し全体(青)」のものを「繰り返し毎回(紫)」へ、以後「繰り返し毎回(紫)」から次の「繰り返し毎回(紫)」へ、都度複製してる感じ。

簡単なコード例とスコープの範囲。外側から関数(赤)、forヘッダー(青)、for本文(紫)、for本文ブロック(緑)。
for で宣言された let i (青)は本文ブロック(緑)からは参照されず、繰り返し毎回(紫)の部分へ複製されたものが見られる。毎回複製されるので、クロージャーで後から利用しても値は変化していない。

これは for で繰り返しのたびに生成されるけれど、ブロック {…} のものとは別に用意される。(ちなみにブロックのは繰り返しごとに毎回作られて消えるので、その中の変数も他に参照がなければ消えます。)

一体これの何が嬉しいかというと、あの!  for 文の罠が! 回避されます!!

// ç½ 
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log('var(1)', i), 1);
}
// => 3, 3, 3

// 罠回避
for (var j = 0; j < 3; j++) {
  (function (j) {
    setTimeout(function () {
      console.log('var(2)', j);
    }, 1);
  })(j);
}
// => 0, 1, 2

// やったぜ
for (let k = 0; k < 3; k++) {
  setTimeout(() => console.log('let(1)', k), 1);
}
// => 0, 1, 2

かつて var だった頃は、繰り返しで用いる i は外側のスコープに存在しているため、クロージャーで利用する頃には更新されて終了値になってしまっていました。(ひとつめ)

それを回避するため、匿名関数を作成して新しいスコープを作ったり、普通に関数の引数に与えたりして別のスコープへその値を与え、ある意味コピーするみたいなことが必要でした。(ふたつめ)

let だとその必要がないと。やったぜ。(みっつめ)

本文

for 文の宣言部分 (…) 終了後、第2式の評価が falsy になるか break 等が行われるまで、本文を繰り返します。

ブロック {…} を伴う本文実行時には毎回新しいレキシカル環境が作成されるので、繰り返し本文に const 宣言を記述してももちろん大丈夫。(なおブロックがなしに直接 const 書けません。)

外側で宣言された変数を、こう、上書きすることもできます。(表現微妙だけど察して。)

for (let i = 0; i < 3; i++) {
  const i = 0;
  console.log(i); // 全部0
}

まあ変数名が重複すると読みづらいのでやめた方が良いけどね。

レキシカル環境 (LexicalEnvironment) ?

簡単に言うといわゆるスコープのこと。もうちょっと言うとブロック構文その他に紐づいて変数等を格納するのもの。

と理解してるけど合ってるかな? よくわかってないです。というか他の呼び方もあるのかな。

while

ただ条件式を評価して、 true である場合は続く本文 { … } を実行します。

実務ではだいたい for 文の出番の方が多いと思うんだけど、こっちはこっちでよく使います。

なんかうまく言えないけど、使い勝手が良い場面がしばしばある。 分野によってはむしろこっちの方が利用回数多かったりしそう。

利用例

DOMで祖先要素を辿る例

単なる i++ じゃないやつ。

<div class="block1">
  <div class="block2">
    <div id="target"></div>
  </div>
</div>
const target = document.querySelector('#target');
for (let el = target; el; el = el.parentNode) {
  console.log(el);
}

// => <div id="target">
//    <div class="block2">
//    <div class="block1">
//    <body>
//    <html>
//    #document

反復

for-of でやれるやつだけど、分解してただの for 文でやることもできます。

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

const iterator = generate();
for (let step = iterator.next(); !step.done; step = iterator.next()) {
  const { value } = step;
  console.log(value);
}

ジェネレーターの説明以外に特に利点はなさそう。

無限ループ

何らかの理由により無限ループしたい場合にも使います。

for (;;) {
  // 実行され続けるコード
}
while (true) {
  // 実行され続けるコード
}

無限ループと見せかけて中で break してる場合も多い。 終了条件がある程度複雑で括弧 ( ... ) 内に書きたくない場合、わざとこういう書き方することもあります。

可能なら関数化したいところ。

条件を満たすまで待つ

ハードウェアに近い制御をするときとかにたぶんよく書くやつ。知らんけど。 break より return する場面の方が多いんじゃないかな、いや知らんけど。

while (true) {
  const result = doSomething();
  if (result === CODE_OK) {
    break;
  }

  sleep(100);
}

普通JavaScriptでこういうの書くことはないと思う。

なお sleep() は各自ご用意ください。

実行されないコード

どこぞの文化ではコメントアウト替わりに使うこともあるとか? 良くないと思うなー。

while (false) {
  // 実行されないコード
}

その他

break, continue

そこそこ実務でも使うことがあります。

あるでしょ? (ある。)

for より while で使うことの方が多い気がするな。いやそうとも限らないか。

const arr = [11, 22, 33];

for (let i = 0; i < arr.length; i++) {
  const item = arr[i];

  // 偶数は無視
  if (item % 2 === 0) {
    continue;
  }
  
  console.log(item);
}
const arr = [11, 22, 33];

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

  // 22が出てきたら満足して終了
  if (item === 22) {
    break;
  }
}

ラベル

実務で使ったことないです。

ないでしょ? (ない。)

入れ子になった繰り返しをまとめて break したりできます。

この例↓だと console.log() は一度しか実行されません。

const arr = [...Array(30)].map((_, i) => i);

outerLoop: for (let i = 0; i < arr.length; i++) {
  for (let j = 0; j < arr.length; j++) {
    console.log(i, j);
    break outerLoop;
  }
}

ベーシックのような古い言語で GOTO 命令と組み合わせて使ってた印象。 現代ではBad practiceの類です。 やめよう、というかまあ使おうとも思わないだろうけど。

do-while

実務で使ったことないです。

ないでしょ? (ない気がする。)

「空でもひとつオブジェクト作らなくちゃ」みたいなのは if で分ける方が良いと思います。

あ、TreeWalkerが do-while に良さそうな仕様だった。

<ul id="root">
  <li id="node-1"><span id="node-1-1">One</span></li>
  <li id="node-2"><span id="node-2-1">Two</span></li>
  <li id="node-3"><span id="node-3-1">three</span></li>
</ul>
const root = document.querySelector("#root");
const w = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
do {
  console.log(w.currentNode.id);
} while (w.nextNode())

// => root
//    node-1
//    node-1-1
//    node-2
//    node-2-1
//    node-3
//    node-3-1

do

繰り返しは関係ないんだけど、 do-while が出たので。

do 式という仕様案が出てます。(案なのでまだ使えない。)

代入 = の右辺に置いて、即時実行関数の戻り値を変数に入れるみたいな使い方ができるもの。Kotlinの run 的なやつ。

const timeText = do {
  const d = new Date();
  `${d.getHours()}:${d.getMinutes()}`
};

これ便利だと思うなー。ほしい。今でも匿名関数の即時実行で近い書き方ができるけど、こっちの方がいいなあ。

おしまい

こんなん読んで誰が嬉しいんだ……みたいに思いながら書きました。 おれは楽しかったよ!

参考

更新履歴

  • 2018-12-13 「 let 用のお役立ち追加レキシカル環境」を追加、それに合わせて前後調整

その名の通りのfilter()で絞り込み。(配列とかおれおれAdvent Calendar2018 – 12日目)

カテゴリー: JavaScript

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

配列の中から条件に合致するものだけに絞るやつです。

const users = [
  { id: 101, name: 'Alice', active: true },
  { id: 102, name: 'Bob', active: false },
  { id: 103, name: 'Charlie', active: true },
];

const activeUsers = users.filter((v) => v.active);
console.log(activeUsers);

仕様

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

newArray = arr.filter(callback(element[, index[, array]])[, thisArg])

引数

filter() は引数に、関数オブジェクト callback(element[, index[, array]]) をひとつ受け取ります。

与える関数には、3つの引数が与えられます。 また前述のように boolean 値を返してください。

  • value … 配列の要素
  • index … インデックス
  • array … 操作中の配列本体

戻り値

関数 callback が true (ないし truthy なもの)を返した際の要素だけを格納した、新しい配列を返します。

該当がなかった場合でも要素数0の配列を返します。

例

undefined を除く

何かやった結果として undefined 混じりの情報を得るみたいな場面。

const result = [
  'foo',
  undefined,
  '',
  'hoge',
  undefined,
  'bar',
];

const validResult = result.filter((v) => v !== undefined);
console.log(validResult); // => [ 'foo', '', 'hoge', 'bar' ]

空文字列等の falsy なものもまとめて除いで良い場合は (v) => v で。短い。

アクティブユーザーだけ

フラグを含むオブジェクトの配列から対象のものだけを抜き出す場面。

const users = [
  { id: 101, name: 'Alice', active: true },
  { id: 102, name: 'Bob', active: false },
  { id: 103, name: 'Charlie', active: true },
];

const activeUsers = users.filter((v) => v.active);
console.log(activeUsers);
// => [ { id: 101, name: 'Alice', active: true },
//      { id: 103, name: 'Charlie', active: true } ]

チェックボックスで選択されたものだけ

document.querySelectorAll() は複数の要素を配列風オブジェクト NodeList で返します。 [...els] でそれを本物の配列へ変換し、 filter() で選択されたものだけにしてから map() で要素からその値を取り出しします。

(コードは抜粋)

<form id="the-form">
  <input type="checkbox" name="favorite" value="apple"> Apple
  <input type="checkbox" name="favorite" value="banana"> Banana
  <input type="checkbox" name="favorite" value="orange"> Orange
</form>
const elFavorites = document.querySelectorAll('#the-form [name="favorite"]');
const favorites = [...elFavorites]
  .filter((el) => el.checked)
  .map((el) => el.value);
console.log(favorites);

他の配列で指定されたID

前項のチェックボックスがIDだったとして、また別の配列からをその選択されたやつだけに絞る場合。

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

const activeUsers = users.filter((v) => selectedIds.includes(v.id));
console.log(activeUsers);
// => [ { id: '101', name: 'Alice' }, { id: '102', name: 'Bob' } ]

その他のメソッド

「絞り込む」以外の用途に合うメソッドがあるので、それぞれ使い分けましょう。

絞り込んだ結果を使って何かするなら map()

find() 後に map() を組み合わせると便利です。

前述したチェックボックスの例を参考に。

ひとつだけ選択する

対象のものが1つとわかっている場合は find() が便利です。

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

const id = '102';
const targetUser = users.find((v) => v.id === id);
console.log(targetUser);
// => { id: '102', name: 'Bob' }

あるかどうかだけわかれば良いなら some()

const existing = users.some((v) => v.id === targetId);
if (existing) {
  console.log('あったよ。');
}
else {
  console.log('なかったよ。');
}

全てが合致するか確認するなら every()

おまけ: あのアイコン

配列とは関係ないんだけど、UI上でフィルターの意味でしばしば使われるじょうご(ろうと、漏斗)、英語だと funnel だそうです。

Font Awesomeの “filter” 。

ガンダムのファンネルは、最初はその形状から名づけられたとか。その後ファンネルだけで例の小型遠隔兵器を指すようになり、フィン・ファンネルとかシールド・ファンネルとか出てくると。発展するにつれ語源から遠ざかるのは世の常ですね。

今日こんにちは! 然様さようなら!

おしまい

関連

参考