クロージャ構文

【JavaScript】 forループとiカウンター変数の秘密

更新日:2021/01/20

 

余談:カウンターにiを使うのはなぜ?

JavaScriptでforループの解説をしているテキスト等には、例として次のようなコードが掲載されていることが多いです。


for( let i = 0 ; i < 10 ; i++ ) {
   // …繰り返し処理
}

「ほう、そうなんだ」と解説を読み進めるわけですが、一部の細かいことを気にしちゃう系の人はこう思うはずです。

「なんでカウンター変数にiを使っているの?」

そうなんです。
多くの解説記事やサンプルプログラム、はては実コードまで、みんなiを使っています。

僕も一時期、『変数名には意味があるものを使用するべきだ』という意見を聞いて、stepとかcounterとか使っていましたが、「なんで企業コーディング規約に縛られているわけでもないのに、めんどくさいことしてるんだろう」と思って、いまだにiを使用しています。

僕は今から20年以上前にNECのPC9801というパソコンとボーランド社のTurbo CというCコンパイラをバイトして購入して、毎日C言語で遊んでいました。
懐かしい思い出です。

そしてその頃は既に、for分のカウンターはiが一般的に使用されていたのです。
僕も何も疑問に思わずにiを使用するわけです。
そして血肉となってしみ込んだ結果、無意識のうちにiを使用するようになったわけです。

同じ人多いのではないでしょうか?

そんな僕らが後輩たちにプログラミング言語を教えたり、書籍やWeb記事にコードを掲載するときもiを使用します。
教えられたり記事を見た人は疑問もなく受け入れ、後世へと伝えられていくのです。

今気が付きましたが、forループでiを使うのは、既に文化になっているのですね。
仕方がないことのようです。

ここまでは余談の余談でした。

そもそも、どうしてiが使用されていたのかという疑問の答えになっていません。

調べてみると次の記事がヒットしました。

なぜループカウンタ変数のほとんどに “i”が使用されるのか?(外部記事)

始まりはFORTRANというプログラミング言語で、この言語では変数名は一文字だけしか使用できなくて、そのうち整数をあらわすものはI~Nの6個のみだったんだそうです。

FORTRANにはfor文がないようですが、FORTRANを学んだ人が他言語を利用するようになり、今までの癖で変数名にiを使用していたのが、後々まで広まってしまったのが現状かもしれません。

 

forループとiカウンターとクロージャ

余談が長すぎました。
本題です。

JavaScriptは関数の中で関数を定義できる不思議ちゃんな言語です。

そしてforループ内で関数を定義し、それを外部に持ち出すとき少し注意が必要です。

letのforループ

次のコードを見てください。


const func = [];

for( let i = 0 ; i < 5 ; i ++ ){
    func.push( ()=>{
       console.log( i );
    });
}

func[0](); // 0
func[1](); // 1
func[2](); // 2
func[3](); // 3
func[4](); // 4

forループ内でアロー関数を定義し、それを配列に格納しています。
つまり、forループ内で関数を定義し、それを外部に持ち出しています。

持ちだした関数はクロージャを形成しています。
実行すると、定義された時点でのカウンター変数iの値をコンソールに出力します。

感覚的には当たり前の動作です。

varのforループ

上のコードを少し書き換えてみます。


const func = [];

for( var i = 0 ; i < 5 ; i ++ ){
    func.push( ()=>{
       console.log( i );
    });
}

func[0](); // 5
func[1](); // 5
func[2](); // 5
func[3](); // 5
func[4](); // 5

カウンター変数iの定義をletからvarに変更しました。
letとvarの違いについては、ここでは振れないので次のページを見てください。
【JavaScript】 適当に使ってた!変数宣言var/let/constの使い分け

関数を実行すると、全て5と出力されてしまいました。

実はvarは関数スコープなので、関数内で一つしか定義できません。
そのため次のようなコードと同等となります。


var i;
for( i = 0 ; i < 5 ; i ++ ){
    func.push( ()=>{
       console.log( i );
    });
}

forループ終了時点で i は 5 になっています。
そのためfunc配列内の関数を実行すると、全て5と表示されます。

letでカウンター変数iが保持される理由

一方letはブロックスコープで定義されます。
そしてforで定義したカウンター変数iは、後に続くブロック({ })内で個別に評価されます。

iの値コードイメージ
0
{
 let i = 0;
 func.push( ()=>{
       console.log( i );
    });
}
1
{
 let i = 1;
 func.push( ()=>{
       console.log( i );
    });
}
・・・

・・・

または、次のように関数が呼ばれているイメージでもいいかもしれません。


for( let i = 0 ; i < 5 ; i ++ ) (function( i ){
    func.push( ()=>{
       console.log( i );
    });
})( i );

変数iが個別にクロージャの環境レコードに取り込まれるので、実行時の値を保持できているわけです。

上のコード、よくわからないという人は次のページを読んでみてください。
【JavaScript】 即時関数の挙動について調べてみた

varは使用しない

古いバージョンのJavaScriptにはletが存在せず、varのみでした。

そのため、ここで紹介したletでの結果を得るために、いろいろなテクニックが考案されました。
今でもネットで検索すると出てきます。
そして、それを読んだ初心者がletで定義したときも同じ結果になると勘違いして、混乱します。
というか、僕は混乱しました。

letを使用するば全く気にしなくていいテクニックです。

そもそもvarが使いにくいということでletがJavaScriptに追加された経緯があります。
今ではvarの使用を推奨していない人が多いです。

僕もあまりおススメしません。

更新日:2021/01/20

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。