Kikuchy's Second Memory

つくる楽しさをもっと伝えたい。プログラムを書いていて、わからなかったこと・気付いた事を書き留めています。

C++ プログラマのためのオブジェクト指向 JavaScript プログラミング

JavaScript でクラスとかってどう作るの?」


意外とこう聞かれることが多いので、この機会にまとめておこうと思います。

想定読者
  • C++ で一通りのプログラムを書くことができる
  • JavaScript の文法はとりあえずわかるが、どう使えばいいのかはよくわからない

クラスのテンプレート

先に結論から行きましょう。

Fooというクラスを作りたければ以下のように書きましょう。

var Foo = (function(){

    // Fooクラスのコンストラクタ
    var Foo = function(a){

        // public: なメンバ変数 bar(パブリックなメンバ変数)
        this.bar = a;

        // private: なメンバ変数 _baz(プライベートなメンバ変数)
        this._baz = "initial value";
    };

    // public: なメソッド qux
    Foo.prototype.qux = function(){
      console.log(this.bar);
    };

    // private: なメソッド _foobar
    Foo.prototype._foobar = function(){
        console.log("foo" + this.bar);
    };

    // public: static なメンバ変数 Hoge(パブリックなクラス変数)
    Foo.Hoge = "this is public";

    // public: static なメソッド Fuga(パブリックなクラスメソッド)
    Foo.Fuga = function(){
        console.log("this is public class method");
    };

    // private: static なメンバ変数 Moge(プライベートなクラス変数)
    var Moge = "this is private";

    // private: static なメソッド Piyo(プライベートなクラスメソッド)
    var Piyo = function(){
        console.log("this is private class method.");
    };

    return Foo;
})();

使うときはこう使います。

var foo = new Foo("bar");


以上!

解説

これで終わってはあまりにも乱暴なので少し解説。まずはソースの概形から。

var Foo = (function(){})();

無名関数を宣言して、即時実行、その結果を Foo という変数に代入、ということをやっています。このままでは Foo には何も入らないので、コンストラクタの代わりになる関数を作って返します。

var Foo = (function(){
    var Foo = function(){};
    return Foo;
})();

コンストラクタも Foo という変数に入っていますが、別に名前は何でもよくて、

var Foo = (function(){
    var Bar = function(){};
    return Bar;
})();

でも動きます。ただ、わかりやすさのためにはクラス名にしておいた方が良いでしょう。それに一部の実行環境では、コンストラクタが初めて変数に束縛されたときの名前をクラス名として認識するようなので( Google Chrome はそうでした)、見栄えの良さ、という面でもクラス名を付けた方が良いかと思います。

さて、次はパブリックメンバを追加しましょう。

var Foo = (function(){
    var Foo = function(a){
        this.bar = a;
    };
    return Foo;
})();


パブリックメンバなので、こうして参照することができます。

var foo = new Foo("bar");
foo.bar;        // -> "bar"

コンストラクタの引数もついでに書いてみました。次はプライベートメンバを追加します。

var Foo = (function(){
    var Foo = function(a){
        this.bar = a;
        this._baz = "initial value";
    };
    return Foo;
})();

「あれ、この書き方だと _baz と bar のアクセスレベルは変わらないんじゃ?」とお思いの方。正解です。実は以下の書き方でアクセスできてしまいます。

var foo = new Foo("bar");
foo._baz;        // -> "initial value"

現在の JavaScript にはアクセスレベル制御子( public とか、 private とか)のようなものはありません! ですので

「頭(またはお尻)にアンダースコアがついたメンバは private とするので、外からはアクセスしないでね」

と取り決めておいて、この辺は運用でカバーします。
別にアンダースコアを付けないといけない決まりはないのですが、 JavaScript 界隈ではアンダースコアを付けて目印とする傾向にあるようです。

お次はインスタンスメソッドを追加します。

var Foo = (function(){
    var Foo = function(a){
        this.bar = a;
        this._baz = "initial value";
    };

    Foo.prototype.qux = function(){
      console.log(this.bar);
    };

    Foo.prototype._foobar = function(){
        console.log("foo" + this.bar);
    };

    return Foo;
})();

関数オブジェクトが持つ prototype というオブジェクトにメンバとして関数を持たせると、これがインスタンスメソッドとして機能します。どうしてこうなるのかは説明すると非常に長くなるので、図入りで解説されているこちらをご覧になると良いかと思います。

メソッドもメンバと同じく、アクセスレベルの制限をかけられないので、運用で public と private を実現するしかありません。

