LoginSignup
28
32

More than 5 years have passed since last update.

クラスの落とし穴4 - プライベート変数の実装

Last updated at Posted at 2014-04-09

はじめに

今回は、Object.definePropertyを使ってプライベート変数を定義した場合の落とし穴です。
プライベート変数を利用するプロパティ・メソッドを実装するにはどうしたら良いかを考えます。
そこにも、落とし穴が存在しました。

動作確認はnodeで行っていますので、モダンブラウザ以外では動作しない場合があります
今回は実験コードです。実務での使用には慎重になってください。

プロパティ

まず、普通のプロパティとの違いと実装方法を説明します。

function Klass() {
};
var k = new Klass();
k.name = 'foo';
k.name = 123;   // 本当は文字列だけ受け取りたいんだけど。。。

実装と言っても、特に何をするわけでもなく利用時に代入すれば良いだけです。
そのためnameにどのような型の値が設定されるかは利用者任せにされます。

Object.defineProperty

Object.definePropertyでは値の設定時や取得時に処理を記述する事が出来ます。
せっかく定義するので、型制限をつけてみましょう。

function Klass() {
};
Object.defineProperty(Klass.prototype, 'name', {
  enumerable: true,
  set: function (value) {
    if (typeof value === 'string') {
      this.name_ = value;
    } else {
      throw new TypeError('文字列ではありません');
    }
  },
  get: function () {
    return this.name_;
  }
});

var k = new Klass();
k.name = 'foo';
k.name = 123;   // 例外発生
k.name_ = 123;  // でも直接設定できてしまう

文字列以外の設定時に例外を発生させる事が可能になりました。
ただし、実際の値が格納されているname_というプロパティが丸見えです。
今回は、このname_をプライベート変数として定義してみます。

プライベート変数を定義

さて早速コードを。
次の実装でname_というプライベート変数を定義できました
Object.definePropertyの第三引数は複雑そうに見えて、即時関数がenumerable,get,setを持ったオブジェクトを返しているだけです。
それ以外はほとんど先ほどと変わりませんね。

function Klass() {
};

Object.defineProperty(Klass.prototype, 'name', (function () {
  var name_ = null;  // プライベート変数
  return {
    enumerable: true,
    set: function (value) {
      if (typeof value === 'string') {
        name_ = value;
      } else {
        throw new TypeError('文字列ではありません');
      }
    },
    get: function () {
      return name_;
    }
  };
})());

var k = new Klass();
k.name = 'foo';
console.log(k.name);
k.name = 123;  // 例外が発生

うまく動作していますね。

落とし穴はどこ?

しかし、今回も最初の例は落とし穴が存在します。
すぐに気がついた人は、prototypeを完全に自分のものにしていますね。

落とし穴をみつけましょう。

var k1 = new Klass();
var k2 = new Klass();

k1.name = 'foo';
k2.name = 'bar';

console.log(k1.name); // barと表示されてしまう
console.log(k2.name);

二つのインスタンスを作成し、それぞれnameを変更してみます。
しかし、あとに設定したものに2つとも変更されています。
実は、prototypeに設定した事でname_がクラス全体で使用されるstaticな変数になってしまいました。
これはいただけません。
しかもname_nameのGetter/Setterからしかアクセスできませんので、使い道が乏しくなります

インスタンスのプライベート変数を定義

コンストラクタ内でプライベート変数を設定するのはどうでしょう。
これならインスタンス毎にプライベート変数を定義出来るはずですし、複数のObject.definePropertyを定義しても共有できるようになります。
そこで次のように実装しました。

function Klass() {
  var name_ = null;
  Object.defineProperty(this, 'name', {
    enumerable: true,
    set: function (value) {
      if (typeof value === 'string') {
        name_ = value;
      } else {
        throw new TypeError('文字列ではありません');
      }
    },
    get: function () {
      return name_;
    }
  });
};

これは、たしかにうまい方法かも知れません。
でもクラスの落とし穴2 - メソッドとクロージャの問題が発生します。
つまり、このままではメモリーリークの危険性が高くなっています。
クラスの利用者を100%信じる事ができないならコンストラクタの中にインスタンスと同じライフサイクルをもつ関数はない方が良い事を覚えて置いてください。

その2

次は、コンストラクタから関数を追放したパターンです。

function Klass() {
  var pv = {};  // プライベート変数を格納するオブジェクト
  Object.defineProperty(this, 'name', getNameProperty(pv));
};

//定義を返す関数
function getNameProperty (pv) {
  return {
    enumerable: true,
    set: function (value) {
      if (typeof value === 'string') {
        pv.name = value;
      } else {
        throw new TypeError('文字列ではありません');
      }
    },
    get: function () {
      return pv.name;
    }
  };
};

クラスの落とし穴2 - メソッドとクロージャの問題を回避し、プライベート変数を定義できました。
ポイントは、コンストラクタでプライベート変数を格納するオブジェクトを作成することです。

メソッドの定義

メソッドの定義も簡単です。

function Klass() {
  var pv = {};  // プライベート変数を格納するオブジェクト
  Object.defineProperty(this, 'getName', getNameMethod(pv));
};

//定義を返す関数
function getNameMethod (pv) {
  return {
    value: function () {
      return pv.name;
    }
  };
};

プライベート変数をメソッドから使用するには、{value: Function}を設定します。
プロパティではenumerable:trueがあり、メソッドで省かれている理由はfor-inObject.keysのためです。

クライアントサイドでの注意点

今回のコードをクライアントサイドで利用する場合はgetNamePropertygetNameMethodがグローバル関数になってしまいます。
クラス定義全体を即時関数でくくってKlassを戻り値にする必要がありますが、それは説明していません。

さいごに

javascriptでプライベート変数を実装するなら自然とクロージャを利用するしかありません。
そのためprototypeの恩恵を得る事ができなくなります。メソッドをインスタンスに直接設定する必要が出てくるのは問題がありそうですね。

javascriptのクラスにはプライベート変数を無理に実装しなくてもいいのでは? と思っています。
しかし、どうしてもカプセル化をしたいのであれば今回の実験コードが役にたつかもしれません。

28
32
2

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
28
32