なぜJavaScript1では、return a;では問題があり、return a();ではエラーが起きず、
JavaScript2では、return a;では問題がなく、return a();ではエラーが起きるのかの理由が分かりません。
a
とa()
の違いは理解されていますか?
a
は、変数aに代入(束縛)されているオブジェクトを返します。ここでの「返す」は置き換わると考えてもいいです。
javvascript
1a = 1
2b = a # 2行目
3return a # 3行目
と書いたとき、2行目の右辺のa
は、代入されているオブジェクトである1に置き換わり、bには1が代入されます。
3行目のreturnの対象であるa
も同様に代入されている1に置き換わり、1が返ります。
変数に代入されるオブジェクトにはいろいろなものがありますが、この質問で重要なのは、関数そのものも変数に代入することができる点です。また、関数定義で名前として定義したものも変数として扱うことができます。以下の定義のときa
は変数であり、その変数には定義した関数が代入されています。
javascript
1 function a() {
2 num = num + 1;
3 console.log(num);
4 }
なので、上の定義のあとに以下の処理をすると、
javvascript
1b = a # 1行目
2return a # 2行目
1行目ではa
は定義した関数のものに置き換わり、bにもその関数が代入されます。
2行目では、a
に代入されている関数がreturnされることになります。
さて、a
に関数が代入されているとき、その関数を呼び出す(実行する)にはどのようにすればいいかというと、aの後に()
を付けて、中に必要な引数を入れてやればいいのです。
ようするに、a()
という記述はaに代入されている関数を呼び出す(実行する)ことを表わしています。また、そのとき、a()
の部分は、aに代入されている関数を実行したときの返り値になります。
これらを踏まえて質問のコードを見てみましょう。
JavaScript1 では、incrementFactory関数はreturn a()
となっているので、この関数は内部で定義している関数aを実行してその結果を返す関数です。 なので、コードでは続く行で
janvascript
1incrementFactory();
2incrementFactory();
3incrementFactory();
4incrementFactory();
このように直接incrementFactoryを実行=後ろに()を付けて並べています。このときそれぞれの実行ごとに、価数のなかで関数aが実行されるので目的どおりの動作をします。(すべて1になる理由はほかの型の指摘どおり)
ここでincrementFactoryを書き換えてreturn a
とするとどうなるでしょう。この関数は内部で定義している関数aそのものを返す関数になります。 関数aを実行していないことに注意が必要です。
このとき、コードの後半でincrementFactoryが4回実行されても、内部で関数aが実行されません。また、incrementFactoryそのものも呼ばれただけで、返り値をどこにも保存していないので捨てられてしまい、結局関数aは実行されることはありません。
JavaSc.pt2では、incrementFactory関数はreturn a
となっているので、この関数は内部で定義している関数aそのものを返す関数になります。
コードの後半は
javascript
1const fn = incrementFactory();
2
3fn();
4fn();
5fn();
6fn();`
このようになっています。
1行目で、incrementFactoryが実行され、返り値がfnに代入されます。 incrementFactoryの返り値は定義した関数aですから、変数fnにはその価数そのものが入ります。 後の行fn()として実行されていますが、fnに入っているのはaとして定義された関数ですのでそのとおり動作します。
ここでincrementFactoryを書き換えて、return a()
とするとどうなるでしょう。この関数は内部で定義している関数aを実行してその結果を返す関数になってしまいます。関数aにはreturnがありませんので返り値が言語の仕様でundefined
という値とされています。
なのでそのように書き換えると、1行目でfnにはundefinedという値が入ります。そして、以降の処理でそれに()を付けて関数として実行しようとしたときに、undefinedは関数でなないので、「Uncaught TypeError: fn is not a function」(fnは関数ではありません)というエラーになるのです。
なぜJavaScript1では、その「状態を保持する」のルールは適用されないのでしょうか
クロージャというのは乱暴に言えば「環境が付きの関数」で、そのものがオブジェクトであって、関数定義時に作られます。 よく、コードそのものがクロージャだと捉えている人がいますが、そうではなく、数値や配列などのように、そのコードを元に作られるオブジェクトです。
質問のコードでは、関数incrementFactory(これもクロージャですが)の中で関数aが定義されています。関数aが定義が実行されると、関数aのオブジェクトは値が0の変数numが含んだクロージャになります。ここで重要なのは、関数aが関数incrementFactoryの中で定義されているため、incrementFactoryが呼ばれる度に新しい関数aが生成されるということです。関数のオブジェクトの概念を()を使って表わすと、関数aは生成されたときに(関数、num=0)のようなものになり、incrementFactoryが呼ばれるたびにこれが1つ生成されるということになります。
これを踏まえて、JavaScript1を見ると、以下のようになっています。
javascript
1incrementFactory();
2incrementFactory();
3incrementFactory();
4incrementFactory();
JavaScript1の定義では、関数incrementFactoryは呼ばれると、(関数、num=0)が生成され、returnのタイミングで関数aが実行されます。 実行時のnumは0ですが1が足されて結果の1が表示されます。 incrementFactoryは4回呼ばれますから、(関数、num=0)が4回生成され、その度に関数aが呼ばれるので、1が4つ出力されます。
JavaScirpt2ではどうでしょう。
javascirpt
1const fn = incrementFactory();
2
3fn();
4fn();
5fn();
6fn();`
JavaScript2の定義では、関数incrementFactoryは呼ばれると、(関数、num=0)が生成され、return でそれが返されます。1行目でincrementFactoryが呼ばれて生成されて返された(関数、num=0)はfnに代入されます。
最初にfnが呼ばれて関数が実行されると、0だったnumに1が足され、結果の1が表示されます。 実行後のクロージャは(関数、num=1)になっています。次にfnが呼ばれると、fnは新しく定義されたりしていないので(関数、num=1)が実行され、2が表示され、結果としてクロージャは((関数、num=2)になります。