iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

スコープについて改めて学んでみた

はじめに


こんにちは、株式会社iimonでエンジニアをしているなかむーです!
実務ではTypeScriptを触っていますが、同じところで何度もエラーを出してしまったりするので、改めて基礎のJavaScriptの動作について学んでみました! 今回はスコープについて学んだことをまとめてみました

スコープとは

実行中のコードから値と式が参照できる範囲のことです。 スコープには4種類ありますが、今回はモジュールスコープの話は除外して、グローバルスコープ、関数スコープ、ブロックスコープについてお話しようと思います。

良ければ、一緒に動作を見てみましょう。 以下のコマンドで適当なディレクトリを作成して、そこにシートを配置することができます。

// コマンド

// 移動
cd ~/Documents

// ディレクトリ作成
mkdir scope_test

// ディレクトリに移動
cd scope_test

// ファイル作成
touch index.html main.js

LiveServerでコードの動作を確認することができます。LiveServerはVSCode拡張機能です。ファイル上の変更をすぐに読み込み反映することができます。 marketplace.visualstudio.com

それでは各スコープの説明に入ります。

グローバルスコープ

グローバルスコープはJavaScriptファイル内でどこからでも参照できるスコープです。 宣言子のvar, functionか、 let, constのどちらかを使うことによって若干の動作が異なります。 確認してみましょう。

// index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script src="./main.js"></script>
</body>
</html>
// main.js
let a = 0;

const b = 1;

var c = 2;

function d () {
    console.log('hi')
};

debugger;

debuggerを書くと、その行でコードの実行を止めることができます。 FnとF12を同時に押すと開発者モードでコードを確認することができます。

ここで確認すると、letとconstで宣言されたaとbはスクリプトスコープ、varとfunctionで宣言したcとdはグローバルスコープに配置されていることがわかります。

グローバルスコープの中にはwindowオブジェクトが格納されていて、cとdはwindowオブジェクトのプロパティになっています。 つまり、「グローバルスコープ = windowオブジェクト」になっています。 そのため、コンソールタブからwindow.cとwindow.dを呼び出すと、値が確認できます。 また、windowを省略しても呼び出すことができます。

では、同じ変数に対して、スクリプトスコープ(let)とグローバルスコープ(window)で代入を行うと、どうなるでしょうか? 同じ変数名を異なる宣言子で重複宣言することはできないので、windowオブジェクトに代入して試します。

let a = 0;
window.a = 2;

console.log(a); // 0 
console.log(window.a); // 2

結果はlet (スクリプトスコープ) で代入された値が出力されます。 これはスコープチェーンという仕組みによるもので、同じ変数に対して代入が行われると内側のスコープから参照されます。

グローバルスコープで宣言されたwindow.a のほうが外側にあるので、let aで宣言した値は、スクリプトスコープで宣言された0が先に参照されます。 しかし、windowオブジェクトを指定することで、グローバルスコープで宣言された値を確認することができます。 スコープチェーンについてはまた後で説明をします。

一般的にスクリプトスコープもまとめてグローバルスコープと呼ばれますが、若干の動作の違いがあることは覚えておくといいと思います。

関数スコープ・ブロックスコープ

関数スコープとブロックスコープの説明をします。

関数スコープは文字通り関数の中のスコープです。

function () {
// ここが関数スコープ
}

関数スコープの中で宣言した変数を、関数スコープの中から呼び出すことはできますが、関数スコープの外から呼び出すことはできません。

// 関数スコープの中から呼び出す

function test() {
    const a = 1
    console.log(a) // 1
}

test();
// 関数スコープの外から呼び出す

function test() {
    const a = 1
}

test();

console.log(a) // ReferenceError: a is not defined

ReferenceError: X is not definedは、JavaScriptにおいて変数Xが宣言されていないか、または定義されていない場合に発生するエラーです。

ブロックスコープの説明をします。

