関数とスコープ

定義された関数はそれぞれのスコープを持っています。スコープとは変数や関数の引数などを参照できる範囲を決めるものです。 JavaScriptでは、新しい関数を定義するとその関数に紐付けられた新しいスコープを作成します。関数を定義するということは処理をまとめるというだけではなく、変数が有効な範囲を決める新しいスコープを作っているといえます。

スコープの仕組みを理解することは関数をより深く理解することにつながります。なぜなら関数とスコープは密接な関係を持っているためです。 この章では関数とスコープの関係を中心に、スコープとはどのような働きをしていて、スコープ内では変数の名前から値がどのように取得されているのかを見ていきます。

JavaScriptのスコープは、ES2015において直感的に理解しやすい仕組みが整備されました。 基本的にはES2015以降の仕組みを理解していればコードを書く場合には問題ありません。

しかし、既存のコードを理解するためには、ES2015より前に決められた古い仕組みについても知る必要があります。 なぜなら、既存のコードは古い仕組みを使って書かれていることもあるためです。 また、JavaScriptでは古い仕組みと新しい仕組みを混在して書くことができます。 古い仕組みによるスコープは直感的でない挙動も多いため、コラムで補足していきます。

スコープとは

スコープとは変数の名前や関数などの参照できる範囲を決めるものです。 スコープの中で定義された変数はスコープ内でのみ参照でき、スコープの外側からは参照できません。

身近なスコープの例として関数によるスコープを見ていきます。 次のコードには、fn関数のブロック({})内で変数xを定義しています。 この変数xfn関数のスコープに定義されているため、fn関数の内側では参照できます。 一方、fn関数の外側から変数xは参照できないためReferenceErrorをなげます。

function fn() {
    const x = 1;
    // fn関数のスコープ内から`x`は参照できる
    console.log(x); // => 1
}
fn();
// fn関数のスコープ外から`x`は参照できないためエラー
console.log(x); // => ReferenceError: x is not defined

このコードを見て分かるように、変数xfn関数のスコープに紐付けて定義されます。 そのため、変数xfn関数のスコープ内でのみ参照できます。

関数は仮引数をもつことができますが、仮引数は関数のスコープに紐付けて定義します。 そのため、仮引数はその関数の中でのみ参照が可能で、関数の外からは参照できません。

function fn(arg) {
    // fn関数のスコープ内から仮引数`arg`は参照できる
    console.log(arg); // => 1
}
fn(1);
// fn関数のスコープ外から`arg`は参照できないためエラー
console.log(arg); // => ReferenceError: arg is not defined

この関数によるスコープのことを関数スコープと呼びます。

変数と宣言の章にて、letconstは同じスコープ内に同じ名前の変数を二重に定義できないという話をしました。 これは、各スコープには同じ名前の変数は1つしか宣言できないためです。(varfunctionによる関数宣言は例外的に可能です)

// スコープ内に同じ"a"を定義すると SyntaxError となる
let a;
let a;

一方、スコープが異なれば同じ名前で変数を宣言できます。 次の例では、fnA関数とfnB関数という異なるスコープで、それぞれ変数xを定義できていることが分かります。

// 異なる関数のスコープには同じ"x"を定義できる
function fnA() {
    let x;
}
function fnB() {
    let x;
}

このように、スコープが異なれば同じ名前の変数を定義できます。 スコープの仕組みがないと、グローバルな空間な一意な変数名を考える必要があります。 スコープがあることで適切な名前の変数を定義できるようになるため、スコープの役割は重要です。

ブロックスコープ

{}で囲んだ範囲をブロックと呼びます。(「文と式」の章を参照) ブロックもスコープを作成します。 ブロック内で宣言された変数は、スコープ内でのみ参照でき、スコープの外側からは参照できません。

// ブロック内で定義した変数はスコープ内でのみ参照できる
{
    const x = 1;
    console.log(x); // => 1
}
// スコープの外から`x`を参照できないためエラー
console.log(x); // => ReferenceError: x is not defined

ブロックによるスコープのことをブロックスコープと呼びます。

if文やwhile文などもブロックスコープを作成します。 単独のブロックと同じく、ブロックの中で宣言した変数は外から参照できません。

