LoginSignup
6
1

More than 1 year has passed since last update.

JavaScript のスコープ、クロージャとは? & var 非推奨との関わり

Last updated at Posted at 2022-12-02

私は初めて勉強したプログラミング言語が JavaScript だったのですが、スコープ、クロージャの概念についてはすごく理解に苦しんだ記憶があります。

今記事では JavaScript におけるスコープとクロージャとは何か? なんのために存在するのか? そして変数宣言の var が非推奨となっている理由とスコープの関わりについてまとめていきます。

この記事で書いてある内容の要約

  • スコープ: 変数、関数が使用可能な範囲のこと。内側のスコープからは外側のスコープの値を参照することができる

    • グローバルスコープ: 一番外側のスコープ。他のすべてのスコープからアクセス可能
    • ローカルスコープ: 関数スコープとも。関数によるスコープであり、関数の内側、またはその関数から返されている関数からアクセスできるスコープ
    • ブロックスコープ: if 文や for ループの中など、 {} で囲まれた範囲であるブロックが作成するスコープ
  • クロージャ: 関数と親関数が持っている変数のセット

    • JavaScript は静的(レキシカル)スコープであり、また内側から外側のスコープを見に行く性質を持つためクロージャが成立する
    • クロージャを使う場面は主に以下3つ
      1. グローバル変数を減らす
      2. 変数の隠蔽
      3. 関数に状態を持たせる
  • var 非推奨の理由の1つとして、ブロックスコープにならない点が挙げられる

スコープとは

実行の現在のコンテキスト。値 と式が「見える」、または参照できる文脈。変数や他の式が "現在のスコープ内にない" 場合、使用できません。スコープを階層構造で階層化して、子スコープから親スコープにアクセスできるようにすることもできますが、その逆はできません。

Scope (スコープ) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN

プログラミングにおけるスコープ(英: scope, 可視範囲)とは、ある変数や関数などの名前(識別子)を参照できる範囲のこと。通常、変数や関数が定義されたスコープの外側からは、それらの名前を用いるだけでは参照できない。このときこれらの変数や関数は「スコープ外」である、あるいは「見えない」といわれる。

スコープ (プログラミング) - Wikipedia

変数、関数が使用可能な範囲のことをスコープと言います。 JavaScript には3つのスコープがあり、それぞれ性質が異なります。

  • グローバルスコープ: 一番外側のスコープ。他のすべてのスコープからアクセス可能
  • ローカルスコープ: 関数スコープとも。関数によるスコープであり、関数の内側、またはその関数から返されている関数からアクセスできるスコープ
  • ブロックスコープ: if 文や for ループの中など、 {} で囲まれた範囲であるブロックが作成するスコープ

参考: 関数とスコープ · JavaScript Primer #jsprimer

表にまとめるとこのような感じでしょうか。

スコープ名 性質 アクセス可能範囲
グローバルスコープ 一番外側に常に存在する どこからでも
ローカルスコープ 関数によって作成される 1. 関数内
2. 関数から返されている関数内
ブロックスコープ if 文や for ループの中など、 {} で囲まれた範囲であるブロックに作成される ブロック内

日常生活に例えてみましょう。

グローバルスコープはパブリックな場であり、誰でも歩くことができます。そこに設置されているベンチや、建設されている公園は自由に使うことができますね。

ローカルスコープは自宅であり、家族や招き入れた人といった特定の人は入ることができます。外側にいる人が勝手に入ってきたらお巡りさんを呼ばなければいけませんが、家の中から外を見るのは自由です。

ブロックスコープは自分の部屋であり、権利は自分に所属します。びっくりしちゃうので勝手に入ってこないでほしいし、部屋にあるものは使われたくないですね。ただ部屋の中にいれば家にあるものを使うことができるし、外を眺めることもできます

scope.png

スコープの実例

ではコードで例を見ていきましょう。


// Global Scope

let username = "";
let password = "";

function checkNameAndPwd() {
  // Local Scope
  console.log("username:", username);
  console.log("password:", password);
}

checkNameAndPwd();

function changeNameAndPwd(newUsername, newPassword) {
  // Local Scope
  if(!newUsername || !newPassword) {
    // Block Scope
    console.log(`invalid username or password.\nname: ${newUsername}\npass: ${newPassword}`);
    return;
  } 
  username = newUsername;
  password = newPassword;
  return;
}

