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 VarDeclaredNames13.7.4.6 VarScopedDeclarationsの部分。(だと思う。))

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

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 用のお役立ち追加レキシカル環境」を追加、それに合わせて前後調整