JavaScript はクラスを利用したオブジェクト指向プログラミングができます。

プログラミング

JavaScript はクラスを利用した OOP の実装が可能なことをご存知でしょうか?
Java のようなクラス構文による OOP 言語に慣れた人達にとって、従来のprototypeベースの構文は非常に使いづらいものでした。
ECMAScript 2015(ES6)でクラスが使えるようになりましたが、メンバ変数が使えなかったり、ブラウザの対応がまちまちだったりと、なかなか本業で利用することが困難な状況が続いていました。
Babel のようなトランスパイラを利用してまで・・・、という方も結構いたのではないでしょうか。

しかし現在そのような問題点はほぼほぼ解消され、今や安心して JavaScript でクラスを利用できる状況になってきたかと思います。
主要なデスクトップブラウザ(Chrome、Edge、FireFox など)やモバイル(iOS Safari など)でも問題なく動作します。
残念ながら Internet Explorer 11 は対応されていませんが、IE 11 は 2022 年 6 月 16 日でサポートが終了するようですので、とっとと Edge へ移行していただきましょう。

各ブラウザの ES 2015 への対応状況については、以下のページで確認できます。

ECMAScript6 compatibility table
http://kangax.github.io/compat-table/es6/

本ページでは、Java や C++ といった言語に慣れ親しんだ方達にとって、理解しやすく安心して JavaScript でクラスが実装できるよう、1つ1つ心配事を払拭していきたいと思います。


インスタンス変数、インスタンス関数
内部プロパティをprototypeの記述なく宣言できます。
thisでアクセスできます。
型宣言はできません。

class MyClass{
	varA;
	varB = 11;

	methodA(arg1){
		this.varA = arg1;
		this.methodB();
	}

	methodB(){
		console.log(this.varA+" "+this.varB);
	}
}

var ins = new MyClass();
ins.methodB(); // undefined 11
ins.varA = 22; // 外側から変数へアクセス
ins.methodB(); // 22 11
ins.methodA(1); // 1 11


クラス変数、クラス関数
static修飾子を使って静的メンバを宣言できます。
クラス名でアクセスできます。
クラスメソッド内であれば、thisでもアクセス可能です。
型宣言はできません。

class MyClass{
	varA = 11; // インスタンス変数 this.varA
	static varB = 22; // クラス変数 MyClass.varB

	// インスタンスメソッド
	methodA(arg1){
		MyClass.varB = arg1;
		MyClass.methodC();
	}

	// クラスメソッド
	static methodB(arg1){
		MyClass.varB = arg1;
		//this.varB = arg1; // = MyClass.varB
		MyClass.methodC();
		//this.methodC(); // = MyClass.methodC();
	}

	static methodC(){
		console.log(MyClass.varB+" "+this.varB);
	}
}

MyClass.methodC(); // 22 22
MyClass.varB = 33; // 外側から変数へアクセス
MyClass.methodC(); // 33 33
MyClass.methodB(1); // 1 1

var ins = new MyClass();
ins.methodA(2); // 2 2

JavaScript は動的なプロパティ追加が可能であるため、誤ってクラス変数をthisでアクセスしたり、インスタンス変数をクラス名でアクセスした場合においても、変数が追加されるだけでエラーにはならないので注意が必要です。
なお、メソッドの動的追加はエラーになります。

class MyClass{
	varA = 11;
	static varB = 22;

	methodA(){
		console.log(this.varA+" "+this.varB+" "+MyClass.varA+" "+MyClass.varB);
		this.varA = 1;
		this.varB = 2; // ★動的追加(this.varB)
		MyClass.varA = 3; // ★動的追加(MyClass.varA)
		MyClass.varB = 4;
		console.log(this.varA+" "+this.varB+" "+MyClass.varA+" "+MyClass.varB);
	}

	static methodB(){
		console.log(this.varA+" "+this.varB+" "+MyClass.varA+" "+MyClass.varB);
		this.varA = 1; // ★動的追加(MyClass.varA)
		this.varB = 2; // = MyClass.varB
		MyClass.varA = 3; // ★動的追加(MyClass.varA、上2行目で動的追加済)
		MyClass.varB = 4;
		console.log(this.varA+" "+this.varB+" "+MyClass.varA+" "+MyClass.varB);
	}

	methodC(){
		this.methodA();
		//this.methodB(); // TypeError: this.methodB is not a function
		//MyClass.methodA(); // TypeError: MyClass.methodA is not a function
		MyClass.methodB();
	}

