classキーワードによるクラス
《 初回公開:2022/11/17 , 最終更新:未 》
この記事の内容の検証はGoogle Chromeを使っておこなっており、他のWebブラウザや同じChromeでもバージョンによっては異なる結果を示すかもしれない。(当サイトの記事は基本的にはWindowsおよびWindowsのChromeブラウザについて記述になります。)
【 目次 】
だいぶ以前に、「不思議の国のJavaScript オブジェクト指向に関するtips - 愚鈍人」という記事で、JavaScriptのオブジェクト指向について書いたが、
その時は、クラスを定義するのにfunctionを使った。
その後classキーワードなど、新たなクラスの定義方法が導入されててきたのでこれについて調べてみる。
従来通りのfunctionを使ったクラスの定義も可能で、実はclassはfunctionのシンタックスシュガーで、classキーワードの背後にはfunctionがあるのだが。
JavaScript オブジェクト クラス インスタンス
クラス構文
クラス構文にはクラス式とクラス宣言の 2 つの定義方法が、 そして、クラスはクラス名を示すnameプロパティを持つ。
クラス宣言
クラス宣言の例
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
関数は巻き上げされるのに対してクラスは巻き上げされない。
巻き上げというのは宣言される前に使う事ができるという意味で。
クラスの定義をする前にクラスを使う事ができないという事で、
以下のコードはエラーになる。
const p = new Rectangle(); // ReferenceError
class Rectangle {}
クラス式
クラス式には名前付きのクラス式と名前なし(無名)のクラス式とがある。
名前付きのクラス式の例
let Rectangle = class Rectangle2 {};
console.log(Rectangle.name); // 出力: "Rectangle2"
名前なしのクラス式の例
let Rectangle = class {};
console.log(Rectangle.name); // 出力: "Rectangle"
クラスを再定義することはできない。
class MyCls { }
// 再度、同名のクラスを定義したため次の行でエラーが発生
class MyCls { } // Uncaught SyntaxError: Identifier 'MyCls' has already been declared
クラスのスコープ
以下のコードはクラスがletやconstと同様にブロックスコープである事を示している。
function func() {
{
// 定数宣言
const C = "const";
let l = "let";
// クラス定義(クラス宣言)
class Rectangle { }
// 名前付きのクラス式
let Rectangle2 = class { };
// 名前なし(無名)のクラス式
let RectangleX = class Rectangle3 { };
// letやconstはブロックスコープ
console.log(C);
console.log(l);
// クラスもブロックスコープ
console.log(Rectangle.name);
console.log(Rectangle2.name);
console.log(RectangleX.name);
//console.log(Rectangle3.name); // エラー: Rectangle3でアクセスする事は許されない
}
// ブロックの外なので以下のコードは全てエラーに
console.log(C);
console.log(l);
console.log(Rectangle.name);
console.log(Rectangle2.name);
console.log(RectangleX.name);
}
func();
// ブロックの外なのでこれもエラー
console.log(Rectangle.name);
フィールド変数(インスタンスフィールド)
パブリックフィールド
フィールド変数はクラス定義の中で変数を指定する事で定義できる。
class Rectangle {
// パブリックフィールド宣言
p = 10;
// p2は値が代入されていないパブリックフィールド
p2;
}
letやvar,constを使ってのフィールド変数の指定はできないようだ。
フィールド変数をletやvar,constを使って指定する事はできない
class Rectangle {
// 以下はすべてエラー
let p3;
var p4;
const pc="xxx";
}
上記のコードの変数pやp2はパブリックフィールドと呼ばれる。
パブリックフィールドはクラスのインスタンスからアクセスできる。
// Rectangleクラスのインスタンスを変数rに代入
let r = new Rectangle();
// フィールド変数pをコンソールに出力
console.log(r.p); // 10
フィールド変数はインスタンス変数で、クラスの変数ではないのでクラスからアクセスできない
console.log(Rectangle.p); // undefined
フィールド変数はインスタンスに定義されていてprototypeのプロパティでは無い。
console.log(Rectangle.prototype.p); // undefined
クラスのパブリックフィールドとオブジェクトのプロパティ
クラスのインスタンスも従来のオブジェクトと同様に、特定のインスタンスだけで使えるプロパティ(フィールド変数やメソッド)をクラス定義の外部から定義できる。
r.a = "xxx";
console.log(r.a); // xxx
// 同じクラスのインスタンスであっても異なるインスタンスからはアクセスできない。
let r2 = new Rectangle();
console.log(r2.a); // undefined
// インスタンスにメソッドも定義できる。
r.f = function () {
console.log("r.f");
}
r.f(); // "r.f"
従来のオブジェクト同様にクラスオブジェクトのprototypeに変数を定義するとパブリックフィールドのように扱える
// 同じクラスのインスタンスであれば同じ値に
Rectangle.prototype.b = "yyy";
console.log(r.b); // yyy
console.log(r2.b); // yyy
// でも変数の値を変更してしまうと異なる値になる
r.b = "aaa";
console.log(r.b); // aaa
console.log(r2.b); // yyy
// そしてprototypeの値は元のまま
console.log(Rectangle.prototype.b); // yyy
クラスを従来のJavaScriptのオブジェクトとして扱うことはできるようだが、クラスの概念と混乱しないためにはprototypeに変数を定義するのは避けた方がいいようだ。
プライベートフィールド
変数名の先頭に#の付いているフィールド変数はプライベートとなり、クラス定義の外部からアクセスできない。(隠蔽できる。)
プライベートフィールドは通常のプロパティとは違い、this への追加によって後から作成することができない。
呼び出される前に宣言されていないプライベートフィールドを参照したり、宣言されているフィールドを delete で削除しようとしても構文エラーになる。
class ClassWithPrivateField {
#privateField;
constructor() {
this.#privateField = 42;
// プライベートフィールドは通常のプロパティとは違い、this への追加によって後から作成することができない
this.#undeclaredField = 444; // Syntax error
// 宣言されているフィールドを delete で削除する事はできない
delete this.#privateField; // Syntax error
}
}
const instance = new ClassWithPrivateField()
// クラスの外部からプライベートフィールドにアクセスする事はできない
instance.#privateField === 42; // Syntax error
コンストラクタ
クラスのインスタンスの初期化をおこなうコンストラクタは以下のようにconstructorキーワードを使って定義する。
Java等の他の言語と異なりコンストラクタはクラスに 1 つしか定義できない。
class Rectangle {
#height = 0;
#width;
constructor(height, width) {
this.#height = height;
this.#width = width;
}
}
上記のコードの中のthisはクラスがインスタンス化された時のインスタンス自身を指す。
this の値は、JavaScriptの実行コンテキストにより異なる。
JavaScript 実行コンテキスト this
インスタンスメソッド
JavaScriptのclassにはC#やJavaのgetter・setterの構文も導入されていて
ゲッター,セッターはメソッドのようなもので、
インスタンスメソッドやインスタンスのゲッター,セッター
さらに、ジェネレーターメソッドも定義できて、
class Foo {
#seed = 3;
// ゲッター
get seed() {
return this.#seed;
}
// セッター
set seed(seed) {
this.#seed = seed
}
// メソッド
multiSeed(multi) {
return this.#seed * multi;
}
// ジェネレーターメソッド
*g() {
let index = 0;
while (true) {
yield index += this.#seed;
}
}
}
const foo = new Foo();
// ゲッタ-, セッターの呼び出し
foo.seed = 5;
console.log(foo.seed); // 5
// メソッドの呼び出し
console.log(foo.multiSeed(4)); // 20
// ジェネレーターメソッドの呼び出し
const it = foo.g()
for (let i = 0; i < 5; i++) {
console.log(it.next().value); // 5, 10, 15, 20, 25
}
プロトタイプメソッド
インスタンスメソッドやインスタンスのゲッター,セッター,ジェネレーターメソッドはクラスのprototypeオブジェクトのメンバーになっている。hasOwnPropertyメソッドでこれを確認。
console.log(Foo.prototype.hasOwnProperty('seed')); // true
console.log(Foo.prototype.hasOwnProperty('multiSeed')); // true
console.log(Foo.prototype.hasOwnProperty('g')); // true
メソッドに関してはクラス定義の後に、後からクラスの外でメソッドを追加しても動作する。
// 後からメソッドを追加
Foo.prototype.m = function () {
return "hello";
};
console.log(foo.m()); // hello
クラスメソッド - 静的メソッドと静的ゲッタ,セッター
静的メソッドや静的ゲッタ,セッター, ジェネレーターはインスタンスではなくクラスによってアクセスする。
ここではこれらをクラスメソッドと呼ぶ事にする。
クラスメソッドを実装するにはstaticキーワードを使用する。
インスタンスメソッドの項のサンプルコードをクラスメソッドに置き換えてみると
class Foo {
static #seed = 3;
// ゲッター
static get seed() {
return this.#seed;
}
// セッター
static set seed(seed) {
this.#seed = seed
}
// メソッド
static multiSeed(multi) {
return this.#seed * multi;
}
// ジェネレーターメソッド
static *g() {
let index = 0;
while (true) {
yield index += this.#seed;
}
}
}
// ゲッタ-, セッターの呼び出し
Foo.seed = 5;
console.log(Foo.seed); // 5
// メソッドの呼び出し
console.log(Foo.multiSeed(4)); // 20
// ジェネレーターメソッドの呼び出し
const it = Foo.g()
for (let i = 0; i < 5; i++) {
console.log(it.next().value); // 5, 10, 15, 20, 25
}
インスタンスメソッドやインスタンスのゲッター,セッター,ジェネレーターメソッドはクラスオブジェクトのメンバーになっていて
フィールド変数やメソッドに関してはクラスのパブリックフィールとしてドクラス定義の後に後からクラスの外でメソッドを追加しても動作する。
// 後からプロパティやメソッドを追加
Foo.m = function () {
return "hello";
};
Foo.p = "xxx";
console.log(Foo.m()); // hello
console.log(Foo.p); // xxx
クラスの継承 - extends
クラスの(インスタンスの)継承
クラスのサブクラスを作成するには extendsキーワードを使う。
サブクラスにコンストラクターが存在する場合は、"this" を使う前に super() を呼ぶ必要がある。
class SuperCls {
constructor(name) {
this.name = name;
}
methodA() {
console.log(`${this.name}: SuperClsクラスのmethodA`);
}
methodB() {
console.log(`${this.name}: SuperClsクラスのmethodB`);
}
methodC() {
console.log(`${this.name}: SuperClsクラスのmethodC`);
}
get age() {
return 999;
}
}
class SubCls extends SuperCls {
constructor(name) {
super(name); // スーパークラスのコンストラクターを呼び出し、name パラメータを渡す
}
// methodAメソッドをオーバーライド
methodA() {
console.log(`${this.name}: SubClsクラスのmethodA`);
}
// スーパークラスの同名のメソッドを呼び出す
methodB() {
super.methodB();
console.log(`${this.name}: SubClsクラスのmethodB`);
}
}
let obj = new SubCls('愚鈍人');
// オーバーライドされたメソッドの呼び出し
obj.methodA(); // 愚鈍人: SubClsクラスのmethodA
// スーパークラスで定義されたメソッドの呼び出し
obj.methodB(); // 愚鈍人: SuperClsクラスのmethodB , 愚鈍人: SubClsクラスのmethodB
obj.methodC(); // 愚鈍人: SuperClsクラスのmethodC
// スーパークラスで定義されたゲッターの呼び出し
console.log(obj.age) // 999
super.methodB()
のようにしてサブクラスからスーパークラスのメソッドをコールする事もできる。
ゲッターやセッター,ジェネレーターも継承される。
クラスのクラスの静的メンバー
クラスの静的メンバーも継承される。
class SuperCls {
static #cvar = "xxx"
static publicField = "aaa"
// クラスメソッド
static cMethod() {
console.log("SuperClsで定義されたメソッド");
}
// ゲッター
static get cvar() {
return SuperCls.#cvar;
}
// ジェネレーターメソッド
static *g() {
let index = 0;
while (true) {
yield index += 3;
}
}
}
class SubCls extends SuperCls {
static #cvar = "yyy"
// ゲッターのオーバーライド
static get cvar() {
console.log(super.cvar);
return SubCls.#cvar;
}
// 新規メソッドの追加
static newMethod() {
console.log("SubClsで追加されたメソッド");
}
}
SubCls.cMethod() // SuperClsで定義されたメソッド
SubCls.newMethod() // SubClsで追加されたメソッド
// SubClsのpublicFieldの初期値はSuperClsの値と同じ
console.log(SubCls.publicField); // aaa
// SubClsのpublicFieldの値を変更
SubCls.publicField = "bbb";
console.log(SubCls.publicField);
// SubClsのpublicFieldの値を変更してもSuperClsのpublicFieldの値は変化しない
console.log(SuperCls.publicField);
// SuperClsのゲッターを呼び出し
console.log(SuperCls.cvar);
// SubClsのゲッターを呼び出し
console.log(SubCls.cvar);
// ジェネレーターメソッドも継承されている
const it = SubCls.g();
for (let i = 0; i < 5; i++) {
console.log(it.next().value); // 3, 6, 9, 12, 15
}
Species
ここの部分についてはよく理解できていないので参考記事のみを示す。
Symbolとは
javascript symbol
仕様によると、オブジェクトのプロパティのキーは文字列型、もしくはシンボル型のいずれかです。数値ではなく、真偽値でもなく、文字列またはシンボル、それら2つの型だけです。
Symbol.species
組み込みのクラスの継承も可能で
ウェルノウンシンボルの Symbol.species は、コンストラクター関数が派生オブジェクトを生成する際に使用する関数値プロパティを指定します。
class Array1 extends Array {
static get [Symbol.species]() { return Array; }
}
const a = new Array1(1, 2, 3);
const mapped = a.map(x => x * x);
console.log(mapped instanceof Array1);
// expected output: false
console.log(mapped instanceof Array);
// expected output: true
species アクセサープロパティを使用すると、オブジェクトの生成に使われるデフォルトコンストラクターを上書きすることができます。
// 組み込みのメソッドは、これをコンストラクタとして使います
static get [Symbol.species]() {
return Array;
}