スコープについて学習しました。この記事は、独習JavaScriptとJavaScript Primerで勉強した内容を記述しています。
まず結論
スコープとは、実行中のコードから見て参照(使用)できる変数や関数の範囲のことです。
例えば、以下のように関数内で定義された変数は別の関数内で使用できません↓
function fnA () {
const val = 100;
}
function fnB() {
console.log(val)
}
fnB() // Uncaught ReferenceError: val is not defined
スコープの性質
スコープの性質には以下の2点があります↓
- スコープは階層化できる
- 静的スコープである
スコープの階層化
スコープは、階層化できるという性質があります。階層化とは、HTMLでdiv要素の中にp要素が入っているような入れ子構造のことです。
例えば、ブロックスコープの中にブロックスコープが入っている状態を指します↓
{
//ブロックスコープ(親スコープ)
const number = 1;
{
//ブロックスコープ(子スコープ)
let string = "文字列";
}
}
上記のコードの場合で変数numberのスコープを親スコープ、変数stringのスコープを子スコープと呼びます。
子スコープは親スコープの変数や関数を参照できます。しかし、親スコープは子スコープの変数や関数を参照できません↓
{
const number = 1;
{
let string = "文字列";
console.log(number) // 1
}
console.log(string); // エラー
}
静的スコープ
静的スコープとは、記述された時点である識別子(変数名)がどこのスコープの変数を参照するか決定する性質のことです。
以下は、コード例です↓
//スクリプトコープ
const number = 1;
function Fn() {
//関数Fn内のスコープ(関数スコープ)から見て、変数numberはレキシカルスコープである。
//console.logに書かれた識別子"number"は、常にレキシカルスコープである変数numberの値を参照する。
console.log(number);
}
function run() {
/*関数run内で新たに変数numberが宣言されていますが、
記述された時点でどの変数を参照するか決定しているので常に1を参照します。*/
const number = 10;
Fn();
}
run();// 常に1が出力
//【識別子numberの参照過程】
//1:Fnの関数スコープに変数numberが定義されていない
//2:ひとつ外側のスコープ(スクリプトスコープ)を確認する
//3:スクリプトスコープにconst number = 1;が定義されているので、識別子xはこの変数を参照する
上記コードのコメントに出てくる「レキシカルスコープ」とは、自分のスコープから見て一つ外側のスコープを指します。スクリプトスコープや関数スコープについては後述します。
スコープの種類
スコープには以下の5種類が存在します↓
- 関数スコープ
- ブロックスコープ
- グローバルスコープ
- スクリプトスコープ
- モジュールスコープ
変数や関数は上記スコープのいずれかに必ず属します。
基本的に変数や関数は同一スコープ内に存在する場合に参照(使用)可能です。
もし、スコープの階層化でも記述した通り、親スコープがあれば親スコープに書かれた変数や関数を使用できます。
以下よりそれぞれのスコープについて見ていきます
関数スコープ
関数内で宣言された変数や関数、関数の引数が属するスコープです。基本的に異なる関数スコープに属する変数や関数を参照できません↓
function fnA () {
//関数スコープ
const val = 100;
}
function fnB() {
//関数スコープ
console.log(val)
}
fnB() // Uncaught ReferenceError: val is not defined
スコープが階層化されている場合、子スコープから親スコープの変数や関数を参照できます。
親スコープから子スコープや、兄弟間のスコープで変数や関数を参照できません↓
function fnA() {
//関数スコープ(親スコープ)
let valA = 1;
console.log(valA); // 1
function fnB() {
//関数スコープ(fnAの子スコープ)
const valB = 10;
console.log(valA); // 1
console.log(valB); // 10
return (valA += 1);
}
function fnC() {
//関数スコープ(fnAの子スコープ)
const valC = 100;
console.log(valC) // 100
console.log(`関数fnBの実行結果: ${fnB()}`); // 関数fnBの実行結果: 2
console.log(`変数numberの値: ${valA}`); // 変数numberの値: 2
console.log(valB); // 参照できない。valBは関数fnB内でのみ参照できる
}
return fnC();
}
fnA();
上記のコードで各スコープにおいて参照できる変数や関数を画像で表してみました↓
fnBやfnCの中で同一の関数を実行(再帰関数)すると、無限ループになります。そのためif文などを使って実行を停止するための処理を記述する必要があります。
ブロックスコープ
JavaScriptでは、{ }(波括弧またはブレイス)のことをブロックと呼びます。このブロック内で、letまたはconstを使って宣言された変数や関数が属するスコープのことです↓
{
// ブロックスコープ
let number = 1;
const string = 1;
const fn = () => {
return 1;
}
}
一般的にブロック単体で使用せず、if文やfor文と一緒に使用します↓
if(true) {
//ブロックスコープ
const number = 1;
}
注意点として、ブロック内でvarを使って変数や関数を宣言したり、functionキーワードを使って関数を宣言(関数宣言文)してもブロックスコープには含まれず、ブロックの外側のスコープに含まれます。(関数スコープの場合は、varを使って変数や関数を宣言しても関数スコープに含まれます。)
//一つ外側のスコープ(今回の場合、グローバルスコープ)
if(true) {
// varやfunctionキーワードの場合、ブロックの外側のスコープに属する。
var number = 1;
function fn() {
return 1;
}
}
console.log(number) // 1 グローバルスコープに属するため、実行できてしまう
console.log(fn()) // 1 上記と同様
"use strict"
を使った厳格モードの場合、関数宣言文はブロックスコープに含まれます。varは厳格モードでもブロックの外側のスコープに含まれます↓
"use strict";
//グローバルスコープ
if(true) {
var number = 1; // グローバルスコープのまま
function fn() {
// ブロックスコープに属する
return 1;
}
}
console.log(number) // 1 グローバルスコープに属するため、実行できてしまう
console.log(fn()) // ブロックスコープに属しており、スコープ外で実行しているためエラー ReferenceError: fn is not defined
厳格モードの話はこちら↓
グローバルスコープ
JavaScriptファイルのトップレベルまたは、script要素直下に記述したコード内で、
- varを使って定義された変数や関数
- 関数宣言文(function 関数名 () {…})によって定義された関数
が属するスコープのことです。
トップレベルとは、関数やブロックで囲まれていない範囲のことです。具体的には、JavaScriptファイル直下のコードやscript要素直下のコードのことです。
グローバルスコープに配置された変数や関数は、コードのどこからでも参照できます。つまり、グローバルスコープは他のスコープよりも外側に存在しています。(Node.jsの場合は違うらしい)
<script>
// トップレベル
//グローバルスコープ
var globalVal = "グローバル変数";
var globalFn = () => {
return "グローバル関数式"
}
function callGlobal() {
console.log(globalVal,globalFn()) // グローバル変数、グローバル関数式
return "グローバル関数宣言文";
}
console.log(callGlobal()); // グローバル関数宣言文
</script>
注意点として、グローバルスコープに配置された変数や関数は、異なるscript要素から参照可能です。また、<script src="jsファイル">
のようにファイルを読み込む形にした場合も同様です。
他のファイルの変数名と衝突して意図しない挙動が発生する可能性(グローバル汚染)があります。Viteやwebpackなどを使用すると、一部のグローバル汚染を防いでくれます↓
スクリプトスコープ
JavaScriptファイルのトップレベル、またはscript要素直下に記述したコード内で、letやconstを使って宣言された変数や関数が属するスコープです。
// jsファイルのトップレベル
let number = 1;
const string = "文字列"
const fn = () => {
return "関数式"
}
グローバルスコープと同様に、スクリプトスコープに配置された変数や関数はどこからでも参照可能です。
グローバルスコープとの違いは、グローバルスコープはブラウザ環境ではWindowオブジェクトのプロパティに格納されますが、スクリプトスコープは格納されません↓
//スクリプトスコープ
const a = 1;
//グローバルスコープ
var b = "string";
// console.log(window.b or b)で呼び出せる
window.a = 100;
console.log(window.a) // 1
以下の動画でも確認できます↓
モジュールスコープ
script要素にtype属性moduleが設定された、
- JavaScriptファイルのトップレベル
- script要素直下
で宣言された変数や関数が属するスコープ。
モジュールスコープは、script要素の単位でモジュールが形成され、他のモジュール内の変数や関数は参照できません↓
<script type="module">
//モジュールスコープ
const number = 1;
console.log(number) // 1
</script>
<script type="module">
//モジュールスコープ
console.log(number) // エラー モジュールスコープでは、異なるscriptタグやjsファイルから参照不可。
</script>
まとめ
最後に、スコープの一覧を表にまとめました↓
トップレベル | 関数内 | ブロック内 | モジュール内のトップレベル | |
---|---|---|---|---|
let | スクリプト | 関数 | ブロック | モジュール |
const | スクリプト | 関数 | ブロック | モジュール |
var | グローバル | 関数 | ブロックの外側※1 | モジュール |
関数宣言 | グローバル | 関数 | ブロックの外側※2 | モジュール |
※1:例えば、ブロックスコープの外側のスコープが関数スコープの場合、関数スコープに配置されます。
※2:"use strict"
を用いた厳格モードの場合、ブロックスコープに配置されます。