質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.33%

for文における、letとvarの挙動の違い

解決済

回答 6

投稿 編集

  • 評価
  • クリップ 11
  • VIEW 2,110

YESYUKI17

score 10

前提・実現したいこと

お世話になります。
JavaScriptで簡単なアイコンのアニメーションを設定したくてfor文で条件分岐で実現しようとしました。
以下のコードでfor文の条件をvarで設定するとうまくいかず、letで書くとうまくいくことまでわかりました。
なぜvarだとエラーが起こるのか根本的な理由を理解したいです。一応var,letの違いは認識しているつもりなのですが、ブロックスコープを認識するletでエラーがでず、認識しないvarでエラーになる理由がわかりません。
アドバイスいただけますと、大変助かります。

'use strict';
{

  const plus = document.querySelectorAll('.plus');
  // 必要ない部分と変数名を変更しました
  for (var i = 0; i < plus.length; i++) {
    console.log(i)
    // 期待した通り012と表示
    plus[i].addEventListener('click', () => {
      console.log(i);
      // なぜ3が表示されるのか?i<plus.lengthは3未満の数値になるはず
      if (plus[i].textContent === '+') {
        plus[i].textContent = '-';
      } else {
        plus[i].textContent = '+';
      }
    });
  }


}
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>Practice</title>
    <link rel="stylesheet" href="css/styles.css">
  </head>
  <body>

      <p class="plus">+</p>
      <p class="plus">+</p>
      <p class="plus">+</p>
      <!-- id削除しました -->

    <script src="js/main.js"></script>

  </body>
</html>
/* クラスに変更しました */
.plus {
  color: #00AED9;
  text-align: center;
  line-height: 15px;
  border: solid #DADADA 2px;
  border-radius: 50%;
  width: 20px;
  height: 20px;
  font-weight: bold;
  font-size: 20px;
}

補足情報(FW/ツールのバージョンなど)

Atomエディタ、CHROME

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • SurferOnWww

    2019/08/14 15:29

    > なぜ3が表示されるのか?i<plus.lengthは3未満の数値になるはず

    for (var i = 0; i < plus.length; i++) で plus.length は 3 なので、i は 3 までインクレメントされてからループを抜け、匿名関数の中の console.log(i); の i とか plus[i] の i は 3 に設定されるのでしょう。

    キャンセル

  • SurferOnWww

    2019/08/14 15:35 編集

    var i = 0 を let i = 0 に変えると、Chrome とか Firefox であれば無名関数が 3 つ、それぞれ内部の console.log(i); の i とか plus[i] の i が 0, 1, 2 のものが plus[0], plus[1], plus[3] の click イベントのリスナとしてアタッチされるからうまくいくのだと思います。

    でも、IE11 の場合は var i = 0 の場合と同じになって期待通りいかないということになっているようです。

    キャンセル

回答 6

+8

varだと1つの変数をずっと使います。なので、forを抜けた時点でi == plusa.lengthになっていますが、これをclickで起動する無名関数で参照して、その添え字で配列要素を参照しようとして範囲外エラーになります。

letだと変数がブロック毎なので、初回にブロックを実行したときに定義される無名関数中ではi==0です。
varしか無いときは、ブロック内にもう一つ無名関数を作ってその関数内のvarで行う必要がありました。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/08/14 20:45

    ↑ああ、なるほど。

    キャンセル

  • 2019/08/15 13:19

    確かに、変数と値の概念をそれぞれ曖昧に捉えていたと思います。

    キャンセル

  • 2019/08/15 13:38

    このご説明が端的にでとてもわかりやすいという事がやっと理解できました。

    キャンセル

checkベストアンサー

+5

ブロックスコープ

letconst 共に存在する性質ですが、for 文などの繰り返し構文で変数宣言された場合、ブロック毎に「新しい変数」が生成されます。

下記に疑似コードを書きました。

