0
1

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 3 years have passed since last update.

javascriptでbindを使ってprivate変数を実現する

Last updated at Posted at 2021-06-15

bindを使うと、javascriptでprivate変数を実現できるという話です。

結論を先に書くと、こうなります。

function Counter(){
    let privates = {
        count : 0,
    };

    this.add = Counter.prototype.add.bind(this, privates);
    this.getCount = Counter.prototype.getCount.bind(this, privates);
}

Counter.prototype.add = function(privates, diff) {
    privates.count += diff;
}

Counter.prototype.getCount = function(privates) {
    return privates.count;
}

let c1 = new Counter();
c1.add(2);
console.log(c1.getCount());	// 2を表示。

以下、解説です。

###クロージャを使う方法(メモリ効率が悪い)
javascriptでprivate変数を作りたいという要望が出た時、一般的には以下のような、クロージャを利用した実装方法がとられることが多いと思います。

// クロージャを利用したprivate変数
function Counter(){
	let _count = 0;
	
	this.add = diff => _count += diff;
	
	this.getCount = () => _count;
}

この方法はシンプルで理解しやすいですが、Counterを生成するたびにaddやgetCountメソッドが生成される為、メモリ効率がよくありません。

let c1 = new Counter();
let c2 = new Counter();
let c3 = new Counter();

上記のc1, c2, c3それぞれに、異なるメモリ空間のaddとgetCountが生成されて割り当てられています。
処理内容は完全に同じなのですから、メソッドの生成は1回にして、それを全インスタンスで使いまわせるようにしたいところです。

###prototypeを使ったメソッド定義(privateにならない)
メソッドがインスタンス毎に生成されないようにする為には、通常、以下のようにprototypeプロパティにメソッドを追加します。
この方法なら、addとgetCountは1回しか生成されず、全てのCounterインスタンスで共有される為、メモリ効率の問題は起こりません。

// prototypeプロパティを使ったfunctionクラス定義(動作しない)
function Counter(){
	let _count = 0;
}

Counter.prototype.add = function(diff) {
	return _count += diff;
}
Counter.prototype.getCount = function() {
	return _count;
}

もちろんこの書き方はエラーになります。なぜなら、_count変数が、addやgetCountから参照できない為です。
_count変数はCounterのコンストラクタ内で定義されたローカル変数ですから、その外で定義されているaddやgetCountからそれを参照することはできません。

_count変数をaddやgetCountから参照する為には、以下のように書きます。

// prototypeプロパティを使ったfunctionクラス定義(動作するがprivateになっていない)
function Counter(){
	this._count = 0;
}

Counter.prototype.add = function(diff) {
	return this._count += diff;
}
Counter.prototype.getCount = function() {
	return this._count;
}

_count変数はthisのメンバ変数になっているので、addやgetCountからも参照できるようになります。

しかしこれでは、当初の目的である、_count変数をprivateにするという点からすると失格です。
thisのメンバ変数だということは、外部からも参照可能であることを意味します。これでは意味がありません。

let c1 = new Counter();
c1.add(1);
console.log(c1._count); // エラーにならず、0が出力される。

###インスタンス変数ではなくクラス変数になっている例
稀に、以下のようなコードを書いて「private変数を実現できました!」と書いている人もいますが、正しくありません。

// private変数がインスタンス毎に生成されていない、つまりクラスメンバ変数になっている
function Counter(){
}

(function(){
	let _count = 0;
	Counter.prototype.add = function(diff) {
		return _count += diff;
	}
	Counter.prototype.getCount = function() {
		return _count;
	}
})();

addやgetCountからのみ参照できる_count変数をクロージャ内で定義しており、一見うまく動くように見えます。
しかし、これは正しく動きません。

let c1 = new Counter();
let c2 = new Counter();
c1.add(1);
console.log(c2.getCount()); // 0ではなく1と表示される。つまりc1とc2が同じ_countを共有している。

上記のやり方は、「クラス変数」を定義するには有効ですが、「インスタンス変数」を定義したことにはなっていません。

###これまでのまとめ
問題をまとめると、次のようなことです。

(1) private変数を作るにはコンストラクタ内のローカル変数として定義しなくてはならない。
(2) そのローカル変数をメソッドから参照するにはメソッドも同じくコンストラクタ内で定義しなくてはならないが、メモリ効率が悪い。
(3) メソッドのメモリ効率をよくしようとコンストラクタの外に出すと、private変数(ローカル変数)を参照できなくなる。

