変数のスコープ
このレッスンで分かること
- 変数には見える範囲があり、それをスコープと呼びます
letとconstはブロック{ }の中だけで生き、外からは見えません- 関数の外の変数を書き換えると、思わぬ場所にバグが出ます
変数のスコープ とは
宣言した変数がどこから見えるかという範囲のことです。関数の外を汚してしまうバグを、ローカル変数に直して防ぎます。
変数が見える範囲
関数を書けるようになると、必ずぶつかる疑問があります。「この変数はどこから使えるのか」です。答えを決めるのがスコープという仕組みです。スコープとは、宣言した変数が見える範囲のことです。
まずは実験してみましょう。
function makeTitle() {
const siteName = "山田 太郎のプロフィールサイト";
return siteName;
}
console.log(makeTitle()); // 山田 太郎のプロフィールサイト
console.log(siteName); // ReferenceError siteName is not definedsiteName は makeTitle の中で宣言されているので、関数の外からは見えません。存在しない変数を読もうとしてエラーになります。この、関数の中だけで生きる変数をローカル変数と呼びます。
逆に、関数の外で宣言した変数は関数の中から見えます。
const siteName = "山田 太郎のプロフィールサイト";
function makeTitle() {
return siteName + " へようこそ";
}
console.log(makeTitle()); // 山田 太郎のプロフィールサイト へようこそ内側から外側は見える、外側から内側は見えない。これがスコープの基本ルールです。家に例えると、部屋の中からは廊下が見えるけれど、廊下から部屋の中は見えない、という関係です。
変数はできるだけ狭いスコープで宣言します。狭ければ、その変数を気にしなければならない範囲も狭くなり、読むのも直すのも楽になります。
ブロックスコープと関数スコープ
JavaScript の変数宣言には let const var の 3 つがありますが、スコープの効き方が違います。
let と const はブロックスコープです。{ } で囲まれた範囲の中だけで生きます。if の中でも for の中でも、{ } があればそこがスコープの境界になります。
if (true) {
const message = "こんにちは";
console.log(message); // こんにちは
}
console.log(message); // ReferenceErrorfor (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 | 関数 | できる | できてしまう |
var は JavaScript の初期からある古い書き方です。同じ名前で何度でも宣言できてしまうため、うっかり上書きしても誰も教えてくれません。いまのコードでは使いません。古いコードや古い記事で見かけたときに「これは関数スコープなんだな」と読めればそれで十分です。新しく書くコードは 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;
}let や const を書き忘れただけで、ローカル変数のつもりが外側の変数への代入になります。
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 を宣言してから使います。
要件
- 関数名は sumViews、引数は数値の配列 views 1 つにすること
- 合計を入れる変数は関数の中で let を使って宣言すること
- 関数の外の変数を書き換えないこと
- 空配列を渡されたら 0 を返すこと
入出力例
sumViews([120,80,45]) → 245
sumViews([120,80,45]) → 245
sumViews([10]) → 10
sumViews([]) → 0
sumViews([0,0,0]) → 0