Help us understand the problem. What is going on with this article?

クラスの落とし穴2 - メソッドとクロージャ

More than 5 years have passed since last update.

はじめに

前回は、プロパティをコンストラクタで 初期化しなかった場合に起きる落とし穴を投稿しました。
今回は、メソッドをコンストラクタで 初期化した場合に起きてしまう落とし穴を投稿します。

プロパティとメソッドでコンストラクタ初期化するかしないか推奨の実装方法に差があるのは、javascriptの興味深いところです。

クラス定義

まずは前回のおさらいで、クラス定義を次のように書くようにしていました

// クラスを定義
function Klass1 () {
  // プロパティ
  this.name = 'foo';
  this.hobbies = [];
};

// メソッド
Klass1.prototype.setName = function setName (value) {
  this.name = value;
};
Klass1.prototype.addHobby = function addHobby (value) {
  this.hobbies.push(value);
};

これを次のように変更しても動作は同じになります。

// クラスを定義
function Klass2 () {
  // プロパティ
  this.name = 'foo';
  this.hobbies = [];

  // メソッド
  this.setName = function setName (value) {
    this.name = value;
  };
  this.addHobby = function addHobby (value) {
    this.hobbies.push(value);
  };
};

prototypeの役割

上の二つのクラス定義(Klass1, Klass2)から作成されたそれぞれのインスタンスの内容を出力してみましょう

var inst1 = new Klass1();
var inst2 = new Klass2();

console.log(inst1);
// -> {name: 'foo',hobbies: []}

console.log(inst2);
// -> {name: 'foo',hobbies: [], setName: [Function: setName], addHobby: [Function: addHobby]}

最初のクラス定義ではメソッドはインスタンスのプロパティではありません。
二番目のクラス定義ではメソッドもプロパティです。
prototypeは、共通の処理を複数のインスタンスから使用することができます。
そのため、最初の方がメモリの節約になる事は分かると思います。
よってこの例では、後者よりも前者の方が優れています。

クロージャの活用

メモリの節約はできますが、インスタンスに定義することでクロージャを使用する事ができるのは便利です。
jsにすこし慣れた頃に活用したくなって次の様にしてみたくなります。

// クラスを定義
function Klass (name) {
  // メソッド
  this.sayName = function sayName () {
    console.log('私は' + name + 'です。');
  };
};

var inst = new Klass('foo');
inst.sayName();

しかし、これはやはりこのようにすべきです。

// クラスを定義
function Klass (name) {
  // プロパティ
  this.name = name;
};

// メソッド
Klass.prototype.sayName = function sayName () {
  console.log('私は' + this.name + 'です。');
};

var inst = new Klass('foo');
inst.sayName();

前者のインスタンスに直接メソッドを追加する利点はなんでしょうか?
それはnameをprivate変数にしてカプセル化に成功している事があげられます。
しかしそれでもクロージャを安易に多用すべきものではありません。
次の項でクロージャによってメモリリークを引き起こす例を挙げます。

メモリリークの危険地帯

コンストラクタでは引数に複雑なオブジェクトが渡される事があります。
じつはその際にクロージャはメモリリークの温床になります。

例えば、次の例で確認できます。

// クラスを定義
function Klass (status) {
  // メソッド
  this.sayName = function sayName () {
    console.log('私は' + status.name + 'です。');
  };
};

// とりあえず全ステータスを管理するオブジェクト
var allStatus = {
  s1: {
    name: 'foo',
    blob: '非常に大きいデータ。例えばBLOBなど'
  }
};

// インスタンス化
var inst = new Klass(allStatus.s1);
inst.sayName();

// メモリの開放を期待
delete allStatus.s1;

上記では最後の行でallStatusからs1のプロパティをdeleteしたことで、GCに回収される事を期待しています。
クラスの定義部分は無視して、var allStatus以下をみるだけではそのように動作しても良さそうです。

しかしコンストラクタにGC対象を渡している先でクロージャにより捕捉され、 回収される事はありません。
しかもクロージャで使用したいのはnameの変数だけですが、 インスタンスが存在する限りblobもメモリに常駐します。
ここでは、blobはallStatus初期化時に設定されていますが、そうとは限りません。
インスタンスが作成された後でもblobの追加は起こりえます。
そうなるとますますメモリリークを調査する事が難しくなります。
コンストラクタの引数がオブジェクトの場合は、クロージャを使用する際に注意する必要がある事がよくわかると思います。

この短いサンプルでも、定義部分の前半とステータスの処理の後半を別々に見ていると、メモリリークの危険性が存在する事は分かりません。

今回はたかが2つのメンバをもつオブジェクトですが、実際にはもっと複雑なオブジェクト例えば循環参照をもつなどのオブジェクトの場合は、多くの不要になったオブジェクトを抱えたままの状態で動作する事になります。

メソッドなどの即時でない関数のスコープの外側にオブジェクト型の変数が存在する状況は メモリリークの危険地帯 である事を認識してください

プライベート変数を実装

それではnameだけ安全にprivate変数として使用するにはどうすれば良いのでしょうか?
それは、次のようにする必要があります。

// クラスを定義
function Klass (status) {
  // プライベート変数
  var name = status.name;
  // メソッド
  this.sayName = sayNameCreator(name);
};

// nameをクロージャで捕捉したsayNameを返す
function sayNameCreator (name) {
  return function sayName () {
    console.log('私は' + name + 'です。');
  };
};

コンストラクタからnameだけを引数にしたクロージャを活用します。
この場合は、statusがGCの対象になります。
また、nameはプリミティブな変数のため、オブジェクトをそのままprivare変数にするよりメモリリークの危険性がぐっと減りました。

クロージャ関数のカプセル化

