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
でアクセスすればSubClass
、BaseClass
でアクセスすれば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
・プライベートメンバの継承
子クラスからは親クラスのプライベートなメンバへの直接的なアクセスはできません。
★がエラーになるのは、this
がSubClass
を指しており、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
の他にlet
とconst
が使えるようになりました。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.
コメント