12周年記念パーティ開催! 2024/5/10(金) 19:00

JavaScriptのスコープ総まとめ 第1回 スコープの種類とその基本

JavaScriptのスコープとは、変数がどの場所から参照できるのかを定義する概念です。まずはその種類をおさらいし、スコープの基礎を学んでみましょう。

発行

著者 山田 敬美 フロントエンド・エンジニア
JavaScriptのスコープ総まとめ シリーズの記事一覧

スコープの役割

JavaScriptにおけるスコープとは、変数がどの場所から参照できるのかを定義する概念です。言い換えれば、変数の有効範囲ということです。同じスコープ上にある変数にはアクセスできますが、スコープが違えば、別々のスコープにある変数にはお互いにアクセスすることができません。

それでは、なぜスコープが必要なのでしょうか。スコープには、次のような役割があります。

変数名の競合を避ける

JavaScriptでは関数やブロックごとに別々のスコープが作られるため、プログラム内で同じ名前の変数があってもスコープが違えば別物となります。

もしスコープがなければ、プログラム全体で使われるすべての変数に、一意な名前を付けて衝突を避けなければならなくなります。ですがJavaScriptにはスコープがありますので、その必要はありません。

メモリの消費を避ける

JavaScriptには、使われなくなったメモリ領域を自動的に解放するガベージコレクションという仕組みがあります。これによって、無駄なメモリの消費を回避しています。

もしスコープがなければ、すべての変数がグローバルに属することになります。そして、グローバルに属する変数はプログラムから参照され続けるため、ガベージコレクションされません。つまり、ページを閉じるまでの間ずっと、不要なメモリ領域を確保し続けてしまいます。

実際にはスコープがあるため、関数の実行が終われば、そのスコープに属する変数は不要とみなされ、ガベージコレクションの対象となります。

スコープの種類

スコープは、グローバルスコープとローカルスコープの2種類が存在します。さらに、ローカルスコープは、関数スコープとブロックスコープに分類できます。

├── グローバルスコープ
└── ローカルスコープ
    ├──関数スコープ
    └──ブロックスコープ

グローバルスコープ

プログラムのトップレベルで宣言された変数は、グローバル変数となり、プログラム全体のどこからでもアクセス可能なグローバルスコープを持ちます。

var scope = 'global';

// トップレベルからのアクセス
console.log(scope); // -> global

// 関数内からのアクセス
(function () {
  console.log(scope); // -> global
})();

ローカルスコープ

グローバル変数以外のすべての変数は、ローカルスコープを持つローカル変数です。先述したように、関数スコープとブロックスコープの2つに分けることができます。

関数スコープ

関数(function)ごとに作られるスコープのことを、関数スコープといいます。

関数スコープ内でvarletconstのいずれかで変数宣言をすると、関数の外部からはアクセスできず、関数の内側からのみ利用可能なローカル変数になります。また、関数の仮引数も同じく関数スコープを持ちます。

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は関数スコープだけを生成し、letconstは関数スコープとブロックスコープの両方を生成します。

ここまでは、各スコープの概要について簡単に紹介しました。次節からは、それぞれのスコープの仕様に基づき、スコープについて知っておきたい基礎的なことをまとめていきます。

グローバルスコープとwindowオブジェクト

グローバル変数を宣言するというのは、実際にはグローバルオブジェクト(ブラウザの場合はwindowオブジェクト)のプロパティを追加することになります。

そのため、グローバル変数は、windowオブジェクトのプロパティとしてアクセスできます。

var scope = 'global';

// windowオブジェクトのプロパティとして追加される
console.log(window.scope); // -> global

なお、letconstをトップレベルで宣言した場合、グローバルスコープにはなるのですが、windowオブジェクトのプロパティには追加されません。

let scope = 'global';

// グローバルスコープを持つため、どこからもアクセス可能
console.log(scope); // -> global
// windowオブジェクトのプロパティには追加されない
console.log(window.scope); // -> undefined

宣言文の省略とスコープ

宣言文(varletconst)を付けずに変数宣言すると、どのスコープ内で宣言しても、自動的にグローバルスコープとなってしまいます。

function fn() {
  // 関数スコープの中で宣言文を付けずに変数宣言
  scope = 'local';
}
fn();

// グローバルスコープとなり、関数スコープの外からアクセスできてしまう
console.log(scope); // -> local

これにより、意図せぬバグを生む可能性があるため、変数宣言では必ず宣言文を付ける*ようにしましょう。

*注:strictモードでの変数宣言

ECMAScript 5のstrictモード('use strict';を指定したJavaScriptコード)では、宣言文を省略した変数宣言をした場合、ReferenceErrorとなります。

変数宣言の巻き上げ(ホイスティング)

関数スコープの中で変数宣言を行うと、変数は関数の先頭に引き上げられます。このことを巻き上げホイスティングといいます。

これがどういうことなのか、具体例と一緒に解説していきます。次のコードでは、scope変数を宣言するより前に、console.logscope変数を参照しようとしています。コンソールに何が表示されるか想像してみてください。

function fn() {
  console.log(scope); // -> ???
  var scope = 'local';
}
fn();

普通に考えれば、変数宣言より前に参照しようとしているのですから、エラーになってしまいそうです。しかし、実際にはundefinedが返されます。

その理由は、スコープの途中で宣言したローカル変数の宣言部分が、スコープの先頭に巻き上げられたからです。

ちょっとわかりづらいかもしれませんが、先ほどのコードが、次のように書き変わったとイメージすると理解しやすいかもしれません。

function fn() {
  // 変数宣言だけ先頭に巻き上げられる
  var scope;
  console.log(scope); // -> undefined
  // 値の代入は元の場所で行われる
  scope = 'local';
}
fn();

巻き上げられるのは宣言だけであり、値の代入自体は元の場所で行われるため、console.logを行った時点では、scopeundefinedを返します。

なお、letconstの場合は、ブロックの先頭に巻き上げはされるのですが、宣言より前で変数にアクセスしようとすると、ReferenceErrorとなってしまいます。このアクセスできない領域をtemporal dead zoneと言います。

巻き上げによる混乱を避けるために、変数宣言はスコープの先頭にまとめて宣言するようにしましょう。

任意の場所でブロックスコープを生成

ブロックスコープは、ブロック({})を必要とする文で生成されます。たとえば、ifelseforfor...inwhileswitchなどです。

if (window) {
  let scope = 'local';
  console.log(scope); // -> local
}
// ブロックスコープ外のためアクセス不可
console.log(scope); // -> ReferenceError

実は、このような文を使わなくても、ブロック({})単体を用いるだけでスコープを作ることもできます。

{
  let scope = 'local';
  console.log(scope); // -> local
}
// ブロックスコープ外のためアクセス不可
console.log(scope); // -> ReferenceError

これにより、任意の場所でスコープを作りたいがために即時関数を使うといったような、わかりづらいコードを書く必要がなくなります。

まとめ

今回は、スコープの種類とそれぞれのスコープで知っておきたいことについて解説しました。

次回以降は、プログラム全体から見てスコープを理解していきます。入れ子になったスコープの関係や、親子関係のスコープ上に同じ変数名がある場合にどうなるのか、その解決の仕組みなどについて解説します。