変数の寿命
これまでプログラミングをするのにたくさんの変数を使ってきましたが、いくつかの宣言の仕方がありました。
はじめは draw
ブロックの中に変数を定義していました。
繰り返しを使うときにはfor文の制御文の中で、また関数を使うようになると引数の部分で変数を定義しました。
実は変数には有効なものとして使うことができる範囲としてスコープと呼ばれる寿命の概念が存在し、宣言する場所によって異なります。
この章では今まではっきりとは説明してこなかった変数のスコープについて整理し、適切に使い分けられるようになることを目標とします。
実はjavascript のスコープについては昔から改良が重ねられてきていて、いまはかなり複雑なルールになっています。 ここでは失敗がしにくくなるような書き方に絞って解説をします。
局所変数
変数のスコープがどこまでかという疑問に対し、はじめに一般的な答えを述べるとlet を使って宣言された変数は宣言されたブロック内で有効です。 ブロックはこれまでfor文やif文、関数を使う際に処理をまとめるものとして使ってきました。 このようなブロックの中で宣言された変数のことを局所変数とよび、局所変数はプログラムの実行が進みそのブロックを抜けると同時に消滅します。 ブロックを抜けたあとにその変数を使うことはできません。
コンソールに文字を出力するだけのプログラムでスコープの役割を確認しましょう。 また、変数にアクセスできていない部分を取り除いて、それぞれのプログラムで起きるエラーを解消してください。
[関数スコープの動作を確かめる]
function function1(arg) {
// function1関数の関数スコープを持つローカル変数を定義
let scope = 'local';
// function1関数のスコープ内のためアクセス可能
print(scope); // -> local
print(arg); // -> argument
}
function setup(){
function1('argument');
// 関数スコープの外側からはローカル変数や仮引数にアクセスできない
print(scope); // -> ReferenceError
print(arg); // -> ReferenceError
}
[ブロックスコープの動作を確かめる]
function setup(){
for(let i=0; i<10; i++){ // iはforループの中で使える変数
print(i);
}
print(i); // i はforブロックの中だけで有効
if(true){
let j = 10; // if ブロックの中でjを宣言
print(j);
}
print(j); // j はifブロックの中だけで有効
}
このように変数のスコープがあることで、違うブロックであれば同じ名前の変数を衝突なく作ることができるという嬉しいことがあります。次の例では、function1の引数として、function2の中で、またdraw関数の中でそれぞれ x, y
という変数が宣言されていますが、これら3つは全て名前が同じだけの別の変数として扱われます。
function function1(x, y){
// このブロックでx, y といえば 括弧内のものをさす。
...
}
function function2(){
let x = 10;
let y = 20;
// このブロックでx, y といえば
// ↑の変数をさす。
...
}
function draw(){
let x = 0;
let y = 100;
// このブロックでx, y といえば
// ↑の変数をさす。
function1(x, y); // x=0. y=100 をfunction1 に渡す。
function2(); // function2 の中で宣言される変数は、このスコープには影響しない。
}
もしスコープの概念がなく宣言した変数がプログラムのどこでも使うことができるとしたらどんなことが問題になるでしょうか。 プログラマは常に全体でどんな名前の変数が宣言されているかを把握し、被らないように変数の名前を作らなければなりません。 数十行程度の小さなプログラムでは頑張れば可能かもしれませんが、規模が大きくなり何個も変数を使うようになるともはや人間の頭でこれを管理することは不可能です。 また、思ってもいない変数を書き換えてしまうなどミスの元になります。 ミスをふせぐためにも、より小さいブロックの中で変数を宣言し、周りへの影響を最小限にとどめるように利用するようにしましょう。
また次の二つも変数のスコープに関する重要なルールです。
- 階層構造のあるブロックではより下位のブロックでも同じ変数が使える
- 階層構造のあるブロックで変数名が衝突したときは小さいほうのスコープが勝つ
次のプログラムでは setup
のブロックとforループのブロックが階層構造を持っています。
setup
の中で宣言された変数は基本的にforブロックの中でも有効となります。
しかし、 i
に関してはfor文のために改めて宣言されているため、より小さいスコープであるこちらが優先されます。
function setup(){
let i = 1000;
let j = 2000;
// これは当然 i = 1000, j = 2000
print("(i, j) in setup block is (" + i + ", " + j + ")"); // ★
for(let i=0; i<10; i++){
// i はfor構文で改めて宣言したもので i = 1, i = 2, ...
// j ははじめに宣言した j = 2000
print("(i, j) in for loop is (" + i + ", " + j + ")");
}
// forブロックを抜けたらi ははじめに宣言した i = 1000, j = 2000
print("(i, j) after loop is (" + i + ", " + j + ")");
}
文字列も変数の一種なのでプログラムで編集することができます。 演算子
+
を使うと文字列を連結できて、例えば"open" + "processing" == "openprocessing"
となります。 演算に数値などを入れると文字列に変換して連結してくれるので、上のコードの★では(i, j) in setup block is (1000, 2000)
とコンソールに出力されます。
大域変数
変数のスコープはなるべく小さくすべきとはいえ、さまざまな関数から共通の変数を利用したいという場面はあるでしょう。 そのようなときには仕方なく大域変数を使います。 大域変数は関数ブロックの外側で宣言し、そのスコープはプログラム全体に渡ります。
[例6-1. 大域変数]
let a = 80; // 関数の外側で大域変数aを定義する
function setup() {
createCanvas(windowWidth, windowHeight);
background(0);
stroke(255);
noLoop(); // アニメーションしないという指定
}
function draw() {
// 大域変数のaをつかって線を引く
line(a, 0, a, height);
a += 10; // 大域変数のaが書き換えられた。この段階でa = 90
drawAnotherLine();
drawYetAnotherLine();
}
function drawAnotherLine() {
// 局所変数としてaを定義し、線を引く
let a = 320;
line(a, 0, a, height);
}
function drawYetAnotherLine() {
// 局所変数にaはないので、このブロックでは大域変数のaが使われる
a += 10; // 大域変数のaが書き換えられた。この時点で a = 100
line(a, 0, a, height);
}
大域変数と局所変数が宣言されたときは、階層構造のあるブロックで変数名が衝突したときは小さいほうのスコープが勝つのルールによって局所変数が優先されます。 大域変数を使うと便利な場面というのも存在しますが、既に述べたように意図しない変数を書き換えてしまうといったミスのもととなるので、最後の手段的に利用するようにしましょう。
また、大域変数はアニメーションを通しても一つの変数として扱われます。 次のプログラム例6-2 では、大域変数のaがどんどん+10されていくので線が増える様子が確認できます(リプレイ押してください)。 p5.js ではこのようにアニメーションの中で変化する情報を持たせるために大域変数を使うことが多いです。
let a = 80; // 関数の外側で大域変数aを定義する
function setup() {
createCanvas(windowWidth, windowHeight);
background(0);
stroke(255);
}
function draw() {
// 大域変数のaをつかって線を引く
line(a, 0, a, height);
a += 10; // 大域変数のaが書き換えられた。
}
定数の宣言
円周率など、決まった値を持たせて書き換えることが無いような情報を記憶しておきたい場合があります。
javascript では、宣言時以降に新しく値を代入できないような種類の変数があり、 let
のかわりに const
を使って宣言します。
このようにして宣言された定数は、変数を書き換えようとするとエラーとなります。
この点以外、スコープなどのルールは let
で宣言する変数と同じです。
定数の宣言は関数の引数を定義するときにも同様で、その関数の中では引数を書き換えることができません。
これまで導入していませんでしたが、意図しない書き換えによるミスを防ぐことができるので積極的に利用しましょう。