はじめに
こんにちは。私は通常業務ではjavaをメインに扱っていますが、Habitat Hubの活動では主にjavascriptをメインに扱っています。今までは最低限の文法を勉強した上でコーディングしていましたが、実際にサーバーサイドのロジックを組んだり、またその他のフレームワークの勉強をしようとしたりすると、「javascriptの基礎知識」が足掛かりになり、作業効率が悪くなっていることを実感しました。
そこで書籍を一通り読んで、何が一番理解できていなかったのかを振り返ってみたところ、ES2015前後でできる記述の仕方、主にオブジェクト指向の構文の部分が問題であった事に気づきました。
そこで今回は、javascriptのオブジェクト指向構文を「ES2015前後」でそれぞれ記述しながら、実装例をまとめていきたいと思います。
私なりに噛み砕いて理解し記述していますので、誤った記載が含まれている場合は、ぜひご指摘ください。
なお、本記事では継承については扱っていません。
目次
- クロージャ
- オブジェクト指向構文
- プロトタイプ(特権メソッドの例)
- プロトタイプ(Object.definePropertyを使用する例)
- クラス
- クラス(補足)
- まとめ
クロージャ
javascriptのオブジェクト指向の理解は、クロージャの理解なしでは進めません。
Javascript本格入門の記述を引用すると、クロージャとは「シンプルなオブジェクト」です。
ではここで、実際にクロージャのコードを書いてみます。
以下は、コンストラクターでカウンターの初期値を設定し、呼び出しの度にインクリメントするクロージャです。
// 1:クロージャを括っている親関数
function Counter(init) {
let _count = init; // 2:クロージャから参照されるローカル変数
// 3:クロージャ自身
return function () {
return ++_count;
};
}
// const (4: クロージャを格納する変数) = (5: クロージャ自身を括っている親関数の呼び出し)
const myCounter1 = Counter(0);
const myCounter2 = Counter(10);
console.log("result : " + myCounter1()); // => result : 1
console.log("result : " + myCounter2()); // => result : 11
console.log("result : " + myCounter1()); // => result : 2
console.log("result : " + myCounter2()); // => result : 12
3:クロージャ自身
は匿名関数です。
また、1:クロージャを括っている親関数
ではその匿名関数を返り値として返しています。
通常の関数呼び出しであれば、関数内のローカル変数はその呼び出しが終わると参照できなくなります。
しかし、上記のような実装をすることで3:クロージャ自身
がローカル変数を参照し続けているので、1:クロージャを括っている親関数
の呼び出し後も関数内のローカル変数を保持し続けられる訳です。
下の表と対応しながらコードを見ると、たしかにオブジェクト指向のそれぞれの用語と対応していることがわかります。
クロージャ | オブジェクト | |
---|---|---|
1 | クロージャを括っている親関数 | コンストラクター |
2 | クロージャから参照されるローカル変数 | プロパティ |
3 | クロージャ自身 | メソッド |
4 | クロージャを括っている親関数の呼び出し | インスタンス化 |
5 | クロージャを格納する変数 | インスタンス |
オブジェクト指向構文
それでは本題のオブジェクト指向構文についてです。
プロトタイプ(特権メソッドの例)
ES2015以前では、prototypes
を使用してオブジェクトを宣言します。
通常は以下のように使い分けをします
- プロパティの宣言はコンストラクター関数内
- メソッドの宣言はプロトタイプ ※例外あり(後述)
それでは上記使い分けの方針に従って、コードを書いてみます。
コンストラクターに渡した引数を、プライベートな変数とパブリックな変数それぞれに設定し、その値を取得する関数をプロトタイプで宣言します。
function PostOffice({ initialInternalPolilcy, initialOperatingHours }) {
let _internalPolicy = initialInternalPolilcy; // 社内方針
this.operatingHours = initialOperatingHours; // 営業時間
}
PostOffice.prototype.showInternalPolicy = function () {
console.log("internalPolicy:", _internalPolicy); //ReferenceError: _internalPolicy is not defined
};
PostOffice.prototype.showOperatingHours = function () {
console.log("operatingHours:", this.operatingHours);
};
const initialParams = {
initialInternalPolicy: "Employees must wear name tags",
initialOperatingHours: "9AM-5PM",
};
const myPostOffice = new PostOffice(initialParams);
myPostOffice.showInternalPolicy();
myPostOffice.showOperatingHours();
実行してみると、showInternalPolicyの呼び出しで、以下のエラーが発生します。
ReferenceError: _internalPolicy is not defined
なぜでしょうか。
理由は、上記showInternalPolicy
のように、プロトタイプで宣言したメソッドから、プライベートなプロパティにはアクセスできない為です。
プライベートなプロパティにアクセスするためには、コンストラクタ内に「プライベートメンバにアクセスするためのメソッド」を宣言する必要があります。
このメソッドのことを特権メソッド(プリビレッジメソッド)と呼びます。
function PostOffice({ initialInternalPolicy, initialOperatingHours }) {
let _internalPolicy = initialInternalPolilcy; // 社内方針
this.operatingHours = initialOperatingHours; // 営業時間
+ //コンストラクタ内にゲッターを定義
+ this.getInternalPolicy = function () {
+ return _internalPolicy;
+ };
+ //書き込みする場合は同様にセッターも定義
+ this.setInternalPolicy = function (value) {
+ _internalPolicy = value;
+ };
}
PostOffice.prototype.showInternalPolicy = function () {
- console.log("internalPolicy:", _internalPolicy);
+ console.log("internalPolicy:", this.getInternalPolicy());
};
PostOffice.prototype.showOperatingHours = function () {
console.log("operatingHours:", this.operatingHours);
};
const initialParams = {
initialInternalPolicy: "Employees must wear name tags",
initialOperatingHours: "9AM-5PM",
};
const myPostOffice = new PostOffice(initialParams);
myPostOffice.showInternalPolicy(); // => "Employees must wear name tags"
myPostOffice.showOperatingHours(); // => "9AM-5PM"
//値の編集
myPostOffice.setInternalPolicy("Employees must wear uniforms");
myPostOffice.operatingHours = "8AM-6PM";
myPostOffice.showInternalPolicy(); // => "Employees must wear uniforms"
myPostOffice.showOperatingHours(); // => "8AM-6PM"
特権メソッドは、プライベートメンバが定義されたスコープにアクセスできるクロージャであると理解すると良さそうです。
プロトタイプで宣言することで、オブジェクトをインスタンス化した場合に、インスタンス毎に「インスタンス共通のメソッド」をコピーする必要がありません。つまりその分のメモリを節約できます。
しかし、プロトタイプで宣言したメソッドからは「プライベートなメンバ」を参照できない点に注意が必要ということですね。
プロトタイプ(Object.definePropertyを使用する例)
前述の特権メソッドの実装は、Object.definePropertyメソッドを使用してよりシンプルに実装できます。
書籍でも、レガシーブラウザへの対応が不要であればこちらのメソッドを使用して実装をすることが推奨されています。
それでは実際に「Object.defineProperty」を使用して書き換えてみます。
※細かい呼び出しの仕方が変わるため、その部分のみハイライトして記載します。
function PostOffice({ initialInternalPolicy, initialOperatingHours }) {
let _internalPolicy = initialInternalPolilcy; // 社内方針
this.operatingHours = initialOperatingHours; // 営業時間
Object.defineProperty(this, "internalPolicy", {
get: function () {
return _internalPolicy;
},
set: function (value) {
//本来はここにバリデーションなどのコードを記述する
_internalPolicy = value;
},
});
}
PostOffice.prototype.showInternalPolicy = function () {
- console.log("internalPolicy:", this.getInternalPolicy());
+ console.log("internalPolicy:", this.internalPolicy);
};
PostOffice.prototype.showOperatingHours = function () {
console.log("operatingHours:", this.operatingHours);
};
const initialParams = {
initialInternalPolicy: "Employees must wear name tags",
initialOperatingHours: "9AM-5PM",
};
const myPostOffice = new PostOffice(initialParams);
myPostOffice.showInternalPolicy(); // => "Employees must wear name tags"
myPostOffice.showOperatingHours(); // => "9AM-5PM"
//値の編集
- myPostOffice.setInternalPolicy("Employees must wear uniforms");
+ myPostOffice.internalPolicy = "Employees must wear uniforms";
myPostOffice.operatingHours = "8AM-6PM";
myPostOffice.showInternalPolicy(); // => "Employees must wear uniforms"
myPostOffice.showOperatingHours(); // => "8AM-6PM"
Object.definePropertyを使用することで、ゲッターやセッターをシンプルに記述できるようになります。
また、それぞれ呼び出しを行う時も、パブリックなメンバとプライベートなメンバを意識せずに値の設定や取得の記述ができるようになっています。
クラス
それでは上記コードをクラス構文を使って記述してみます。
class PostOffice {
constructor({ initialInternalPolicy, initialOperatingHours }) {
this._internalPolicy = initialInternalPolicy; // 社内方針
this.operatingHours = initialOperatingHours; // 営業時間
}
set internalPolicy(value) {
this._internalPolicy = value;
}
get internalPolicy() {
return this._internalPolicy;
}
showInternalPolicy() {
console.log("Internal Policy:", this.internalPolicy);
}
showOperatingHours() {
console.log("Operating Hours:", this.operatingHours);
}
}
const initialParams = {
initialInternalPolicy: "Employees must wear name tags",
initialOperatingHours: "9AM-5PM",
};
const myPostOffice = new PostOffice(initialParams);
myPostOffice.showInternalPolicy(); // => Employees must wear name tags
myPostOffice.showOperatingHours(); // => 9AM-5PM
myPostOffice.internalPolicy = "Employees must wear uniforms";
myPostOffice.operatingHours = "8AM-6PM";
myPostOffice.showInternalPolicy(); // => Employees must wear uniforms
myPostOffice.showOperatingHours(); // => 8AM-6PM
get / set命令を使用することで、非常にシンプルにクラスが定義できるようになっています。
※本例では、showInternalPolicy, showOperatingHoursをクラス内に定義していますが、prototypeで定義することもできます。
prototypeでの実装例
PostOffice.prototype.showInternalPolicy = function () {
console.log("Internal Policy:", this.internalPolicy);
};
PostOffice.prototype.showOperatingHours = function () {
console.log("Operating Hours:", this.operatingHours);
};
ここで注意が必要なのは、
最初に_
をつけることでプライベートなメンバを示していますが、強制力はないという点です。
myPostOffice._internalPolicy = "Employees can wear anything they want";
console.log("Internal Policy:", myPostOffice._internalPolicy);
myPostOffice.showInternalPolicy();
_internalPolicyに直接アクセスすれば、普通に編集・参照できます。
クラス(補足)
以下のような方法で、プライベートなメンバを定義(もしくは再現)することができます。
Symbolの利用
Symbolを使用した実装例
const PostOffice = (function () {
const _internalPolicy = Symbol();
return class PostOffice {
constructor({ initialInternalPolicy, initialOperatingHours }) {
this[_internalPolicy] = initialInternalPolicy; // 社内方針
this.operatingHours = initialOperatingHours; // 営業時間
}
set internalPolicy(value) {
this[_internalPolicy] = value;
}
get internalPolicy() {
return this[_internalPolicy];
}
showInternalPolicy() {
console.log("Internal Policy:", this.internalPolicy);
}
showOperatingHours() {
console.log("Operating Hours:", this.operatingHours);
}
};
})();
const initialParams = {
initialInternalPolicy: "Employees must wear name tags",
initialOperatingHours: "9AM-5PM",
};
const myPostOffice = new PostOffice(initialParams);
myPostOffice.showInternalPolicy(); // => Employees must wear name tags
myPostOffice.showOperatingHours(); // => 9AM-5PM
myPostOffice.internalPolicy = "Employees must wear uniforms";
myPostOffice.operatingHours = "8AM-6PM";
myPostOffice.showInternalPolicy(); // => Employees must wear uniforms
myPostOffice.showOperatingHours(); // => 8AM-6PM
console.log("Internal Policy:", myPostOffice._internalPolicy); // => undefined
プライベートフィールドの利用
ES2022 で追加された「private field」を使う事でより厳密にプライベートなメンバを定義できます。
プライベートプロパティを含む詳しい説明はMDN Web Docsを参照ください。
private fieldを使用した実装例
class PostOffice {
#internalPolicy;
constructor({ initialInternalPolicy, initialOperatingHours }) {
this.#internalPolicy = initialInternalPolicy; // 社内方針
this.operatingHours = initialOperatingHours; // 営業時間
}
set internalPolicy(value) {
this.#internalPolicy = value;
}
get internalPolicy() {
return this.#internalPolicy;
}
showInternalPolicy() {
console.log("Internal Policy:", this.#internalPolicy);
}
showOperatingHours() {
console.log("Operating Hours:", this.operatingHours);
}
}
const initialParams = {
initialInternalPolicy: "Employees must wear name tags",
initialOperatingHours: "9AM-5PM",
};
const myPostOffice = new PostOffice(initialParams);
myPostOffice.showInternalPolicy(); // => Employees must wear name tags
myPostOffice.showOperatingHours(); // => 9AM-5PM
console.log("Internal Policy:", myPostOffice.#internalPolicy); // SyntaxError
まとめ
本記事では、クロージャの説明から、プロトタイプ・クラスの基本的な構文についてまとめてみました。
私が参考にした書籍の改訂3版が2023/2に発売されているようです。改訂3版では「ECMAScript 2022」に対応しているようなので、より新しい情報を参照されたい方は改訂3版を参考にしてみてください。
また、「ES2015以前」のコードを「ES2015+」に書き換える演習問題がこちらに公開されています。興味のある方は確認してみてください。
さいごに
私自身もまだまだわからないことだらけですが、今後も知見が溜まりましたらまた記事の投稿を行う予定です。
よかったらHabitat Hubのフォローよろしくお願いします!