	static methodD(){
		//this.methodA(); // TypeError: this.methodA is not a function
		this.methodB();
		//MyClass.methodA(); // TypeError: MyClass.methodA is not a function
		MyClass.methodB();
	}
}
// インスタンスメソッドからの内部アクセス
var ins = new MyClass();
ins.methodA(); // 11 undefined undefined 22 → 1 2 3 4
//ins.methodB(); // TypeError: ins.methodB is not a function
// クラスメソッドからの内部アクセス
//MyClass.methodA(); // TypeError: MyClass.methodA is not a function
MyClass.methodB(); // undefined 22 undefined 22 → 3 4 3 4
// 外側から変数へアクセス
var ins = new MyClass();
ins.varA = 33;
ins.varB = 44; // ★動的追加(this.varB)
MyClass.varA = 55; // ★動的追加(MyClass.varA)
MyClass.varB = 66;


アクセス修飾子
メンバ名の頭に#をつけることでprivateになります。
残念ながらprotectedはありません。

class MyClass{
	#varA; // private
	static #varB = 11; // private static

	// private method
	#methodA(arg1, arg2){
		this.#varA = arg1;
		MyClass.#varB = arg2;
		console.log("A:"+this.#varA+" "+MyClass.#varB);
	}

	// private static method
	static #methodB(arg1){
		MyClass.#varB = arg1;
		//this.#varB = arg1; // = MyClass.#varB
		console.log("B:"+MyClass.#varB+" "+this.#varB);
	}

	// public
	methodC(){
		this.#methodA(2, 3);
		MyClass.#methodB(4);
	}

	// public static
	static methodD(){
		MyClass.#methodB(5);
	}
}

var ins = new MyClass();
//ins.#varA = 1; // SyntaxError: Private field '#varA' must be declared in an enclosing class
//ins.#methodA(1, 2); // SyntaxError: Private field '#methodA' must be declared in an enclosing class
//MyClass.#varB = 2; // SyntaxError: Private field '#varB' must be declared in an enclosing class
//MyClass.#methodB(3); // SyntaxError: Private field '#methodB' must be declared in an enclosing class
ins.methodC(); // A:2 3 B:4 4
MyClass.methodD(); // B:5 5

privateなメンバ変数・メンバ関数は、動的なプロパティ追加ができないようです。
したがって、誤ってthisでアクセスしたりクラス名でアクセスしてもエラーとなります。

class MyClass{
	#varA = 11;
	static #varB = 22;

	#methodA(){
		this.#varA = 1;
		//this.#varB = 2; // TypeError: Cannot write private member #varB to an object whose class did not declare it
		//MyClass.#varA = 3; // TypeError: Cannot write private member #varA to an object whose class did not declare it
		MyClass.#varB = 4;
		console.log("A:"+this.#varA+" "+MyClass.#varB);
	}

	static #methodB(){
		//this.#varA = 1; // TypeError: Cannot write private member #varA to an object whose class did not declare it
		this.#varB = 2; // = MyClass.#varB
		//MyClass.#varA = 3; // TypeError: Cannot write private member #varA to an object whose class did not declare it
		MyClass.#varB = 4;
		console.log("B:"+this.#varB+" "+MyClass.#varB);
	}

	methodC(){
		this.#methodA();
		//this.#methodB(); // TypeError: Receiver must be class MyClass
		//MyClass.#methodA(); // TypeError: Receiver must be an instance of class MyClass
		MyClass.#methodB();
	}

	static methodD(){
		//this.#methodA(); // TypeError: Receiver must be an instance of class MyClass
		this.#methodB();
		//MyClass.#methodA(); // TypeError: Receiver must be an instance of class MyClass
		MyClass.#methodB();
	}
}

var ins = new MyClass();
ins.methodC(); // A:1 4 B:4 4
MyClass.methodD(); // A:4 4 B:4 4


コンストラクタ
constructorというメソッド名で定義します。

class MyClass{
	#varA = 11;
	static #varB = 22;

	// コンストラクタ
	constructor(arg1, arg2){
		this.#varA = arg1;
		MyClass.#varB = arg2;
		this.methodA();
		MyClass.methodB(3);
	}

