「スコープは習ったけど本質的に理解できていない」
「自作した関数や繰り返し処理がなぜかエラーになることがある」
「変数でletとvarの違いが分からないけど、とりあえず使って何とか動作している」
本日はスコープについて取り上げていきます。
プログラミングを学習し始めて序盤の方に登場する「スコープ」について理解して進めた駆け出しエンジニアの方はどれくらいいるでしょうか?
ぶっちゃけ最初の段階ではそこまで真剣に理解しなくても大丈夫なんですが、実務に入ってエラーに遭遇した時に原因がスコープであることがあります。
今回はスコープの基本を初学者でも理解しやすいJavaScriptを使って解説していきます。
また動画もあるので必要に応じて活用してください。
JavaScriptにおけるスコープの基本
スコープというのは簡単にいうと「変数や定数の書き方」のことです。
まずスコープには大きく分けて2種類あることから知っておきましょう。
最初のスコープは「グローバルスコープ」と呼ばれるもので、以下のように書くことを指します。
const num = 10;
console.log("ただのコンソール:", num);
if (true) {
console.log("ifのコンソール:", num);
}

定数numに10を代入しておきコンソールに表示するだけの簡単なコードです。
コンソールログを使ってnumを2箇所に渡って呼び出しているのが分かります。
グローバルスコープとは上記のコードのように、関数や条件式の外で設定されるものだと覚えてもらえれば大丈夫です。
もう一つのスコープは「ブロックスコープ」と呼ばれるもので、以下のように書くことを指します。
if (true) {
const num = 10;
console.log("ifのコンソール:", num);
}
console.log("ただのコンソール:", num);

コードの仕様については先ほどの例とほとんど変わりません。
こちらも定数numに10を代入してコンソールログで表示するだけですが、最終行に書いたコンソールログのみエラーになっているのが分かります。
これがブロックスコープの特徴で、言葉の通り限られたエリアでしか変数や定数を使い回せないことになります。
もう少し具体的に言うと、関数や条件式の中で設定した変数、定数は外のエリアに出ると使い回すことが出来なくなるんです。
イマイチ分かりづらいと感じている初学者の方は、「{ }で囲われた変数や定数は{ }の外で使えない」と言う覚え方でも良いでしょう。
ちなみに{ }が入れ子になっている場合はどうなるでしょうか?
function test() {
if (true) {
const num = 100;
}
console.log(num);
}
test();

functionの中で完結していますが、結局はif文の中で作った定数numをif文の外で使おうとしたのでブロックスコープのエラーになりました。
こちらはイメージしやすいと思います、一方で以下のような場合も見てみましょう。
function test() {
const num = 100;
if (true) {
console.log(num);
}
}
test();