// if文のブロック内で定義した変数はブロックスコープの中でのみ参照できる
if (true) {
    const x = "inner";
    console.log(x); // => "inner"
}
console.log(x); // => ReferenceError: x is not defined

for文は、ループごとに新しいブロックスコープを作成します。 このことは「各スコープには同じ名前の変数は1つしか宣言できない」のルールを考えてみると分かりやすいです。 次のコードでは、ループ毎にconstelement変数を定義していますが、エラーなく定義できています。 これは、ループ毎に別々のブロックスコープが作成され、変数の宣言もそれぞれ別々のスコープで行われるためです。

const array = [1, 2, 3, 4, 5];
// ループごとに新しいブロックスコープを作成する
for (const element of array) {
    // forのブロックスコープの中でのみ`element`を参照できる
    console.log(element);
}
// ループの外からはブロックスコープ内の変数は参照できない
console.log(element); // => ReferenceError: element is not defined

スコープチェーン

関数やブロックはネスト(入れ子)して書けますが、同様にスコープもネストできます。 次のコードではブロックの中にブロックを書いています。 このとき外側のブロックスコープのことをOUTER、内側のブロックスコープのことをINNERと呼ぶことにします。

{
    // OUTERブロックスコープ
    {
        // INNERブロックスコープ
    }
}

スコープがネストしている場合に、内側のスコープから外側のスコープにある変数を参照できます。 次のコードでは、内側のINNERブロックスコープから外側のOUTERブロックスコープに定義されている変数xを参照できます。 これは、ブロックスコープに限らず関数スコープでも同様です。

{
    // OUTERブロックスコープ
    const x = "x";
    {
        // INNERブロックスコープからOUTERブロックスコープの変数を参照できる
        console.log(x); // => "x"
    }
}

このとき、現在のスコープ(変数を参照する式が書かれているスコープ)から外側のスコープへと順番に変数が定義されているかを確認します。このとき、内側のINNERブロックスコープには変数xはありませんが外側のOUTERブロックスコープに変数xが定義されているため参照できます。つまり次のようなステップで参照したい変数を探索しています。

  1. INNERブロックスコープに変数xがあるかを確認 => ない
  2. ひとつ外側のOUTERブロックスコープに変数xがあるかを確認 => ある

一方、現在のスコープも含めどの外側のスコープに該当する変数が定義されていない場合は、ReferenceErrorの例外が発生します。 次の例では、どのスコープにも存在しないxyzを参照しているため、ReferenceErrorの例外が発生します。

{
    // OUTERブロックスコープ
    {
        // INNERブロックスコープ
        console.log(xyz); // => ReferenceError: xyz is not defined
    }
}

このときも、現在のスコープ(変数を参照する式が書かれているスコープ)から外側のスコープへと順番に変数が定義されているかを確認します。しかし、どのスコープにも変数xyzは定義されていないため、ReferenceErrorの例外が発生します。つまり次のようなステップで参照したい変数を探索しています。

  1. INNERブロックスコープに変数xyzがあるかを確認 => ない
  2. ひとつ外側のOUTERブロックスコープに変数xyzがあるかを確認 => ない
  3. 一番外側のスコープにも変数xyzは定義されていない => ReferenceErrorが発生

この内側から外側のスコープへと順番に変数が定義されているか探す仕組みのことをスコープチェーンと呼びます。

内側と外側のスコープ両方に同じ名前の変数が定義されている場合もスコープチェーンの仕組みで解決できます。 次のコードでは、内側のINNERブロックスコープと外側のOUTERブロックスコープに同じ名前の変数xが定義されています。 スコープチェーンの仕組みより、現在のスコープに定義されている変数xを優先的に参照します。

{
    // OUTERブロックスコープ
    const x = "outer";
    {
        // INNERブロックスコープ
        const x = "inner";
        // 現在のスコープ(INNERブロックスコープ)にある`x`を参照する
        console.log(x); // => "inner"
    }
    // 現在のスコープ(OUTERブロックスコープ)にある`x`を参照する
    console.log(x); // => "outer"
}

このようにスコープは階層的な構造となっており、その際にどの変数が参照できるかはスコープチェーンによって解決されています。