今度はパブリックなクラスメンバ・クラスメソッドを追加します。

var Foo = (function(){
    var Foo = function(a){
        this.bar = a;
        this._baz = "initial value";
    };

    Foo.prototype.qux = function(){
      console.log(this.bar);
    };

    Foo.prototype._foobar = function(){
        console.log("foo" + this.bar);
    };

    Foo.Hoge = "this is public";

    Foo.Fuga = function(){
        console.log("this is public class method");
    };

    return Foo;
})();

「なぜ関数に新しいプロパティを追加できるんだ!」と驚いた友人も居ましたが、 JavaScript においては関数もオブジェクトなので、好きなタイミングでプロパティを追加できます。
(正しくは Object は Function のインスタンスなのですが、話がややこしくなるので詳しいことはここでは説明しません)

prototype に追加したメソッドと何が違うかというと、 Foo のメンバは Foo のインスタンスからは参照できないという点で異なります。なので、ちゃんとクラスメンバやクラスメソッドとして機能する、という訳です。

var foo = new Foo("bar");
Foo.Hoge;                // ->"this is public"
foo.Hoge;                // ->undefined

ということはインスタンスメソッドやコンストラクタからも直接は参照できないので、使いたいときは以下のようにする必要があります。

    // コンストラクタ
    var Foo = function(a){
        this.bar = a;
        this._baz = "initial value";

        // クラス名を含めて指定しないと、「 Fuga なんてメソッドはどこにもないぞ!」と怒られます
        Foo.Fuga();
    };

最後にプライベートなクラスメンバ・クラスメソッドを追加します。

var Foo = (function(){
    var Foo = function(a){
        this.bar = a;
        this._baz = "initial value";
    };

    Foo.prototype.qux = function(){
      console.log(this.bar);
    };

    Foo.prototype._foobar = function(){
        console.log("foo" + this.bar);
    };

    Foo.Hoge = "this is public";

    Foo.Fuga = function(){
        console.log("this is public class method");
    };

    var Moge = "this is private";

    var Piyo = function(){
        console.log("this is private class method.");
    };

    return Foo;
})();

これらは運用でカバーしなくともきちんと private になってくれるので、こう使います。
ぱっと見ただけでは「一番外側の無名関数の中で宣言された変数」です。C++ に馴染みが深い方だと、スコープが切れて変数も消えてしまうのではないかと思われるかも知れません。

しかしこいつら、消えません。こちらをご覧ください。

var gen = finction(){
    var i = 0;
    return function(){
        i++;
        return i;
    };
};

var inc = gen();
inc();            // -> 1
inc();            // -> 2
inc();            // -> 3

いわゆるクロージャーというものです。 JavaScript において変数が消えるのは、変数がどこからも参照されなくなったときですから、参照が残っている限りは変数は生き続けます。

プライベートなクラスメンバ・クラスメソッドはこれを使って実現しています。外から直接参照する術はなく、かつ内部からは同じ変数を参照するので、期待した役割を果たしてくれる、という訳です。

ちなみにプライベートなクラスメンバ・クラスメソッドは、クラス名付きで参照することはできません。

    // コンストラクタ
    var Foo = function(a){
        this.bar = a;
        this._baz = "initial value";

        // クラス名を含めて指定してしまうと、「 Piyo は undefined であって関数ではないぞ!」と怒られます
        Piyo();
    };

もちろん、インスタンスにプライベートなメンバを作るときと同じようにして運用でカバーしてもOKです。




という訳で、出来上がりは冒頭のテンプレートのようになります。


おまけ C++ で書くと


上の Foo クラスとなるべく等価になるように C++ でも書いてみました。説明がわからなくとも、なんとなく C++JavaScript の対応がわかると思います。

コンパイルしてテストした訳ではないので、間違っていたらごめんなさい。

class Foo{
private:
    static String Moge;
    static void Piyo();

    String baz;
    void foobar();

public:
    static String Hoge;
    static void Fuga();

    String bar;
    void qux();

    Foo(String a);
}

Foo::Moge = "this is private";

void Foo::Piyo(){
    cout << "this is private class method." << endl;
}

void Foo::foobar(){
    cout << "foo" << this.bar << endl;
}

Foo::Hoge = "this is public";

void Foo::Fuga(){
    cout << "this is public class method" << endl;
}

void Foo::qux(){
    cout << this.bar << endl;
}

Foo::Foo(String a){
    this.bar = a;
    this.baz = "initial value";
}