前提・実現したいこと
お世話になります。
JavaScriptで簡単なアイコンのアニメーションを設定したくてfor文で条件分岐で実現しようとしました。
以下のコードでfor文の条件をvarで設定するとうまくいかず、letで書くとうまくいくことまでわかりました。
なぜvarだとエラーが起こるのか根本的な理由を理解したいです。一応var,letの違いは認識しているつもりなのですが、ブロックスコープを認識するletでエラーがでず、認識しないvarでエラーになる理由がわかりません。
アドバイスいただけますと、大変助かります。
JavaScript
1'use strict'; 2{ 3 4 const plus = document.querySelectorAll('.plus'); 5 // 必要ない部分と変数名を変更しました 6 for (var i = 0; i < plus.length; i++) { 7 console.log(i) 8 // 期待した通り012と表示 9 plus[i].addEventListener('click', () => { 10 console.log(i); 11 // なぜ3が表示されるのか?i<plus.lengthは3未満の数値になるはず 12 if (plus[i].textContent === '+') { 13 plus[i].textContent = '-'; 14 } else { 15 plus[i].textContent = '+'; 16 } 17 }); 18 } 19 20 21} 22
HTML
1<!DOCTYPE html> 2<html lang="ja"> 3 <head> 4 <meta charset="utf-8"> 5 <title>Practice</title> 6 <link rel="stylesheet" href="css/styles.css"> 7 </head> 8 <body> 9 10 <p class="plus">+</p> 11 <p class="plus">+</p> 12 <p class="plus">+</p> 13 <!-- id削除しました --> 14 15 <script src="js/main.js"></script> 16 17 </body> 18</html> 19
CSS
1/* クラスに変更しました */ 2.plus { 3 color: #00AED9; 4 text-align: center; 5 line-height: 15px; 6 border: solid #DADADA 2px; 7 border-radius: 50%; 8 width: 20px; 9 height: 20px; 10 font-weight: bold; 11 font-size: 20px; 12}
補足情報(FW/ツールのバージョンなど)
Atomエディタ、CHROME
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
退会済みユーザー
2019/08/14 06:46 編集
回答6件
0
var
だと1つの変数をずっと使います。なので、for
を抜けた時点でi == plusa.length
になっていますが、これをclick
で起動する無名関数で参照して、その添え字で配列要素を参照しようとして範囲外エラーになります。
let
だと変数がブロック毎なので、初回にブロックを実行したときに定義される無名関数中ではi==0
です。
var
しか無いときは、ブロック内にもう一つ無名関数を作ってその関数内のvar
で行う必要がありました。
投稿2019/08/13 03:15
総合スコア85768
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
退会済みユーザー
2019/08/14 02:15
2019/08/14 05:59
2019/08/14 06:01
2019/08/14 07:14
2019/08/14 07:37
2019/08/14 07:53
2019/08/14 10:50
2019/08/14 11:45
2019/08/15 04:19
2019/08/15 04:38
0
ベストアンサー
ブロックスコープ
let
, const
共に存在する性質ですが、for 文などの繰り返し構文で変数宣言された場合、ブロック毎に「新しい変数」が生成されます。
下記に疑似コードを書きました。
HTML
1<h2>let + var (type1) ※IE11は未対応</h2> 2<p class="plus1">+</p> 3<p class="plus1">+</p> 4<p class="plus1">+</p> 5 6<h2>let + var (type2) ※ブロックスコープの再現コード</h2> 7<p class="plus2">+</p> 8<p class="plus2">+</p> 9<p class="plus2">+</p> 10 11<script> 12'use strict'; 13/* 14 * type 1 15 */ 16const plus1 = document.querySelectorAll('.plus1'); 17 18for (let i = 0; i < plus1.length; i++) { 19 plus1[i].addEventListener('click', function handleClick () { 20 console.log(i); 21 plus1[i].textContent = plus1[i].textContent === '+' ? '-' : '+'; 22 }, false); 23} 24</script> 25<script> 26'use strict'; 27/* 28 * type 2 29 */ 30 const plus2 = document.querySelectorAll('.plus2'); 31 32for (let i = 0; i < plus2.length; i++) { 33 let i2 = i; 34 35 plus2[i2].addEventListener('click', function handleClick () { 36 console.log(i2); 37 plus2[i2].textContent = plus2[i2].textContent === '+' ? '-' : '+'; 38 }, false); 39} 40</script>
変数 i2
はブロック文の中でのみ有効であり、0~2が格納された3つの変数は別の変数です。
変数 i
も同様の扱いです。
問題点
- HTML上で同じID(id="plus")を複数回記述する事は出来ません(文法違反)
- CSSのIDセレクタは初めに出現した要素のみに適用される仕様です
変数の巻き上げ(Variable hoisting)
JavaScript では変数の「初期化」と「代入」は分けて実行されます。
- 変数は関数呼び出し時に生成されます
- 関数呼び出し直後に、関数内で宣言された**全ての変数を undefined で初期化+*します
var a = 1;
が実行された時、1 が代入されます(初期化は既に完了しています)
JavaSctipt
1function sample1 () { // 変数 a が undefined で「初期化」される 2 console.log(a); // undefined 3 4 var a = 1; // 1 が「代入」される 5} 6 7sample1();
この挙動は分かりづらい為、あえて初期化と代入を分けてコートを書く事を好む人もいます。
JavaSctipt
1function sample2 () { // 変数 a が undefined で「初期化」される 2 var a; // 初期化済なので何もしないが、このコードはundefined で初期化を意図している為、分かりやすい 3 4 console.log(a); // undefined 5 a = 1; // 1 が「代入」される 6} 7 8sample2();
for 文の挙動は下記になります。
JavaSctipt
1function sample3 () { // 変数 elememts,i,len が undefined で「初期化」される 2 3 var elememts = document.querySelectorAll('.plus'); // NodeListが「代入」される(3つの要素を持つとする) 4 5 for (var i = 0, len = elememts.length; i < len; i++) { // i, len が代入される(3回繰り返し) 6 elements.addEventListener('click', () => console.log(i), false); // clickリスナー実行時にはループ処理が終わっている為、i 値は常に3となる 7 } 8} 9 10sample3();
ところで、let
, const
でも巻き上げは発生しています。
ただし、変数定義文より前に変数参照が行われると、ReferenceError
で停止する為、事実上、巻き上げを考慮する必要がなくなっています。
Re: YESYUKI17 さん
投稿2019/08/13 10:51
編集2019/08/14 09:39総合スコア18189
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2019/08/13 11:03 編集
2019/08/14 05:58
2019/08/14 10:51
2019/08/15 09:05 編集
2019/08/15 04:17
2019/08/15 09:27
2019/08/15 11:38
0
FAQですね。
for文におけるletとvarの挙動の違いについてQiitaに記事を書いたことがあります。
varとletは箱の数が違います。
varは1つ、letは箱の数がいっぱいあります。
letとvarを箱の数の違いで表現されていますが、もう少し詳しく、箱のというイメージで捉えるためのご説明をお願いできませんか?
比喩でなく説明すると、箱はメモリ上の場所です。
letはブロックスコープなので、forの繰り返しに対して別の箱が用意されるようです。
varはforの繰り返しに対して同じ1つの箱を使い回しします。
varのときクリックイベントで1つの箱を使い回そうとしたときループが進んで別の値に変わってしまっていたというのがよくある不具合です。
またリンク先の説明でvarとletは箱の数が違うに関して
のところでconsoleに4が表示されるのはなぜでしょうか?
他のteratailの質問でfor文の制御フローを解説するために作成した図です。
変数i=3
のとき終了条件i<4
が真になり、命令文本体が実行されます。
次に「増減」のi++
が実行され、i=4
となり終了条件i<4
が偽になり、for文が終了します。
forループ後にi=4
となります。
投稿2019/08/13 10:51
編集2019/08/14 08:05総合スコア777
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2019/08/14 05:59
2019/08/14 08:07
2019/08/15 04:18
2019/08/15 04:35
2019/08/15 08:39
2019/08/15 11:38
0
認識しないvarでエラーになる理由がわかりません。
addEventListener
で割り当てた関数内でi
の値を確認してみてください。
【varよりすごいletとconst。(現代的JavaScriptおれおれアドベントカレンダー2017 – 02日目) | Ginpen.com】
https://ginpen.com/2017/12/02/var-let-const/
投稿2019/08/13 02:55
総合スコア69583
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2019/08/14 06:01
2019/08/14 06:25
2019/08/14 07:14
2019/08/14 10:52
0
余計に混乱させてしまうかもしれませんが、varでなんとかしようとしたら以下のようにしないとだめですね。わからなければ、即時関数、クロージャーも合わせて調べると良いです。
js
1'use strict'; 2{ 3 4 const plus = document.querySelectorAll('.plus'); 5 for (var i = 0; i < plus.length; i++) { 6 console.log(i); 7 (i => { 8 plus[i].addEventListener('click', () => { 9 console.log(i); 10 if (plus[i].textContent === '+') { 11 plus[i].textContent = '-'; 12 } else { 13 plus[i].textContent = '+'; 14 } 15 }); 16 })(i); 17 } 18}
投稿2019/08/15 00:00
総合スコア910
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/13 03:52
編集2019/08/14 01:51退会済みユーザー
総合スコア0
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2019/08/13 06:42
退会済みユーザー
2019/08/13 07:03
退会済みユーザー
2019/08/13 07:53
2019/08/13 10:55
退会済みユーザー
2019/08/13 12:57 編集
退会済みユーザー
2019/08/13 13:10
2019/08/13 14:32
退会済みユーザー
2019/08/14 01:47
2019/08/14 06:04
退会済みユーザー
2019/08/14 07:44 編集
退会済みユーザー
2019/08/14 07:04 編集
2019/08/14 11:02
退会済みユーザー
2019/08/14 11:46 編集
2019/08/14 11:51
退会済みユーザー
2019/08/14 11:57
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。