グローバルスコープ

今までコードをプログラム直下に書いていましたが、ここにも暗黙的なグローバルスコープ(大域スコープ)と呼ばれるスコープが存在します。 グローバルスコープとは名前のとおりもっとも外側にあるスコープで、プログラム実行時に暗黙的に作成されます。

// プログラム直下はグローバルスコープ
const x = "x";
console.log(x);

グローバルスコープに定義した変数はグローバル変数と呼ばれ、グローバル変数はあらゆるスコープから参照できる変数となります。 なぜなら、スコープチェーンの仕組みにより、最終的にもっとも外側のグローバルスコープに定義されている変数を参照できるためです。

// グローバル変数はどのスコープからも参照できる
const globalVariable = "グローバル";

{   
    // ブロックスコープ内には該当する変数が定義されてない -> 外側のスコープへ
    console.log(globalVariable); // => "グローバル"
}
function fn() {
    // 関数ブロックスコープ内には該当する変数が定義されてない -> 外側のスコープへ
    console.log(globalVariable); // => "グローバル"
}
fn();

グローバルスコープには自分で定義したグローバル変数以外に、プログラム実行時に自動的に定義されるビルトインオブジェクトがあります。 ビルトインオブジェクトには大きく分けて2種類のものがあります。 1つ目はECMAScript仕様が定義するundefinedのような変数(「undefinedはリテラルではない」を参照)やisNaNのような関数、ArrayRegExpなどのコンストラクタ関数です。もう一方は実行環境(ブラウザやNode.jsなど)が定義するオブジェクトでdocumentmoduleなどがあります。 どちらもグローバルスコープに自動的に定義されているという点で大きな使い分けはないため、この章ではどちらもビルトインオブジェクトと呼ぶことにします。

ビルトインオブジェクトは、プログラム開始時にグローバルスコープへ自動的に定義されているためどのスコープからも参照できます。

// ビルトインオブジェクトは実行環境が自動的に定義している
// どこのスコープから参照してもReferenceErrorにはならない
console.log(undefined); // => undefined
console.log(Array); // => Array

自分で定義したグローバル変数とビルトインオブジェクトでは、グローバル変数が優先して参照されます。 つまり次のようにビルトインオブジェクト同じ名前の変数を定義すると、定義した変数が参照されます。

// "Array"という名前の変数を定義
const Array = 1;
// 自分で定義した変数がビルトインオブジェクトより優先される
console.log(Array); // => 1

ビルトインオブジェクトと同じ名前の変数を定義したことにより、ビルトインオブジェクトを参照できなくなる問題は変数の隠蔽(shadowing)とも呼ばれます。 この問題を回避する方法としては、むやみにグローバルスコープへ変数を定義しないことです。グローバルスコープでビルトインオブジェクトと名前が衝突するとすべてのスコープへ影響を与えますが、関数のスコープ内ではその関数の中だけの影響範囲はとどまります。

ビルトインオブジェクトと同じ名前を避けることは難しいです。なぜならビルトインオブジェクトは実行環境(ブラウザやNode.jsなど)がそれぞれ独自に定義したものも多く存在します。そのため、関数などを活用し小さなスコープを中心にしてプログラムを書くことで、ビルトインオブジェクトと同じ名前の変数があっても影響範囲が限定的な状態にすることが望ましいです。

[コラム] 変数を参照できる範囲を小さくする

グローバル変数に限らず、特定の変数を参照できる範囲を小さくすることはよいことです。 なぜなら、現在のスコープの変数を参照するつもりがグローバル変数を参照したり、その逆も起きることがあるからです。 あらゆる変数がグローバルスコープにあると、どこでその変数が参照されているのかを把握できなくなります。 これを避けるシンプルな考え方は、変数はできるだけ利用する近くのスコープ内に定義するということです。

次のコードでは、doHeavyTask関数の実行時間を計測しようとしています。 Date.nowメソッドは現在の時刻をミリ秒にして返す関数で、実行後の時刻から実行前の時刻を引くことで間に行われた処理の実行時間を得ることができます。

