クラス

【JavaScript】クラス定義と使い方とその他いろいろ

更新日:2024/02/27

JavaScriptにもクラスがあった。
今更知った僕です。

そこでさっそく、JavaScriptのクラスについて調べてみました。


2023/10 ソースコードの内容が意図しないものになっていたため訂正しました

 

クラスの定義と使い方

まずはJavaScriptのクラス定義と使い方について見ていきます。

class定義

class定義で使用できる機能を、コードで一覧してみます。


class a extends b{

    // ********** インスタンス初期化メソッド
    constructor( v ) {   // constructorメソッド
        super(); // extendsした場合は必ず記述 
        this.member = v;
        this.#privateValue = v; 
    }

    // ********** パブリックメンバーに関する定義
    value = 1;   // パブリックフィールド
    method = function(){}; // パブリックフィールド

    prototypeChainMethod(){ console.log(this.#privateValue) };// パブリックメソッド
    get setGet(){ return this.value }; //  パブリックゲッター
    set setGet( v ){ this.value = v; }; //  パブリックセッター

    get CONSTANT_VALUE (){ return "定数値"; };  // インスタンス定数

    // ********** プライベートメンバーに関する定義
    #privateValue = 2; // プライベートフィールド
    #privateMethod(){}; // プライベートメソッド
    get #privateSetGet(){ return this.#privateValue };  // プライベートゲッター
    set #privateSetGet( v ){ this.#privateValue = v; }; // プライベートセッター

    // ********** 静的メンバーに関する定義
    static staticField; // 静的変数
    static staticMethod(){ // 静的メソッド
        this.#staticPrivetMethod();
    };
    static get staticGetSet(){ return this.staticField; }
    static set staticGetSet( v ){ this.staticField = v; }

    static #staticPrivetFIled; // 静的プライベート変数
    static #staticPrivetMethod(){ // 静的プライベートメソッド
        console.log( this.staticGetSet * this.#staticPrivetGetSet );
    };
    static get #staticPrivetGetSet(){ return this.#staticPrivetFIled; }
    static set #staticPrivetGetSet( v ){ this.#staticPrivetFIled = v; }
    static {  // 静的ブロック
        this.staticGetSet = 5;
        this.#staticPrivetGetSet = 100;
    }

    static get CONSTANT_VALUE (){ return "定数値"; };  // クラス定数
}

インスタンスの生成

次のコードは、一番簡単なクラス定義と、定義からインスタンスを作成する方法です。

class宣言


class a{  // クラスaを宣言
}
const b = new a(); // クラスaのインスタンスを作成

次のようにクラス式として、変数に代入できます。

class式


const a = class{ // クラスを宣言し、変数aに代入
};
const b = new a(); // 変数aから、インスタンスを作成

また、constructorメソッドに引数を渡さないときは( )を省略できます。


const b = new a();
const c = new a; // ( )を省略できる

インスタンスの初期化

constructorメソッドは、classをnew演算子でインスタンス化するときに呼び出されます。
次のように、クラス内部でconstructorメソッドを定義します。


class a{
    constructor( ) {   // constructorメソッド
            this.value = 1;
            this.func = function(){ };
    }
}
const b = new a();
console.log( b ); // Object { value: 1 }

constructorメソッド内ではthis値にプロパティを定義していきます。
最後にthis値がリターンされ、インスタンスとして扱われます。
これはコンストラクター関数と同じですね。

ただし、後述のパブリックフィールドプライベートフィールドなどが定義されている場合、this値にはあらかじめそれらフィールドがプロパティとしてセットされています。


class a{
    value = 1; // パブリックフィールド
    #value2 = 2;  // プライベートフィールド
    constructor( ) {   // constructorメソッド
            console.log( this ); // Object { value: 1, #value2: 2 }
            this.value = 10;
            this.#value2 = 100;
    }
}
const b = new a();
console.log( b ); // Object { value: 10 , #value2: 100 }

プライベートフィールドが定義されていない場合、constructorメソッド内でプライベートプロパティの追加や値変更することはできません。


class a{
    // value = 1; ←定義を削除
    // #value2 = 2; ←定義を削除
    constructor( ) {   // constructorメソッド
            console.log( this ); // Object {  }
            this.value = 10; // これは可能
            this.#value2 = 100; // Uncaught SyntaxError: reference to undeclared private field or method #value2
    }
}
const b = new a();

また、constructorは引数を受け取ることができます。

constructorに引数を渡す


class a{
    constructor( v ) {   // constructorメソッド
            this.value = v;
    }
}
const b = new a(100);
console.log( b ); // Object { value: 100 }

パブリックフィールド定義

パブリックメンバーは、インスタンスのプロパティとして生成されます。
生成されたプロパティは、値の変更や削除など通常のプロパティと同じように扱うことができます。


class a{
    value = 1;  // パブリックフィールド
}
const b = new a();
console.log( b ); // Object { value: 1 }
b.value = 100;
delete b.value;

なおプリミティブ値ではなくて関数を代入した場合でも、パブリックフィールドです。
パブリックメソッドではありません。


class a{
    value = function(){ };  // パブリックフィールド
}
const b = new a();
console.log( b ); // Object { value: function(){ } }

プリミティブ値も関数もJavaScriptではデータとして扱われます。
上記はあくまで、プロパティにデータをセットしただけなのです。

パブリックフィールドは、ECMAScript2022で導入された機能です。

パブリックメソッド定義

パブリックメソッドは、インスタンスのプロトタイプチェーンに組み込まれるメソッドです。


class a{
    value = 1;
    getValue(){
             return this.value1;
     }
}
const b = new a();
console.log( b ); // Object { value: 1 }
console.log(  Object.getPrototypeOf( b ) ); // Object { getValue: function(){ } , ・・・ }

パブリックメソッドは全てのインスタンスが共有します。
そのため、パブリックメソッドの内容を変更すると他のインスタンスも影響を受けます。


class aa{
    func(){
        return 1;
    }
}
const obj1 = new aa;
const obj2 = new aa;
console.log( obj1.func() , obj2.func() ); // 1 , 1
Object.getPrototypeOf( obj1 ).func = function (){ return 100; } // obj1のfuncメソッドを変更
console.log( obj1.func() , obj2.func() ); // 100 , 100 ← obj2も影響を受けた

パブリックゲッター/セッター

パブリックゲッター/セッターはパブリックメソッド

と同じように、インスタンスのプロトタイプチェーンに組み込まれるゲッター/セッターです。


class a{
    value = 1;
    get setGet(){ return this.value }; //  パブリックゲッター
    set setGet( v ){ this.value = v; }; //  パブリックセッター
}
const b = new a();
console.log( b ); // Object { value: 1 }
console.log(  Object.getPrototypeOf( b ) ); // Object { <get setGet()>: function setGet() , <set setGet()>: function setGet(v) }

プライベートフィールド定義

プライベートフィールドは先頭に"#"を付加することで、自動的に生成されます。
プライベートフィールドはインスタンス内に生成されるプロパティですが、外部からアクセスできません。


class aa{
    #privateValue = 2;
}
const b = new aa();
console.log( b.#privateValue); // Uncaught SyntaxError: reference to undeclared private field or method #privateValue

プライベートフィールドはclass定義内で定義されているメソッドおよびゲッター/セッターからthisキーワードを経由してのみアクセスできます。
ただし、静的メソッドからはアクセスできません。


class aa{
    #privateValue = 2;
    method = function(){ return this.#privateValue};
    publicMethod(){ return this.#privateMethod() };
    #privateMethod(){ return this.#privateValue};
}
const b = new aa();
console.log( b.method() , b.publicMethod() ); // 2 , 2

プライベートフィールドは、ECMAScript2022で導入された機能です。

プライベートメソッド定義

プライベートメソッドはプライベートフィールドと同じように、メソッドおよびゲッター/セッターからthisキーワードを経由してのみアクセスできます。
ただし、プライベートメソッドは値を上書きできません。


class aa{
    #privateValue = 2;
    #privateMethod(){ return this.#privateValue};
    publicMethod(){ this.#privateMethod = function (){}};

}
const b = new aa();
b.publicMethod(); // Uncaught TypeError: cannot assign to private method

プライベートメソッドは、ECMAScript2022で導入された機能です。

プライベートゲッター/セッター

プライベートゲッター/セッターはプライベートフィールドと同じように、メソッドおよびゲッター/セッターからthisキーワードを経由してのみアクセスできます。

プライベートゲッター/セッターは、ECMAScript2022で導入された機能です。

静的メンバー

静的メンバーは、classが所持するプロパティです。
パブリックなプロパティは、class名で直接アクセスできます。


class aa{
   static value = 2;
   static func(){ return this.value * 2; };
}
console.log( aa.value , aa.func() ); // 2 , 4

プライベートなプロパティは、静的メソッドからthis経由でアクセスします。


class aa{
   static #value = 2;
   static func(){ return this.#value * 2; };
}
console.log(  aa.func() ); //  4

次のコードは、パブリックと静的メンバーの両方に同名のフィールドとメソッドを生成しています。


class aa{
    value = 2;
    method(){ return this.value}; // インスタンスのthisを参照

    static value = 20;
    static method(){ return this.value; }  // class aaのthisを参照
}
console.log( aa ); // class { value:20 , method:function(){} , ・・・}
console.log( aa.method() ); // 20

const b = new aa();
console.log( b ); // class { value:2 }
console.log( b.method() ); // 2

二つのmethodメソッド内のthisは、それぞれ異なるものを参照していることに注意する必要があります。

静的メソッドおよびゲッター/セッター以外は、ECMAScript2022で導入された機能です。

静的ブロック

静的ブロックは、class定義がソースコードから読み込まれてclassが生成されるときに実行されます。
静的ブロック内でのthisのメンバーは、静的フィールドやメソッドです。


class aa{
    value = 2;

    static value = 20; // ①
    static {
       this.value = 200; // このvalueは①
   }
}

静的ブロックは、ECMAScript2022で導入された機能です。

補足:定数の定義

classには定数を定義する構文がありません。
ですが既存の構文やメソッドを使用することで、定義できます。

クラス定数の定義

クラス定数は、クラス名.定数名 でアクセスする定数です。
方法は2種類あります。

静的ゲッターで定義

静的ゲッターでプリミティブを返すことで、定数を定義します。


class a {
    static get  CONSTANT_VALUE (){ return "定数値"; };
}
console.log( a.CONSTANT_VALUE ); // "定数値"

Object.definePropertyで定義

静的ブロックでObject.definePropertyを実行して、this値にプロパティを追加します。
このとき上書き属性をfalseにすることで、定数として機能します。


class a {
    static {
        Object.defineProperty( this , "CONSTANT_VALUE",{
            value:"定数値",
            /* writable:false */ // 規定値はfalse
        })
    }
}
console.log( a.CONSTANT_VALUE ); // "定数値"

Object.definePropertyのwritable属性の規定値はfalseなので、valueのみ指定すればOKです。

静的ゲッターが呼び出した関数内でプリミティブを返すのに対して、こちらは直接プリミティブを返しています。
処理速度はこちらの方が速いですが、アプリケーションの動作に影響を与えるほど遅くなることはほとんどありません。
静的ゲッターの方がわかりやすいので、そちらがおススメです。

インスタンス定数の定義

インスタンス定数は、インスタンスが所持する定数です。
こちらも、方法は2種類あります。

パブリックゲッターで定義

パブリックゲッターでプリミティブを返すことで、定数を定義します。


class a {
    get  CONSTANT_VALUE (){ return "定数値"; };
}
const b = new a();
console.log( b.CONSTANT_VALUE ); // "定数値"

Object.definePropertyで定義

静的ブロックでObject.definePropertyを実行して、this値のprototypeプロパティにプロパティを追加します。
このとき上書き属性をfalseにすることで、定数として機能します。


class a {
    static {
        Object.defineProperty( this.prototype , "CONSTANT_VALUE",{
            value:"定数値",
            /* writable:false */ // 規定値はfalse
        })
    }
}
const b = new a();
console.log( b.CONSTANT_VALUE ); // "定数値"

Object.definePropertyのwritable属性の規定値はfalseなので、valueのみ指定すればOKです。
ここで設定した定数プロパティは、インスタンスのプロトタイプチェーンに組み込まれます。

 

クラスで継承をおこなう

他の言語でプログラムをしてきた人が、クラスに期待することの一つに継承があると思います。

JavaScriptの継承は他の言語と少し意味合いが異なります。
詳しくは次のページを読んでみてください。
【JavaScript】 JSにおける継承とは何を指しているのか

ここでは、class構文の継承機能についてお伝えします。

継承の宣言

class構文はextendsキーワードを使用することで、継承を行うことができます。


class b{
}
class a extends b{
}

親クラスはクラス宣言したものだけでなく、コンストラクターを指定できます。


function b( ){
}
b.prototype.x = function(){};

class a extends b{
}

ArrayやStringなどの組み込みオブジェクトも、コンストラクターなので継承できます。


class a extends String{
}

親クラスのコンストラクタ呼び出し(必須)

extendsキーワードを使用したら、必ず子クラスのconstructorで最初にsuper()を使用します。
使用しないとエラーになります。


class b {
    constructor( ) {
        this.x = 5;
    }
}
class a extends b{
    constructor( ) {
                //  super( ) を呼び出していない
                // ReferenceError: 
                //     Must call super constructor in derived class 
                //     before accessing 'this' or returning from derived constructor
    }
}
const c = new a; // エラーになる

super()は、親クラスのconstructorを呼び出します。
このとき親クラスのconstructor内のthisは、子クラスのthisです。


class b {
    constructor( ) {
        this.x = 5;  // 子クラスaのthisを操作
    }
}
class a extends b{
    constructor( ) {
          super( ) // this { x:5 }
          this.x = 100; // this { x:100 }
                              // 親クラスでセットした x : 5 は失われる
    }
}
const c = new a;

上の例のように、親クラスのconstructorは子クラスのthisを操作します。
親クラス自身は、thisを保持していません。
子クラスで同名のプロパティに値をセットすると、親クラスでセットした値は失われます。

メソッドのオーバーライドと親メソッドの呼び出し

親クラスと子クラスで同名のメソッドを持っている場合、インスタンスからの呼び出しは子クラスのメソッドが実行されます。
子クラスのメソッドでsuperを使用すると親が持つメソッドを実行できます。


class b {  // 親クラス
    constructor(  ) { }
    aaa(){  // (2)
       console.log("Super Class"); 
    }
}
class a extends b {  // 子クラス
    constructor(  ) { }
    aaa(){  // (1)
        super.aaa();  // (2)が呼ばれる
    }
}

const c = new a;
c.aaa(  );  // (1)が呼ばれる

親クラスからthisでメソッドを実行すると、子クラスのメソッドが呼び出されます。


class b {  // 親クラス
    constructor(  ) { }
    aaa(){ // (1)
        this.bbb();  // 子クラスのbbb()が呼ばれる
    }
    bbb(){
        console.log("Super Class"); // 呼び出されない!!
    }
}
class a extends b {  // 子クラス
    constructor(  ) { }
    aaa(){  // (2)
        this.bbb();  // 子クラスのbbb()が呼ばれる
        super.aaa();
    }
    bbb(){
        console.log("Sub Class");
    }

}

const c = new a;
c.aaa(  );

上の例は親と子それぞれ、bbb()というメソッドを持っています。
そして(1)(2)で、親子ともにbbb()というメソッドを呼び出しています。

その結果親クラスも子クラスも、子のbbb()を呼び出します。

親クラスのbbb()が実行されないという点がポイントですね。

 

JavaScriptのclassでカプセル化

他のプログラム言語のクラスでできることの一つが、情報を隠蔽してカプセル化できるということですね。

class構文導入時はprivateもprotectedもなかったので、JavaScriptのプロパティはオープンでpublicなままでした。
しかしECMAScript2022でプライベートフィールドとメソッドが導入されたことで、ようやく簡単にカプセル化できる環境が整いました。

従来のclass構文での情報隠蔽はクロージャ的な手法で実現していました。


class a {
    constructor( v1 , v2 ) {
        let _name = v2;
        let _familyName = v1;
        Object.defineProperties( this ,{
            name:{
                set:( name )=>_name=name,
                get: () => _name
            },
            familyName:{
                set:( familyName )=>_familyName=familyName,
                get: () => _familyName
            },
            fullName:{
                get: () => `${_familyName} ${_name}`
            }
        })
    }
}

const b = new a( "山田 " , "たろう" );
console.log( b.fullName ); // 山田たろう

プライベートフィールドが導入される以前に書いたので、あまり意味がなくなってしまった記事(涙
【JavaScript】 class構文でのprivate変数定義のひな型パターン

ECMAScript2022以降は、プライベートフィールドだけでなくパブリックフィールドなどを使用することで簡単にカプセル化とカプセル化したデータへのアクセスができます。


class a {
    #name; // プライベートフィールド
    #familyName; // プライベートフィールド
    constructor( v1 , v2 ) {
        this.#name = v2;
        this.#familyName = v1;
    }
    get name(){ return this.#name; }
    set name( name ){ this.#name = name; }
    get familyName(){ return this.#familyName; }
    set familyName( familyName ){ this.#familyName = familyName; }
    get fullName(){ return `${this.#familyName} ${this.#name}`; }
}

const b = new a( "山田 " , "たろう" );
console.log( b.fullName ); // 山田たろう

 

クラスはstrictモードで動作する

class構文内のコードは強制的にstrictモードになります。

strictモードは変数の定義をしなくてもそれなりに動いてくれるなど少し適当だったJavaScriptを、ほんの少し厳格にするモードです。

デフォルトでは非strictモードで動作するので、strictモードを使用するときは"use strict";と記述する必要があります。

しかしクラスは、記述してもしなくてもstrictモードとなります。

非strictモードでプログラムを組んでいる人は、クラス内だけエラーとなり戸惑うかもしれませんね。

 

JavaScriptのクラスと一般的なクラスとの違い

ここからはJavaScriptのクラスと一般的なクラスとの違いについて、解説してみます。

一般的なクラスとインスタンス

一般的にはクラスとは、オブジェクトが所持する変数やメソッドなどを定義したものです。
クラスそのものは『定義しているだけ』です。

クラスという設計図から、オブジェクトという実体を作成することで、初めて変数やメソッドを実行できるようになります。
(静的メソッドは実体がなくても実行できるけれど、とりあえずおいておいてください…)

クラスから帯ジェクトが作られる

クラスから生成されたオブジェクトを○○(クラス名)のインスタンスと呼びます。

JavaScriptのインスタンス

class構文が導入される以前から、JavaScriptにはインスタンスが存在していました。
しかしクラスが存在していないので、一般的なインスタンスとは異なるものです。

JavaScriptのインスタンスはコンストラクター関数をnew演算子で実行して、その結果得られたオブジェクトを指します。

コンストラクター関数からインスタンスを生成


 // コンストラクター関数
const constructorFunction = function (){
    this.value = 100; // インスタンスにvalueプロパティを追加
    this.func = ()=>{}; // インスタンスにfuncメソッドを追加
}
 // constructorFunctionからinstanceObjを生成
const instanceObj = new constructorFunction();

JavaScriptの関数はfunctionという名前のオブジェクトです。
そのためJavaScriptのインスタンスは、オブジェクトから生成されたオブジェクトとも言えます。

生成されたインスタンスのプロトタイプチェーンには、コンストラクター関数のprototypeプロパティが組み込まれます。
そのため、インスタンスはコンストラクター関数を継承していると表現されることもあります。

JavaScriptのクラス

JavaScriptはプロトタイプベースなプログラミング言語と位置付けられています。
プロトタイプベースは既存オブジェクトからクローンを作成して、そこにメンバを構成していきます。
その対義的なものにクラスベースがあります。

”オブジェクト指向にはクラスがあるのが当然”という考え方がるのか、コンストラクター関数をクラスとして紹介しているネット記事が以前は多くありました。
クラスだと言われて素直に納得できる人は、あまりいませんでした。
プロトタイプベースをクラスベースとして考えることに無理があるので当然なのです。

しかしどうしても”オブジェクト指向にはクラスがあるのが当然”という声が強いのか、ECMAScript2015でclass構文が追加されました。
『JavaScriptにやっとクラスが導入された!』という喜びの声が上がりましたが、プロトタイプベースからクラスベースに変更とかしたら別の言語になってしまいます。さすがにそこまではやれません。

その実態はコンストラクター関数(functionオブジェクト)を生成するものでした。
クラスからインスタンスを生成しているように見えても、実際にはコンストラクター関数がインスタンスを生成しているのです。

つまりクラスが導入されたのではなくて、クラスっぽい構文でコンストラクター関数を生成する機能が導入されたのです。

例えば次のコードのコンストラクター関数とクラス定義は、ほぼ同じ内容のfunctionオブジェクトを生成します。


 // コンストラクター関数
const constructorFunction = function (){
    this.value = 100; // インスタンスにvalueプロパティを追加
    this.func = ()=>{}; // インスタンスにfuncメソッドを追加
}
console.log( typeof constructorFunction  ); // "function"

 // クラス
const classFunction = class {
    constructor( ) {
        this.value = 100; // インスタンスにvalueプロパティを追加
        this.func = ()=>{}; // インスタンスにfuncメソッドを追加
   }
}
console.log( typeof classFunction  ); // "function"

typeof演算子は値の型を文字列で返します。
値がfunctionオブジェクトのときは、"function"が返ります。
クラスも"function"を返すので、functionオブジェクトだということがわかります。

ECMAScript2015のclass構文

ECMAScript2015のclass構文は、メソッドのみ指定できました。
プロパティについてはconstructorメソッドの中で設定します。

ECMAScript2015のclass構文


const classFunction = class {
    constructor( ) { // プロパティの初期化
        this.value = 100; // インスタンスにvalueプロパティを追加
        this.func = ()=>{}; // インスタンスにfuncメソッドを追加
   }
   method(){
       // 処理
   }
   static staticMethod(){ // 静的メソッド
       // 処理
   }
}

class構文導入以前からJavaScriptを使用してきたプログラマーからすると、あまり魅力を感じるものではありませんでした。

ECMAScript2022のclass構文

ECMAScript2022では、class定義内でプロパティを定義できるようになりました。
またプライベートなプロパティとメソッドを定義できるようになりました。
これにより、他言語のクラス定義に近づいてきました。

また従来のオブジェクト操作との差別化なのか、プライベートなプロパティとメソッドをclass定義のみで実現可能です。
そのためclass構文の使用を拒否していた層も、class構文を利用するケースが増えると予想されます。

導入されたプライベートプロパティについては、次のページを読んでみてください!
【JavaScript】 クラス定義でプライベートプロパティが使用できるようになったと聞いて確認してみた

 

まとめ

ECMAScript2022でのclass定義の機能追加で、他言語のプログラマがイメージするクラスに近づいてきました。
また、プライベート要素をclass構文のみに導入することで、他のオブジェクト操作よりもclass構文を優位な位置に置くことができました。

今後はclass構文の使用頻度が増えていくのが予想できます。

class構文を使用すれば、プロトタイプチェーンなどの仕組みを理解しなくてもインスタンスが作成できます。
『プロトタイプ何それ?』という声が増えそうです。

更新日:2024/02/27

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。