Programmierkurs
für Naturwissenschaftler/innen

スコープ

スコープとは

一言で言うと変数(等)の有効範囲のことである。

簡単な復習

復習がてら、変数について見てみよう。

これまでは、 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 から始まっている。 以下のプログラムで確認してみよう。

  1. 次のプログラムを実行すると何が表示されるか、理由とともに予想せよ。 その後で実際に入力し、実行してみよ。

  2. その後、 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);