function doHeavyTask() {
    // 計測したい処理
}
const startTime = Date.now();
doHeavyTask();
const endTime = Date.now();
console.log(`実行時間は${endTime - startTime}ミリ秒`);

このコードでは、計測処理以外で利用しないstartTimeendTimeという変数がグローバルスコープに定義されています。 プログラム全体が短い場合はあまり問題になりませんが、プログラムが長くなっていくにつれ影響の範囲が広がっていきます。 この2つの変数を参照できる範囲を小さくする簡単な方法はこの実行時間を計測する処理を関数にすることです。

// 実行時間を計測したい関数を引数に渡す
const measureTask = (taskFn) => {
    const startTime = Date.now();
    taskFn();
    const endTime = Date.now();
    console.log(`実行時間は${endTime - startTime}ミリ秒`);
};
function doHeavyTask() {
    // 計測したい処理
}
measureTask(doHeavyTask);

これにより、startTimeendTimeという変数を外側のスコープから参照できなくなりました。 また、実行時間を計測するという処理を関数にしたことで再利用できます。

これは単純なように見えますが、コードの量が増えていくにつれ、人が一度に把握できる量にも限界がやってきます。 そのため、人が一度に把握できる範囲のサイズに処理をまとめていくことが必要です。 この問題を解決するアプローチとして、変数の参照できる範囲を小さくすることや処理を関数にまとめるという手法がよく利用されます。

関数スコープとvarの巻き上げ

変数宣言にはvarletconstが利用できます。 変数と宣言の章において、letは「よりよいvar」と紹介したように、varを改善する目的で導入された構文です。constは再代入できないという点以外はletと同じ動作になります。そのため、letが使える場合にvarを使う理由はありませんが、既存のコードや既存のライブラリなどではvarが利用されている場面もあるため、varの動作を理解する必要があります。

まず最初に、letvarで共通する動作を見ていきます。 letvarどちらも、初期値を指定せずに宣言した変数の評価結果は暗黙的にundefinedになります。 また、letvarどちらも、変数宣言をした後に値を代入できます。

次のコードでは、それぞれ初期値を持たない変数を宣言した後に参照すると、変数の評価結果はundefinedとなっています。

let let_x;
var var_x;
// 宣言後にそれぞれの変数を参照すると`undefined`となる
console.log(let_x); // => undefined
console.log(var_x); // => undefined
// 宣言後に値を代入できる
let_x = "letのx";
var_x = "varのx";

次に、letvarで異なる動作を見ていきます。

letでは、変数を宣言する前にその変数を参照するとReferenceErrorとなります。 次のコードでは、変数を宣言する前に、変数xを参照したためReferenceErrorとなっています。 エラーメッセージから、変数xが存在しないからエラーになっているのではなく、実際に宣言した行より前に参照したためエラーとなることが分かります。TDZ

console.log(x); // => ReferenceError: can't access lexical declaration `x' before initialization
let x = "letのx";

一方varでは、変数を宣言する前にその変数を参照してもundefinedとなります。 次のコードは、変数を宣言する前に参照しているにもかかわらずエラーにはならず、変数xの評価結果はundefiendとなります。

// var宣言より前に参照してもエラーにならない
console.log(x); // => undefined
var x = "varのx";

このようにvarで宣言された変数が宣言前に参照でき、その値がundefinedとなる特殊な動きをしていることが分かります。

このvarの振る舞いを理解するために、変数宣言が宣言代入の2つの部分から構成されていると考えてみましょう。 varによる変数宣言は、暗黙的に宣言部分がもっと近い関数またはグローバルスコープの先頭に巻き上げられ、代入部分はそのままの位置に残るという特殊な動作をします。

この動作により、変数xを参照するコードより前に変数xの宣言部分が移動し、変数xの評価結果は暗黙的にundefinedとなっています。 つまり、先ほどのコードは実際の実行時には、次のように解釈されて実行されていると考えられます。

// 解釈されたコード
// スコープの先頭に宣言部分が巻き上げられる
var x;
console.log(x); // => undefined
// 変数への代入はそのままの位置に残る
x = "varのx";
console.log(x); // => "varのx"