###バインドを利用したprivate変数
これらの問題を解決する方法として、バインドを利用した以下のような中間的な方法がありますので、今回ご紹介したいと思います。

function Counter(){
    let privates = {
        count : 0,
    };

    this.add = Counter.prototype.add.bind(this, privates);
    this.getCount = Counter.prototype.getCount.bind(this, privates);
}

Counter.prototype.add = function(privates, diff) {
    privates.count += diff;
}

Counter.prototype.getCount = function(privates) {
    return privates.count;
}

let c1 = new Counter();
c1.add(2);
console.log(c1.getCount());	// 2を表示。

考え方としては、private変数をまとめて「privates」という名前のローカルオブジェクト変数に格納し、それを、prototypeで定義したメソッドにバインドし、自身の新たなメソッドとして再定義します。

こうすることで、addやgetCountに、コンストラクタで生成したprivate変数を渡しつつ、外部からはそれらを参照できないようにします。
このやり方もインスタンス毎にメソッドを生成していますが、メソッドの大部分はprototypeの方に存在する唯一のものを利用しているので、そこまで無駄にはなりません。

ちなみに、prototypeプロパティを使ってはいますが、実際にはすぐに同名のメソッドで上書きしており、prototypeチェーンとしては機能していません。
なので、prototypeを使わず、以下のようにfnというプロパティを別途作ってそこに定義しても、同様の結果になります。

function Counter(){
    let privates = {
        count : 0,
    };

    this.add = Counter.fn.add.bind(this, privates);
    this.getCount = Counter.fn.getCount.bind(this, privates);
}

Counter.fn = {
    add: function(privates, diff) {
        privates.count += diff;
    },
    
    getCount: function(privates) {
        return privates.count;
    }
}

prototypeチェーンを放棄していますので、prototypeチェーンを前提にした継承構造を作ったりする際には向きませんが、比較的シンプルに、理解しやすく、管理もしやすいやり方でprivate変数を実現する方法として、覚えておいても良いかもしれません。IE11を含め、ほぼすべてのブラウザで動作します。

ところで、今回のやり方はIE11でも動作するやり方ですが、もしこの書き方をclass構文で書きたい場合は、以下のように書けます。class構文はfunctionとprototypeを使ったクラス宣言の糖衣構文にすぎないので、同じようにかけるわけです。

class Counter {
    constructor() {
        let privates = {
            count : 0,
        }

        this.add = Counter.prototype.add.bind(this, privates);
        this.getCount = Counter.prototype.getCount.bind(this, privates);
    }

    add(privates, diff) {
        privates.count += diff;
    }

    getCount(privates) {
        return privates.count;
    }
}

ただ、この書き方はほぼすべてのブラウザで動作するものの、IE11ではclass構文がサポートされていない為、動作しません。
ですので、どうしてもIE11でclassを使ってprivateフィールドを実現したい、という場合には、Babelなどのプリプロセッサを使って上記の書き方をする、というのも手かもしれません。

###本物のprivateフィールド構文(ES6)
もしIE11を捨てても良いのであれば、class構文自体にまもなく正式導入される、本物のprivateフィールド構文を利用した方がいいかもしれません。

javascriptの最新仕様ES6では、もうすぐclassにprivateフィールドが導入される予定です。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Classes/Private_class_fields

これが実現すると、次のように書くことができるようになります。フィールドの先頭に # を付けるとprivateになります。
シンプルでわかりやすいですね。
privateフィールドについては様々な実装方法が検討されたようですが、現在のjavascriptの動作速度を落とさずに実装する為には、プロパティ名を見ればそれがprivateかどうかをjavascriptエンジンが判断できるというのが大事だった模様です。

class Counter {
    #pcount = 0;

    add(diff) {
        this.#pcount += diff;
    }

    get count() {
        return this.#pcount;
    }
}

そして実は、privateフィールドはもう、いくつかのブラウザ使うことができます(IE11はダメです。今後もサポートされることはないでしょう)。
Chromeでは2019/4/23から使えており、EdgeやOperaでも最新版で問題なく使えます。
SafariとFirefoxの対応が遅れていたようですが、Safariは2021/4/26に、Firefoxも2021/6/29にサポートされる見込みとのこと。
モバイル版のブラウザの対応も同様のようです。

レガシーブラウザ対応は相変わらず重要とは思いますが、数年後にはもう、javascriptのクラス定義は、classを使ったやり方に統一されていくのでしょう。

0
1
0

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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?