JavaScriptのスコープ総まとめ 第1回 スコープの種類とその基本
JavaScriptのスコープとは、変数がどの場所から参照できるのかを定義する概念です。まずはその種類をおさらいし、スコープの基礎を学んでみましょう。
- カテゴリー
- JavaScript >
- ECMAScript
発行
スコープの役割
JavaScriptにおけるスコープとは、変数がどの場所から参照できるのかを定義する概念です。言い換えれば、変数の有効範囲ということです。同じスコープ上にある変数にはアクセスできますが、スコープが違えば、別々のスコープにある変数にはお互いにアクセスすることができません。
それでは、なぜスコープが必要なのでしょうか。スコープには、次のような役割があります。
変数名の競合を避ける
JavaScriptでは関数やブロックごとに別々のスコープが作られるため、プログラム内で同じ名前の変数があってもスコープが違えば別物となります。
もしスコープがなければ、プログラム全体で使われるすべての変数に、一意な名前を付けて衝突を避けなければならなくなります。ですがJavaScriptにはスコープがありますので、その必要はありません。
メモリの消費を避ける
JavaScriptには、使われなくなったメモリ領域を自動的に解放するガベージコレクションという仕組みがあります。これによって、無駄なメモリの消費を回避しています。
もしスコープがなければ、すべての変数がグローバルに属することになります。そして、グローバルに属する変数はプログラムから参照され続けるため、ガベージコレクションされません。つまり、ページを閉じるまでの間ずっと、不要なメモリ領域を確保し続けてしまいます。
実際にはスコープがあるため、関数の実行が終われば、そのスコープに属する変数は不要とみなされ、ガベージコレクションの対象となります。
スコープの種類
スコープは、グローバルスコープとローカルスコープの2種類が存在します。さらに、ローカルスコープは、関数スコープとブロックスコープに分類できます。
├── グローバルスコープ
└── ローカルスコープ
├──関数スコープ
└──ブロックスコープ
グローバルスコープ
プログラムのトップレベルで宣言された変数は、グローバル変数となり、プログラム全体のどこからでもアクセス可能なグローバルスコープを持ちます。
var scope = 'global';
// トップレベルからのアクセス
console.log(scope); // -> global
// 関数内からのアクセス
(function () {
console.log(scope); // -> global
})();
ローカルスコープ
グローバル変数以外のすべての変数は、ローカルスコープを持つローカル変数です。先述したように、関数スコープとブロックスコープの2つに分けることができます。
関数スコープ
関数(function
)ごとに作られるスコープのことを、関数スコープといいます。
関数スコープ内でvar
、let
、const
のいずれかで変数宣言をすると、関数の外部からはアクセスできず、関数の内側からのみ利用可能なローカル変数になります。また、関数の仮引数も同じく関数スコープを持ちます。
function fn(arg) {
// fn関数の関数スコープを持つローカル変数を定義
var scope = 'local';
// fn関数のスコープ内のためアクセス可能
console.log(scope); // -> local
console.log(arg); // -> argument
}
fn('argument');
// 関数スコープの外側からはローカル変数や仮引数にアクセスできない
console.log(scope); // -> ReferenceError
console.log(arg); // -> ReferenceError
ブロックスコープ
ブロック({}
)ごとに作られるスコープのことを、ブロックスコープといいます。
ブロックスコープ内でlet
またはconst
を用いて宣言した変数は、ブロックの外側からはアクセスできず、ブロックの内側からのみアクセス可能なローカル変数になります。
function fn() {
// for文のブロックスコープを持つローカル変数 i を定義
for (let i = 0; i < 3; i++) {
console.log(i); // -> 0, 1, 2
}
// for文のブロックスコープの外側からはアクセスできない
console.log(i); // -> ReferenceError
}
fn();
ES2015からのローカルスコープ
JavaScriptには当初、グローバルスコープと関数スコープしか存在しなかったため、「関数スコープ=ローカルスコープ」という図式が成り立っており、そのように解説している媒体がほとんどです。
しかし、ES2015でブロックスコープが追加されたことにより、ローカルスコープには関数スコープとブロックスコープの2種類が存在するようになりました。そのため、この記事中でローカルスコープと記載するときには、関数スコープとブロックスコープ両方を含む意味として使用します。次の記事も参考にしてください。
宣言文とスコープ
関数の仮引数を含む、各種変数の宣言文が生成するスコープは次のようになります。
宣言 | 関数スコープ(function )を生成する |
ブロックスコープ({} )を生成する |
---|---|---|
関数の仮引数 | ○ | x |
var |
○ | x |
let |
○ | ○ |
const |
○ | ○ |
関数の仮引数とvar
は関数スコープだけを生成し、let
とconst
は関数スコープとブロックスコープの両方を生成します。
ここまでは、各スコープの概要について簡単に紹介しました。次節からは、それぞれのスコープの仕様に基づき、スコープについて知っておきたい基礎的なことをまとめていきます。
グローバルスコープとwindowオブジェクト
グローバル変数を宣言するというのは、実際にはグローバルオブジェクト(ブラウザの場合はwindow
オブジェクト)のプロパティを追加することになります。
そのため、グローバル変数は、window
オブジェクトのプロパティとしてアクセスできます。
var scope = 'global';
// windowオブジェクトのプロパティとして追加される
console.log(window.scope); // -> global
なお、let
やconst
をトップレベルで宣言した場合、グローバルスコープにはなるのですが、window
オブジェクトのプロパティには追加されません。
let scope = 'global';
// グローバルスコープを持つため、どこからもアクセス可能
console.log(scope); // -> global
// windowオブジェクトのプロパティには追加されない
console.log(window.scope); // -> undefined
宣言文の省略とスコープ
宣言文(var
、let
、const
)を付けずに変数宣言すると、どのスコープ内で宣言しても、自動的にグローバルスコープとなってしまいます。
function fn() {
// 関数スコープの中で宣言文を付けずに変数宣言
scope = 'local';
}
fn();
// グローバルスコープとなり、関数スコープの外からアクセスできてしまう
console.log(scope); // -> local
これにより、意図せぬバグを生む可能性があるため、変数宣言では必ず宣言文を付ける*ようにしましょう。
*注:strictモードでの変数宣言
ECMAScript 5のstrictモード('use strict';
を指定したJavaScriptコード)では、宣言文を省略した変数宣言をした場合、ReferenceErrorとなります。
変数宣言の巻き上げ(ホイスティング)
関数スコープの中で変数宣言を行うと、変数は関数の先頭に引き上げられます。このことを巻き上げやホイスティングといいます。
これがどういうことなのか、具体例と一緒に解説していきます。次のコードでは、scope
変数を宣言するより前に、console.log
でscope
変数を参照しようとしています。コンソールに何が表示されるか想像してみてください。
function fn() {
console.log(scope); // -> ???
var scope = 'local';
}
fn();
普通に考えれば、変数宣言より前に参照しようとしているのですから、エラーになってしまいそうです。しかし、実際にはundefined
が返されます。
その理由は、スコープの途中で宣言したローカル変数の宣言部分が、スコープの先頭に巻き上げられたからです。
ちょっとわかりづらいかもしれませんが、先ほどのコードが、次のように書き変わったとイメージすると理解しやすいかもしれません。
function fn() {
// 変数宣言だけ先頭に巻き上げられる
var scope;
console.log(scope); // -> undefined
// 値の代入は元の場所で行われる
scope = 'local';
}
fn();
巻き上げられるのは宣言だけであり、値の代入自体は元の場所で行われるため、console.log
を行った時点では、scope
はundefined
を返します。
なお、let
とconst
の場合は、ブロックの先頭に巻き上げはされるのですが、宣言より前で変数にアクセスしようとすると、ReferenceErrorとなってしまいます。このアクセスできない領域をtemporal dead zoneと言います。
巻き上げによる混乱を避けるために、変数宣言はスコープの先頭にまとめて宣言するようにしましょう。
任意の場所でブロックスコープを生成
ブロックスコープは、ブロック({}
)を必要とする文で生成されます。たとえば、if
、else
、for
、for...in
、while
、switch
などです。
if (window) {
let scope = 'local';
console.log(scope); // -> local
}
// ブロックスコープ外のためアクセス不可
console.log(scope); // -> ReferenceError
実は、このような文を使わなくても、ブロック({}
)単体を用いるだけでスコープを作ることもできます。
{
let scope = 'local';
console.log(scope); // -> local
}
// ブロックスコープ外のためアクセス不可
console.log(scope); // -> ReferenceError
これにより、任意の場所でスコープを作りたいがために即時関数を使うといったような、わかりづらいコードを書く必要がなくなります。
まとめ
今回は、スコープの種類とそれぞれのスコープで知っておきたいことについて解説しました。
次回以降は、プログラム全体から見てスコープを理解していきます。入れ子になったスコープの関係や、親子関係のスコープ上に同じ変数名がある場合にどうなるのか、その解決の仕組みなどについて解説します。