本記事は『ステップアップJavaScript フロントエンド開発の初級から中級へ進むために』(サークルアラウンド株式会社、佐藤正志、小笠原寛)の「STEP4 押さえておくべきJavaScriptの言語特性について」から一部を抜粋したものです。掲載にあたって編集しています。
スコープ
スコープは多くの他の言語にも存在する「変数の有効範囲」のことです。JavaScriptは細かくスコープを扱える一方、少しの記述の抜け漏れでスコープの種類が変わる落とし穴的な挙動もあります。
さらにステップ7ではスコープに関連する特徴としてクロージャを学びますが、そのためにも前提知識となるスコープの理解を深めておきましょう。
JavaScriptには以下のようなスコープが存在します。
- グローバルスコープ
-
ローカルスコープ
- 関数スコープ
- ブロックスコープ
また、クラスに関係する変数もスコープに関連します。一般的には次のようなものがあります。
- インスタンス変数
- クラス変数(static変数)
※ES6レベルのJavaScriptには仕様が存在しないので参考にしてください。
簡単なプログラムを書いているうちは、スコープをそれほど意識しなくても思ったようなコードが書けることも多いです。複雑なコードを書くようになると、スコープを小さくすることでバグの原因を減らしたり、見通しを良くすることを意識する必要が出てきます。下に記した順にスコープが小さくなるので、特別な意図がない場合にはなるべく右側のスコープを心掛けると良いでしょう。
グローバルスコープ > クラス変数 > インスタンス変数 > 関数スコープ > ブロックスコープ
スコープの大きさと役割
スコープが大きいならば「その変数を変更できる場所」がよりたくさんあります。「コードを書いた人の意図に反し、変数の値が予想外の場所で変更されない」ことを判断する際に、スコープは重要な働きをしています。小さいスコープなら判断の間違いが少なくなりますよね。
ブロックスコープ
ブロックとは{}で囲まれたコード群を指します。ifやforと一緒に使っていますね。constやletで宣言された定数や変数はこのブロックスコープで扱われます(この後の説明ではまとめて「変数」と示しますがconstは正確には定数です)。
具体的にはリスト4-1-01のような動作になります。他のスコープに比べてチェックが厳密に行われることと、「ブロックの範囲」というコードを見た時にわかりやすいスコープであることが特徴です(デモはこちら)。
if(true) { const myBlockVar1 = 'myBlockVar1-true'; // これがブロックスコープの変数です console.log(myBlockVar1); } else { const myBlockVar1 = 'myBlockVar1-false'; // これがブロックスコープの変数です console.log(myBlockVar1); }
上記のコードのようにif-else文の二つのブロック内にmyBlockVar1という変数をそれぞれ宣言して利用することができます。コード中に二回myBlockVar1が出ていますが、それぞれ別の変数として扱われます(ifの条件がtrueのためロジックとして意味はありませんが、ブロックの性質の理解のためのサンプルです)。
また、ブロックは特にifやforなどと一緒に利用せずとも、プログラマが自由にロジック中に追加できます。リスト4-1-02の例を確認しましょう。ブロックがスコープの範囲なので、その外で変数を利用しようとするとエラーになります。
// ブロックは意図的に書くこともできます { const myBlockVar2 = 'myBlockVar2'; // これがブロックスコープの変数です console.log(myBlockVar2); } // console.log(myBlockVar2); // エラー:ブロックの外なので利用できません
ブロックスコープはES6で導入された新しいスコープの概念です。なるべくこのスコープの変数を活用すると堅牢なプログラムを書くことができます。
関数スコープ
関数の中が有効範囲になるスコープです。関数の中でvarで宣言される変数のみがこれに該当します(リスト4-1-03)。
function funcScope() { var myFuncVar1 = 'myFuncVar1'; // これが関数スコープの変数です console.log(myFuncVar1); } funcScope(); // console.log(myFuncVar1); // エラー:関数の外なのでmyFuncVar1は利用できません
古くはvarを利用するしかありませんでしたが、今後は新規でコードを書く際にはほとんど使われないでしょう。後に説明する「変数の巻き上げ」のような問題もあり、気をつけることが多いものでした。
グローバルスコープ
最も広いスコープで、プログラムのどこからでも参照できます。
関数に全く囲まれていない最上位の領域で変数を宣言したり、関数内でも変数を宣言する際にvarやconst、letなどを付けずに使うとグローバルスコープになります(リスト4-1-04)。
// 最上位(関数等で囲まれていない)でのvarはグローバルスコープに変数を宣言します var myGlobalVar = 'myGlobalVar'; // これもグローバルスコープに変数を宣言しています myGlobalVar1 = 'myGlobalVar1'; function myFunction1() { // 関数の中で初めて使いましたが、varやconstが付いていないのでグローバル変数です myGlobalVar2 = 'myGlobalVar2'; } console.log(myGlobalVar1); //console.log(myGlobalVar2); // まだ宣言していないのでここで呼ぶとエラー myFunction1(); // 関数の中でグローバル変数myGlobalVar2が宣言されます console.log(myGlobalVar2); // ここでmyGlobalVar2は利用できます
ブラウザにおけるグローバル変数はwindowオブジェクトのプロパティとしても操作できます。
console.log(window.myGlobalVar2);
意図していないグローバル変数
「varやconst、letの宣言を付けずに変数を利用するとグローバル変数になる」ので、付け忘れにより意図しない変数がグローバル変数になってしまう場合があり、バグの原因になることがよく知られています。
変数の巻き上げとブロックスコープ
関数スコープで特に覚えておきたい性質として「変数の巻き上げ」という動作があります。「同じ関数内で同名の変数を複数回宣言した場合に、同一の変数として扱われる」というものです。
varで変数宣言した場合のコードで巻き上げについて確認します(リスト4-1-05)。
function funcHoisting() { var myHoistingVar1 = 'myHoistingVar1'; ❶ console.log(myHoistingVar1); if(true) { var myHoistingVar1 = '変更! '; ❷ console.log(myHoistingVar1); } console.log(myHoistingVar1); // => "変更! " ❸ }
❶と❷でmyHoistingVar1という変数が宣言されていますが、どちらもfuncHoisting全体がスコープとなるので、同一の変数として扱われます。ただ、おそらくこのコードを書く人は❶と❷の変数は別のものとして考えているはずです。ところが❸では❶と同値だと思っていた値が変更されてしまっており、直感に反する動作になってしまいます。
同様のコードでletを利用するとリスト4-1-06のような動作になり、より厳密にチェックがなされます。
function blockHoisting() { let myHoistingVar1 = 'myHoistingVar1'; ❶ console.log(myHoistingVar1); if(true) { let myHoistingVar1 = '変更! '; ❷ console.log(myHoistingVar1); } // varの時には変更されましたが、ブロック変数なので影響を受けません console.log(myHoistingVar1); // => "myHoistingVar1" ❸ // let myHoistingVar1 = '重複'; // エラー: 同じスコープ内には同名の変数は作れません ❹ }
❶と❷の変数はそれぞれ別のものとして扱われます。❸での結果は❶で宣言した時の変数の内容のままです。さらに❹のように、同じスコープ内に同名の変数を宣言しようとするとエラーになります。今回の場合、変数の変更がされなかったのでconstを利用しても同様の結果が得られるでしょう。
constやletを利用すると「変数の巻き上げへの気配り」をしなくて済むようになります。利用できる場所ではできる限りこちらを活用しましょう(厳密に言うと巻き上げはあるのですが、気をつけて対応する必要がなくなるのです)。
コードの堅牢性を考えれば、以下の指針に従うとよりバグの少ない記述ができます。基本はconstを用いて、変数の指す内容が変更される場合にはletを選ぶと、varを使う機会はほとんどなくなるはずです。
const → let → var
var と変数の巻き上げ
varは古くからある変数宣言の方法ですが、巻き上げについては「気をつけなければいけないポイント」として以前から扱われてきました(特に例示したような「ブロックに囲まれあたかもスコープが違うように見える」場合に勘違いしやすいです)。後発のconstやletによる変数は、より厳密なチェックをされるなど、バグが起きにくい仕組みで実現されています。
スコープのまとめ
- スコープは変数の有効範囲のことです。
- JavaScriptは変数の宣言の方法により、ブロックスコープ(const/let)、関数スコープ(var)などスコープの範囲が変化します。
- varでは変数の巻き上げがバグの原因になることもありましたが、const/letを活用するとよりバグが発生しにくいプログラムを書けます。
本書の詳しい紹介やデモについて
本書の内容について、著者のサイトで詳しく紹介されています。また、書籍内で作成するアプリケーションのデモもダウンロードできますので、ぜひご覧ください。