(JavaScript: IE)自作クラスの代入値を、参照は出来ても、メソッド以外では変更できないようにしてみる


注意: 本記事はSymbolなどの画期的な機能が存在しないIEなどの邪悪な古いブラウザを対象としたものです。「IEなんてアウト・オブ・眼中!オレは時代の最先端を征く!」という方はブラウザバックするか、ページ最後の「おまけ」にある、class式のプライベートフィールドやSymbolを使った例を参照してください。


普通のクラスの場合、代入値は簡単に変更できてしまいます。

/* カウンター用のクラス */
function X() {
    this.num = 0;
}
X.prototype.add = function(v) {
    this.num++;
}

var x = new X;

x.add();
console.log(x.num);
// 1

/* カウントの値を不正なものに書き換える */
x.num = 'aaa';
console.log(x.num);
// 'aaa'
x.add();
console.log(x.num);
// NaN

本来想定している値とは違うものに変えられてしまうと、予期せぬエラーの原因になってしまいますね。

解決策

  • Object.definePropertiesなどを使ってゲッターを設定し、代入値ではなくクラス内の変数を返却させる。
  • クラス内の変数にアクセスするため、メソッドはprototypeへの追加ではなく直接thisで代入する。
    • メソッドを書き換えられないよう、thisをロックする。
function X() {
    var _num = 0;
    Object.defineProperties(this, {
        'num': {
            get: function() {
                return _num;
            }
        }
    })
    this.add = function() {
        _num++;
    }

    Object.freeze(this);
}

var x = new X;

x.add();
console.log(x.num);
// 1

/* カウントの値の書き換えは無効 */
x.num = 'aaa';
console.log(x.num);
// 1

/* Object.freezeによって、メソッド・代入値の書き換えもロック */
x.add = function(){};
console.log(x.add);
// function() {
//     _num++;
// }
x.b = 1;
console.log(x.b);
// undefined

最後に

自分で書いておいてそれはどうかとは思いますが、この手法は使うべきではないでしょう。

メソッドがprototypeに登録されていなかったり、代入値の新規作成・変更が不可だったり(これについてはthisをロックしなければいいだけですが)と、普通の仕様とは全くの別物であるため、面倒な制約も数えきれないくらい付いてくることになるでしょう。

この手法はそんなに複雑なものではないですし、同じようなものを考え付く方は普通にいらっしゃると思います。しかしネットで検索してもこのようなものが出てこないということは、つまるところ使うメリットが無いということなんだと私は思います。「どうしても、そう、どうしても、書き換えられたくないんだ!」という人以外は、書き換えられるリスクを飲み込んで普通のクラスを書くことをお勧めします。

おまけ

class式をサポートしているブラウザのうち、その一部のブラウザには、「プライベートフィールド」なるものがあります(Safariは未サポート、Firefoxは試験的機能をオンにしている場合のみ。Babelに通してご利用ください。)。これを使えば、上と同様のことをデメリット無しに行えます。

class X {
    #num;
    constructor() {
        this.#num = 0;
    }
    add() {
        this.#num++;
    }
    get num() {
        return this.#num;
    }
}

var x = new X;

x.add();
console.log(x.num);
// 1

console.log(x.#num);
// Uncaught SyntaxError: Private field '#num' must be declared in an enclosing class

あるいはSymbolを使うという手もあります。

(global=>{
    var num = Symbol('num');

    var X = class {
        constructor() {
            this[num] = 0;
        }
        add() {
            this[num]++;
        }
        get num() {
            return this[num];
        }
    }

    global.X = X;
})(this);

丸ごと無名関数などに放り込み、クラスのみグローバルに放り出せば、外部からのシンボルへのアクセス方法が絶たれることで、実質的に代入値がプライベート化されます。何よりこちらはES6で規定されたものなので、モダンブラウザ全てで対応しています。