💪

【JavaScript】setTimeout()メソッドでクロージャを完全に理解する【ES6】

2022/10/11に公開約2,400字2件のコメント

はじめに

閲覧ありがとうございます。来月から実務デビューのYunosukeです。

今回はSetTimeout()を使った際に、思った通りの挙動してくれなかったので色々調べていたら、クロージャの概念について理解出来たので記事を残します。

まず思った挙動してくれなかったソースコードがこちら。

function hello(name) {
  console.log(`hello ${name}`);
}

setTimeout(hello('Tom'), 1000);

1秒後にhello Tomと表示されると思ったら、setTimeout()メソッドを無視してしまいます。

下記ソースコードのように修正すれば思った挙動で表示することが出来ます。

setTimeout(function hello() {
  console.log(`hello Tom`);
}, 1000);

しかし、関数hello()に引数を取って表示させたい場合はどうすれば良いのでしょう?

ここでクロージャの概念を理解する必要があります。

クロージャとは

クロージャを理解する前に、前提としてレキシカルスコープについて知っておく必要があります。

レキシカルスコープとは、コードを書く場所によって参照できる変数が変わるスコープのことを言います。
また、コードを記述した時点で決定するため、静的スコープとも呼ばれています。

言葉だと分かり難いと思うので、サンプルコードを用いてどのように参照先が変わっていくのかを理解してみましょう。

let a = 1;

function fn1() {
  let b = 1;
  function fn2() {
    console.log(b);
  }
  fn2();
}
fn1();

まず、ブレークポイントを使ってfn1()が実行された時点では、どうなっているかと言うと...

赤い枠で囲っている部分のように、グローバル変数のlet a = 1が参照出来ていることがわかるかと思います。
スクリプトというのは、どこからでも参照出来るスコープのことを指します。

ブレークポイントを次に進めてみると...


fn1()に入っていきましたね。
赤い枠を見てみると、スクリプトに加えてローカルというスコープが出てきました。

ローカルというのは、fn1()内で参照出来るスコープのことを指します。
let b = 2を宣言した時点では、undefinedになることに注意です。

ブレークポイントを更に進めてみると...

おっ、クロージャが遂に出てきましたね!
クロージャとは、レキシカルスコープの値を保持している状態のことを言います。

つまり、赤い枠で囲まれている箇所を見るとfn2()console.log(b)でレキシカルスコープであるfn1()let b = 2を参照しているので、クロージャlet b = 2が表示されているということになります。

以上でクロージャのことを何となく理解出来たと思いますので、最初のソースコードを修正してみましょう!

いざ、修正!

結構文章が長くなってしまったので、改めてNGのソースコードを下記します。

function hello(name) {
  console.log(`hello ${name}`);
}

setTimeout(hello('Tom'), 1000);

上記で学んだことを踏まえて、何故このソースコードだと1秒待たずに実行されてしまうかというと、function hello(name)setTimeout(hello('Tom'), 1000);が同じスコープ内であるため、既に実行されてしまっているという訳です。

これを回避するには、下記ソースコードのようにconsole.log(`hello ${name}`);のスコープを落としてあげればOKです。
returnを使用しているのは、setTimeout()メソッドは、第1引数に関数を取る必要があるためです。

function hello(name) {
  return function () {
    console.log(`hello ${name}`);
  };
}

setTimeout(hello('Tom'), 1000);

これで1秒後に表示されるようになります。

プログラミングはメカニズムまで理解しようとすると格段に難易度が跳ね上がると同時に、奥がめちゃくちゃ深いですね...笑

それでは良いエンジニアライフを。

Discussion

setTimeoutからどうクロージャの話に繋がるのか興味深く読ませていただきました。

今回のような場合、本質的なのはsetTimeoutの第一引数に何を渡すべきかに思えます。
hello('Tom')は関数を実行していますが、helloはhelloという関数そのものを示します。
setTimeoutの第一引数には関数を渡すべきなので、hello('Tome')を渡しても想定通り動かなかったのだと思われます。コンソールにはエラーログが出力されてたのではないでしょうか。

やり方は様々ですが、私の場合、以下のようにして1秒後にhelloを実行させると思います。

function hello(name) {
  console.log(`hello ${name}`);
}

setTimeout(() => hello('Tom'), 1000);

yshさん
読んでいただき、誠にありがとうございます!

なるほど、ではsetTimeout第一引数に渡すものがそもそも間違っていたということですね...!

ログインするとコメントできます