【JavaScript入門】スコープ

スコープとは

JavaScriptにおけるスコープ(範囲)とは、変数、関数、オブジェクトがコードの異なる部分でどのようにアクセス可能かを示す概念です。つまり、変数と関数が定義され、どこからアクセスできるかを決定します。基本的には変数や関数の宣言文と、それらを使用する行が同一スコープ内に存在する場合に参照可能になります。

さらに、JavaScriptにはES6で導入されたletおよびconstキーワードによるブロックスコープがあります。letまたはconstで宣言された変数はブロックスコープを持ち、定義されたコードブロック内(波括弧 {} のペア内)でのみアクセスできます。

JavaScriptにおけるスコープの仕組みを理解することは重要です。スコープを正しく使用しないと、コードの動作に影響を与え、予期しない結果を引き起こす可能性があるためです。

関数スコープ

関数スコープは、関数内で宣言された変数や関数のアクセス可能な範囲を決定します。関数内で宣言された変数や関数はローカルスコープを持ち、その関数とそのネストされた関数内でしかアクセスできません。

例えば、次のコードを考えてみます。

 function outer() {
   var x = 10;
   
   function inner() {
     var y = 20;
     console.log(x + y);
   }
   
   inner(); // 出力: 30
 }
 
 outer();

このコードでは、変数xがouter関数で宣言され、変数yがinner関数で宣言されています。console.logステートメントは、inner関数がouter関数にネストされているため、両方の変数にアクセスできます。

ただし、それぞれの関数の外で変数にアクセスしようとすると、参照エラーが発生します。

 function outer() {
   var x = 10;
   
   function inner() {
     var y = 20;
     console.log(x + y);
   }
   
   inner();
 }
 
 outer();
 
 console.log(x); // ReferenceError: x is not defined
 console.log(y); // ReferenceError: y is not defined

これは、xとyがローカルスコープを持っており、それぞれの関数内でしかアクセスできないためです。

ブロックスコープ

ブロックスコープは、if文やfor文などの中括弧{ }内で、let および constを使い宣言された変数や関数が属するスコープです。

ES6(ECMAScript 2015)以前は、JavaScriptには関数スコープしかありませんでした。つまり、関数内で宣言された変数や関数は、その関数内でのみアクセスでき、その外部からはアクセスできませんでした。しかし、ES6で let および const キーワードが導入されたことで、JavaScriptにはブロックスコープも追加されました。

 if (true) {
   let x = 10;
   const y = 20;
   var z = 30;
 }
 
 console.log(x); // ReferenceError: x is not defined
 console.log(y); // ReferenceError: y is not defined
 console.log(z); // 30

このコードでは、x と y は if ブロック内で let および const を使用して宣言されています。したがって、それらは if ブロック内でのみアクセス可能であり、その外部からはアクセスできません。一方、z は var キーワードを使用して宣言されており、関数スコープを持つため、if ブロックの外部からもアクセス可能です。

ブロックスコープは、変数や関数を特定のコードブロック内に限定し、名前の競合の可能性を減らし、コードを理解しやすくするのに役立ちます。

グローバルスコープ

グローバルスコープとは、プログラム全体でアクセス可能な変数や関数が属するスコープです。どの関数やブロックの内側でも宣言されていない変数や関数は、グローバル変数やグローバル関数と見なされます。

グローバル変数は、プログラムのどこからでもアクセスして変更することができます。

 var x = 10; // トップレベルで変数を宣言
 
 function foo() { // トップレベルで関数を宣言
   console.log(x);
 }
 
 foo(); // 10

このコードでは、x はグローバルスコープで宣言されており、プログラムのどこからでもアクセスして変更することができます。foo() 関数もグローバルスコープにあり、x の値にアクセスして出力することができます。

しかし、グローバル変数の使用は一般的に避けられるべきです。グローバル変数を使うと、名前の競合が発生しやすく、コードの理解が難しくなります。可能な限り、関数やブロック内でローカル変数を使用することが推奨されます。

グローバルスコープとWindowオブジェクト

グローバル変数やグローバル関数は、Windowオブジェクトというオブジェクトのプロパティやメソッドとして値が保持されます。

 var globalVal = "これはグローバル変数です";
 function globalFunc() { return "これはグローバル関数です"; }
 
 console.log(globalVal, window.globalVal);
// 出力:これはグローバル変数です これはグローバル変数です
 console.log(globalFunc(), window.globalFunc());
// 出力:これはグローバル関数です これはグローバル関数です

上のコードからWindowオブジェクトのプロパティやメソッドは、windowを省略して記述できることが分かります。組み込み関数の頭にwindowを付けずとも呼び出せるのも同じ理由です。

スクリプトスコープ

スクリプトスコープとは、JavaScriptファイルのトップレベル、またはHTMLファイルのscriptタグの直下に記述したコード内で、letおよびconstを使って定義された変数や関数が属するスコープです。

スクリプトスコープ内の変数や関数はグローバルスコープと同様、コード内のどこからでも参照可能であり、基本的にグローバルスコープと区別せず使うことができます。

ただし、スクリプトスコープ内の変数や関数は、グルーバルスコープと異なりWindowオブジェクトのプロパティとして格納されることはありません。

モジュールスコープ

モジュールスコープとは、import と export キーワードを使用して、モジュールまたはファイル内でアクセス可能な変数や関数を指します。ES Modulesを有効化するには、HTMLのscriptタグに対しtype=”module”を指定します。