changeNameAndPwd("", "helloworld");
/* 
→ invalid username or password.
     name: 
     pass: helloworld
*/

changeNameAndPwd("kotaro", "helloworld");
checkNameAndPwd()
/*

kotaro
helloworld

*/

グローバルスコープに位置している変数、

  • username
  • password

の2つはどこからでもアクセス可能です。

ローカルスコープに位置している変数、

  • newUsername(引数)
  • newPassword(引数)

は関数内であればどこからでも使用可能であり、if 文中の console.log でも使用しています。

スコープのまとめ :

  • スコープ: 変数、関数が使用可能な範囲のこと。内側のスコープからは外側のスコープの値を参照することができる
    • グローバルスコープ: 一番外側のスコープ。他のすべてのスコープからアクセス可能
    • ローカルスコープ: 関数スコープとも。関数によるスコープであり、関数の内側、またはその関数から返されている関数からアクセスできるスコープ
    • ブロックスコープ: if 文や for ループの中など、 { と } で囲まれた範囲であるブロックが作成するスコープ

クロージャとは

クロージャはローカルスコープ(関数スコープ)に関わりの深い概念です。

クロージャは、組み合わされた(囲まれた)関数と、その周囲の状態(レキシカル環境)への参照の組み合わせです。言い換えれば、クロージャは内側の関数から外側の関数スコープへのアクセスを提供します。JavaScript では、関数が作成されるたびにクロージャが作成されます。

クロージャ - JavaScript | MDN

うーん、不可解🙃
よく分からないので早速ですがコードの例を見てみましょう。


function createCounter(start) {
  let counter = start;
  return function() {
    return counter++
  }
}

const counter1 = createCounter(0);
console.log(counter1()); // -> 0
console.log(counter1()); // -> 1
console.log(counter1()); // -> 2

const counter2 = createCounter(0);
console.log(counter2()); // -> 0
console.log(counter2()); // -> 1
console.log(counter2()); // -> 2

上のカウンターの例を分解して考えていきます。

まず JavaScript の関数は 第一級オブジェクト であり、他の関数の引数に渡したり、返り値として返したりできます。

今回は関数 createCounter を用意し、 createCounter から関数を返すというプログラムを組みました。返されている無名関数は createCounter 内で作成された変数、 counter を使用しています。
先ほどのスコープの例で、関数内で作成された変数に関数内の if 文がアクセスしていたのを覚えているでしょうか?
今回の例では関数の中に関数がある形ですが、 同様に 内側の関数は外側の関数に内包されており、外側の関数のスコープにもアクセスできます

実行していただけると分かるのですが、エラーは出ず動作します。

これが MDN に書かれていた「クロージャは内側の関数から外側の関数スコープへのアクセスを提供します」という動作です。

そしてクロージャには2つの要素があります。

  1. 返される関数
  2. 1を返している外側の関数が持っている変数

上のコード例であれば、

  1. createCounter によって返されている関数
  2. counter

のことです。
createCounter によって返されている関数は、返された後でも createCounter が持っている変数の値を使うことができます。

これが「クロージャは、組み合わされた(囲まれた)関数と、その周囲の状態(レキシカル環境)への参照の組み合わせです。」の意味するところですね。

なぜクロージャが成立するのか

JavaScript のクロージャは2つの要因から成り立っています。

  1. JavaScript のスコープは定義した時点でスコープが決まる静的スコープである
  2. JavaScript は内側のスコープから外側のスコープを見に行くという性質がある

静的スコープについて

スコープには静的スコープ(レキシカルスコープ)と動的スコープ(ダイナミックスコープ)の2種類があります。JavaScript が採用しているのは前者です。

動的スコープは扱いが難しくモダンな言語ではあまり採用されないようなので詳しい説明は省きますが(正直さっぱり理解できず😢)私のざっくり理解では以下のような違いがあるようです。

  • 静的スコープ: 書いたコードが読み込まれた時点でスコープが決定し、いつどこで呼び出されても同じ結果となる
  • 動的スコープ: スコープが実行時に決まるので呼び出される場所によって動作が異なる

JavaScript が動的スコープを採用していた場合、「関数から返される関数は 常に 外側の関数の変数を使える」という前提が成り立たなくなります。