さらに、var変数の宣言の巻き上げは、ブロックスコープを無視してもっと近い関数またはグローバルスコープに変数を紐付けます。 そのため、次のようにブロック{}varによる変数宣言を囲んでも、もっとも近い関数スコープであるfn関数の直下に宣言部分が巻き上げられます。 (if文やfor文におけるブロックスコープも同様に無視されます)

function fn() {
    // 内側のスコープにあるはずの変数`x`が参照できる
    console.log(x); // => undefined
    {
        var x = "varのx";
    }
    console.log(x); // => "varのx"
}
fn();

つまり、先ほどのコードは実際の実行時には、次のように解釈されて実行されていると考えられます。

// 解釈されたコード
function fn() {
    // もっと近い関数スコープの先頭に宣言部分が巻き上げられる
    var x;
    console.log(x); // => undefined
    {
        // 変数への代入はそのままの位置に残る
        x = "varのx";
    }
    console.log(x); // => "varのx"
}
fn();

この変数の宣言部分がもっと近い関数またはグローバルスコープの先頭に移動しているように見える動作のことを変数の巻き上げ(hoisting)と呼びます。

このようにletconstに対してvarは異なった動作をしています。 varは巻き上げによりブロックスコープを無視して、宣言部分を自動的にスコープの先頭に移動します。 もっとも簡単な回避方法はvarを使わないことですが、varを含んだコードではこの動作に気をつける必要があります。

関数宣言と巻き上げ

functionキーワードを使った関数宣言もvarと同様に、もっと近い関数またはグローバルスコープの先頭に巻き上げされます。 次のコードでは、実際にhello関数を宣言した行より前に関数を呼び出せます。

// `hello`関数の宣言より前に呼び出せる
hello(); // => "Hello"

function hello(){
    return "Hello";
}

これは、関数宣言は宣言そのものであるため、hello関数そのものがスコープの先頭に巻き上げされます。 つまり、先ほどのコードは実際の実行時には、次のように解釈されて実行されていると考えられます。

// 解釈されたコード
// `hello`関数の宣言が巻き上げされる
function hello(){
    return "Hello";
}

hello(); // => "Hello"

注意点として、varletなどで宣言された変数へ関数を代入した場合はvarのルールで巻き上げされます。 そのため、varで変数へ関数を代入する関数式では、hello変数が巻き上げによりundefinedとなるため呼び出すことができません。(「関数と宣言(関数式)」を参照)

// `hello`変数は巻き上げされ、暗黙的に`undefined`となる
hello(); // => TypeError: hello is not a function

// `hello`変数へ関数を代入している
var hello = function(){
    return "Hello";
};

クロージャー

最後にこの章ではクロージャーと呼ばれる関数とスコープに関わる性質について見ていきます。 クロージャーとは「外側のスコープにある変数への参照を保持できる」という関数がもつ性質のことです。

クロージャーは言葉で説明しただけでは分かりにくい性質です。 このセクションでは、クロージャを使ったコードがどのようにして動くのかを理解することを目標にします。

次の例ではcreateCounter関数は、関数内で定義したincrement関数を返しています。 その返されたincrement関数をmyCounter変数を代入しています。このmyCounter変数を実行するたびに1,2,3と1ずつ増えた値を返しています。

さらに、もう一度createCounter関数を実行しその返り値をnewCounter変数に代入します。 newCounter変数も実行するたびに1ずつ増えていますが、myCounter変数とその値を共有しているわけではないことが分かります。

// `increment`関数を定義し返す関数
function createCounter() {
    let count = 0;
    // `increment`関数は`count`変数を参照
    function increment() {
        count = count + 1;
        return count;
    }
    return increment;
}
// `myCounter`は`createCounter`が返した関数を参照
const myCounter = createCounter();
myCounter(); // => 1
myCounter(); // => 2
// 新しく`newCounter`を定義する
const newCounter = createCounter();
newCounter(); // => 1
newCounter(); // => 2
// `myCounter`と`newCounter`は別々の状態持っている
myCounter(); // => 3
newCounter(); // => 3

このように、まるで関数が状態(ここでは1ずつ増えるcountという値)を持っているように振る舞える仕組みの背景にはクロージャがあります。 クロージャーを直感的には理解しにくいため、まずはクロージャを理解するために必要な「静的スコープ」と「メモリ管理の仕組み」についてを見ていきます。