<h2>let + var (type1) ※IE11は未対応</h2>
<p class="plus1">+</p>
<p class="plus1">+</p>
<p class="plus1">+</p>

<h2>let + var (type2) ※ブロックスコープの再現コード</h2>
<p class="plus2">+</p>
<p class="plus2">+</p>
<p class="plus2">+</p>

<script>
'use strict';
/*
 * type 1
 */
const plus1 = document.querySelectorAll('.plus1');

for (let i = 0; i < plus1.length; i++) {
  plus1[i].addEventListener('click', function handleClick () {
    console.log(i);
    plus1[i].textContent = plus1[i].textContent === '+' ? '-' : '+';
  }, false);
}
</script>
<script>
'use strict';
/*
 * type 2
 */
 const plus2 = document.querySelectorAll('.plus2');

for (let i = 0; i < plus2.length; i++) {
  let i2 = i;

  plus2[i2].addEventListener('click', function handleClick () {
    console.log(i2);
    plus2[i2].textContent = plus2[i2].textContent === '+' ? '-' : '+';
  }, false);
}
</script>

変数 i2 はブロック文の中でのみ有効であり、0~2が格納された3つの変数は別の変数です。
変数 i も同様の扱いです。

問題点

  • HTML上で同じID(id="plus")を複数回記述する事は出来ません(文法違反)
  • CSSのIDセレクタは初めに出現した要素のみに適用される仕様です

変数の巻き上げ(Variable hoisting)

JavaScript では変数の「初期化」と「代入」は分けて実行されます。

  • 変数は関数呼び出し時に生成されます
  • 関数呼び出し直後に、関数内で宣言された**全ての変数を undefined で初期化+*します
  • var a = 1; が実行された時、1 が代入されます(初期化は既に完了しています)
function sample1 () { // 変数 a が undefined で「初期化」される
  console.log(a); // undefined

  var a = 1; // 1 が「代入」される
}

sample1();

この挙動は分かりづらい為、あえて初期化と代入を分けてコートを書く事を好む人もいます。

function sample2 () { // 変数 a が undefined で「初期化」される
  var a;  // 初期化済なので何もしないが、このコードはundefined で初期化を意図している為、分かりやすい

  console.log(a); // undefined
  a = 1; // 1 が「代入」される
}

sample2();

for 文の挙動は下記になります。

function sample3 () { // 変数 elememts,i,len が undefined で「初期化」される

  var elememts = document.querySelectorAll('.plus'); // NodeListが「代入」される(3つの要素を持つとする)

  for (var i = 0, len = elememts.length; i < len; i++) { // i, len が代入される(3回繰り返し)
    elements.addEventListener('click', () => console.log(i), false); // clickリスナー実行時にはループ処理が終わっている為、i 値は常に3となる
  } 
}

sample3();

ところで、letconst でも巻き上げは発生しています。
ただし、変数定義文より前に変数参照が行われると、ReferenceError で停止する為、事実上、巻き上げを考慮する必要がなくなっています。

