JavaScript 関数とスコープ

目次

関数とスコープ

関数とスコープ

定義された関数はそれぞれのスコープを持っています。スコープとは変数や関数の引数などを参照できる範囲を決めるものです。
JavaScriptのスコープは、ES2015において直感的に理解しやすい仕組みが整備されました。 基本的にはES2015以降の仕組みを理解していればコードを書く場合には問題ありません。

スコープとは

スコープとは

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

  • 関数によるスコープのことを関数スコープ
  • スコープが異なれば同じ名前で変数を宣言できます。

ブロックスコープ

ブロックスコープ

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

// ブロック内で定義した変数はスコープ内でのみ参照できる
{
    const x = 1;
    console.log(x); // => 1
}
// スコープの外から`x`を参照できないためエラー
console.log(x); // => ReferenceError: x is not defined
  • ブロックによるスコープのことをブロックスコープ
  • if文やwhile文などもブロックスコープを作成
  • for文は、ループごとに新しいブロックスコープを作成

スコープチェーン

スコープチェーン

関数やブロックはネスト(入れ子)して書けますが、同様にスコープもネストできます。

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

内側のINNERブロックスコープから外側のOUTERブロックスコープに定義されている変数xを参照できます。

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

ポイント

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

  1. INNERブロックスコープに変数xがあるかを確認
  2. ひとつ外側のOUTERブロックスコープに変数xがあるかを確認
  3. 一番外側のスコープにも変数xは定義があるかを確認
{
    // OUTERブロックスコープ
    const x = "outer";
    {
        // INNERブロックスコープ
        const x = "inner";
        // 現在のスコープ(INNERブロックスコープ)にある`x`を参照する
        console.log(x); // => "inner"
    }
    // 現在のスコープ(OUTERブロックスコープ)にある`x`を参照する
    console.log(x); // => "outer"
}

グローバルスコープ

グローバルスコープ

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

グローバルスコープには自分で定義したグローバル変数以外に、プログラム実行時に自動的に定義されるビルトインオブジェクトがあります。

ビルトインオブジェクト

  • ECMAScript仕様が定義するundefinedのような変数、isNaNのような関数、ArrayやRegExpなどのコンストラクタ関数
  • 実行環境(ブラウザやNode.jsなど)が定義するオブジェクトでdocumentやmoduleなどがある

ビルトインオブジェクトは、プログラム開始時にグローバルスコープへ自動的に定義されているためどのスコープからも参照できます。
ビルトインオブジェクトと同じ名前の変数を定義すると、上書きされる。外側の変数が参照できなくなることを変数の隠蔽(shadowing)と呼びます。

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

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

letとvarで異なる動作

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

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

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

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

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

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

関数宣言と巻き上げ

関数宣言と巻き上げ

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

function hello(){
    return "Hello";
}

functionキーワードによる関数宣言も巻き上げられます。 しかし、varによる変数宣言の巻き上げとは異なり、問題となることはほとんどありません。 なぜなら、実際に巻き上げられた関数を呼び出せるためです。

ポイント

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

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

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

即時実行関数

即時実行関数

即時実行関数(IIFE, Immediately-Invoked Function Expression)は、 グローバルスコープの汚染を避けるために生まれたイディオムです。

次のように、匿名関数を宣言した直後に呼び出すことで、任意の処理を関数のスコープに閉じて実行できます。 関数スコープを作ることでfoo変数は匿名関数の外側からはアクセスできません。

// 匿名関数を宣言 + 実行を同時に行っている
(function() {
    // 関数のスコープ内でfoo変数を宣言している
    var foo = "foo";
    console.log(foo); // => "foo"
})();
// foo変数のスコープ外
console.log(typeof foo === "undefined"); // => true

ECMAScript 5までは、変数を宣言する方法はvarしか存在しませんでした。 そのため、即時実行関数はvarによるグローバルスコープの汚染を防ぐために使われていました。

クロージャー

クロージャー

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

const x = 10; // *1

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

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

run();

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

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

メモリ管理の仕組み

メモリ管理の仕組み

  • JavaScriptではガベージコレクションが採用
    • ガベージコレクションとは、どこからも参照されなくなったデータを不要なデータと判断して自動的にメモリ上から解放する仕組みのことです。
  • 手動でメモリを解放するコードを書く必要はありません。しかし、ガベージコレクションといったメモリ管理の仕組みを理解することは、スコープやクロージャーに関係するため大切です。

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

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

const createCounter = () => {
    let count = 0;
    return function increment() {
        // `increment`関数は`createCounter`関数のスコープに定義された`変数`count`を参照している
        count = count + 1;
        return count;
    };
};
// createCounter()の実行結果は、内側で定義されていた`increment`関数
const myCounter = createCounter();
// myCounter関数の実行結果は`count`の評価結果
console.log(myCounter()); // => 1
console.log(myCounter()); // => 2
  • 次のような参照の関係がmyCounter変数とcount変数の間にはある
    1. myCounter変数はcreateCounter関数の返り値であるincrement関数を参照している
    2. myCounter変数はincrement関数を経由してcount変数を参照している
    3. myCounter変数を実行した後もcount変数への参照は保たれている
  • count変数を参照するものがいるため、count変数は自動的に解放されません。 そのためcount変数の値は保持され続け、myCounter変数を実行するたびに1ずつ大きくなっていきます。
  • このようにcount変数が自動解放されずに保持できているのは「increment関数内から外側のcreateCounter関数スコープにあるcount変数を参照している」ためです。 このような性質のことをクロージャー(関数閉包)と呼びます。
    • クロージャーは「静的スコープ」と「参照され続けている変数のデータが保持される」という2つの性質によって成り立っています。

クロージャーの用途

クロージャーの用途

  • 関数に状態を持たせる手段として
  • 外から参照できない変数を定義する手段として
  • グローバル変数を減らす手段として
  • 高階関数の一部分として ※関数を返す関数のことを高階関数という
const createCounter = () => {
    // 外のスコープから`privateCount`を直接参照できない
    let privateCount = 0;
    return () => {
        privateCount++;
        return `${privateCount}回目`;
    };
};
const counter = createCounter();
console.log(counter()); // => "1回目"
console.log(counter()); // => "2回目"

高階関数を使うことで条件を後から定義できるなどの柔軟性があります。

function greaterThan(n) {
    return function(m) {
        return m > n;
    };
}
// 5より大きな値かを判定する関数を作成する
const greaterThan5 = greaterThan(5);
console.log(greaterThan5(4)); // => false
console.log(greaterThan5(5)); // => false
console.log(greaterThan5(6)); // => true

状態を持つ関数オブジェクト

状態を持つ関数オブジェクト

JavaScriptでは関数はオブジェクトの一種です。オブジェクトであるため直接プロパティに値を代入できます。 そのため、クロージャーを使わなくても、次のように関数にプロパティとして状態を持たせることが可能です。

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

しかし、この方法は推奨されていません。なぜなら、関数の外からcountプロパティを変更できるためです。 関数オブジェクトのプロパティは外からも参照でき、そのプロパティ値は変更できます。 関数の中でのみ参照可能な状態を扱いたい場合には、それを強制できるクロージャーが有効です。

まとめ

  • 関数やブロックはスコープを持つ
  • スコープはネストできる
  • もっとも外側にはグローバルスコープがある
  • スコープチェーンは内側から外側のスコープへと順番に変数が定義されているか探す仕組みのこと
    • varキーワードでの変数宣言やfunctionでの関数宣言では巻き上げが発生する
  • クロージャーは静的スコープとメモリ管理の仕組みからなる関数が持つ性質