スコープとは
一言で言うと変数(等)の有効範囲のことである。
簡単な復習
復習がてら、変数について見てみよう。
これまでは、 let
で宣言 (定義) したところから最後までその変数を使うことができた。
すなわち有効範囲は let
以降ということである。
// test
let a = 10;
console.log(a);
一方、関数, 条件分岐 及び
ループ で見たように、 {
… }
で囲まれた部分を
ブロック という。
通常はこのような書き方はしないが、単独のブロックを使うこともできる。 ここでは説明のためにこのような書き方をしてみる。
let i = 10;
console.log("before block: i =", i);
{
// ブロック
}
console.log(" after block: i =", i);
このとき次のプログラムを実行するとどのような結果になるであろうか。
let i = 10;
console.log("before block: i =", i);
{
console.log(" in block: i =", i);
}
console.log(" after block: i =", i);
いずれも i
は 10 と表示されたはずである。
特に疑問の余地はないだろう。
さて、次の問題はどうであろうか。
次のプログラムを実行すると何が表示されるか予想せよ。 その後、実際に入力・実行してみて確認せよ。
let i = 10;
console.log("before block: i =", i);
{
i = 20;
console.log(" in block: i =", i);
}
console.log(" after block: i =", i);
1 つ目の練習問題との違いは、ブロック内で i
の値が変更されている点である。
順に 10, 20, 20 と表示されたであろう。
つまり、この状況でブロックの {
と }
はあってもなくても全く同じである。
これも、疑問の余地はないであろう。
気になった人は上で {
と }
を消して試してみよう。
ブロックスコープ
さて、ようやく本題である。 以下のようにすると何が起こるであろうか。
let i = 10;
console.log("before block: i =", i);
{
let i = 1;
console.log(" in block: i =", i);
}
console.log(" after block: i =", i);
ポイントはブロック内で let
が使われている点である。
このとき、新しい変数が作られる。
古い (1行目で宣言/定義されている) i
は見えなくなるが、消えてはいない。
ブロック内で宣言された変数は、ブロックを抜けると消えてしまう。
その結果ブロック終了後はもともとの i
が再び見えるようになり、元の値である 10 が表示される。
for の場合
ループ で簡単に説明したが、 for 文では通常、以下のような書き方をする。
for (let i = 0; i < 3; i++) {
console.log(" in for-loop: i =", i);
}
このとき i
は for ループ内でのみ有効である。
このときブロックは実質的に for
から始まっている。
以下のプログラムで確認してみよう。
-
次のプログラムを実行すると何が表示されるか、理由とともに予想せよ。 その後で実際に入力し、実行してみよ。
-
その後、
for
でのlet
を消して実行してみよ。 上の結果との違いはどこか、なぜそのようになったか説明せよ。
let i = 10;
console.log("before for-loop: i =", i);
for (let i = 0; i < 3; i++) { // 設問 2 で let を消す
console.log(" in for-loop: i =", i);
}
console.log(" after for-loop: i =", i);
例外はあるが、 for
文でのループ変数は for
文内でのみ必要な場合がほとんどであるから、 for
では for (let i = 0; ...)
のように必ず let
を付けるものと思っておくのが良いであろう。
: for
の復習
本題から逸れるが、上の練習問題で let
を取り除いて実行したとき、
最後に after for-loop: i = 3
と表示されたはずである。
なぜ最後に i
の値が (2 ではなく) 3 になっているのか、 for
の動作に基づいて説明せよ。
関数の場合
関数の引数は、自動的にその関数内でのみ使える変数として扱われる。
function string_length(str) { // str はこの関数内でのみ有効
...
}
と書いたとき、 str
はこの関数内でのみ有効な変数である。
関数内で新しい変数を作ることもできる。 これは単にブロック内で変数を作っているだけであるから、これまでの話と何ら変わりはない。
function string_length(str) {
let length = 0; // ブロック内でのみ有効な変数
...
}
関数と変数
関数の中から外部の変数を読んだり書いたりするのはよくない。 呼び出し (引数) と返り値でのみやりとりするように書く。
次のプログラムを見てみよう。
// ! 良くない書き方 !
let sum = 10;
console.log(sum);
increment();
console.log(sum);
function increment() {
sum += 1;
}
このプログラムは問題なく動く。 しかし、別の問題を引き起こしやすい。
increment()
がものすごく離れたところに (あるいは別のファイルに) 書かれていると想定してみよう。
プログラムの前半だけ見ると sum
の値が変更されているように見えない。
ところが実際は「知らぬ間に」変更されている。
sum
を変更するのが increment()
だけだと分かっているのであればまだマシであるが、もし他の様々な関数が sum
を変更しうる場合は問題がさらに複雑化する。
そのような場合に sum
の値が正しくないことが判明したとする。
その問題がどこで発生したかを突き止めるには、かなりの労力を要する。
従って、例えば以下のように書くのがよい。
let sum = 10;
console.log(sum);
sum = increment(sum);
console.log(sum);
function increment(num) {
return num + 1;
}
この場合、4 行目で sum
に変更が加えられていることが明確である。
すなわち、
言い方を変えると、関数は
-
引数のみを通じて外部の情報 (外部からの指示) を受けとり、
-
返り値でのみ外部へ情報を渡し、
-
それ以外、外部には一切影響を与えない
ように書くべきである。
もう少し具体的に言いかえるなら、
-
関数では、引数とその関数内で定義した変数のみを使用し、
-
関数外にある変数を関数内で直接読んだり変更したりしないようにすべきである
と言える。
繰り返しになるが、良いプログラムというのは、間違いが起こりにくい、あるいは万一起こっても発見がしやすいようなものである。 最初のうちは実践するのは難しいであろうが、このことは頭の片隅には置いておいて欲しい。
補足
同一のスコープ内で、同一の変数を複数回宣言(定義)することはできないことに注意せよ。
let a = 10;
console.log(a);
let a = 20; // ! 間違い。2 回目の宣言はできない !
console.log(a);