関数とスコープ

定義された関数はそれぞれのスコープを持っています。スコープとは変数や関数の引数などを参照できる範囲を決めるものです。 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つしか宣言できないためです。(varとfunction宣言は例外的に可能ですが、詳細は後述します)

// スコープ内に同じ"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

スコープチェーン

  • スコープは入れ子にできる
  • シャドーイングについて

グローバルスコープ

  • グローバルスコープ
  • グローバル変数
  • ビルトイン関数
  • シャドーイングの実害

[コラム] スコープは小さく

  • 大きなスコープに変数を定義することはそれだけ依存や影響範囲を広げている
  • 小さなスコープに必要な変数を定義する or 引数で必要な変数を受け取るのが基本

varとfunction宣言の例外

この章では意図的にvarについての解説を省いています。なぜならvarを使うべきケースはないからです。 これから各コードは必ずletconstを利用してください。 しかし、varを含むコードなどを読む際には、var特有の巻き上げという仕組みについて理解する必要があります。

  • hoisting
  • varは関数スコープにbindする
  • functionもhositingする

ローカル変数の寿命とガーベッジコレクション

クロージャー

  • ファクトリとしての関数
  • IIFE

results matching ""

    No results matching ""