【JavaScript】 JSにおけるprivate変数と定義のひな型パターン
更新日:2023/10/05
プライベートフィールドについては、次のページを読んでみてください。
■【JavaScript】 JSにおけるクラスとは?正体についても調べてみた
このページはclass構文を使用しないケースでのアイデアとして残しておきます。
JavaScriptにクラスが導入されて、やっとprivateなメソッドやプロパティを利用できると思ったら、できないらしい。
多くの人が嘆き悲しんだことでしょう。
そもそもクラスは、既存の機能を異なる記述で実装できるようにしただけだから仕方がありません。
参考記事:【JavaScript】 クラスとは?正体についても調べてみた
今後は基幹部分にも手を入れて、privateなメソッドやプロパティを使えるようになるかもしれないけれど、今のところはクロージャの仕組みを利用するしかありません。
ということで、自分なりに『クロージャの仕組みを利用したprivateメソッドおよびプロパティのテンプレート』を用意したいと思います。
private変数とは
オブジェクト指向プログラミングにおいてのprivate変数は、オブジェクトメンバーのみから参照可能な変数を指します。
多くのオブジェクト指向では、変数に対してprivate属性を設定することで、private変数を定義することができます。
しかし今のところJavaScriptにprivate変数を作成する構文がありません。
今後導入される動きがあるようですが、しばらくは現状のままでしょう。
そのため、オブジェクトのプロパティは全てpublic(範囲外からも参照可能)です。
そこでJavaScriptでは、次のような手法でprivate変数を再現します。
クロージャによる情報隠蔽とカプセル化
function getFunc2(){
let sum = 0; // 外部から隠蔽されたprivate変数
return function (arg1){
sum += arg1;
return sum;
}
}
const func2 = getFunc2();
console.log( func2( 4 ) ); // 4
console.log( func2( 7 ) ); // 11
console.log( func2( 200 ) ); // 211
このコードとオブジェクトのカプセル化については、次のページを参考にしてください。
【JavaScript】 JSにおけるカプセル化手法
インスタンスとコンストラクタとprivate変数
コンストラクター関数でオブジェクトのインスタンスに対してprivate変数を定義したいとき、次のようにします。
function getObj(){
let counter = 0; // 外部から隠蔽されたprivate変数
// メソッド追加
this.up = function(){ counter ++; return this;};
this.down = function(){ counter --; return this;};
// ゲッターセッター追加
Object.defineProperty(this,"count",{
get:()=>counter,
set:( val )=>{
if( !Number.isFinite(val) )
throw new Error("数字ではない!!");
counter = val;
},
});
Object.freeze(this); // thisを凍結
}
const obj = new getObj();
console.log( obj.up().up().up().count ); // 3
letで変数を定義して、あとはthisにメソッドを追加していきますす。
▶注1:
thisは上書き禁止のため、次のようにオブジェクトリテラルを使用したプロパティ追加ができません。
this = { // エラー:Uncaught SyntaxError: invalid assignment left-hand side
up:function(){},
down:function(){},
};
そのため、一つ一つプロパティを追加していく必要があります。
上書きでないのに、プロパティを追加できることに疑問を感じた人は、次のページを読んでみてください。
■【JavaScript】 thisは上書き不可なのにプロパティ追加できる理由
▶注2:
ゲッターとセッターは、プロパティに直接追加できないため、Object.definePropertyメソッドを使用しています。
▶注3:
オブジェクトのプロパティはデフォルト状態では、上書き可能です。
そのため、メソッドを他のものに置き換えられる可能性があります。
これを防止するために、Object.freeze(this)でthis値を凍結しています。
ただしthisのメンバプロパティをpublicな変数として使用する場合、凍結すると変更ができなくなります。
プロトタイプを含めたprivate変数定義
次のコードのthis.counterを、隠蔽してprivate変数として扱う方法を考えてみます。
function getObj( v ){
this.counter = v;
}
getObj.prototype={
up : function(){this.counter ++;return this;},
get count(){ return this.counter }
};
手っ取り早いのが、次のパターン。
function getObj( v ){
let counter = v;
this.up = function(){counter ++;return this;};
Object.defineProperty(this,"count",{
get:function(){ return counter;}
});
}
const obj1 = new getObj(100);
console.log( obj1.up().up().up().count ); // 103
const obj2 = new getObj(200);
console.log( obj2.up().up().up().count ); // 203
プロトタイプをやめて、thisのプロパティとして追加します。
コンストラクター関数を頻繁に呼び出したり、他のオブジェクトにプロトタイプを継承させたいなどの理由がなければ、これで十分です。
どうしてもプロトタイプを使用したいなら、this値と変数を結びつける仕組みを構築する必要があります。
const getObj = (()=>{
const wMap = new WeakMap(); // (1) : WeakMapオブジェクトのインスタンス
// (2) : (1) にデータをセット/取得するメソッド
const getData = ( thisObj , prop ) => wMap.get(thisObj)[prop];
const setData = ( thisObj , prop , val ) => wMap.get(thisObj)[prop]=val;
// コンストラクター関数の定義
const getObj = function ( v ){
let counter=v;
wMap.set( this , { counter:v } );
};
// コンストラクター関数にprototypeメソッドを追加
getObj.prototype = {
up : function(){ setData( this ,"counter", getData(this,"counter") +1 ); return this;},
get count(){ return getData( this ,"counter" ) }
};
return getObj;
})();
const obj1 = new getObj(100);
console.log( obj1.up().up().up().count ); // 103
const obj2 = new getObj(200);
console.log( obj2.up().up().up().count ); // 103
即時関数で、(1)と(2)をprivate変数として持つコンストラクター関数を作成して返しています。
(1)のWeakMapは、オブジェクトをキーとしてデータを所持できるオブジェクトです。
今回はthis値をキーとすることで、インスタンス毎にデータを所持することが可能となっています。
なお上のコードではオブジェクトを凍結していませんが、上書きを禁止したいなら下記のように凍結することも可能です。
Object.freeze(getObj.prototype);
return Object.freeze(getObj);
グループ毎にprivateな共有データを定義する
上項のプロトタイプを含めたprivate変数定義では即時関数でコンストラクター関数を定義していましたが、関数化することで共通のデータを持った任意のコンストラクター関数を定義できます。
function makeClass( gakunen , kumi ){
const className = `${gakunen}年${kumi}組`;
const student = function(family,name){
Object.defineProperty(this,"studentName",{
get:()=>`${family} ${name}`
});
};
student.prototype={
get fullName() { return `${className} ${this.studentName}`}
}
return student;
}
const c31 = makeClass( 3 , 1 );
const c31menber = [];
c31menber.push( new c31( "佐藤","太郎" ) );
c31menber.push( new c31( "山田","花子" ) );
c31menber.forEach( e=>console.log( e.fullname) );
// 結果:3年1組 佐藤 太郎
// 3年1組 山田 花子
const c53 = makeClass( 5 , 3 );
const c53menber = [];
c53menber.push( new c53( "鈴木","三郎" ) );
c53menber.push( new c53( "山崎","桃子" ) );
c53menber.forEach( e=>console.log( e.fullname) );
// 結果:5年3組 鈴木 三郎
// 5年3組 山崎 桃子
makeClass関数で定義されたコンストラクター関数は、classNameだけでなく、引数で受け取ったgakunen、kumiもprivate変数として所持しています。
更新日:2023/10/05
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。