読者です 読者をやめる 読者になる 読者になる

新しい日記

新しい日記

クロージャを理解したような気になっていたが…

先日クロージャの説明を求められて、堂々とスコープのことを説明をしてしまい、ウーン…違う…とさせてしまうという出来事がありました。

その場でクロージャの例についてお教え頂き、そのときは「クロージャってそういう意味なのか!」と納得した気になっていたのですが、もう一度自分で試してみたら自分ではクロージャを始めとして javascript の文法など基本的なことでも理解せずに使っている部分がたくさんあることに気付いてしまった。

例に出されたものを自分で試してみた

以下のように、 html 上に .button というボタンが3つあります。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <a href="#" class="button">button</a>
  <a href="#" class="button">button</a>
  <a href="#" class="button">button</a>
  <script src="./scripts.js"></script>
</body>
</html>

ボタンを押すと何番目のボタンなのかを知らせる alert を出したいとします。
そんなとき以下のように書くと、どのボタンを押しても「3」と出てしまいます。

var buttons = document.getElementsByClassName('button');

for(var i=0;i<buttons.length;i++){
  buttons[i].addEventListener('click', function(){
    alert(i);
  });
}

.button がクリックされるとコールバック関数 function(){ ... }; が実行され、i が alert されるということなのですが、クリックするタイミングでは for を回し終わった後なので、 i の値は3になっている。なので、この段階ではどのボタンを押しても3が alert されてしまいます。

ここで、以下のように alertNumber という関数を作成しxi を閉じ込めるように書き換えます。

function alertNumber(num){
  var x = num;
  return function(){
    alert(x);
  };
}
for(var i=0;i<buttons.length;i++){
  buttons[i].addEventListener('click', alertNumber(i));
}

結果は希望どおり、各ボタンのクリックで、「0」「1」「2」を alert するようになります。

これがクロージャらしい。

クロージャの定義は

自分を囲むスコープにある変数を参照できる関数
(引用元: JavaScriptでクロージャ入門。関数はすべてクロージャ? - Qiita

らしい。
確かに、alertNumber では外の変数 i を参照して処理しているな。なんかざっくりしててイマイチピンとこないな〜…

return って何してるんだろう問題

クロージャの作り方はわかったものの、そもそも関数に return function(){ ... } はなぜいるんだという疑問を持ちました。
以下のように、x を宣言してすぐに alert 出しても同じことできるんじゃないの?と。

function alertNumber(num){
  var x = num;
  alert(x);
}
for(var i=0;i<buttons.length;i++){
  buttons[i].addEventListener('click', alertNumber(i));
}

これだと、 ロード後すぐに「0」「1」「2」と alert され、その後はボタンをクリックしても alert が出ない。
オ?!ていうか return ってそもそも何よ。

return 文は関数の実行を終了して、関数の呼び出し元に返す値を指定します。
構文

return [[expression]];

expression
返す式。もし省略されたなら、undefined が代わりに返ります。
(引用元: return - JavaScript | MDN

「いちいち return ってしなきゃいけないもんなの?」と思ってたが、実際のところ、 return が省略された関数は undefined が返ってくる! というだけなのを知らなかっただけだった…。そりゃ、何も返ってこないわけないよな…。

関数の宣言と実行

前述のと近い話ですが、関数の宣言と実行について、タイミングをよくわかってなかったという話…。
そもそも

function alertNumber(num){
  var x = num;
  alert(x);
}
for(var i=0;i<buttons.length;i++){
  buttons[i].addEventListener('click', alertNumber(i));
}

でロード後にすぐ alert が出るというのは、関数を宣言したタイミングで alert が出てるということなんですよね。
言うなれば、ある働きをするロボを作る(function alertNumber(){ ... })→関数を宣言する
作ったロボを使う(return)→関数を実行する
っていうイメージでしょうか。

関数は()がないと値が返ってこない

あと、 alertNumber(100); とすると、返ってくるのは

function (){
    alert(x);
  }

これだけで、alert は出ない。一方で、 alertNumber(100)(); とすると、 100と書かれた alert が出て、さらに undefined が返ってくる。(さっき調べたやつだ)
「きっとこの () を付けることで、関数内の return を呼び出すことがやっとできるのだな〜!そして return 以外のは一度実行されたらもう二度と繰り返されないのだろうな〜?」という感覚的な咀嚼をした。関数の宣言と実行という項目を追加したので先日の感覚的な咀嚼はちょっと間違っていることがわかった。

addEventListener の第二引数に入れるべきものは?

addEventListener のコールバック関数に alertNumber を内包させても同じ結果になるのでは?と思ったので試してみた。

function alertNumber(num){
  var x = num;
  return function(){
    alert(x);
  };
}
for(var i=0;i<buttons.length;i++){
  buttons[i].addEventListener('click', function(){
    alertNumber(i);
  });
}

でも、上述のコードではボタンをクリックしてもなんにも起きない。 これはきっと感覚的な咀嚼をした () の問題だろう。 alertNumber(3); という意味になっていて、alertNumber(3)(); という処理にはなってないのだろう。

以下のようにすると、クリックするたびに alertNumber(3)(); になる。

function alertNumber(num){
  var x = num;
  return function(){
    alert(x);
  };
}
var f = alertNumber(100);
for(var i=0;i<buttons.length;i++){
  buttons[i].addEventListener('click', function(){
    alertNumber(i)();
  });
}

これは想定どおりだな。でも、 addEventListener の第二引数に無名関数っぽいものを入れても返り値がないのはなぜなんだ?!もしかしてこれは関数ではない?!お前は何なのか?!

一方、絶対こいつは関数なんだからなという強い意思を持ち以下のように異常に冗長な書き方をすると希望通りの結果が得られました。

function alertNumber(num){
  var x = num;
  return function(){
    alert(x);
  };
}
for(var i=0;i<buttons.length;i++){
  buttons[i].addEventListener('click', ( function(){
      return alertNumber(i);
    }())
  );
}

絶対こんな書き方したくないな〜

俺の考えた最強の仮説

function alertNumber(num){
  var x = num;
  return function(){
    alert(x);
  };
}
for(var i=0;i<buttons.length;i++){
  buttons[i].addEventListener('click', alertNumber(i));
}

このコードでは、まず buttons[i] のクリックイベントに alertNumber(i) が紐付けられる&宣言される。(ので、ローカル変数 x に そのときの i の値が閉じ込められる)
そして、クリックイベントが発生したときは、再度紐付けられた alertNumber(i) が呼び出されるが、その中の var x = num; は宣言時に処理が終わっているので、閉じ込められた i の値を内包した return の値が返ってくる。

どうでしょう?!私の仮説はどこまで合っているんでしょうか?”!?!?!?!?!?!?!?!!?!?!?!???????

それにしても全然 javascript 理解してないのを露わにしてしまう恥ずかしい記事となってしまいました。
javascript はブラウザさえあればすぐに書いた文字を実行できる手軽な言語ですが、
私みたいなよくわかってないけどとりあえず書いたものが動いてプログラミングは楽しいな!私プログラム書けるといっても過言ではないのでは?みたいな人間を生産することができてしまいますね…。
恐ろしいことに私はこんな感じでもお仕事をしてお給料をもらっていて、ヤバイ以外の語彙を失ってしまいました。
小手先な tips やミーハーな技術情報を追うばかりでなく、基礎的な知識を体系づけて身につけねばと大反省しました!たくさん勉強してプログラム書けるようになるぞ!