【JavaScript】スコープとクロージャ


この記事ではJavaScriptを書いていく上で最も重要な知識と言っても過言ではない、
スコープクロージャの概念や使い方について説明していけたらと思います。

「なんとなく分かるし、なんとなく使っているけど説明はできない....」って人少なくないと思います。
この機会に「なんとなく分かる、なんとなく使える」から一緒に抜け出しましょう。

スコープとは

スコープとは、名前(変数や関数)の有効範囲のことです。

スコープには、グローバルスコープとローカルスコープが存在し、
ローカルスコープには、ブロックスコープと関数スコープが存在します。

  • グローバルスコープ
  • ローカルスコープ
    • ブロックスコープ
    • 関数スコープ

グローバルスコープ

グローバルスコープとは、グローバルコンテキスト内で宣言された名前(変数や関数)がもつスコープのことです。
所謂グローバル変数やグローバル関数のことです。

var a = 0; // グローバル変数a
function b() {  // グローバル関数b
  console.log('fn is called');
};

console.log(a); // 0
b(); // 'b is called'
console.log(window.a); // 0
window.b(); //'b is called'

グローバル変数を定義するということは、windowオブジェクトにプロパティを追加することになります。

スクリプトスコープ

グローバルコンテキストで、letやconstを用いて変数を宣言をした場合には、
windowオブジェクトにプロパティを追加しないため以下のようになります。

let a = 0;
const b = 0;
console.log(a); // 0
console.log(window.a); // undefined
console.log(b); // 0
console.log(window.b); //undefined

このようにletconstを用いて変数を宣言をすると、スクリプトスコープというスコープを持つことになります。
なのでwindowオブジェクトのプロパティとして呼び出しても、undefinedが返ってきます。

ローカルスコープ

ローカルスコープには関数スコープとブロックスコープが存在します。

関数スコープ

関数スコープとは、関数で囲まれた { }(波カッコ)のスコープのことです。

function fn() { 
  let a = 0;
  console.log(a); // 0が出力
}
console.log(a); // a is not define

関数内で変数aを呼び出すと正しく0が出力されますが、関数外で変数aを呼び出そうとするとエラーが発生します。
これは、変数aは関数スコープ内でのみ参照可能だということです。

ブロックスコープとは

ブロックスコープとは、{ }(波カッコ)で囲まれた範囲のことです。

{
  var a = 0;
  let b = 0;
  function c() {
    let d = 0;
    console.log(d)
  }
  console.log(a); // 0
  console.log(b); // 0
  c(); // 0
}
console.log(a); // 0
console.log(b); // b is not defined
c(); // 0

変数を{ }の中で宣言することでブロックスコープを持つことができます。
これは関数スコープと同様に{ }の中で宣言された変数は、{ }の中でのみ参照可能だということです。

しかしvarや関数はブロックスコープを無視するので、外から呼び出しても正しい値が出力されてしまいます。
外部からのアクセスを防止するにはletまたはconstで定義しましょう。

また関数は関数式で定義することによってブロックスコープを持つことができます。

{
  const c = fn() {
    console.log('function is called');
  }
}
c(); // Uncaught Reference Error: call is not defineds

このブロックスコープはif文やfor文などで使いますので、if文やfor文で変数宣言をする場合には気をつけましょう。

レキシカルスコープ

レキシカルスコープとは、実行中のコードから見た外部スコープのことです。
コードを書く場所によって参照できる変数が変わるのが特徴です。
これはコードを記述した時点でスコープが決定されるため「静的スコープ」とも言います。

var a = 1; 
function fn1(){
  console.log(a);  // 1
}
function fn2(){
  var a = 10;  
  fn1();
}
fn1(); // 1
fn2(); // 1

この場合、関数fn2では変数aの値が再代入されているので、関数fn2は10を結果として返してくる思うかもしれません。
しかし、レキシカルスコープでは関数fn1で出力する変数aのスコープは、fn1が定義された時点で決定し、そのまま保持されます。
つまり、呼び出し元の関数fn2で新たに変数aが定義されていたとしても、関数fn1が定義された時点で参照していた変数aが出力されます。

スコープチェーン

スコープチェーンとは、スコープが多階層になっている状態のことです。

let a = 1;
function fn1() {
  let b = 2;
  function fn2() {
    let c = 3;  
  }
  fn2();
}
fn1();

上記コードを図解で確認してみましょう。

このように、スコープチェーンは関数内に変数が見つからなかった場合に、外側のスコープに変数を探しにいく仕組みです。

var a = 1;
function fn1() {
  var a = 2;
  function fn2() {
    var a = 3;
    console.log(a); // 3が出力
  }
  fn2();
}
fn1();

この場合、関数fn2内に変数aが定義されているため、結果は3が出力されます。

let a = 1;
function fn1() {
  let a = 2;
  function fn2() {
    console.log(a); // 2が出力
  }
  fn2();
}
fn1();

この場合、関数fn2内に変数aが定義されていないので外側のスコープを変数を探しにいきます。
そして、関数fn1内に変数aが定義されているので、結果は2が出力されます。

では、グローバルコンテキスト内にwindowオブジェクトで生成したプロパティaがあった場合はどちらを取得するでしょうか。

let a = 1;
window.a = 4
function fn1() {
  console.log(a); // 1が出力
}
fn1();

結果は1が出力されました。
このことからグローバルスコープはスクリプトスコープよりも外側のスコープであることがわかります。

クロージャ

クロージャとは、レキシカルスコープの変数を関数が使用している状態のことをいいます。

function fn1() {
  let a = 1; 
  function fn2() { // 外側のスコープで定義された変数を使用している。これをクロージャと言います
    console.log(a); 
  }
  fn2();
}
fn1();

関数incrementを実行すると1ずつカウントアップしていく関数を作りながらプライベート変数について学んでいきましょう。

let num = 0;
function increment() {
  num = num + 1;
  console.log(num);
}
increment(); // 1
increment(); // 2
increment(); // 3

このように記述することで関数incrementを呼び出すことでカウントアップする機能ができました。
しかしこのままでは、変数numはグローバル変数のため、どこからでも値を変更することができるようになっています。

そこでクロージャーの仕組みを利用したプライベート変数を作成します。

function incrementFactory() {
  let num = 0;
  return function() {
    num = num + 1;
    console.log(num);
  }
}
const increment = incrementFactory(); 
increment(); // 1
increment(); // 2
increment(); // 3

まず変数numをincrementFactory関数内で定義します。
そしてカウントアップさせるための関数を定義し、その関数を呼び出し元に返すという動きになります。

変数numをincrementFactory関数内で定義することで、関数内で変数を定義すると関数スコープが生成され、グローバルスコープからは値が参照できなくなります。したがって外部からのアクセスはできなくなるということですね。
またincrementFactory関数を実行して変数に代入しているので、再びincrementFactoryが呼ばれるまでは変数numは初期化されません。

これがクロージャーを用いたプライベート変数の仕組みです。

以上

宣伝

自分のTwitterでも有益な情報を発信していますので、興味があればフォローしていただけると幸いです。