静的スコープ

クロージャーを理解するために、今まで意識してことなかったスコープの性質について見ていきます。 JavaScriptのスコープでは、どの識別子がどの変数を参照するかが静的に決定されるという性質を持ちます。 つまり、コードを実行する前にどの識別子がどの変数を参照しているかが分かるということです。

次のような例を見てみます。 printX関数内から変数xを参照していますが、変数xはグローバルスコープと関数runの中にそれぞれ定義されています。 このときprintX関数内のxという識別子がどちらの変数xを参照するかは静的に決定されます。 結論からいえば、識別子xは*1の変数xを参照するため、printX関数の実行結果は常に10となります。

const x = 10; // *1

function printX() {
    // この識別子`x`は常に *1 の変数`x`を参照する
    console.log(x); // => 10
}

function run() {
    const x = 20; // *2
    printX(); // 常に10が出力される
}

run();

スコープチェーンの仕組みを思い出すと、この識別子xは次のように名前解決されグローバルスコープの変数xを参照することが分かります。

  1. printXの関数スコープに変数xが定義されていない
  2. ひとつ外側のスコープ(グローバルスコープ)を確認する
  3. ひとつ外側のスコープにconst x = 10;が定義されているので、識別子xはこの変数を参照する

つまり、printX関数内に書かれたxという識別子は、run関数を実行されるかは関係なく、静的に*1で定義された変数xを参照することが決定されます。 このように、どの識別子がどの変数を参照しているかを静的に決定する性質を静的スコープと呼びます。

[コラム] 動的スコープ

多くの言語は静的スコープですが、BashやPerl4などは呼び出し元によって、識別子がどの変数を参照するかが変わる仕組みを持っています。

次のコードは、もしJavaScriptが呼び出し元によって参照する変数が変わる場合の結果を表した擬似的なコードです。 JavaScriptは静的スコープであるので、実際には次のような結果にはなりませんが、 識別子xが呼び出し元のスコープを参照する仕組みである場合には次のような結果になります。

// JavaScriptが静的スコープでないとした場合の擬似的なコード例
const x = 10; // *1

function printX() {
    // この識別子`x`は呼び出し元によってどの変数`x`を参照するかが変わる
    console.log(x);
}

function run() {
    const x = 20;
    // 呼び出し元で変数`x`を定義している
    printX();
}

printX(); // ここでは 10 が出力される
run(); // ここでは 20 が出力される

このように呼び出し方によって動的に参照する変数が変わる仕組みのことを動的スコープと呼びます。

JavaScriptは静的スコープです。 しかしthisといった特別なキーワードは動的スコープのように、呼び出し元によって参照先が変わります。 このthisというキーワードについては次章で解説します。

メモリ管理の仕組み

ほとんどのプログラミング言語では使わなくなった変数やデータを解放する仕組みを持っています。 なぜなら、変数や関数を定義すると定義されたデータはメモリ上に確保されますが、ハードウェアのメモリは有限であるためです。 そのため、メモリからデータが溢れないようにするため必要なタイミングで不要なデータをメモリから解放する必要があります。

不要なデータをメモリから解放する方法は言語によって異なりますが、JavaScriptではガベージコレクションが採用されています。 ガベージコレクションとは、どこからも参照されなくなったデータを不要なデータと判断して自動でメモリ上から解放する仕組みのことです。

JavaScriptではガベージコレクションがあるため、手動でメモリから解放するコードは書く必要がありません。 しかし、ガベージコレクションといったメモリ管理の仕組みを理解することは、スコープやクロージャーに関係するため大切です。

どのようなタイミングでメモリ上から不要なデータが解放されるのか、具体的な例を見てみましょう。

次の例では、最初に"before text"という文字列のデータがメモリ上に確保され、変数xはそのメモリ上のデータを参照しています。 その後、"after text"という新しい文字列のデータを作り、変数xはその新しいデータへ参照先を変えています。

