JavaScript

スコープチェーンとコールスタックについて

JavaScriptにおけるスコープとは、有効範囲のことです。 JavaScriptを扱う上で、スコープを理解しておかないと、思ったとおりの挙動になりません。 スコープをしっかりと理解していきましょう。

この記事の目次
  1. JavaScriptのスコープを理解していないとどうなるの?
  2. グローバルスコープとローカルスコープ
  3. スコープ(Scope)について
  4. グローバルスコープ (global scope)とスクリプトスコープ(Script scope)
  5. 関数スコープ (function scope)
  6. ブロックスコープ (block scope)
  7. レキシカルスコープ(lexical scope)
  8. スコープチェーン
  9. スコープチェーンとコールスタック

JavaScriptにおけるスコープとは、有効範囲のことです。
JavaScriptを扱う上で、スコープを理解しておかないと、思ったとおりの挙動になりません。
スコープをしっかりと理解していきましょう。

JavaScriptのスコープを理解していないとどうなるの?

定数や変数がブロック内で宣言されたとき、そのブロック内でしか有効ではありません。
たとえば、次のようなコードはエラーになります。

'use strict';

function num() {
  const a = 2;
  console.log(a);
}

num();
console.log(a);

上の定数aの定義しているスコープ{}の範囲ではないので定義されていないと警告がでます。
そのため、次のように、{}の枠の外で定数を宣言する必要があります。

'use strict';

  const a = 2;

  function num() {
    console.log(a);
  }
  
  num();
  console.log(a);

順番が変わっただけではありますが、これならすべてが正しく表示されます。

グローバルスコープとローカルスコープ

ブロックの外で宣言されている場合、グローバルスコープと呼びます。
グローバルスコープで宣言されると、すべての範囲で有効となります。
ブロック内で同じ名前の定数や変数があれば、そちらが優先されます。

同じ定数名をグローバルスコープで定義した場合、scriptタグを分けて書いても、スコープが分かれるわけではなく、エラーがでてしまいます。そのため、スコープは極力狭めることが大事になります。

どうしても同じ名前の定数を使いたい場合は、ブロック{}で囲って上げるとローカルスコープにすることができます。ブロック{}で囲うよう心がけましょう。

{
処理内容
}

ローカルスコープは、複数あるので詳しくみていきましょう。

スコープ(Scope)について

スコープとは、実行中のコードから値と式が参照できる範囲のことです。
5種類のスコープがあります。

  • グローバルスコープ
  • スクリプトスコープ
  • 関数スコープ
  • ブロックスコープ
  • モジュールスコープ

モジュールスコープはこちらの記事を参照してください。
モダンなJavaScript開発の流れと必ず必要になるモジュールについて

スコープで大事なことは、親のスコープには子スコープからアクセスが可能ですが、
親から子スコープ内で宣言された変数にはアクセスできません。
また、親子関係には兄弟スコープ同士もその中で宣言された変数にはアクセスできません。

グローバルスコープ (global scope)とスクリプトスコープ(Script scope)

グローバルスコープというは、関数の外の領域です。
グローバルスコープで宣言された変数はどこでもアクセス可能になります。


let a = 9;
var aa = 3;
function aaa(){}
debugger;

開発ツールのSourcesのタブでリロードするとdebuggerで止まります。
Scopeを確認すると、
Scriptにletで宣言したaがあり、Global(windowオブジェクト)の中にaaとaaaがあります。
wiindow.aaと呼び出すこともできますし、省略してaaとしても呼び出すことができます。
windowオブジェクト自体がグローバルスコープということになります。

スクリプトスコープもグローバルスコープも使い勝手は同じため、まとめてグローバルスコープと呼ばれることが多いです。
ただし、厳密にはグローバルスコープが親スコープでスクリプトスコープが子スコープになるため、どちらでも値を宣言した場合はスクリプトスコープが優先されます。

関数スコープ (function scope)

関数スコープの中で宣言された変数はその関数の中でしかアクセスできません。
ローカルスコープとも呼ばれます。

