繰り返しにもいろいろあるよ。
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
文、実行順序としては、
- 初期化
let i = 0
- 検証
i < arr.length
false
なら終了
- 繰り返し処理
console.log(item)
- 繰り返し終端の処理
i++
- 「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
文なんて皆知ってておもしろくないと思うので、仕様の方もあたってみました。
- 13.7.4.7 Runtime Semantics: LabelledEvaluation
- 13.7.4.8 Runtime Semantics: ForBodyEvaluation ( test, increment, stmt, perIterationBindings, labelSet )
第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
文実行時、まず現在の実行コンテキストの、えーとなにレキシカル環境? (LexicalEnvironment) を元に、繰り返し全体用のレキシカル環境を用意します。
そこに let
なり const
なりの変数を作成、宣言の内容を評価します。
本文ブロック {…}
実行時はまた新しいレキシカル環境が用意されるので、都合「外側(赤)」「繰り返し全体(青)」「本文ブロック(緑)」の3つのレキシカル環境が生まれることになります。へーそうなんだ。
加えて、 let
の場合は繰り返し全体とブロックの間にもうひとつ、「繰り返し毎回(紫)」を作成します。次項。
あ、これらの命名はおれです。
また本文の評価終了後、途中でエラーが発生していたとしても、レキシカル環境を元に戻します。途中でコケると const
が露出するとか嫌だもんね。
let
用のお役立ち追加レキシカル環境
第1式の評価が終わったので本文の評価へ移るところですが、その前にもう一点やることがあります。
ヘッダー (…)
で let
を利用している場合、繰り返しの度に新しいレキシカル環境を作り、それらの変数を移すという特殊な動きがあります。
- 13.7.4.8 Runtime Semantics: ForBodyEvaluation ( test, increment, stmt, perIterationBindings, labelSet )
- 13.7.4.9 Runtime Semantics: CreatePerIterationEnvironment ( perIterationBindings )
さっきの例だと初回は「繰り返し全体(青)」のものを「繰り返し毎回(紫)」へ、以後「繰り返し毎回(紫)」から次の「繰り返し毎回(紫)」へ、都度複製してる感じ。
これは 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
実務で使ったことないです。
ないでしょ? (ない気がする。)
GitHubで検索しても繰り返し用途で使ってないのばっか出てくる……。https://t.co/wsR5Fy3EOA pic.twitter.com/4jIyW0OPJt
— 高梨ギンペイ (@ginpei_jp) November 21, 2018
「空でもひとつオブジェクト作らなくちゃ」みたいなのは 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()}` };
これ便利だと思うなー。ほしい。今でも匿名関数の即時実行で近い書き方ができるけど、こっちの方がいいなあ。
おしまい
こんなん読んで誰が嬉しいんだ……みたいに思いながら書きました。 おれは楽しかったよ!
参考
- for – JavaScript | MDN
- while – JavaScript | MDN
- do…while – JavaScript | MDN
- treeWalker | MDN
- ECMAScript® 2018 Language Specification
- 13.7.4 The for Statement
- 13.7.4.7 Runtime Semantics: LabelledEvaluation
- 13.7.4.8 Runtime Semantics: ForBodyEvaluation ( test, increment, stmt, perIterationBindings, labelSet )
- 13.7.4.9 Runtime Semantics: CreatePerIterationEnvironment ( perIterationBindings )
- 13.7.3 The while Statement
- 13.7.2 The do-while Statement
- 13.8 The continue Statement
- 13.9 The break Statement
- 13.1. 7Runtime Semantics: LabelledEvaluation
- 8.1.2.2 NewDeclarativeEnvironment ( E )
- 13.2.13 Runtime Semantics: Evaluation … ブロック
{…}
の評価
更新履歴
- 2018-12-13 「
let
用のお役立ち追加レキシカル環境」を追加、それに合わせて前後調整