このとき、最初にメモリ上へ確保した"before text"という文字列のデータはどこからも参照されなくなっています。 どこからも参照されなくなった時点で不要になったデータと判断されるためガベージコレクションの回収対象となります。 その後、任意のタイミングでガベージコレクションによって回収されメモリ上から解放されます。GC

let x = "before text";
// 変数`x`に新しいデータを代入する
x = "after text";
// このとき"before text"というデータはどこからも参照されなくなる
// その後、ガベージコレクションによってメモリ上から解放される

次にこのガベージコレクションと関数の関係性について考えてみましょう。 よくある誤解として「関数の中で作成したデータは、その関数が実行し終了したら解放される」という誤解があります。 関数の中で作成したデータは、その関数の実行が終了した時点では必ずしも解放されるわけではありません。

具体的に、「関数の実行が終了した際に解放される場合」と「関数の実装が終了しても解放されない場合」の例をそれぞれ見ていきます。

まずは、関数の実行が終了した際に解放されるデータの例です。 次のコードでは、printX関数の中で変数xを定義しています。 この変数xは、printX関数が実行されるたびに定義され、実行終了後にどこからも参照されなくなります。 どこからも参照できなくなったものは、ガベージコレクションによって回収されメモリ上から解放されます。

function printX() {
    const x = "X";
    console.log(x); // => "X";
}

printX();
// この時点で`"X"`を参照するものはなくなる -> 解放される

次に、関数の実行が終了しても解放されないデータの例です。 次のコードでは、createArray関数の中で定義された変数tempArrayは、createArray関数の返り値となっています。 この、関数で定義された変数tempArrayは返り値として、別の変数arrayに代入されています。 つまり、変数tempArrayが参照している配列オブジェクトは、createArray関数の実行終了後も変数arrayから参照され続けています。 ひとつでも参照されているならば、そのデータは自動的に解放されることはありません。

function createArray() {
    const tempArray = [1, 2, 3];
    return tempArray;
}
const array = createArray();
console.log(array); // => [1, 2, 3]
// 変数`array`が`[1, 2, 3]`という値を参照してる -> 解放されない

つまり、関数の実行が終了したことと関数内で定義したデータの解放のタイミングは直接関係ないことが分かります。 そのデータがメモリ上から解放されるかどうかはあくまで、そのデータが参照されているかによって決定されます。

クロージャーがなぜ動くのか

ここまでで「静的スコープ」と「メモリ管理の仕組み」について説明してきました。

  • 静的スコープ: ある変数がどの値を参照するかは静的に決まる
  • メモリ管理の仕組み: 参照されなくなったデータはガベージコレクションにより解放される

クロージャーとはこの2つの仕組みを利用して、関数内から特定の変数を参照し続けることで関数が状態をもつことができる仕組みのことを言います。

最初にクロージャーの例として紹介したcreateCounter関数の例を改めて見てみましょう。

const createCounter = () => {
    let count = 0;
    return function increment() {
        // `increment`関数は外のスコープの変数`count`を参照している
        // これがクロージャーと呼ばれる
        count = count + 1;
        return count;
    };
};
// createCounter()の実行結果は、内側で定義されていた`increment`関数
const myCounter = createCounter();
// myCounter関数の実行結果は`count`の評価結果
myCounter(); // => 1
myCounter(); // => 2

つまり次のような参照の関係がmyCounter変数とcount変数の間にはあることがわかります。

  • myCounter変数はcreateCounter関数の返り値であるincrement関数を参照している
  • myCounter変数はincrement関数を経由してcount変数を参照している
  • myCounter変数実行した後もcount変数を参照している

count変数を参照するものがいるため、count変数は自動的に解放されません。 そのためcount変数の値は保持され続け、myCounter変数を実行するたびに1ずつ大きくなっていきます。

myCounter -> increment -> count

このようにcount変数が自動解放されずに保持できているのは「(increment)関数が外側のスコープにある(count)変数への参照を保持できる」ためです。このような性質のことをクロージャー(関数閉包)と呼びます。クロージャーは静的スコープと変数は参照され続けていればデータは保持されるという2つの性質によって成り立っています。

JavaScriptの関数は静的スコープとメモリ管理という2つの性質を常に持っています。そのため、ある意味ではすべての関数がクロージャーとなりますが、ここでは関数が特定の変数を参照することで関数が状態をもっていることを指すことにします。

