私は初めて勉強したプログラミング言語が JavaScript だったのですが、スコープ、クロージャの概念についてはすごく理解に苦しんだ記憶があります。
今記事では JavaScript におけるスコープとクロージャとは何か? なんのために存在するのか? そして変数宣言の var
が非推奨となっている理由とスコープの関わりについてまとめていきます。
この記事で書いてある内容の要約
-
スコープ: 変数、関数が使用可能な範囲のこと。内側のスコープからは外側のスコープの値を参照することができる
- グローバルスコープ: 一番外側のスコープ。他のすべてのスコープからアクセス可能
- ローカルスコープ: 関数スコープとも。関数によるスコープであり、関数の内側、またはその関数から返されている関数からアクセスできるスコープ
- ブロックスコープ: if 文や for ループの中など、
{
と}
で囲まれた範囲であるブロックが作成するスコープ
-
クロージャ: 関数と親関数が持っている変数のセット
- JavaScript は静的(レキシカル)スコープであり、また内側から外側のスコープを見に行く性質を持つためクロージャが成立する
- クロージャを使う場面は主に以下3つ
- グローバル変数を減らす
- 変数の隠蔽
- 関数に状態を持たせる
-
var
非推奨の理由の1つとして、ブロックスコープにならない点が挙げられる
スコープとは
実行の現在のコンテキスト。値 と式が「見える」、または参照できる文脈。変数や他の式が "現在のスコープ内にない" 場合、使用できません。スコープを階層構造で階層化して、子スコープから親スコープにアクセスできるようにすることもできますが、その逆はできません。
プログラミングにおけるスコープ(英: scope, 可視範囲)とは、ある変数や関数などの名前(識別子)を参照できる範囲のこと。通常、変数や関数が定義されたスコープの外側からは、それらの名前を用いるだけでは参照できない。このときこれらの変数や関数は「スコープ外」である、あるいは「見えない」といわれる。
変数、関数が使用可能な範囲のことをスコープと言います。 JavaScript には3つのスコープがあり、それぞれ性質が異なります。
- グローバルスコープ: 一番外側のスコープ。他のすべてのスコープからアクセス可能
- ローカルスコープ: 関数スコープとも。関数によるスコープであり、関数の内側、またはその関数から返されている関数からアクセスできるスコープ
- ブロックスコープ: if 文や for ループの中など、
{
と}
で囲まれた範囲であるブロックが作成するスコープ
参考: 関数とスコープ · JavaScript Primer #jsprimer
表にまとめるとこのような感じでしょうか。
スコープ名 | 性質 | アクセス可能範囲 |
グローバルスコープ | 一番外側に常に存在する | どこからでも |
ローカルスコープ | 関数によって作成される | 1. 関数内 2. 関数から返されている関数内 |
ブロックスコープ | if 文や for ループの中など、 { と } で囲まれた範囲であるブロックに作成される |
ブロック内 |
日常生活に例えてみましょう。
グローバルスコープはパブリックな場であり、誰でも歩くことができます。そこに設置されているベンチや、建設されている公園は自由に使うことができますね。
ローカルスコープは自宅であり、家族や招き入れた人といった特定の人は入ることができます。外側にいる人が勝手に入ってきたらお巡りさんを呼ばなければいけませんが、家の中から外を見るのは自由です。
ブロックスコープは自分の部屋であり、権利は自分に所属します。びっくりしちゃうので勝手に入ってこないでほしいし、部屋にあるものは使われたくないですね。ただ部屋の中にいれば家にあるものを使うことができるし、外を眺めることもできます。
スコープの実例
ではコードで例を見ていきましょう。
// 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 では、関数が作成されるたびにクロージャが作成されます。
うーん、不可解🙃
よく分からないので早速ですがコードの例を見てみましょう。
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を返している外側の関数が持っている変数
上のコード例であれば、
-
createCounter
によって返されている関数 counter
のことです。
createCounter
によって返されている関数は、返された後でも createCounter
が持っている変数の値を使うことができます。
これが「クロージャは、組み合わされた(囲まれた)関数と、その周囲の状態(レキシカル環境)への参照の組み合わせです。」の意味するところですね。
なぜクロージャが成立するのか
JavaScript のクロージャは2つの要因から成り立っています。
- JavaScript のスコープは定義した時点でスコープが決まる静的スコープである
- JavaScript は内側のスコープから外側のスコープを見に行くという性質がある
静的スコープについて
スコープには静的スコープ(レキシカルスコープ)と動的スコープ(ダイナミックスコープ)の2種類があります。JavaScript が採用しているのは前者です。
動的スコープは扱いが難しくモダンな言語ではあまり採用されないようなので詳しい説明は省きますが(正直さっぱり理解できず😢)私のざっくり理解では以下のような違いがあるようです。
- 静的スコープ: 書いたコードが読み込まれた時点でスコープが決定し、いつどこで呼び出されても同じ結果となる
- 動的スコープ: スコープが実行時に決まるので呼び出される場所によって動作が異なる
JavaScript が動的スコープを採用していた場合、「関数から返される関数は 常に 外側の関数の変数を使える」という前提が成り立たなくなります。
スコープの性質について
JavaScript では内側のスコープから外側のスコープを覗きに行けるというのは前述した通りです。
この働きがなければ、そもそも外側の関数が持っている変数にアクセスできないですね。
どこでクロージャを使うのか
- グローバル変数を減らす
- 変数の隠蔽
- 関数に状態を持たせる
まず関数内で使う変数を親関数の中に入れておくことでグローバル変数を減らせますね。
また関数の中に入れておいた変数は外側のスコープからアクセスできないため、間違って操作するリスクを減らすことにもつながります。
そして関数に状態を持たせることもできるんです。クロージャの例に出していた先ほどのコードを見返してみましょう。
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
から返されている関数は、変数 counter1
、 counter2
に格納されています。counter1
と counter2
はどちらも 0 からスタートしており、続きのカウンターではなく新しいカウンターとして作成できていることがわかるかと思います。
これは createCounter
関数を動作させるたびに新しく関数内の変数 counter
が作成されているためです。このように関数に状態を持たせることもできます。
クロージャのまとめ:
- クロージャ: 関数と親関数が持っている変数のセット
- JavaScript は静的(レキシカル)スコープであり、また内側から外側のスコープを見に行く性質を持つためクロージャが成立する
- クロージャを使う場面は主に以下3つ
- グローバル変数を減らす
- 変数の隠蔽
- 関数に状態を持たせる
なぜ JavaScript で var は非推奨なのか
JavaScript には 3 つの変数宣言方法がありますね。
let
const
var
let
と const
は ES6 で追加された新しい変数宣言の方法であり、それまでは var
が使われていました。しかし現在 var
は非推奨とまで言われています。なぜでしょう?
理由は var
の性質がちょっと厄介だからです。
- 巻き上げされる
- 重複した宣言が可能
- ブロックスコープにならない
このうち、今回は「ブロックスコープにならない」について深掘っていきます。
例えば 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つ
- グローバル変数を減らす
- 変数の隠蔽
- 関数に状態を持たせる
-
var
非推奨の理由の1つとして、ブロックスコープにならない点が挙げられる