今度はfunctionの中ですがif文の外で定数numを設定しました。
その状態でif文の中で定数numを使用すると問題なく使用できています。
こちらの方がイメージしにくいかもしれませんが、function側から見た時にif文は「{ }の中」ということになるためです。
ブロックスコープは「{ }の外では使えない」と言いましたが、上記のようなケースだと問題なく使用できます。
似たようなコードでもスコープという意味では別物です、この違いを持ってスコープの動作がさらに理解できるかと思います。
今回はJavaScriptを例題にしていますが、他の言語でも大体はグローバルスコープとブロックスコープの2種類の分け方になっています。
ここまで読んだ方の中で「グローバルスコープとブロックスコープのどちらが良いの?」と思われた方がいるかもしれません。
結論、どちらも使います。
とはいえイメージしてもらうとするならば、ブロックスコープは限られたエリアなので「他の場所で同じ変数、定数を使い回さないとき」に使うことが好ましいでしょう。
例えば特定の関数でしか使わない変数はブロックスコープで設定して、「ユーザー名」「本日の日付」「APIのURL」など、同じファイルの中で何回も使うような変数はグローバルスコープで設定する感じです。
この辺りは経験しながらコツを掴むことが多いので、初学者の方は2種類の違いを頭に叩き込むことに専念してもらえればと思います。
モジュールについて
スコープとは少しニュアンスが違いますが同じような考え方をするモジュールというものがありますので一緒に紹介しておきます。
モジュールは複数のJSファイル同士でデータをバケツリレーのように引き継ぐことを言います。
例えば以下のようなHTMLファイルとJSファイルがあったとします。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="./style.css" />
<script src="./script.js" defer></script>
</head>
<body></body>
</html>
console.log("テスト");
index.htmlにscript.jsを読み込んでいてscript.jsの中はコンソールログを出力する内容になっています。
この状態から追加で別のJSファイルをmain.jsと言う名前で作ったとします。
const num = 100;
const value = 200;
main.jsにある定数をコンソールログで表示するのにscript.js側で以下のように書いてもエラーになります。
理由としてはindex.htmlはscript.jsを読み込んでいるだけでmain.jsは読み込んでいないので、main.jsの中で宣言されたものをscript.jsに呼ぼうとしても見当たらないからです。
スコープとは厳密には違いますが考え方は似ていますね。
console.log("テスト");
// ここから追加
console.log(num); // script.jsでは宣言されていない
console.log(value); // script.jsでは宣言されていない
上記のコードをエラーなしで実行するためにモジュールというものがあります。
まずHTMLファイルであるindex.htmlに専用の属性を追加します。
JSファイルを読み込むためのscriptタグの属性に「type=”module”」というものです。
これを追加することでモジュールを取り入れることができます。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="./style.css" />
<!-- scriptタグにtype="module"を追加 -->
<script src="./script.js" defer type="module"></script>
</head>
<body></body>
</html>
続いてエラーのもとになっていたmain.jsについて引き継ぎしたいデータの先頭に「export」を付けます。
今回は違いをわかりやすくするために2つあるうちの片方だけに「export」を付けてみましょう。
// 定数numを引き継ぐようにする
export const num = 100;
const value = 200;
最後にscript.jsではimport文というものを書くことでmain.jsからexportしたデータを受け取ることができます。
console.log("テスト");
// ここを追加
import { num } from "./main.js";
console.log(num);
console.log(value);
fromの後にはmain.jsの名前ではなくパスを書くので注意してください。

main.jsで宣言していたデータのうちexportしてscript.jsでimportした「num = 100」だけがコンソールに表示できました。
valueについては引き続きエラーになったままですね。
このモジュールという考え方は主にReactなどモダンJSでは必須の知識になるので初学者の方も覚えておくようにしましょう。
またモジュールについては別の記事でも解説しているので詳しく知りたい方はどうぞ。
JavaScriptにおけるletとvarの違い
JavaScriptで変数を書くときにletとvarの2種類がありますが、こちらの違いにもスコープが関係しています。
例えばletを使って以下のようなコードを書いたとします。
if (true) {
let num = 10;
console.log("ifのコンソール:", num);
}
console.log("ただのコンソール:", num);

先ほどと同じようなコードですね。
スコープの種類としてはif文の中で変数を設定しているので「ブロックスコープ」になります。
そのため最終行にあるような、if文の外で変数numを使おうとするとエラーになります。
それでは上記のコードをvarに変えてみます。
if (true) {
var num = 10;
console.log("ifのコンソール:", num);
}
console.log("ただのコンソール:", num);

するとなんとブロックスコープで変数numを設定しているはずなんですが、if文の外で書いたコンソールログも実行できてしまっています。
これがletとvarの違いです。
letは2種類のスコープの違いを使い分けるのに対して、varで書いた変数は「ブロックスコープとして動作する」という特徴があります。
つまりvarで書いた変数はどこで設定しようが、色んな場所で自由に使い回せるということですね。
一見するとvarの方が自由が効いて便利そうですが、実務においてはletを使うようにしましょう。
varは自由であるからこそ、「コードの間違いに気付きにくい」「予期せぬエラーを誘導する」ような危険性があるとも言い換えることが出来ます。
サンプルコードをググっていると意外とvarで書かれた記事はそれなりに見かけると思います。
遊びや初歩の勉強の間だけは良いとしても実務ではletのみで書くようにしましょう。
こんな感じでスコープはいきなり覚えようとしても中々難しいものですが、小さくてもコードを書いている内にスコープの大事さを実感するので初学者の方も焦らず覚えてください。