22
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

JavaScriptでPHPの静的変数(static変数)みたいな事をやる方法

Last updated at Posted at 2014-06-06

導入

PHPには、静的変数(static変数)という、便利な構文が存在します。

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変数)を宣言する方法は、このようなやり方になります。
後述しますが、重大なデメリットが存在するため、このやり方よりも**クロージャを駆使したやり方を強くオススメ**します。

関数のプロパティとして静的変数(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"が定義済みかを確認し、未定義であれば0を代入して初期化する部分
// プロパティ"a"が定義済みか確認
if (!('a' in test)) {
    // 定義されていなければ、初期値の0を代入して初期化
    test.a = 0;
}

以降、このtest.aを静的変数(static変数)$aと同じように扱い、同じ処理を実現させています。

メリット

デメリット

特に、最後の2つはけっこう重大な問題になります。

横着して変数に代入すると上手く動かない

test.aを短くしたくなっても、横着して以下のように変数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に代入すれば解決します。

変数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を使った関数を書いてしまうと…

静的変数(static変数)の名前を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

このように、おかしな事が起きてしまいます。
またそれだけでなく、この例の関数testapplyメソッドは、もはや使い物にならなくなってしまうのです。

クロージャ

クロージャを駆使する方法は、このようなやり方になります。

// 処理内容の関数を生成
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関数を定義する箇所では、無名関数と即時関数を駆使しています。なのでまず、関数に名前をつけて分解してみます。

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関数の名前を取り、即時関数にしてしまいます。

  1. 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();
    
  2. 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;
    })();
    
  3. 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文の前に持ってくることができます。

  1. f_main_process関数の定義内容を、return文で返している場所に持っていく。

    // クロージャを定義するための関数を定義&実行
    var test = function() {
        // 変数"a"を初期化
        var a = 0;
    
        // 処理内容の関数を定義&返す
        return function() {
            // 変数"a"をカウントアップ
            a++;
    
            // 変数"a"の値を返す
            return a;
        };
    }();
    

このように、関数名を省略し、コンパクトにすることで、こんな感じになるわけです。

コンパクトになったtest関数の宣言
// 処理内容の関数を生成
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宣言を忘れたために、グローバル変数aの書き換えで影響が出てしまう例
// 処理内容の関数を生成
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
  1. これ以外の方法もあるかもしれませんが、著者は、この2つの方法しか知りません。

  2. 初期値は0ですが、出力前に必ずカウントアップされるため、返り値は1になります。

22
25
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?