モジュールはJavaScriptのコードを再利用可能な部品に分割する方法であり、それぞれ独自のスコープを持ちます。モジュールを作成すると、他のモジュールからアクセス可能な変数や関数をエクスポートすることができます。同様に、他のモジュールから変数や関数をインポートして自分自身のモジュールで使用することができます。

以下は、モジュール内で変数をエクスポートし、他のモジュールでインポートする例です。

 // module.js
 let x = 10;
 function foo() {
   console.log("hello");
 }
 export { x, foo };
 
 // main.js
 import { x, foo } from "./module.js";
 console.log(x); // 10
 foo(); // "hello"

このコードでは、module.js は変数 x と関数 foo をエクスポートし、main.js でインポートして使用します。x と foo のスコープは module.js に限定され、それ以外の場所でアクセスすることはできません。

モジュールは独自のスコープを持ち、グローバル名前空間を汚染しないため、大規模なコードベースを管理し、異なる部分のコード間で名前の競合を回避することが容易になります。

JavaScriptモジュールは、コードを整理し再利用可能にするための強力なツールであり、そのスコープを理解することは、効果的で保守的なコードを書くために重要です。

レキシカルスコープ

レキシカルスコープとは、実行中のコードから見た外側のスコープのこと。レキシカルスコープで宣言された変数や関数は参照可能です。

関数が定義されると、その関数内で宣言されたすべての変数や関数を含む新しいスコープが作成されます。このスコープは、親関数のスコープまたはグローバルスコープにネストされています。

関数内で宣言された変数は、その関数のスコープと、入れ子になった関数内でのみアクセス可能です。ただし、現在のスコープで変数が見つからない場合、JavaScriptエンジンは外側のスコープを探し、変数を見つけるかグローバルスコープに到達するまで探します。

以下は、レキシカルスコープを説明する例です。

 function outer() {
   let x = 10;
   
   function inner() {
     console.log(x);
   }
   
   inner();
 }
 
 outer(); // 出力: 10

このコードでは、xはouter関数で宣言され、inner関数で参照可能です。これは、innerがouterにネストされているため、console.log(x);の実行文から見ると、outer関数はレキシカルスコープ内に存在するからです。

このように実行中の命令文(スコープ)の外側のスコープがレキシカルスコープとなるため、レキシカルスコープは外部スコープや親スコープ、または静的スコープ(記述した時点で決定するため)などと呼んだりもします。

スコープチェーン

レキシカルスコープが多階層に連なっている状態をスコープチェーンと言います。変数や関数が自スコープに無い場合、JavaScriptエンジンは外側のスコープへ探しに行きます。スコープチェーンに存在するスコープを1つずつ辿り、最終的にグローバル変数まで遡り、そこまで行っても見付からない場合はエラーを返します。

以下は、スコープチェーンの動作を示す例です。

 let x = 1;
 
 function outer() {
   let x = 2;
 
   function inner() {
     console.log(x);
   }
 
   inner();
 }
 
 outer(); // logs 2

この例では、outer関数には独自のスコープがあり、xの値が2で定義されています。inner関数がxにアクセスしようとすると、JavaScriptエンジンは最初にinner関数のスコープを検索しますが、そのスコープにはxが定義されていないため、次にouter関数のスコープを検索し、値が2のxを見つけます。

クロージャ

クロージャとは、関数内で使用されている変数が、レキシカルスコープの変数の値を保持し続ける状態を言います。関数内で使用される変数や引数は、関数が終了した時点で破棄(ガベージコレクション)されます。

しかし、レキシカルスコープの変数が保持する値を使用している関数が戻り値として実行元に返された場合は、その値はガベージコレクションの対象とならず保持されます。

以下は、クロージャの動作を示す例です。

function outer(lastName) {
  function inner(firstName) {
    console.log(lastName + ":" + firstName);
  }
    
  return inner;
}

const closureFunc = outer("山田");
closureFunc("太郎"); // 山田:太郎

①まず、9行目でouter関数が実行され引数に”山田”が渡されます。

②outer関数の実行中にinner関数が宣言されます。inner関数ではlastNameが使われており、この参照先はレキシカルスコープのouter関数の引数であるlastNameの値の”山田”となります。

③次に、outer関数の戻り値として、inner関数が返されます。9行目のclosureFunc変数には、outer関数で宣言されたinner関数が代入されます。

④10行目でclosureFuncを実行すると、まず②の部分のinner関数が実行されます。inner関数ではlastNameを参照していますが、本来ならinner関数の実行終了時に、inner関数内のlastNameは破棄されます。しかし、inner関数のlastNameはレキシカルスコープの値を使用しており、さらにinner関数がouter関数の戻り値として使用されています。これは外部からinner関数が実行される可能性があります。そのためinner関数内のlastName(が参照する”山田”という値)は破棄対象にならず、保持し続けられます。この状態がクロージャです。

クロージャを利用することで処理内容の異なる関数を動的に作成できます。動的とは実行状況によって結果が変わる状態です。上のコード例の場合、outer関数に渡す引数により、outer関数ひとつで異なる挙動を取る関数を実質的に複数作成できていることになります。

また、クロージャを利用することで、継続的に値の状態を保存できるため、関数内で使う値の状態を保持し続けたい時に有用です。

以下がコード例です。

function outer() {
	let count = 0;
    function inner() {
        console.log(count++);
    }
    
    return inner;
}
const increment = outer();
increment(); // 0
increment(); // 1
increment(); // 2

コメント

タイトルとURLをコピーしました