	methodA(){
		console.log("A:"+this.#varA+" "+MyClass.#varB);
	}

	static methodB(arg1){
		MyClass.#varB = arg1;
		console.log("B:"+MyClass.#varB);
	}
}

var ins = new MyClass(1, 2); // A:1 2 B:3
ins.methodA(); // A:1 3
MyClass.methodB(4); // B:4


継承
継承ももちろん使えます。
継承先サブクラス(子クラス)から継承元ベースクラス(親クラス)のメンバにアクセスできます。

class BaseClass{
	varA = 11;
	static varB = 22;

	methodA(){
		console.log("A:"+this.varA);
	}

	static methodB(){
		console.log("B:"+this.varB);
	}
}

class SubClass extends BaseClass{
	methodC(){
		this.methodA();
		SubClass.methodB();
		console.log("C:"+this.varA+" "+SubClass.varB);
	}

	static methodD(){
		this.methodB(); // = SubClass.methodB();
		console.log("D:"+this.varB+" "+SubClass.varB);
	}
}

var ins = new SubClass();
ins.methodA(); // A:11
SubClass.methodB(); // B:22
ins.methodC(); // A:11 B:22 C:11 22
SubClass.methodD(); // B:22 D:22 22

// 外側から変数へアクセス
ins.varA = 44;
SubClass.varB = 55;
ins.methodA(); // A:44
SubClass.methodB(); // B:55

・メンバ変数の上書き
インスタンス変数は子クラスで上書きになりますが、クラス変数は親と子個別にアクセスできます
なお、クラスメソッド内のthisはあくまで自分自身(SubClassでアクセスすればSubClassBaseClassでアクセスすればBaseClass)になります。

class BaseClass{
	varA = 11;
	static varB = 22;

	methodA(){
		console.log("A:"+this.varA);
	}

	static methodB(){
		console.log("B:"+this.varB+" "+BaseClass.varB);
	}
}

class SubClass extends BaseClass{
	varA = 33;
	static varB = 44;

	methodC(){
		console.log("C:"+this.varA+" "+SubClass.varB+" "+BaseClass.varB);
		this.varA = 1;
		SubClass.varB = 2;
		console.log("C:"+this.varA+" "+SubClass.varB+" "+BaseClass.varB);
	}

	static methodD(){
		console.log("D:"+this.varB+" "+SubClass.varB+" "+BaseClass.varB);
		this.varB = 3;
		console.log("D:"+this.varB+" "+SubClass.varB+" "+BaseClass.varB);
	}
}

var ins = new SubClass();
ins.methodC(); // C:33 44 22 → C:1 2 22
ins.methodA(); // A:1 ★上書き
SubClass.methodD(); // D:2 2 22 → D:3 3 22
SubClass.methodB(); // B:3 22 ★値が異なることに注意
BaseClass.methodB(); // B:22 22

・メンバメソッドの上書き
インスタンスメソッドは上書きできます。
JavaScript は動的型付けであり引数もチェックされませんので、オーバーライドとは呼べないと思いますが、エラーにならないというだけで気を付けてコーディングすればポリモーフィズム的な実装も可能です。
クラスメソッドは親と子で個別にアクセスできます

class BaseClass{
	varA = 11;
	static varB = 22;

	methodA(arg1){
		console.log("A:"+this.varA);
	}

	static methodB(arg1){
		console.log("B:"+this.varB);
	}
}

class SubClass extends BaseClass{
	methodA(arg1){
		this.varA = arg1;
		console.log("AA:"+this.varA);
	}

	static methodB(arg1){
		this.varB = arg1;
		console.log("BB:"+this.varB);
	}
}

var ins = new SubClass();
ins.methodA(33); // AA:33
SubClass.methodB(44); // BB:44
BaseClass.methodB(55); // B:22
class BaseClass{
	methodA(arg1){
		return arg1;
	}
}

class DoubleClass extends BaseClass{
	methodA(arg1){
		return arg1 * 2;
	}
}

class TripleClass extends BaseClass{
	methodA(arg1){
		return arg1 * 3;
	}
}

function methodA(baseObj, val){
	var rc = baseObj.methodA(val);
	console.log(rc);
}

methodA(new BaseClass(), 33); // 33
methodA(new DoubleClass(), 33); // 66
methodA(new TripleClass(), 33); // 99

・継承元メンバへのアクセス
先ほどインスタンス変数、インスタンスメソッドは上書きだと解説しましたが、内部からは修飾子superで継承元のメンバへアクセスできます(外側からはアクセスできません)。
クラス変数、クラスメソッドも同様にsuperで継承元にアクセスできます。
コンストラクタでは、this参照の前に必ずsuper()継承元のコンストラクタを呼び出さなければなりません

class BaseClass{
	varA = 11;
	static varB = 22;

	constructor(arg1){
		this.varA = arg1;
		console.log("C:"+this.varA);
	}

	methodA(arg1){
		console.log("A:"+this.varA);
	}

	static methodB(){
		console.log("B:"+this.varB);
	}
}

class SubClass extends BaseClass{
	constructor(arg1){
		//this.varA = 33; // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
		super(arg1);
		this.varA = 33;
		console.log("CC:"+this.varA);
	}

	methodA(){
		this.varA = 55;
		console.log("AA:"+this.varA);
		super.varA = 44;
		super.methodA();
		console.log("AA:"+this.varA);
	}

	static methodB(){
		this.varB = 77;
		console.log("BB:"+this.varB);
		super.varB = 66;
		super.methodB();
		console.log("BB:"+this.varB);
	}
}

var ins = new SubClass(1); // C:1 CC:33
ins.methodA(1); // AA:55 A:44 AA:44
SubClass.methodB(); // BB:77 B:66 BB:66

・プライベートメンバの継承
子クラスからは親クラスのプライベートなメンバへの直接的なアクセスはできません。
★がエラーになるのは、thisSubClassを指しており、SubClassからBaseClassのプライベート変数にアクセスしようとしているからです。

class BaseClass{
	#varA = 11;
	static #varB = 22;

	#methodA(){
		console.log("A#:"+this.#varA);
	}

	static #methodB(){
		console.log("B#:"+this.#varB);
	}

	methodA(){
		this.#methodA();
		console.log("A:"+this.#varA);
	}

	static methodB(){
		//this.#methodB(); // ★TypeError: Receiver must be class BaseClass
		BaseClass.#methodB();
		//console.log("B:"+this.#varB); // ★TypeError: Cannot read private member #varB from an object whose class did not declare it
		console.log("B:"+BaseClass.#varB);
	}
}

class SubClass extends BaseClass{
	#varA = 33;
	static #varB = 44;

	methodA(){
		this.#methodA();
	}

	static methodB(){
		this.#methodB();
	}

	#methodA(){
		//super.#varA = 1; // SyntaxError: Unexpected private field
		//super.#methodA(); // SyntaxError: Unexpected private field
		super.methodA();
	}

	static #methodB(){
		//super.#varB = 2; // SyntaxError: Unexpected private field
		//super.#methodB(); // SyntaxError: Unexpected private field
		super.methodB();
	}
}

var ins = new SubClass();
ins.methodA(); // A#:11 A:11
SubClass.methodB(); // B#:22 B:22


メソッドのオーバーロード
メソッドのオーバーロードはありません。
異なる引数のメソッドが存在する場合、一番最後に定義されたメソッドのみ有効になります。
したがって引数の違いによってメソッドの上書き(オーバーライド)が NG になることはありません。
JavaScript ではメソッド引数の違いがエラーにならないため注意が必要です
なお、コンストラクタは1つのクラスに複数定義するとエラーになります。

以下の例は引数の受け渡しがでたらめですがエラーにならずsuperも機能しています。

class BaseClass{
	varA = 11;
	static varB = 22;

	constructor(arg1){
		this.varA = arg1;
		console.log("C:"+this.varA);
	}

	methodA(){ // 無効
		console.log("A:"+this.varA);
	}

	methodA(arg1){ // 有効
		this.varA = arg1;
		console.log("Ax:"+this.varA);
	}

	static methodB(){
		console.log("B:"+this.varB);
	}
}

class SubClass extends BaseClass{
	constructor(arg1){
		super(33, 44);
		this.varA = 55;
		console.log("CC:"+this.varA);
	}

	// SyntaxError: A class may only have one constructor
	//constructor(arg1, arg2){
	//}

	methodA(){
		this.varA = 66;
		console.log("AA:"+this.varA);
		super.varA = 77;
		super.methodA();
	}

	static methodB(){ // 無効
		this.varB = 88;
		console.log("BB:"+this.varB);
	}

	static methodB(arg1){ // 有効
		this.varB = arg1;
		console.log("BBx:"+this.varB);
	}
}

var ins = new SubClass(1, 2); // C:33 CC:55
ins.methodA(1); // AA:66 Ax:undefined
SubClass.methodB(); // BBx:undefined


デフォルト引数
メソッッドに値が渡されない場合やundefinedが渡された場合に、デフォルト値で初期化される形式上の引数を指定することができます。

class MyClass{
	#varA;
	#varB;

	constructor(arg1, arg2=0){
		this.#varA = arg1;
		this.#varB = arg2;
	}

	methodA(){
		console.log(this.#varA+" "+this.#varB);
	}
}

var ins = new MyClass(11, 12);
ins.methodA(); // 11 12
var ins = new MyClass(13);
ins.methodA(); // 13 0


let と const の変数宣言
クラスとは関係ありませんが、重要なのでこちらも解説します。
変数宣言にvarの他にletconstが使えるようになりました。
letは再宣言ができません。
constは再宣言も再代入もできません。
varよりもこちらを使う方が良いでしょう。

var varA = 1;
var varA = 2;
varA = 3;

let varB = 1;
//let varB = 2; // SyntaxError: Identifier 'varB' has already been declared
varB = 3;

const varC = 1;
//const varC = 2; // SyntaxError: Identifier 'varC' has already been declared
//varC = 3; // TypeError: Assignment to constant variable.

コメント