GitBookjs-primer関数とスコープ定義された関数はそれぞれのスコープを持っています。スコープとは変数や関数の引数などを参照できる範囲を決めるものです。JavaScriptでは、新しい関数を定義するとその関数にひもづけられた新しいスコープが作成されます。関数を定義するということは処理をまとめるというだけではなく、変数が有効な範囲を決める新しいスコープを作っていると言えます。スコープの仕組みを理解することは関数をより深く理解することにつながります。関数とスコープは密接な関係を持っているからです。この章では関数とスコープの関係を中心に、スコープとはどのような働きをしていて、スコープ内では変数の名前から取得する値がどのように決まるかを見ていきます。JavaScriptのスコープは、ES2015において直感的に理解しやすい仕組みが整備されました。基本的にはES2015以降の仕組みを理解していればコードを書く場合には問題ありません。既存のコードを理解するためには、ES2015より前に決められた古い仕組みについても知る必要があります。既存のコードは古い仕組みを使って書かれていることもあるためです。JavaScriptでは古い仕組みと新しい仕組みを混在して書くことができます。古い仕組みによるスコープは直感的でない挙動も多いため、古い仕組みについても補足していきます。スコープとはスコープとは変数の名前や関数などの参照できる範囲を決めるものです。スコープの中で定義された変数はスコープの内側でのみ参照でき、スコープの外側からは参照できません。身近なスコープの例として関数によるスコープを見ていきます。次のコードでは、fn関数のブロック({と})内で変数xを定義しています。この変数xはfn関数のスコープに定義されているため、fn関数の内側では参照できます。fn関数の外側から変数xは参照できないためReferenceErrorが発生します。definedこのコードを見てわかるように、変数xはfn関数のスコープにひもづけて定義されます。そのため、変数xはfn関数のスコープ内でのみ参照できます。関数は仮引数を持てますが、仮引数は関数のスコープにひもづけて定義されます。そのため、仮引数はその関数の中でのみ参照が可能で、関数の外からは参照できません。definedこのような、関数によるスコープのことを関数スコープと呼びます。「変数と宣言」の章にて、letやconstは同じスコープ内に同じ名前の変数を二重に定義できないという話をしました。これは、各スコープには同じ名前の変数は1つしか宣言できないためです(varによる変数宣言とfunctionによる関数宣言は例外的に可能です)。a;一方、スコープが異なれば同じ名前で変数を宣言できます。次のコードでは、fnA関数とfnB関数という異なるスコープで、それぞれ変数xを定義できていることがわかります。x;}このように、スコープが異なれば同じ名前の変数を定義できます。スコープの仕組みがないと、グローバルな空間内で一意な変数名を考える必要があります。スコープがあることで同じ名前の変数をスコープごとに定義できるため、スコープの役割は重要です。ブロックスコープ{と}で囲んだ範囲をブロックと呼びます(「文と式」の章を参照)。ブロックもスコープを作成します。ブロック内で宣言された変数は、スコープ内でのみ参照でき、スコープの外側からは参照できません。definedブロックによるスコープのことをブロックスコープと呼びます。if文やwhile文などもブロックスコープを作成します。単独のブロックと同じく、ブロックの中で宣言した変数は外から参照できません。definedfor文は、ループごとに新しいブロックスコープを作成します。このことは「各スコープには同じ名前の変数は1つしか宣言できない」のルールを考えてみるとわかりやすいです。次のコードでは、ループごとにconstでelement変数を定義していますが、エラーなく定義できています。これは、ループごとに別々のブロックスコープが作成され、変数の宣言もそれぞれ別々のスコープで行われるためです。definedスコープチェーン関数やブロックはネスト(入れ子)して書けますが、同様にスコープもネストできます。次のコードではブロックの中にブロックを書いています。このとき外側のブロックスコープのことをOUTER、内側のブロックスコープのことをINNERと呼ぶことにします。}}スコープがネストしている場合に、内側のスコープから外側のスコープにある変数を参照できます。次のコードでは、内側のINNERブロックスコープから外側のOUTERブロックスコープに定義されている変数xを参照できます。これは、ブロックスコープに限らず関数スコープでも同様です。}}変数を参照する際には、現在のスコープ(変数を参照する式が書かれているスコープ)から外側のスコープへと順番に変数が定義されているかを確認します。上記のコードでは、内側のINNERブロックスコープには変数xはありませんが、外側のOUTERブロックスコープに変数xが定義されているため参照できます。次のようなステップで参照したい変数を探索しています。ある一方、現在のスコープも含め、外側のどのスコープにも該当する変数が定義されていない場合は、ReferenceErrorの例外が発生します。次の例では、どのスコープにも存在しないxyzを参照しているため、ReferenceErrorの例外が発生します。}}このときも、現在のスコープ(変数を参照する式が書かれているスコープ)から外側のスコープへと順番に変数が定義されているかを確認しています。どのスコープにも変数xyzは定義されていないため、ReferenceErrorの例外が発生していました。次のようなステップで参照したい変数を探索しています。ReferenceErrorが発生この内側から外側のスコープへと順番に変数が定義されているか探す仕組みのことをスコープチェーンと呼びます。内側と外側のスコープ両方に同じ名前の変数が定義されている場合もスコープチェーンの仕組みで解決できます。次のコードでは、内側のINNERブロックスコープと外側のOUTERブロックスコープに同じ名前の変数xが定義されています。スコープチェーンの仕組みにより、現在のスコープに定義されている変数xを優先的に参照します。"outer"}このようにスコープは階層的な構造となっており、変数を参照する際にどの変数が参照できるかはスコープチェーンによって解決されています。グローバルスコープ今までコードをプログラム直下に書いていましたが、ここにも暗黙的なグローバルスコープ(大域スコープ)と呼ばれるスコープが存在します。グローバルスコープとは名前のとおりもっとも外側にあるスコープで、プログラム実行時に暗黙的に作成されます。"x"グローバルスコープで定義した変数はグローバル変数と呼ばれ、グローバル変数はあらゆるスコープから参照できる変数となります。スコープチェーンの仕組みにより、最終的にもっとも外側のグローバルスコープに定義されている変数を参照できるためです。"グローバル"}fn();グローバルスコープには自分で定義したグローバル変数以外に、プログラム実行時に自動的に定義されるビルトインオブジェクトがあります。ビルトインオブジェクトには、大きく分けて2種類のものがあります。1つ目はECMAScript仕様が定義するundefinedのような変数(「undefinedはリテラルではない」を参照)やisNaNのような関数、ArrayやRegExpなどのコンストラクタ関数です。2つ目は実行環境(ブラウザやNode.jsなど)が定義するオブジェクトでdocumentやmoduleなどがあります。どちらもグローバルスコープに自動的に定義されているという点で大きな使い分けはないため、この章ではどちらもビルトインオブジェクトと呼ぶことにします。ビルトインオブジェクトは、プログラム開始時にグローバルスコープへ自動的に定義されているためどのスコープからも参照できます。Array自分で定義したグローバル変数とビルトインオブジェクトでは、グローバル変数が優先して参照されます。次のようにビルトインオブジェクトと同じ名前の変数を定義すると、定義した変数が参照されます。1ビルトインオブジェクトと同じ名前の変数を定義したことにより、ビルトインオブジェクトを参照できなくなります。このように内側のスコープで外側のスコープと同じ名前の変数を定義することで、外側の変数が参照できなくなることを変数の隠蔽(shadowing)と呼びます。この問題を回避する方法としては、むやみにグローバルスコープへ変数を定義しないことです。グローバルスコープでビルトインオブジェクトと名前が衝突するとすべてのスコープへ影響を与えますが、関数のスコープ内では影響範囲がその関数の中だけにとどまります。ビルトインオブジェクトと同じ名前を避けることは難しいです。ビルトインオブジェクトには実行環境(ブラウザやNode.jsなど)がそれぞれ独自に定義したものが多く存在するためです。関数などを活用して小さなスコープを中心にしてプログラムを書くことで、ビルトインオブジェクトと同じ名前の変数があっても影響範囲を限定できます。変数を参照できる範囲を小さくするグローバル変数に限らず、特定の変数を参照できる範囲を小さくするのはよいことです。現在のスコープの変数を参照するつもりがグローバル変数を参照したり、その逆も起きることがあるからです。あらゆる変数がグローバルスコープにあると、どこでその変数が参照されているのかを把握できなくなります。これを避けるシンプルな考え方は、変数はできるだけ利用するスコープ内に定義するというものです。次のコードでは、doHeavyTask関数の実行時間を計測しようとしています。Date.nowメソッドは現在の時刻をミリ秒にして返す関数です。Date.nowメソッドを使った実行後の時刻から実行前の時刻を引くことで、間に行われた処理の実行時間が得られます。startTime}ミリ秒`);このコードでは、計測処理以外で利用しないstartTimeとendTimeという変数がグローバルスコープに定義されています。プログラム全体が短い場合はあまり問題になりませんが、プログラムが長くなっていくにつれ影響の範囲が広がっていきます。この2つの変数を参照できる範囲を小さくする簡単な方法は、この実行時間を計測する処理を関数にすることです。計測したい処理}measureTask(doHeavyTask);これにより、startTimeとendTimeという変数をグローバルスコープからなくせました。実行時間を計測するという処理をmeasureTaskという関数にしたことで再利用できます。コードの量が増えていくにつれ、人が一度に把握できる量にも限界がやってきます。そのため、人が一度に把握できる範囲のサイズに処理をまとめていくことが必要です。この問題を解決するアプローチとして、変数の参照できる範囲を小さくすることや処理を関数にまとめるという手法がよく利用されます。関数スコープとvarの巻き上げ変数宣言にはvar、let、constが利用できます。「変数と宣言」の章において、「letはvarを改善したバージョン」と紹介したように、letはvarを改善する目的で導入された構文です。constは再代入できないという点以外はletと同じ動作になります。そのため、letが使える場合にvarを使う理由はありませんが、既存のコードや既存のライブラリなどではvarが利用されている場面もあるため、varの動作を理解する必要があります。最初に、letとvarで共通する動作を見ていきます。letとvarどちらも、初期値を指定せずに宣言した変数の評価結果は暗黙的にundefinedになります。letとvarどちらも、変数宣言をした後に値を代入できます。次のコードでは、それぞれ初期値を持たない変数を宣言した後に参照すると、変数の評価結果はundefinedとなっています。"varのx";次に、letとvarで異なる動作を見ていきます。letでは、変数を宣言する前にその変数を参照するとReferenceErrorの例外が発生して参照できません。次のコードでは、変数を宣言する前に、変数xを参照したためReferenceErrorとなっています。エラーメッセージから、変数xが存在しないからエラーになっているのではなく、実際に宣言した行より前に参照したためエラーとなっているのがわかります。"letのx";一方varでは、変数を宣言する前にその変数を参照してもundefinedとなります。次のコードは、変数を宣言する前に参照しているにもかかわらずエラーにはならず、変数xの評価結果はundefinedとなります。"varのx";このようにvarで宣言された変数が宣言前に参照でき、その値がundefinedとなる特殊な動きをしていることがわかります。このvarの振る舞いを理解するために、変数宣言が宣言と代入の2つの部分から構成されていると考えてみましょう。varによる変数宣言は、宣言部分が暗黙的にもっとも近い関数またはグローバルスコープの先頭に巻き上げられ、代入部分はそのままの位置に残るという特殊な動作をします。この動作により、変数xを参照するコードより前に変数xの宣言部分が移動し、変数xの評価結果は暗黙的にundefinedとなっています。先ほどのコードは実際の実行時には、次のように解釈されて実行されていると考えられます。"varのx"さらに、var変数の宣言の巻き上げは、ブロックスコープを無視してもっとも近い関数またはグローバルスコープに変数をひもづけます。(if文やfor文におけるブロックスコープも同様に無視されます)。"varのx"}fn();つまり、先ほどのコードは実際の実行時には、次のように解釈されて実行されていると考えられます。"varのx"}fn();この変数の宣言部分がもっとも近い関数またはグローバルスコープの先頭に移動しているように見える動作のことを変数の巻き上げ(hoisting)と呼びます。このようにvarはlet、constとは異なった動作をしています。varは巻き上げによりブロックスコープを無視して、宣言部分を自動的に関数スコープの先頭に移動するという予測しにくい問題を持っています。この問題のもっとも簡単な回避方法はvarを使わないことですが、varを含んだコードではこの動作に気をつける必要があります。関数宣言と巻き上げfunctionキーワードを使った関数宣言もvarと同様に、もっとも近い関数またはグローバルスコープの先頭に巻き上げられます。次のコードでは、実際にhello関数を宣言した行より前に関数を呼び出せます。"Hello";}これは、関数宣言は宣言そのものであるため、hello関数そのものがスコープの先頭に巻き上げられます。先ほどのコードは、次のように解釈されて実行されていると考えられます。"Hello"functionキーワードによる関数宣言も巻き上げられます。varによる変数宣言の巻き上げとは異なり、問題となることはほとんどありません。実際に巻き上げられた関数を呼び出せるためです。注意点として、varやletなどで宣言された変数へ関数を代入した場合はvarのルールで巻き上げられます。そのため、varで変数へ関数を代入する関数式では、hello変数が巻き上げによりundefinedとなるため呼び出せません(「関数と宣言(関数式)」を参照)。グローバルスコープの汚染を避けるために生まれたイディオムです。次のように、匿名関数を宣言した直後に呼び出すことで、任意の処理を関数のスコープに閉じて実行できます。関数スコープを作ることでfoo変数は匿名関数の外側からはアクセスできません。true関数を式として定義して、そのまま呼び出しています。functionからはじまってしまうとJavaScriptエンジンが関数宣言と解釈してしまうため、無害なカッコなどで囲んで関数式として解釈させるのが特徴的な記法です。これは次のように書いた場合と意味は同じですが、匿名関数を定義して実行するため短く書くことができ、余計な関数定義がグローバルスコープに残りません。5までは、変数を宣言する方法はvarしか存在しません。即時実行関数はvarによるグローバルスコープの汚染を防ぐために必要でした。2015で導入されたletとconstにより、ブロックスコープに対して変数宣言できるようになりました。そのため、グローバルスコープの汚染を防ぐための即時実行関数は不要です。先ほどの即時実行関数は次のようにletやconstとブロックスコープで置き換えられます。最後にこの章ではクロージャーと呼ばれる関数とスコープに関わる性質について見ていきます。クロージャーとは「外側のスコープにある変数への参照を保持できる」という関数が持つ性質のことです。クロージャーは言葉で説明しただけではわかりにくい性質です。このセクションでは、クロージャーを使ったコードがどのように動くのかを理解することを目標にします。次の例ではcreateCounter関数が、関数内で定義したincrement関数を返しています。その返されたincrement関数をmyCounter変数に代入しています。3と1ずつ増えた値を返しています。一度createCounter関数を実行して、その返り値をnewCounter変数に代入します。newCounter変数も実行するたびに1ずつ増えていますが、myCounter変数とその値を共有しているわけではないことがわかります。3このように、まるで関数が状態(ここでは1ずつ増えるcountという値)を持っているように振る舞える仕組みの背景にはクロージャーがあります。クロージャーは直感的に理解しにくいため、まずはクロージャーを理解するために必要な「静的スコープ」と「メモリ管理の仕組み」について見ていきます。静的スコープクロージャーを理解するために、今まで意識してこなかったスコープの性質について見ていきます。JavaScriptのスコープには、どの識別子がどの変数を参照するかが静的に決定されるという性質があります。コードを実行する前にどの識別子がどの変数を参照しているかがわかるということです。次のような例を見てみます。printX関数内で変数xを参照していますが、変数xはグローバルスコープと関数runの中で、それぞれ定義されています。このときprintX関数内のxという識別子がどの変数xを参照するかは静的に決定されます。結論から言えば、printX関数中にある識別子xはグローバルスコープ(*1)の変数xを参照します。そのため、printX関数の実行結果は常に10となります。常に10が出力される}run();スコープチェーンの仕組みを思い出すと、この識別子xは次のように名前解決されてグローバルスコープの変数xを参照することがわかります。10;が定義されているので、識別子xはこの変数を参照するつまり、printX関数中に書かれたxという識別子は、run関数の実行とは関係なく、静的に*1で定義された変数xを参照することが決定されます。このように、どの識別子がどの変数を参照しているかを静的に決定する性質を静的スコープと呼びます。Functionなどすべての関数で共通する性質です。動的スコープJavaScriptは静的スコープです。動的スコープという呼び出し元により識別子がどの変数を参照するかが変わる仕組みを持つ言語もあります。次のコードは、動的スコープの動きを説明する疑似的な言語のコード例です。識別子xが呼び出し元のスコープを参照する仕組みである場合には、次のような結果になります。出力されるこのように関数呼び出し時に呼び出し元のスコープの変数を参照する仕組みを動的スコープと呼びます。JavaScriptは変数や関数の参照先は静的スコープで決まるため、上記のような動的スコープではありません。JavaScriptでもthisという特別なキーワードだけは、呼び出し元によって動的に参照先が変わります。thisというキーワードについては次の章で解説します。メモリ管理の仕組みプログラミング言語は、使わなくなった変数やデータを解放する仕組みを持っています。変数や関数を定義すると定義されたデータはメモリ上に確保されますが、ハードウェアのメモリは有限だからです。そのため、メモリからデータがあふれないように、必要なタイミングで不要なデータをメモリから解放する必要があります。不要なデータをメモリから解放する方法は言語によって異なりますが、JavaScriptではガベージコレクションが採用されています。ガベージコレクションとは、どこからも参照されなくなったデータを不要なデータと判断して自動的にメモリ上から解放する仕組みのことです。JavaScriptにはガベージコレクションがあるため、手動でメモリを解放するコードを書く必要はありません。ガベージコレクションといったメモリ管理の仕組みを理解することは、スコープやクロージャーに関係するため大切です。どのようなタイミングでメモリ上から不要なデータが解放されるのか、具体的な例を見てみましょう。text"という文字列のデータがメモリ上に確保され、変数xはそのメモリ上のデータを参照しています。text"という新しい文字列のデータを作り、変数xはその新しいデータへ参照先を変えています。text"という文字列のデータはどこからも参照されなくなっています。どこからも参照されなくなった時点で不要になったデータと判断されるためガベージコレクションの回収対象となります。その後、任意のタイミングでガベージコレクションによって回収されてメモリ上から解放されます。その後、ガベージコレクションによってメモリ上から解放される次にこのガベージコレクションと関数の関係性について考えてみましょう。ある誤解として「関数の中で作成したデータは、その関数の実行が終了したら解放される」というのがあります。関数の中で作成したデータは、その関数の実行が終了した時点で必ずしも解放されるわけではありません。具体的に、「関数の実行が終了した際に解放される場合」と「関数の実行が終了しても解放されない場合」の例をそれぞれ見ていきます。関数の実行が終了した際に解放されるデータの例です。次のコードでは、printX関数の中で変数xを定義しています。この変数xは、printX関数が実行されるたびに定義され、実行終了後にどこからも参照されなくなります。どこからも参照されなくなったものは、ガベージコレクションによって回収されてメモリ上から解放されます。解放される次に、関数の実行が終了しても解放されないデータの例です。次のコードでは、createArray関数の中で定義された変数tempArrayは、createArray関数の返り値となっています。この、関数で定義された変数tempArrayは返り値として、別の変数arrayに代入されています。変数tempArrayが参照している配列オブジェクトは、createArray関数の実行終了後も変数arrayから参照され続けています。ひとつでも参照されているならば、そのデータが自動的に解放されることはありません。解放されないつまり、関数の実行が終了したことと関数内で定義したデータの解放のタイミングは直接関係ないことがわかります。そのデータがメモリ上から解放されるかどうかはあくまで、そのデータが参照されているかによって決定されます。クロージャーがなぜ動くのかここまでで「静的スコープ」と「メモリ管理の仕組み」について説明してきました。参照されなくなったデータはガベージコレクションにより解放されるクロージャーとはこの2つの仕組みを利用して、関数内から特定の変数を参照し続けることで関数が状態を持てる仕組みのことを言います。最初にクロージャーの例として紹介したcreateCounter関数の例を改めて見てみましょう。2つまり次のような参照の関係がmyCounter変数とcount変数の間にはあることがわかります。countcount変数を参照するものがいるため、count変数は自動的に解放されません。そのためcount変数の値は保持され続け、myCounter変数を実行するたびに1ずつ大きくなっていきます。このようにcount変数が自動解放されずに保持できているのは「increment関数内から外側のcreateCounter関数スコープにあるcount変数を参照している」ためです。このような性質のことをクロージャー(関数閉包)と呼びます。クロージャーは「静的スコープ」と「参照され続けている変数のデータが保持される」という2つの性質によって成り立っています。JavaScriptの関数は静的スコープとメモリ管理という2つの性質を常に持っています。そのため、ある意味ではすべての関数がクロージャーとなりますが、ここでは関数が特定の変数を参照することで関数が状態を持っていることを指します。先ほどの例ではcreateCounter関数を実行するたびに、それぞれcountとincrement関数が定義されます。そのため、createCounter関数を実行すると、それぞれ別々のincrement関数が定義され、別々のcount変数を参照します。次のようにcreateCounter関数を複数回呼び出してみると、別々の状態を持っていることが確認できます。1クロージャーの用途クロージャーはさまざまな用途に利用されますが、次のような用途で利用されることが多いです。関数に状態を持たせる手段として外から参照できない変数を定義する手段としてグローバル変数を減らす手段として高階関数の一部部分としてこれらはクロージャーの特徴でもあるので、同時に使われることがあります。次の例では、privateCountという変数を関数の中に定義しています。このprivateCount変数は、外のグローバルスコープからは直接参照できません。外から参照する必要がない変数をクロージャーとなる関数に閉じ込めることで、グローバルに定義する変数を減らせています。"2回目"また、関数を返す関数のことを高階関数と呼びますが、クロージャーの性質を使うことで次のようにnより大きいかを判定する高階関数を作れます。最初からgreaterThan5という関数を定義すればよいのですが、高階関数を使うことで条件を後から定義できるなどの柔軟性があります。trueクロージャーは、変数が参照する値が静的に決まる静的スコープという性質とデータは参照されていれば保持されるという2つの性質によって成り立っています。Functionや高階関数であるArray#forEachメソッドなどクロージャーを自然と利用しやすい環境があります。関数を理解する上ではクロージャーを理解することが大切です。状態を持つ関数オブジェクトJavaScriptでは関数はオブジェクトの一種です。オブジェクトであるため直接プロパティに値を代入できます。そのため、クロージャーを使わなくても、次のように関数にプロパティとして状態を持たせることが可能です。2しかし、この方法は推奨されていません。関数の外からcountプロパティを変更できるためです。関数オブジェクトのプロパティは外からも参照でき、そのプロパティ値は変更できます。関数の中でのみ参照可能な状態を扱いたい場合には、それを強制できるクロージャーが有効です。11まとめこの章では関数を中心にスコープについて学びました。Zone(TDZ)と呼ばれます。ECMAScriptの仕様ではガベージコレクションの実装の規定はないため、実装依存の処理となります。