しかしこれでは、sayNameCreatorがKlassと同じスコープに変数として存在してしまいます。
sayNameCreatorをクラス定義にカプセル化できないでしょうか?
その為には、更に関数スコープで包括する必要がありそうです。
ここでは即時無名関数を使用する方法を紹介します。

引数はまたnameだけとしましょう

klass.js
// クラスを定義
var Klass = (function () {

  function Klass (name) {
    // メソッド
    this.sayName = sayNameCreator(name);
  };

  function sayNameCreator (name) {
    return function sayName () {
      console.log('私は' + name + 'です。');
    };
  };

  // クラス定義を返す
  return Klass;
})();

sayNameCreatorは関数内のローカル関数になりました。
そして、Klassはreturnによって外部から参照出来ます。

Node.jsではカプセル化は不要

最近は、上記のようにクラス定義を関数スコープに閉じ込めて記述している例が多いので、あまり理解せずにクラス定義はこのようにするものだと思ってしまっているかもしれません。

そのため、いつでもカプセル化をしておけば良いと思いがちですが、node.jsなどCommonJSに準拠している場合は、カプセル化はまったくの不要です。
ファイル内ではすべてローカル変数になるため、クラスとして定義された関数はmodule.exportsに代入するだけです。

変更ができるprivate変数

じつは気づいているかもしれませんが、先のprivate変数なのにクラスからも値を変更できない欠点があります。
では、setNameメソッドを追加してprivate変数を変更するにはどうすれば良いでしょうか?
それは、private変数をオブジェクトに変更します。

// クラスを定義
var Klass = (function () {

  function Klass (status) {
    // プライベート変数
    var pr = {
      name: status.name
    };
    // メソッド
    this.sayName = sayNameCreator(pr);
    this.setName = setNameCreator(pr);
  };

  function sayNameCreator (pr) {
    return function sayName () {
      console.log('私は' + pr.name + 'です。');
    };
  };

  function setNameCreator (pr) {
    return function setName (name) {
      pr.name = name;
    };
  };

  // クラス定義を返す
  return Klass;
})();

せっかくオブジェクトからメモリリークの起きにくいよう文字列のみカプセル化したのに、またオブジェクトにしたら意味がないと思うかもしれません。しかし状況は以前と全く異なります。プライベート変数を再構築する事でコントロール出来ています。
そのためクラスで必要としない余分な参照が削除されています。

jsにprivate変数は必要なのか?

ここからは知識の共有というよりは概念やコーディングルールといった内容のため、絶対これが正解といったものではありません。

とここまで、コンストラクタ内で(メソッドの初期化による)クロージャを使用しないでprivate変数を作成する方法を考えてきました。

しかし

クラス定義で必ずしもprivate変数が必要になるかと思うと個人的にはそうでもないのでは?と思っています
今回は簡単な例でしたので、比較的追っかけるのが楽でしたが、複雑なコードではprivate変数をウォッチするのは大変です。
そのため、クロージャ使用を最低限にしメモリリークの危険を少なくするように心がけた方が良いと思います。。。が
コレばかりはオブジェクト指向支持者との宗教戦争になるのでなんとも判断しにくい部分として「わからない」を答えにしてまとめたいと思います。

もしprivate変数を無理に作成しないでコーディング規約のみ処理するのであれば、次のようにします。
結局最初のクラス定義と変わらないように見えますが、どこが違うでしょうか?

// クラスを定義
function Klass (name) {
  // _が最後についた変数は仮想private変数です
  this.name_ = name;
};
// メソッド
Klass.prototype.sayName = function sayName () {
  console.log('私は' + this.name_ + 'です。');
};

ただ変数名の後ろに_をつけただけです。
ふざけているようですが。コレによって外部から操作しないという目印にします。
実際にこの方法はよく見かけます。
もちろん_でなくてもかまいせんし、位置も前でも後ろでもかまいません。
jsはプロトタイプベースの言語です。忠実にOOを実装することをこだわるのではなく、時にはこういった割り切りも必要です。

また、ECMAScript5環境下ではObject.definePropertyを使用する事でよりprivate変数ぽくなります。
(実際にはアクセスできます)

// クラスを定義
function Klass (name) {
  // _が最後についた変数は仮想private変数です
  Object.defineProperty(this, 'name_', {value: name, writable: true});
};
// メソッド
Klass.prototype.sayName = function sayName () {
  console.log('私は' + this.name_ + 'です。');
};

var inst = new Klass('foo');
console.log(inst); // {}となり_nameは列挙されない
console.log(inst.name_); // 'foo'

クロージャはprivate変数を定義する為に安易に利用しない方がいい場合があるという内容でした。
それでもクロージャは強力な機能です。クラス定義よりもっと活用する場面があります。それについてはまた次回。

メモリリークについて

本当のメモリリークと今回あげたメモリリークは厳密に言えば違うのですが、解説のためにわかりやすくしています。
実はコンストラクタ内の関数のスコープで捕捉されたオブジェクトは、インスタンスが破棄されればGCの対象になります。
しかし、javascriptでは一度作成されたインスタンスが確実に破棄されたかを確認しにくいという問題があります。
deleteキーワードがありますが、それは参照を切るだけで解放の対象にすることを意味しません。
Cのfreeのようなものが存在しません。
別の場所で参照が存在し、それに気がつかないのであればメモリリークと同じです。
そして、クロージャを使用すると無駄に参照が残っていることがあることも多いのも事実です。

さいごに

いろいろと説明しましたが、実は覚えておくことは多くありません。
「コンストラクタ内にfunctionのキーワードを含まない」 これだけ覚えておくだけで、不要な参照をずいぶんとなくすことができます。

cocottejs
Java,C#,PHPと業務系に携わってきました。最近はnodejsでフレームワークを作成中。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away