Javascriptでprivateフィールドを実現する

 javascriptでprivate変数や、それに準ずるスコープを定義することは永遠の命題でした。

 アンダースコアを付けて規約で縛る方法やら、ややこしい実装でかえって可読性を犠牲にしているような方法まで。

 不可能であると結論づけたいところですが、無理やり実現する方法を考えてみます。

クロージャで定義する

var privateObject = (function(){
    var privateNumber = 0;

    return {
        set: function(number){
            privateNumber = number;
        },
        get: function(){
            return privateNumber;
        }
    }
})();

privateObject.set(1);
console.log(privateObject.get()); // 1

 これで、privateNumberに外部からアクセスする方法は無いにも関わらず、getter/setterを通じて自由に変更、読み取りを行うことができます。

 しかし、privateNumberは言わば常にstaticな変数であり、インスタンス変数として持つことが出来ないという点が問題です。

 これは、いわゆるシングルトン、staticなオブジェクトに有効な方法ですが、では、newを通じて作るインスタンスオブジェクトではどうでしょうか。

インスタンス作成も可能

 waniwaniさんより、コメントを頂きました。ありがとうございます!

 クロージャを使う場合でも、new演算子でインスタンスを生成することができるようです。private変数を求めていた方には朗報ですね。

function sample(num) {
  let number = num;
  return {
    get number(){ return number; }
  };
}

let s1 = new sample(1);
let s2 = new sample(2);
console.log(s1.number);  // -> 1
console.log(s2.number);  // -> 2

 注意点としては、prototypeではないため、継承するときに少し気を使わなければいけない点です。とはいえ、昨今の端末は大変リッチなので、オブジェクトに持ってしまっても問題はないでしょう。

// これは動かない
class extended extends sample{
  say(){
    console.log("hello!");
  }
}


let s3 = new extended(3);
s3.say(); // -> TypeError: s1.say is not a function

 上記コードは、当然ですがprototypeチェーンが切れてしまっているため、思うように動きません。なので、以下のようにします。

// これは動く。
function extended(num){
  let _super = new sample(num);
  _super.say = function(){
    console.log("hello!");
  };
  _super.sayNumber = function(){
    console.log(_super.number);
  };
  return _super;
}

let s4 = new extended(4);
s4.say(); // -> Hello!
s4.sayNumber(); // -> 4;

 カジュアルに使う分には、簡潔で非常に素晴らしいと思います。

 ただ、インスタンス毎にreturnしているオブジェクト内にfunctionが生成されるため、メモリを気にしなければならない場合は十分注意して使ってください。

配列を使う

 privateNumberの値が他インスタンスと共有されていなければよいと考えることができます。それなら配列を利用してみるのはどうでしょうか。

var privateMultiObject = (function(){
    var currentId = 0;
    var privateNumbers = [];

    var privateObject = function(){
        // constructor
        this.id = ++currentId;
        Object.freeze(this);
    }

    privateObject.prototype.setPrivateNumber = function(number){
        privateNumbers[this.id] = number;
    }

    privateObject.prototype.getPrivateNumber = function(){
        return privateNumbers[this.id];
    }

    return privateObject;
})();

var a = new privateMultiObject();
var b = new privateMultiObject();
a.setPrivateNumber(5);
b.setPrivateNumber(10);
console.log(a.getPrivateNumber());  // 5
console.log(b.getPrivateNumber());  // 10

a.id = 2
console.log(a.getPrivateNumber());  // 5

 この場合、this.idが変更されてしまうと他の変数を参照されたりしてしまうデメリットがあります。

 なので、本例では、Object.freeze()メソッドでオブジェクトを凍結し、外部から変更できないようにしています。

 ソースコードでidに2を代入(しようと)しているにも関わらず、返却される値はid=1の状態のものであることを確認してください。

 しかしながら、本例は真の意味でのprivate変数ではなく、言わば無理矢理に近い実装です。

 やはり、javascriptではその流儀に則り、このような小手先の技を使うことなく、利点と欠点を認識して使うことが大切だと思います。

 ご存知のとおり、Javascriptは自由度が高く、なんでも出来る言語です。もしかしたら、もっと良い方法があるかもしれませんし、無いかもしれません。是非いろいろ考えてみてください。

まとめ

 無理にprivate定義する必要があるかは悩ましいことではありますが、こういった問題も時には生じ得ることを踏まえ、言語仕様と仲良くやっていきたいものですね。

 また、今後はES6が流行してくることも予想されます。現在でもBabelをはじめ、様々なaltJSが存在しますので、他の言語のような記述を行いたい場合は、そちらも検討してみることをお勧めします。

コメント

  1. waniwani より:

    いつも、クロージャとnewを使ってやってるんですけど、これ話題が違いますか?

    function sample(num) {
    let number = num;
    return {
    get number(){ return number;}
    }
    }

    let s1 = new sample(1)
    let s2 = new sample(2)
    console.log(s1.number)
    console.log(s2.number)

    • tam-tam より:

      コメントありがとうございます。
      確かに、privateフィールド実現できてますね!素晴らしいです!

      ただ、以下のデメリットがあります。

      * privateフィールドにアクセスするメソッド及びオブジェクトは、インスタンス生成ごとに作成する必要があるため、メモリ及びコストの増加を懸念する必要がある

      * prototypeチェーンからprivateフィールドを参照できないため、継承に少し手間がかかる

      とはいえ、現代はPCスペックもどんどん上がってきているので、継承を考慮しない、もしくはfinal扱いの具象クラスということであればベストな方法だと思います。

      よろしければ本記事内でご紹介させて頂いてもよろしいでしょうか?

      • waniwani より:

        返信ありがとうございます。あと、お返事遅くなってすみません。(blogのコメントへの返信って通知こないんすよね)

        結構カジュアルな感じで使ってます。コード補完するときに余計なものが出ないだけでもコーディングしやすいので。

        < よろしければ本記事内でご紹介させて頂いてもよろしいでしょうか?

        はい、適当にわかりやすいようにいじって使って下さい。

        あと、publicなメソッドから、別のpublicなメソッドを呼ぶケースがあるときは、
        return {public object部分}
        とせずに
        let _ = {public object部分};
        return _;
        として、public object部分で _ を使って自己参照してます。

        • tam-tam より:

          ご返信ありがとうございます。
          そうですねえ、メール通知送るようには出来ると思いますが、スパムも多いですし、他人のメールアドレスを記入する可能性もありますから、なかなか難しいですよね…。

          コード補完は考えていませんでした!確かにそれはありますね。publicから別のpublicを参照する方法についても、併せて掲載させて頂きますね。