JavaScriptにおいて、スコープとはこの{}波括弧で囲まれた部分を指します。ただし前述した、関数スコープは除きます。 例えばif 文や、for文のスコープはブロックスコープです。ブロックスコープには一つ決まりがあります。 それは、ブロックスコープの中ではlet, constの宣言子のみ使う、ということです。 なぜなら、varやfunctionの宣言子はブロックスコープを作ることができないからです。動作を見てみましょう。

// let, constの宣言子の場合
// ブロックスコープ外での呼び出し

{
    let a = 1;
    const b = 2;
    
}

console.log(a);  // ReferenceError: a is not defined
console.log(b); // この行は上の行がエラーで処理が終了して実行されません
// var, functionの宣言子の場合
// ブロックスコープ外での呼び出しができてしまう!

{
    var a = 1;
    function b () {
        console.log('hi')
    } 
}

console.log(a); // 1
b(); // hi

ここで注目してほしいのは、varやfunctionをブロックスコープの中で宣言しても外から呼び出しができてしまうことです。 この理由を次に説明します。

関数スコープを作らないvarとfunction

varとfunctionはlet, constと動作が違い、ブロックスコープを作りません。

これは、初期のJavaScriptの開発背景が関係あります。 JavaScriptは1995年にはシンプルな言語として設計されました。 関数スコープも狭くシンプルな処理のみ記載されることを想定されていました。 そのため、関数スコープ内ならどこからでもアクセスができるようになっていました。

// 1995年当時の設計思想
function example() {
    if (true) {
        var x = 1; 

        function inner() {
            return x;
        }

    }
    console.log(x); // 1  if文のブロックスコープがないから、xにアクセス可能!
    console.log(inner()); // 1 if文のブロックスコープがないから、inner関数ににアクセス可能!
}

しかし、時代が進むにつれて、アプリケーションの大規模化やチーム開発がされるようになりました。 このため、関数スコープ内ならどこからでも呼び出したり、上書きができる仕様は解読困難になり、2015年ES6でlet, constが導入されるようになりました。 現在はvarは非推奨になりましたが、functionは独自の機能があるため使用されています。 ブロックスコープ内で関数を宣言したい場合は、関数宣言ではなく、関数式const a = function() {}を使用する使い分けが必要です。 関数式であれば、constで宣言しているためブロックスコープを作ることができます。

スコープチェーン

スコープが階層状になっているとき、内側から順番に外側のスコープを探しに行く仕組みをスコープチェーンといいます。 動作を見てみましょう。

// level2で宣言したaを参照する

let a = 0;

function level1 () {
    a = 1;
    
    function level2 () {
        a = 2;
        console.log(`level2:${a}`)
    }
    level2(); // 2
}
level1();
// level1で宣言したaを参照する
let a = 0;

function level1 () {
    a = 1;
    
    function level2 () {
        // a = 2;
        console.log(`level2:${a}`)
    }
    level2(); // 1
}
level1();
// スクリプトスコープで宣言したaを参照する
let a = 0;

function level1 () {
    // a = 1;
    
    function level2 () {
        // a = 2;
        console.log(`level2:${a}`)
    }
    level2(); // 0
}
level1();

このように、letで上書き可能な変数は一番内側のスコープで代入されたlevel2→level1→グローバルスコープ(スクリプトスコープ)というふうに変数を探す動作をします。

まとめ

  • スコープには4種類あること(グローバルスコープ・関数スコープ・ブロックスコープ・モジュールスコープ)
  • そのうちグローバルスコープ(・スクリプトスコープ)は若干の動作の違いがあること
  • varとfunctionはブロックスコープを作らないことと、その歴史上の背景
  • 実行中のコードの内側のスコープから外に変数を探しに行くことをスコープチェーンと呼ぶこと

がわかりました! 基本的なことですが、仕組みを理解するとエラーの内容がよく理解できるようになったので改めて調べられて良かったです。

少しでも不備・不足等ありましたらご指導・コメントいただけると幸いです!

おわりに

弊社ではエンジニアを募集しています!

この記事を読んで少しでも興味を持ってくださった方は、ぜひカジュアル面談でお話ししましょう!

iimon採用サイト / Wantedly / Green

参考記事

www.udemy.com