function a() {
  //この{}内が関数スコープ
  let aa = 0;
  console.log(aa); //0
}
  console.log(aa); //エラー
  a();

このように関数スコープの中で宣言された変数はスコープの外では呼び出すことができません。

ブロックスコープ (block scope)

ブロックとはJavaScriptの場合は{}のことです。
let, constのみブロックスコープが生成されます。
if文やfor文でもブロックスコープが生成されます。
ブロックスコープの中で宣言された変数はそのブロック内でのみアクセスできます。
ただし、ブロックスコープはES6から導入されたスコープであり、varはブロックスコープが生成されないので使用しないようにしましょう。
strict modeのとき、関数もまたブロックスコープになります。
ただし、関数宣言でもブロックスコープは無視されます。
letやconstで宣言する関数式はブロックスコープが生成されます。

{
  let a = 1;
  var aa = 1;
  function aaa() {
    console.log('aaa');
  }
  const aaaa = function() {
    console.log('aaaa');
  }

}
console.log(a);//エラー
console.log(aa);//aa
aaa();//aaa
aaaa();//エラー

レキシカルスコープ(lexical scope)

JavaScriptにはレキシカルスコープ(lexical scope)というスコープが存在します。
レキシカルスコープとはコードを書く場所によって参照できる変数が変わるスコープのことです。

コードを記述した時点で決まるので静的スコープとも言われます。

レキシカルスコープはコード内の関数の場所とブロックによってコントロールされます。
実行中のコードから見た外部スコープという意味でも使われますし、どのようにしてスコープを決定するかの仕様を指す場合もあります。

スコープチェーン

スコープチェーンとは、スコープが複数の階層で連なっている状態のことです。
次のような状態がスコープチェーンでです。

let = b = 1;
function a() {
  let b = 2;
  function aa() {
    let b = 3;
    console.log(b);//3
  }
  function aa();
}
a();

let b = 3をコメントアウトすると、bは2になり、 let b = 2をコメントアウトするとbは1になります。
このようにスコープの内側で宣言されている変数を優先して値を取得します。

スコープチェーンとコールスタック

たとえば次のようなコードがあります。


const a = 'Sasaki';
first();

function first() {
  const b = 'こんにちは';
  second();

  function second() {
    const c = 'こんばんは';
    third();
  }
}

function third() {
  const d = 'おはよう';
  console.log(d + c + b + a);
  //ReferenceError
}

コールスタックには、次の順番で積み上がっています。

  1. グローバルEC(Execution context)
  2. first() EC
  3. second() EC
  4. third() EC

コールスタックとは何かについては次の記事を参照してみてください。
どうやってJavaScriptは実行されているのか
スコープチェーンは関数が呼ばれる順番は関係がありません。
スコープチェーンは関数が書かれてる場所が重要なだけです。
そのため、コールスタックにどの順番で積み上がっているかはスコープチェーンには無関係です。
上の例では、second()関数がthrid()関数をコールバックしていますが、
second()関数内で宣言されている定数cはthird()関数からではスコープ外でアクセスできません。
よってReferenceErrorがでます。
このことからも、コールバックしている関数とスコープチェーンが無関係であることがわかります。

次の例も同様に、呼び出すことができません。


'use strict';

function calcAge(birthYear) {
  const age = 2040 - birthYear;

  function printAge() {
    const output = `${firstName}, you are ${age}, born in ${birthYear}`;
    console.log(output);

    if (birthYear >= 1988 && birthYear <= 1994) {
      const str = `Oh, you  are a millenial, ${firstName}`;
      console.log(str);

      function add(a, b) {
        return a + b;
      }
    }
    // console.log(add(2,3));これはできない。スコープチェーンは親から子に変数を探しにいけないから。
    // console.log(str); これもできない。同じ理由。
  }
  printAge();
  return age;
}

const firstName = 'Yamada';
calcAge(1988);

// console.log(age); これはできない。スコープチェーンは親から子に変数を探しにいけないから。
// printAge(); これもできない。同じ理由。