Re: YESYUKI17 さん

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/08/15 13:17

    ありがとうございます。やっと理解できたと思います。これはループ処理において、varのスコープがブロックスコープ にならないため、3回のループ処理が全て一つのブロックにまとめられてしまい、関数内の変数は呼び出し時に代入されるため、クリックイベント時に変数iを代入する際に、ループを抜けてi = 3になったiを参照してきてしまうという事が問題なのですね。対して、letであれば各ループがそれぞれ一つのブロックとして完結するので、それぞれその際に生成されたiの値を参照できる。なので関数呼び出し時に値が代入されてもそのループで生成されたiをちゃんと参照できるというなんですね。関数呼び出し時に変数に値が代入されるという観点の漏れが私の理解を妨げていたような気がします。varだと
    ① plus[i].addEventListener('click', () => {
    ② if (plus[i].textContent === '+') {
    クリックを押して関数が呼び出されると、①のiに入っている値と②のiに入っている値が違う上に②の値はループを抜けた後のiを参照してしまうという事で理解しました。この理解で問題ないでしょうか?

    キャンセル

  • 2019/08/15 18:27

    > 3回のループ処理が全て一つのブロックにまとめられてしまい、

    varは関数スコープなので、「ブロック」は妥当でなく、「同じ変数 i を参照してしまい」になるかと思います。

    > ① plus[i].addEventListener('click', () => {
    > ② if (plus[i].textContent === '+') {

    var においては、同じ変数 i を参照しますが、「参照するタイミング」が異なり、②は「forループを抜けた後の変数 i」を参照します。

    他は問題ないと思います。

    キャンセル

  • 2019/08/15 20:38

    ありがとうございます。大変勉強になりました。

    キャンセル

+4

FAQですね。
for文におけるletとvarの挙動の違いについてQiitaに記事を書いたことがあります。

実はletのメモリ効率が悪かった

varとletは箱の数が違います。
varは1つ、letは箱の数がいっぱいあります。

letとvarを箱の数の違いで表現されていますが、もう少し詳しく、箱のというイメージで捉えるためのご説明をお願いできませんか?

箱のイメージ

比喩でなく説明すると、箱はメモリ上の場所です。

letはブロックスコープなので、forの繰り返しに対して別の箱が用意されるようです。
varはforの繰り返しに対して同じ1つの箱を使い回しします。

varのときクリックイベントで1つの箱を使い回そうとしたときループが進んで別の値に変わってしまっていたというのがよくある不具合です。

またリンク先の説明でvarとletは箱の数が違うに関して
のところでconsoleに4が表示されるのはなぜでしょうか?

for文の説明

他のteratailの質問でfor文の制御フローを解説するために作成した図です。
変数i=3のとき終了条件i<4が真になり、命令文本体が実行されます。
次に「増減」のi++が実行され、i=4となり終了条件i<4が偽になり、for文が終了します。
forループ後にi=4となります。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/08/15 13:35

    for (let i = 0; i < plus1.length; i++)
    この部分がブロックだとループの数だけそれぞれ独立した箱として用意されるイメージだけど
    varだと一つのfor (let i = 0; i < plus1.length; i++)を全てのループに使い回す。
    なので、クリックで関数を呼び出した時にループを抜けたi = 3を参照してきてしまうというような
    イメージであっていますでしょうか?

    キャンセル

  • 2019/08/15 17:39

    varだと一つのvarの箱をfor (var i = 0; i < 3; i++)を全てのループに使い回す。
    なので、クリックで関数を呼び出した時にループを抜けたi = 3を参照してきてしまうということです。

    キャンセル

  • 2019/08/15 20:38

    ありがとうございます。おかげで理解する事ができました。

    キャンセル

+3

認識しないvarでエラーになる理由がわかりません。

addEventListenerで割り当てた関数内でiの値を確認してみてください。

【varよりすごいletとconst。(現代的JavaScriptおれおれアドベントカレンダー2017 – 02日目) | Ginpen.com】
https://ginpen.com/2017/12/02/var-let-const/

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/08/14 15:01

    ※わかりやすように少しコードを書き換えました
    ありがとうございます。返信に時間がかかってすいません。自分なりに理解し、質問に落とし込むのに時間がかかりました。もう少しで理解できそうなので、アドバイスもらえると大変助かります。

    リンク先の説明で
    なぜかというと、 setTimeout() はすぐに実行されて、コールバック実行の待機を開始し、ループは終了します。その後 setTimeout() のコールバックが実行される頃には i の中身がもう 5 になっちゃってるからです、
    とあります。確かにご指摘の通り、イベントリスナーの中にconsole.logでiを表示させると
    3と表示されます。setTimeoutの説明でI < max = 5となっているのに5と表示されるのはなぜなんでしょうか?

    キャンセル

  • 2019/08/14 15:25

    for (var i = 0; i < 5; i++)
    forは上記条件の時、初回ループ前にi = 0を実行し、最初のループを実行します。
    ループが1回終わった時点で i++をします。
    そのあと、 i < 5であれば次のループを実行し、そうでなければ終了します。
    スコープがブロックスコープで無いvar の場合、for の後のi は 5になります。

    キャンセル

  • 2019/08/14 16:14

    i < 5を超えたら(i = 5になった時点で)それ以降は{}のループは回さないが、iに5を代入する処理は行われるという理解であってますか?

    var max = 5;
    for (var i = 0; i < max; i++) {
    setTimeout(() => {
    console.log(max - i);
    }, i * 1000);
    }
    確かに上のコードを実行すると、0が5回カウントされることが分かります。
    これはつまり、for文の()内のiは0-4の値が5回代入されそれが5回分のループ処理と認識されるが、setTimeout関数のブロックの中にはループを回している途中のiは入っていくことができず、forを抜けた時のi=5のみが入っていく事ができるのでconsole.log(max - i);が(max = 5) - (i = 5) = 0が表示されるという理解でよろしいでしょうか?

    キャンセル

  • 2019/08/14 19:52

    「i < 5を超えたら」を判断する前に「iに5を代入する処理」は行われています。
    後半の処理は「入っていく」など例がよくわかりません。0が表示されるという事象はそのとおりです。

    キャンセル

+1

余計に混乱させてしまうかもしれませんが、varでなんとかしようとしたら以下のようにしないとだめですね。わからなければ、即時関数、クロージャーも合わせて調べると良いです。

'use strict';
{

  const plus = document.querySelectorAll('.plus');
  for (var i = 0; i < plus.length; i++) {
    console.log(i);
    (i => {
      plus[i].addEventListener('click', () => {
        console.log(i);
        if (plus[i].textContent === '+') {
          plus[i].textContent = '-';
        } else {
          plus[i].textContent = '+';
        }
      });
    })(i);
  }
}

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/08/15 20:46

    ありがとうございます。おかげで即時関数と特にクロージャーの理解の必要に気づく事ができました

    キャンセル

0

【追記2】注:質問のタイトルにある「for文における、letとvarの挙動の違い」に限って話をしろと言うことですと、私の回答はズレているかもしれませんが、そういう話に限定するわけではなくて、質問にあるサンプルコードが動くようにするにはどうすべきかという話だと理解して回答しています。ちゃんと動くコードを書くというのが本来の目的のはずで、var と let の違いを知りたいというのは目的を果たすための手段の一つでしかないと自分は思うので。

var でも let でもどちらも問題で、リスナに渡される event から target でイベントを発生させた要素を取得し、それを操作しないと期待通りにならないと思いますが?

分かりやすく書くと以下のような感じです。(スクリプトは質問者さんのコードの js/main.js にあるのが前提)

const plusa = document.querySelectorAll('.plus3');
for (var i = 0; i < plusa.length; i++) {
    plusa[i].addEventListener('click', listener);
}

function listener(event) {
    if (event.target.textContent === '+') {
        event.target.textContent = '-';
    } else {
        event.target.textContent = '+';
    }
}

【追記】

IE11 での結果です。IE11 では () => は使えないそうなので function () に変えています。Chrome, Edge, Firefox は期待通り動きます。

イメージ説明

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/08/14 20:44 編集

    それはあなたにとっては大きな問題ではないというだけです。あなたの横レスは不要です。お控えください。

    キャンセル

  • 2019/08/14 20:51

    回避手段を書かないのは公平性に欠けるので横レスしました。それだけです。

    キャンセル

  • 2019/08/14 20:57

    何を言ってるのか分かりませんが、とにかくあなたの横レスは不要と申し上げた通りです。よろしくお願いします。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 90.33%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

同じタグがついた質問を見る