スコープの性質について

JavaScript では内側のスコープから外側のスコープを覗きに行けるというのは前述した通りです。
この働きがなければ、そもそも外側の関数が持っている変数にアクセスできないですね。

どこでクロージャを使うのか

  1. グローバル変数を減らす
  2. 変数の隠蔽
  3. 関数に状態を持たせる

まず関数内で使う変数を親関数の中に入れておくことでグローバル変数を減らせますね。
また関数の中に入れておいた変数は外側のスコープからアクセスできないため、間違って操作するリスクを減らすことにもつながります。

そして関数に状態を持たせることもできるんです。クロージャの例に出していた先ほどのコードを見返してみましょう。


function createCounter(start) {
  let counter = start;
  return function() {
    return counter++
  }
}

const counter1 = createCounter(0);
console.log(counter1()); // -> 0
console.log(counter1()); // -> 1
console.log(counter1()); // -> 2

const counter2 = createCounter(0);
console.log(counter2()); // -> 0
console.log(counter2()); // -> 1
console.log(counter2()); // -> 2

createCounter から返されている関数は、変数 counter1counter2 に格納されています。counter1counter2 はどちらも 0 からスタートしており、続きのカウンターではなく新しいカウンターとして作成できていることがわかるかと思います。

これは createCounter 関数を動作させるたびに新しく関数内の変数 counter が作成されているためです。このように関数に状態を持たせることもできます。

クロージャのまとめ:

  • クロージャ: 関数と親関数が持っている変数のセット
    • JavaScript は静的(レキシカル)スコープであり、また内側から外側のスコープを見に行く性質を持つためクロージャが成立する
    • クロージャを使う場面は主に以下3つ
      1. グローバル変数を減らす
      2. 変数の隠蔽
      3. 関数に状態を持たせる

なぜ JavaScript で var は非推奨なのか

JavaScript には 3 つの変数宣言方法がありますね。

  • let
  • const
  • var

letconst は ES6 で追加された新しい変数宣言の方法であり、それまでは var が使われていました。しかし現在 var は非推奨とまで言われています。なぜでしょう?

理由は var の性質がちょっと厄介だからです。

  1. 巻き上げされる
  2. 重複した宣言が可能
  3. ブロックスコープにならない

このうち、今回は「ブロックスコープにならない」について深掘っていきます。

例えば var を用いて for ループを作成するとしましょう。すると以下のように for ループの中で使っていたはずの i がループ外からでも参照できてしまいます。


for(var i = 0; i < 3; i++) {
  console.log("for ループの中:", i);
}

console.log("for ループの外:", i);

/*

-> for ループの中: 0
   for ループの中: 1
   for ループの中: 2
   for ループの外: 3

*/

let で宣言した場合にはこのようなことは起こりません。

for(let i = 0; i < 3; i++) {
  console.log("for ループの中:", i);
}

console.log("for ループの外:", i);

/*

-> for ループの中: 0
   for ループの中: 1
   for ループの中: 2
   Uncaught ReferenceError: i is not defined

*/

今回はループでしか使わなそうな i という名前の変数だったのでさしたる影響はなさそうですが、変数がブロックスコープの中に収まってくれなければ変数の管理が大変になるのは間違いないでしょう。

let const のありがたさがわかりますね 👼

まとめ

  • スコープ: 変数、関数が使用可能な範囲のこと。内側のスコープからは外側のスコープの値を参照することができる

    • グローバルスコープ: 一番外側のスコープ。他のすべてのスコープからアクセス可能
    • ローカルスコープ: 関数スコープとも。関数によるスコープであり、関数の内側、またはその関数から返されている関数からアクセスできるスコープ
    • ブロックスコープ: if 文や for ループの中など、 {} で囲まれた範囲であるブロックが作成するスコープ
  • クロージャ: 関数と親関数が持っている変数のセット

    • JavaScript は静的(レキシカル)スコープであり、また内側から外側のスコープを見に行く性質を持つためクロージャが成立する
    • クロージャを使う場面は主に以下3つ
      1. グローバル変数を減らす
      2. 変数の隠蔽
      3. 関数に状態を持たせる
  • var 非推奨の理由の1つとして、ブロックスコープにならない点が挙げられる

参考資料

6
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1