導入
PHPには、静的変数(static変数)という、便利な構文が存在します。
<?php
function test(){
// 変数"$a"を初期化
static $a = 0;
// 変数"$a"をカウントアップ
$a++;
// 変数"$a"の値を返す
return $a;
}
// ループでtest関数を10回実行する
for($i = 0; $i < 10; $i++){
echo 'out: ' . test();
echo '<br>';
}
out: 1
out: 2
out: 3
out: 4
out: 5
out: 6
out: 7
out: 8
out: 9
out: 10
このように、二回目以降の関数呼び出しで、直前に実行した変数の値を再利用することができます。この構文を応用すれば、生成に時間がかかる値をキャッシュしたりすることができます。
さて、JavaScriptには、この便利な静的変数(static変数)が存在しません。しかし、似たような事を行う方法が存在します。
関数のプロパティとして宣言する方法と、クロージャを駆使する方法の2つです1。
関数のプロパティ
関数のプロパティとして静的変数(static変数)を宣言する方法は、このようなやり方になります。
後述しますが、重大なデメリットが存在するため、このやり方よりも**クロージャを駆使したやり方を強くオススメ**します。
function test() {
// プロパティ"a"を初期化
if (!('a' in test)) {
// もし、プロパティ"a"が未定義であれば、
// 初期値の0を代入する
test.a = 0;
}
// プロパティ"a"をカウントアップ
test.a++;
// プロパティ"a"の値を返す
return test.a;
}
// ループでtest関数を10回実行する
for (var i = 0; i < 10; i++) {
console.log(
'out: ' + test()
);
}
out: 1
out: 2
out: 3
out: 4
out: 5
out: 6
out: 7
out: 8
out: 9
out: 10
原理
JavaScriptの関数は、オブジェクト(連想配列、辞書、ハッシュみたいなモノ)の側面も持っています。よって、関数test
のプロパティa
を動的に追加し、そこに値を格納することで静的変数(static変数)を再現できます。
例では、test.a
(関数test
のプロパティa
)が、PHPにおける静的変数(static変数)$a
の役割を果たしています。最初にtest.a
が定義されているか確認し、未定義の場合に初期値の0
を代入して初期化しています。
// プロパティ"a"が定義済みか確認
if (!('a' in test)) {
// 定義されていなければ、初期値の0を代入して初期化
test.a = 0;
}
以降、このtest.a
を静的変数(static変数)$a
と同じように扱い、同じ処理を実現させています。
メリット
- 後述のクロージャを利用した方法に比べると、PHPの静的変数(static変数)に近い書き方ができる
デメリット
-
test.a
のように、書き方が長くなる - 初期化の構文が割とややこしい
- 横着して変数に代入すると上手く動かない
- 外部から書き換えられる危険がある
- 名前が組み込みのプロパティやメソッドと被るとヤバイ
特に、最後の2つはけっこう重大な問題になります。
横着して変数に代入すると上手く動かない
test.a
を短くしたくなっても、横着して以下のように変数a
に代入してはいけません。上手く動作しなくなります。
function test() {
// プロパティ"a"を初期化
if (!('a' in test)) {
// もし、プロパティ"a"が未定義であれば、
// 初期値の0を代入する
test.a = 0;
}
// プロパティ"a"を変数"a"に代入
var a = test.a;
// 変数"a"をカウントアップ
a++;
// 変数"a"の値を返す
return a;
}
// ループでtest関数を10回実行する
for (var i = 0; i < 10; i++) {
console.log(
'out: ' + test()
);
}
out: 1
out: 1
out: 1
out: 1
out: 1
out: 1
out: 1
out: 1
out: 1
out: 1
test.a
を変数a
に代入した時点で、この2つは別の変数として扱われてしまいます。変数a
に1を加算しても、それがtest.a
に反映されず、結果、test.a
の値は初期値の0
のまま変わらず、いつまでも1
が出力されてしまうのです2。
もし、test.a
を変数a
に代入して利用したい場合、変更を加えた変数a
を最後にtest.a
に代入すれば解決します。
function test() {
// プロパティ"a"を初期化
if (!('a' in test)) {
// もし、プロパティ"a"が未定義であれば、
// 初期値の0を代入する
test.a = 0;
}
// プロパティ"a"を変数"a"に代入
var a = test.a;
// 変数"a"をカウントアップ
a++;
// returnの直前で変数"a"をプロパティ"a"に代入
test.a = a;
// 変数"a"の値を返す
return a;
}
// ループでtest関数を10回実行する
for (var i = 0; i < 10; i++) {
console.log(
'out: ' + test()
);
}
out: 1
out: 2
out: 3
out: 4
out: 5
out: 6
out: 7
out: 8
out: 9
out: 10
外部から書き換えられる危険がある
プロパティという特性上、その値は、関数の外からでも容易に読み書きできてしまいます。
function test() {
// プロパティ"a"を初期化
if (!('a' in test)) {
// もし、プロパティ"a"が未定義であれば、
// 初期値の0を代入する
test.a = 0;
}
// プロパティ"a"をカウントアップ
test.a++;
// プロパティ"a"の値を返す
return test.a;
}
// ループでtest関数を10回実行する
for (var i = 0; i < 10; i++) {
console.log(
'out: ' + test()
);
}
// プロパティ"a"の値を読み出す
console.log('aの値は、');
console.log(test.a);
// プロパティ"a"の値を書き換えてみる
console.log('aを-10に上書き');
test.a = -10;
// 再び、ループでtest関数を10回実行する
for (var i = 0; i < 10; i++) {
console.log(
'out: ' + test()
);
}
out: 1
out: 2
out: 3
out: 4
out: 5
out: 6
out: 7
out: 8
out: 9
out: 10
aの値は、
10
aを-10に上書き
out: -9
out: -8
out: -7
out: -6
out: -5
out: -4
out: -3
out: -2
out: -1
out: 0
この例では、単に迷惑なだけですが、使い方によってはエラーを吐いて動きが止められてしまう危険性もあります。
組み込みのプロパティやメソッドと被るとヤバイ
JavaScriptの関数はオブジェクトです。そして、JavaScript関数には、最初から存在するプロパティやメソッドが存在しています。
この「最初から存在するプロパティやメソッド」は、JavaScriptを使いこなす上で有用な機能を提供してくれます。特にapply
メソッドやbind
メソッドは、けっこう高頻度で使用します。
しかし、このプロパティやメソッドと被る名前のプロパティを静的変数(static変数)として使うコードを書いてしまうと、いろいろ問題が起きてしまいます。
例えば、test.apply
を使った関数を書いてしまうと…
function test() {
// プロパティ"apply"を初期化
if (!('apply' in test)) {
// もし、プロパティ"apply"が未定義であれば、
// 初期値の0を代入する
test.apply = 0;
}
// プロパティ"apply"をカウントアップ
test.apply++;
// プロパティ"apply"の値を返す
return test.apply;
}
// ループでtest関数を10回実行する
for (var i = 0; i < 10; i++) {
console.log(
'out: ' + test()
);
}
out: NaN
out: NaN
out: NaN
out: NaN
out: NaN
out: NaN
out: NaN
out: NaN
out: NaN
out: NaN
このように、おかしな事が起きてしまいます。
またそれだけでなく、この例の関数test
のapply
メソッドは、もはや使い物にならなくなってしまうのです。
クロージャ
クロージャを駆使する方法は、このようなやり方になります。
// 処理内容の関数を生成
var test = function() {
// 変数"a"を初期化
var a = 0;
// 処理内容の(外側でtest関数になる)無名関数を返す
return function() {
// 変数"a"をカウントアップ
a++;
// 変数"a"の値を返す
return a;
};
}();
// ループでtest関数を10回実行する
for (var i = 0; i < 10; i++) {
console.log(
'out: ' + test()
);
}
out: 1
out: 2
out: 3
out: 4
out: 5
out: 6
out: 7
out: 8
out: 9
out: 10
原理
なぜクロージャ(Closure)と言うのか? - Qiita
クロージャについては、この記事が分かりやすいので、コッチを読んでください。この記事を読んだという前提で解説します。
このコードのtest関数を定義する箇所では、無名関数と即時関数を駆使しています。なのでまず、関数に名前をつけて分解してみます。
// クロージャを定義するための関数"f_def_static"(仮の名前)を定義
var f_def_static = function() {
// 静的変数(static変数)"a"を初期化
var a = 0;
// 処理内容の関数"f_main_process"(仮の名前)を定義
var f_main_process = function() {
// 変数"a"をカウントアップ
a++;
// 変数"a"の値を返す
return a;
};
// 処理内容の関数"f_main_process"そのものを返す
return f_main_process;
};
// 関数"f_def_static"を実行し、関数"f_main_process"に"test"という名前をつけてtest関数にする
var test = f_def_static();
まず、f_main_process
関数が、本来のtest
関数に相当する処理を実行する関数です。
変数a
の宣言を含んでいない点以外は、PHPのtest
関数と変わりません。
次に、f_def_static
関数は、以下の処理を行います。
- 静的変数(static変数)
a
の初期化 -
f_main_process
関数の定義 -
f_main_process
関数を返り値として返す
初期化された変数a
は、f_main_process
関数の内部から再利用できます。
そして、f_main_process
関数を返り値としてf_def_static
関数の外に出し、test
変数に格納して、test
関数として実行した時ですら、変数a
は再使用できてしまいます。
この仕組みを応用し、静的変数(static変数)を再現します。
関数名の省略
ただし、いちいち関数に名前をつけても面倒です。なので、f_def_static
関数の名前を取り、即時関数にしてしまいます。
-
f_def_static
関数を括弧で囲む// クロージャを定義するための関数"f_def_static"(仮の名前)を定義 // 値なので、丸括弧で囲っても問題なし var f_def_static = (function() { // 変数"a"を初期化 var a = 0; // 処理内容の関数"f_main_process"(仮の名前)を定義 var f_main_process = function() { // 変数"a"をカウントアップ a++; // 変数"a"の値を返す return a; }; // 処理内容の関数"f_main_process"そのものを返す return f_main_process; }); // 関数"f_def_static"を実行し、関数"f_main_process"に"test"という名前をつけてtest関数にする var test = f_def_static();
-
f_def_static
関数の定義内容を、実行している場所に持ってくる// クロージャを定義するための関数を定義&実行 // 関数"f_main_process"に"test"という名前をつけてtest関数にする var test = (function() { // 変数"a"を初期化 var a = 0; // 処理内容の関数"f_main_process"(仮の名前)を定義 var f_main_process = function() { // 変数"a"をカウントアップ a++; // 変数"a"の値を返す return a; }; // 処理内容の関数"f_main_process"そのものを返す return f_main_process; })();
-
f_def_static
関数を囲っていた括弧は要らなくなったので、取る// クロージャを定義するための関数を定義&実行 // 関数"f_main_process"に"test"という名前をつけてtest関数にする var test = function() { // 変数"a"を初期化 var a = 0; // 処理内容の関数"f_main_process"(仮の名前)を定義 var f_main_process = function() { // 変数"a"をカウントアップ a++; // 変数"a"の値を返す return a; }; // 処理内容の関数"f_main_process"そのものを返す return f_main_process; }();
また、f_main_process
関数も、定義内容をreturn
文の前に持ってくることができます。
-
f_main_process
関数の定義内容を、return
文で返している場所に持っていく。// クロージャを定義するための関数を定義&実行 var test = function() { // 変数"a"を初期化 var a = 0; // 処理内容の関数を定義&返す return function() { // 変数"a"をカウントアップ a++; // 変数"a"の値を返す return a; }; }();
このように、関数名を省略し、コンパクトにすることで、こんな感じになるわけです。
// 処理内容の関数を生成
var test = function() {
/*
* 静的変数(static変数)の定義
*/
// 変数"a"を初期化
var a = 0;
// 処理内容の(外側でtest関数になる)無名関数を返す
return function() {
/*
* 静的変数(static変数)を使用した処理内容
*/
// 変数"a"をカウントアップ
a++;
// 変数"a"の値を返す
return a;
};
}();
メリット
- PHPの静的変数(static変数)と同じように、単なる変数として利用できる
- 外部から書き換えられない
- 単なる変数なので、プロパティやメソッドと被る心配はしなくても良い
デメリット
全体的に構文がややこしい
クロージャを駆使しているため、はじめて読むと「???」となってしまう可能性があります。
即時関数や無名関数を駆使してコンパクトにしてしまうと、さらによく分からない構文になってしまいます。
また、関数の処理内容を内側に書かなくてはならないため、書き換えも若干面倒くさくなります。
静的変数(static変数)のためだけに、余計な関数が要る
静的変数(static変数)を使うという、ただそれだけのために、余計な関数(名前をつけて分解したコードのf_def_static
関数に相当する関数)を1つ定義しなくてはなりません。
加えて、その余計な関数は、一回実行してしまえば二度と使いません。
このため、関数を生成する負荷により、パフォーマンスが劣化する恐れがあります。
もっとも、昨今の端末やブラウザは高性能になっているため、普通のWebページやWebアプリでそれほど気にする必要はありません。
ただし、例えばゲーム(特に、高負荷な処理を短い間にこなさなくてはならないVRゲームなど)を作ろうとした時などは、この僅かな劣化がFPSに影響してしまうかもしれません。
うっかりvar
宣言を忘れてしまうと、ヤバイ
静的変数(static変数)の定義部分で使用しているvar
宣言をうっかり忘れてしまうと、グローバル変数になってしまいます。
グローバル変数はどこからでも書き換えられるため、外部から書き換えられる危険が生じてしまいます。
// 処理内容の関数を生成
var test = function() {
// 変数"a"を初期化
// …あ、var宣言忘れてる
a = 0;
// 処理内容の(外側でtest関数になる)無名関数を返す
return function() {
// 変数"a"をカウントアップ
a++;
// 変数"a"の値を返す
return a;
};
}();
// ループでtest関数を10回実行する
for (var i = 0; i < 10; i++) {
console.log(
'out: ' + test()
);
}
// グローバル変数"a"の値を読み出す
console.log('aの値は、');
console.log(a);
// グローバル変数"a"の値を書き換えてみる
console.log('aを-10に上書き');
a = -10;
// もう一度、ループでtest関数を10回実行する
for (var i = 0; i < 10; i++) {
console.log(
'out: ' + test()
);
}
out: 1
out: 2
out: 3
out: 4
out: 5
out: 6
out: 7
out: 8
out: 9
out: 10
aの値は、
10
aを-10に上書き
out: -9
out: -8
out: -7
out: -6
out: -5
out: -4
out: -3
out: -2
out: -1
out: 0