変数のスコープ

生田 陸人
ゆめさくエンジニア / 現役ソフトウェアエンジニア
編集 LuaGate編集部

このレッスンで分かること

  • 変数には見える範囲があり、それをスコープと呼びます
  • letconst はブロック { } の中だけで生き、外からは見えません
  • 関数の外の変数を書き換えると、思わぬ場所にバグが出ます

変数のスコープ とは

宣言した変数がどこから見えるかという範囲のことです。関数の外を汚してしまうバグを、ローカル変数に直して防ぎます。

変数が見える範囲

関数を書けるようになると、必ずぶつかる疑問があります。「この変数はどこから使えるのか」です。答えを決めるのがスコープという仕組みです。スコープとは、宣言した変数が見える範囲のことです。

まずは実験してみましょう。

function makeTitle() { const siteName = "山田 太郎のプロフィールサイト"; return siteName; } console.log(makeTitle()); // 山田 太郎のプロフィールサイト console.log(siteName); // ReferenceError siteName is not defined

siteNamemakeTitle の中で宣言されているので、関数の外からは見えません。存在しない変数を読もうとしてエラーになります。この、関数の中だけで生きる変数をローカル変数と呼びます。

逆に、関数の外で宣言した変数は関数の中から見えます。

const siteName = "山田 太郎のプロフィールサイト"; function makeTitle() { return siteName + " へようこそ"; } console.log(makeTitle()); // 山田 太郎のプロフィールサイト へようこそ

内側から外側は見える、外側から内側は見えない。これがスコープの基本ルールです。家に例えると、部屋の中からは廊下が見えるけれど、廊下から部屋の中は見えない、という関係です。

変数はできるだけ狭いスコープで宣言します。狭ければ、その変数を気にしなければならない範囲も狭くなり、読むのも直すのも楽になります。

ブロックスコープと関数スコープ

JavaScript の変数宣言には let const var の 3 つがありますが、スコープの効き方が違います。

letconstブロックスコープです。{ } で囲まれた範囲の中だけで生きます。if の中でも for の中でも、{ } があればそこがスコープの境界になります。

if (true) { const message = "こんにちは"; console.log(message); // こんにちは } console.log(message); // ReferenceError
for (let i = 0; i < 3; i++) { console.log(i); // 0 1 2 } console.log(i); // ReferenceError

一方 var関数スコープです。{ } を無視して、関数の中ならどこからでも見えてしまいます。

function demo() { if (true) { var count = 3; } console.log(count); // 3 が見えてしまう }

3 つの違いをまとめると下記のとおりです。

宣言スコープ再代入同じ名前で再宣言
constブロックできないできない
letブロックできるできない
var関数できるできてしまう

varJavaScript の初期からある古い書き方です。同じ名前で何度でも宣言できてしまうため、うっかり上書きしても誰も教えてくれません。いまのコードでは使いません。古いコードや古い記事で見かけたときに「これは関数スコープなんだな」と読めればそれで十分です。新しく書くコードは const を基本にし、再代入が要るときだけ let を使います。

const から書き始めて、再代入が必要になった行で let に変える。この順番で書くと、意図せず値が変わる変数を最小限にできます。

グローバル汚染とシャドーイング

スコープを意識しないと起きる代表的な事故が 2 つあります。

関数の外を書き換えてしまう

関数の中から外側の変数を書き換えると、その関数を呼んだ副作用が関数の外にまで及びます。

let count = 0; function countWorks(works) { count = 0; for (const work of works) { count = count + 1; } return count; } console.log(countWorks(["プロフィールサイト", "自己紹介カード"])); // 2 console.log(count); // 2 関数の外の変数が書き換わっている

countWorks を呼んだだけなのに、関数の外の count の値が変わってしまいました。この count を他の場所でも使っていたら、まったく関係ないところで表示が狂います。しかも原因は countWorks の中にあるので、追跡がとても大変です。これをグローバル汚染と呼びます。

直し方は簡単で、変数を関数の中で宣言するだけです。

function countWorks(works) { let count = 0; for (const work of works) { count = count + 1; } return count; }

let count と書き足しただけで、count は関数の中だけの変数になりました。関数の外には何の影響も与えません。関数の中で使う作業用の変数は、必ず関数の中で宣言する。これを守るだけで、原因不明のバグの多くが消えます。

シャドーイング

内側のスコープで外側と同じ名前の変数を宣言すると、内側では外側の変数が隠れます。これをシャドーイングと呼びます。

const name = "山田 太郎"; function greet() { const name = "鈴木 花子"; return "こんにちは、" + name + "さん"; } console.log(greet()); // こんにちは、鈴木 花子さん console.log(name); // 山田 太郎

関数の中の name は別物なので、外側の name は無傷です。書き換えてしまうより安全ですが、読み手は同じ名前の変数が 2 つあることに気付きにくく、混乱のもとになります。意図的に使う場面は限られるので、内側では別の名前を付けるほうが親切です。

スコープを狭く保つ習慣は、あとから効いてきます。ファイルが 100 行を超えたあたりから、どこで値が変わったのか追えるかどうかで、デバッグにかかる時間が何倍も変わります。

よくある間違い

1. 関数の中で宣言を忘れて外の変数を使ってしまう

let total = 0; // NG 外の total を書き換えている function sumViews(views) { for (const v of views) { total = total + v; } return total; } // OK 関数の中で宣言する function sumViewsOk(views) { let total = 0; for (const v of views) { total = total + v; } return total; }

letconst を書き忘れただけで、ローカル変数のつもりが外側の変数への代入になります。

2. ブロックの外で使おうとする

// NG result はブロックの中でしか生きていない function judge(age) { if (age >= 20) { const result = "成人"; } return result; // ReferenceError } // OK 使う範囲でいちばん外側で宣言する function judgeOk(age) { let result = "未成年"; if (age >= 20) { result = "成人"; } return result; }

3. ループ変数を外から参照しようとする

// NG i は for のブロックの中だけ for (let i = 0; i < 3; i++) { // 処理 } console.log(i); // ReferenceError

ループが終わったあとに回数を使いたいなら、for の外で let を宣言してから使います。

要件

  1. 関数名は sumViews、引数は数値の配列 views 1 つにすること
  2. 合計を入れる変数は関数の中で let を使って宣言すること
  3. 関数の外の変数を書き換えないこと
  4. 空配列を渡されたら 0 を返すこと

入出力例

sumViews([120,80,45])245 sumViews([120,80,45])245 sumViews([10])10 sumViews([])0 sumViews([0,0,0])0

ヒント

main.js
main.js
学習モード

メモ

変数のスコープ

⌘S で保存