先ほどの例ではcreateCounter関数を実行するたびに、それぞれcountincrement関数が定義しています。そのため、createCounter関数の実行するとそれぞれ別々のincrement関数が定義され、別々のcount変数を参照しています。

次のようにcreateCounter関数を複数回呼び出してみると、別々の状態を持っていることが確認できます。

const createCounter = () => {
    let count = 0;
    return function increment() {
        // 変数`count`を参照し続けている
        count = count + 1;
        return count;
    };
};
// countUpとnewCountUpはそれぞれ別のincrement関数(内側にあるのも別のcount変数)
const countUp = createCounter();
const newCountUp = createCounter();
// 参照してる関数(オブジェクト)は別であるため===は一致しない
console.log(countUp === newCountUp);// false
// それぞれの状態も別となる
countUp(); // => 1
newCountUp(); // => 1

クロージャーの用途

クロージャーはさまざまな用途に利用されますが、次のような用途で利用されることが多いです。

  • 関数に状態を持たせる手段として
  • 外から参照できない変数を定義する手段として
  • グローバル変数を減らす手段として
  • 高階関数の一部部分として

これらはクロージャーの特徴でもあるので、同時に使われることがあります。

たとえば次の例では、privateCountという変数を関数の中に定義していますが、 外からはその変数を直接参照はできません。言い換えると外から直接参照して値を変更することはできません。 外から参照する必要がない変数をクロージャーとなる関数に閉じ込めることは、言い換えるとグローバルに定義する変数を減らすことができます。

const createCounter = () => {
    // 外のスコープから`privateCount`を直接参照できない
    let privateCount = 0;
    return () => {
        privateCount++;
        return `${privateCount}回目`;
    };
};
const counter = createCounter();
counter(); // => "1回目"
counter(); // => "2回目"

また、関数を返す関数のことを高階関数と呼びますが、クロージャの性質を使うことで次のようにnより大きいかを判定する高階関数を作れます。 最初からgereterThan5という関数を定義すればよいのですが、高階関数を使うことで文字列などの値と同じように関数を値としてやり取りできます。

function greaterThan(n) {
    return function(m) {
        return m > n; 
    };
}
const greaterThan5 = greaterThan(5);
greaterThan5(5); // => false
greaterThan5(6); // => true

[コラム] 状態をもつ関数オブジェクト

JavaScriptでは関数はオブジェクトの一種です。オブジェクトであるということは直接プロパティに値を入れることが可能です。 つまり、状態を関数をもつ方法として、次のように直接関数に値を代入するという手段をとることができます。

function countUp() {
    // countプロパティを参照して変更する
    countUp.count = countUp.count + 1;
    return countUp.count;
}
// 関数オブジェクトにプロパティとして値を代入する
countUp.count = 0;
// 呼び出すことにcountが更新される
countUp(); // => 1
countUp(); // => 2

しかし、この方法は推奨されていません。理由としては、外から直接countプロパティが変更できるためです。 関数オブジェクトのプロパティは外からも参照でき、またそのプロパティ値を変更できます。 その値を外から見えないように隠しているつもりなら、それを強制できるクロージャーが有効です。

function countUp() {
    // countプロパティを参照して変更する
    countUp.count = countUp.count + 1;
    return countUp.count;
}
countUp.count = 0;
// 呼び出すことにcountが更新される
countUp(); // => 1
// 直接値を変更できてしまう
countUp.count = 10;
countUp(); // => 11

クロージャーのまとめ

クロージャーは、変数が参照する値は静的に決まる静的スコープという性質とデータは参照されていれば保持されるという2つの性質によって成り立っています。 JavaScriptでは、関数を短く定義できるArrow Functionや高階関数であるメソッドなどクロージャーを自然と利用しやすい環境があります。 関数を理解する上ではクロージャーを理解することは大切です。

TDZ. この仕組みはTemporal Dead Zoneと呼ばれます。
GC. ECMAScriptの仕様ではガベージコレクションの実装の規定はないため、実装依存の処理となります

results matching ""

    No results matching ""