Home
JavaScript
for文内でsetTimeoutを使うときの変数のスコープとクロージャ

for文内でsetTimeoutを使うときの変数のスコープとクロージャ

公開日
2021.12.11
更新日
2022.04.02
for文内でsetTimeoutを使うときの変数のスコープとクロージャ

どうも!かけちまるです!

JavaScriptやjQueryでfor文を使い、そのfor文内にsetTimeoutとfor文で定義した変数を使って処理を遅らせるようなコードを書いたときに思ったように動かなかったので解決策をメモしていきます。

この問題を解決するには、スコープとクロージャについて理解する必要があります。

この記事では、

  • ・今回の問題の解決
  • ・JavaScriptのスコープについて
  • ・JavaScriptのクロージャについて

を解説します。

今回の問題の解決

今回やりたいこととしては、for文を使ってConsoleに1秒ごと時間差で1~10を表示したいとします。
実装成功するとこんな感じになります。

実装結果

うまくいかない例

JavaScript
コピー
function count(){ for (var i=1; i <= 10; i += 1){ setTimeout(function() { console.log(i); }, i*1000); } } count();

うまくいく例

JavaScript
コピー
function count(){ for(var i=1; i <= 10; i += 1){ (function(i){ //追加 setTimeout(function() { console.log(i); }, i*1000); })(i); //追加 } } count();

スコープとは?

スコープとは、影響範囲のこと。

例えば、関数「red」の中で変数「apple」を定義します。
一方で関数「blue」を定義します。

変数「apple」は、関数「red」の中では使うことができます。
しかし、関数「blue」の中では使えないのです。

つまり、変数「apple」は関数「red」の中でしか影響されません。
これがスコープの仕組みです。

JavaScript
コピー
function red(){ var apple = 'りんご'; //変数appleを定義 } function blue(){ console.log(apple); //コンソールに変数appleを出力 } blue(); //結果:ReferenceErrorエラーになる
実装結果

プログラミング言語によってスコープの規則はそれぞれ異なります。
JavaScriptでは関数レベルとブロックレベルのレキシカルスコープを採用しています。

関数レベルのスコープ

関数レベルのスコープは文字通り関数内で有効ということになります。
JavaScriptでvarを使って宣言した変数は、関数レベルのスコープを持ちます。

例えば以下のコードであればConsoleにredが問題なく出力されます。

JavaScript
コピー
function name(){ if (true) { var color = 'red'; } console.log(color); // red } name();

もしvarを使って宣言した変数がブロックレベルのスコープであれば変数var colorif文内でしか出力できません。
試しにvar部分をconstletに変えてみるとそれがわかります。

ブロックレベルのスコープ

JavaScriptでは、varの他にconstletでも変数を作成できます。
constletで宣言した変数は、ブロックレベルのスコープ変数を作成します。

JavaScript
コピー
function name(){ if (true) { let color = 'red'; console.log(color); // red } console.log(color); // Uncaught ReferenceError: color is not defined } name();

letで宣言した変数はブロックレベルのスコープになります。
let colorif文内で宣言しているのでif文内でしか許容できないということになります。
if文外でlet colorを使用した場合はエラーになります。

クロージャとは?

クロージャとは?と一言で説明するのが難しかったのでまず覚えておいてもらいたいキーワードがあります。

「JavaScriptの関数は全てクロージャになり得る」

ということです。

クロージャになり得るということは関数にある条件が加わるとその関数はクロージャになるということがわかります。
では、今回の問題だったコードを例に説明します。

改めて、問題だったのはfor文を使ってConsoleに1秒ごと時間差で1~10を表示したいけど思い通りにならなっかたということ。

で、うまくいかない例が以下です。

JavaScript
コピー
function count(){ for (var i=1; i <= 10; i += 1){ setTimeout(function() { console.log(i); }, i*1000); } } count();

これだとConsoleに「11」が10回出力されてしまいます。
setTimeoutは1秒後に変数iを上位スコープの関数count()内から探して実行します。
しかし、最初の1秒間に変数iはすでに「11」になってしましいます。
そのため、1秒ごとに「11」が出力されてしまうということになってしまいます。

じゃあ期待通の結果にしたい時はどう知ればいいかというと、

  • 1. 新しいスコープを追加して反復する度に個別の値を保存する方法
  • 2. const、letで変数を定義してブロックスコープを利用する方法

の2つの方法があります。

1. 新しいスコープを追加して反復する度に個別の値を保存する方法

JavaScript
コピー
function count(){ for(var i=1; i <= 10; i += 1){ (function(i){ setTimeout(function() { console.log(i); }, i*1000); })(i); } } count();

2. const、letで変数を定義してブロックスコープを利用する方法

JavaScript
コピー
function count(){ for(let i=1; i <= 10; i += 1){ setTimeout(function() { console.log(i); }, i*1000); } } count();

おわり

かけちまる
かけちまる
Webエンジニアをしています。
HTML/CSS/JavaScript/jQuery/PHPができます。
WEB制作を中心に日々学びになったこと、興味が沸いたことについて初心者の方でもわかりやすいようにアウトプットしていくブログです。
Xserver

関連記事

CSS変数(カスタムプロパティ)の使い方

CSS変数(カスタムプロパティ)の使い方

gsapのstaggerで順番にアニメーションさせる方法

gsapのstaggerで順番にアニメーションさせる方法

GLSLの修飾子varyingの使い方【Three.js】

GLSLの修飾子varyingの使い方【Three.js】

【GSAP】registerEffectでアニメーションをテンプレート化する方法

【GSAP】registerEffectでアニメーションをテンプレート化する方法

【PHP】配列の要素の数を取得する

【PHP】配列の要素の数を取得する

【縦横比を保つ】YouTube埋め込みをレスポンシブ対応にする

【縦横比を保つ】YouTube埋め込みをレスポンシブ対応にする