お知らせ:2021/2/18 ツールサイト(affi-sapo-sv.com)から、開発ノートを独立させました。
【JavaScript】 JSにおけるクラスとは?正体についても調べてみた
更新日:2021/02/04
ツイートJavaScriptにもクラスがあった。
今更知った僕です。
そこでさっそく、JavaScriptのクラスについて調べてみました。
クラスとは
クラスとは、オブジェクトが所持する変数やメソッドなどを定義したものです。
クラスそのものは『定義しているだけ』です。
クラスという設計図から、オブジェクトという実体を作成することで、初めて変数やメソッドを実行できるようになります。
(静的メソッドは実体がなくても実行できるけれど、とりあえずおいておいてください…)
一方今までのJavaScriptは最初からオブジェクトを用意して、そのオブジェクトにプロパティを追加していきます。
設計図を見ないで、家を建てているような感じです。
JavaScriptにクラスが存在していなかったので、そのようにするしかなかったのです。
しかしJavaScriptの2015年版の仕様書(ECMAScript 2015)で、ようやくクラスのようなものが使用できるようになりました。
クラスの定義と使い方
JavaScriptのclass構文は、基本的にはコンストラクター関数やプロトタイプ機能を、他言語のclass構文風に置き換えたものです。
そのため他言語のclass構文でできて、JavaScriptではできないことが多いです。
そのことを念頭に置きながら、JavaScriptのクラスについて、既存のコンストラクター関数およびプロトタイプ機能と対比しながら解説していきます。
一番簡単なクラス定義
次のコードは、一番簡単なクラス定義と、定義からインスタンスを作成する方法です。
class宣言
class a{ // クラスaを宣言 } const b = new a(); // クラスaのインスタンスを作成
クラス宣言を、次のように変数に代入して使用できます。
class式
const a = class{ // クラスを宣言し、変数aに代入 }; const b = new a(); // 変数aから、インスタンスを作成
この中身が何もないクラスは、関数コードが空欄のコンストラクター関数と同等です。
空のコンストラクター関数
function a(){ }; const b = new a();
constructorを定義する
次に、クラス内部にconstructorメソッドを定義してみます。
class a{ constructor( ) { // constructorメソッド this.value = 1; } } const b = new a();
これは、次のコンストラクター関数と同等です。
function a(){ this.value = 1; } const b = new a();
インスタンスのメンバーはどこで記述する?
class構文が使用できる言語の多くは、class構文の一部としてメンバ変数を定義します。
多言語のメンバ変数定義イメージ
class クラス名{ public メンバ変数名; }
しかしJavaScriptは、constructorメソッド内で処理の流れとして、作成していきます。
JavaScriptのメンバ変数定義
class a{ constructor( ) { // constructorメソッド this.value = 1; // メンバ変数を作成 this.name = "taro"; // メンバ変数を作成 } }
他の言語でクラスを使用してきた人が、戸惑う点ですね。
※今後はどうなる?
一部のブラウザでは、次のようにクラス内でメンバ変数を定義できます。
class a { value1 = 100; // メンバ変数定義 getValue(){ return this.value1; } }
使用できるブラウザが限られるので、今のところは使用すべきではありません。
しかし数年後には、この書き方が普通になりそうです。
メンバーメソッドの定義
メンバーメソッドも変数と同様に、 constructor()内で定義します。
JavaScriptのメンバメソッド定義
class a{ constructor( ) { // constructorメソッド this.value = 1; // メンバ変数を作成 this.name = "taro"; // メンバ変数を作成 this.method = function(){ }; // メンバメソッドを作成 } }
ただし、class構文内でメソッドを定義することができます。
class構文内でのメソッド定義
class a{ constructor( ) { // constructorメソッド this.value = 1; // メンバ変数を作成 } addValue( v ){ // メソッドを定義 this.value += v; } }
このとき、functionや : などは必要ありません。
メソッドをそのまま記述します。
ただし、この方法で定義したメソッドは、constructor( )でthis値にセットしたメソッドと同等ではありません。
class構文は、評価時にインスタンスを作成する機能を持ったオブジェクトに変換されるのですが、そのとき、class構文内のメソッドは変換されたオブジェクトのprototypeプロパティのメンバとして登録されてしまうのです。
つまり、次のコードと同等になります。
classのメソッド定義と同等なコード
function a(){ this.value = 1; } a.prototype.addValue = function( v ){ this.value += v; }
prototypeプロパティは、new演算子によりインスタンスのプロトタイプチェーンに組み込まれます。
そのため、インスタンス共通のメソッドとして実行可能となります。
他言語では、クラスで定義したメソッドはクラスがインスタンス化されるまで使用できません。
しかしJavaScriptは、インスタンス化しなくてもアクセス可能です。
静的メソッド/変数
class構文内のメソッド定義の前に、staticキーワードを付加すると静的メソッドとして扱われます。
class構文内での静的メソッド定義
class a{ constructor( ) { // constructorメソッド this.value = 1; // メンバ変数を作成 } static addValue( v ){ // 静的メソッドを定義 this.value += v; } static sValue = 100; // 静的変数を定義 }
staticキーワードが付加されたメソッドや変数は、class構文を変換したオブジェクトのプロパティとして追加されます。
これは、次のコードと同等です。
classの静的メソッド定義と同等なコード
function a(){ this.value = 1; } a.addValue = function( v ){ this.value += v; } a.sValue = 100;
静的メソッドはクラスが直接所持するプロパティのため、インスタンスのプロトタイプチェーンに組み込まれません。
(組み込まれるのはprototypeプロパティだけです)
そのため、インスタンスから静的メソッドを呼び出すことができません。
(プロトタイプチェーン上にないからです)
なお、静的メソッドのaddValueは、this.valueがスコープ解決できないため、実行するとエラーになります。
ゲッター/セッターの定義
クラスでもゲッター/セッターを定義できます。
ゲッター/セッターは、プロパティのようにイコール(=)で呼び出せるメソッドです。
参考:【JavaScript】 ゲッター・セッターとは?必要性はあるのか?
class a { constructor( val1 ) { this.prop1 = val1; } get prop(){ return this.prop1; } set prop( v ){ this.prop1 = v; } }
ゲッター/セッター定義はメソッド定義と同様に、クラスのprototypeプロパティに定義されます。
静的ゲッター・セッターの定義
ゲッター・セッター定義の前に、staticキーワードを付加すると、静的ゲッター・セッターとして扱われます。
class a{ static value = 0; staic get prop( ){ return a.value; } // ゲッター staic set prop( v ){ a.value = v; } // セッター }
クラス定数の定義
クラス共通の定数は、次のようにゲッターで定義できます。
class a { get classconst(){ return 値; } }
同名のセッターを定義しないことで、読み込み専用となります。
『定数を定義したいだけなのにゲッター使うとか気持ち悪い』という人は、次のようにObject.definePropertyを使用します。
Object.defineProperty( a.prototype , "classconst", {
value: "定数を持ちたい!",
writable: false
});
Object.definePropertyの第一引数には、クラスのプロトタイプを指定します。
第ニ引数に定数の名前を、valueには定数としての値を指定します。
そしてwriteableをfalseにすることで、値を変更できなくなります。
結果的に、定数として定義したことになります。
クラスで継承をおこなう
他の言語でプログラムをしてきた人が、クラスに期待することの一つに継承があると思います。
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でカプセル化できるか?
他のプログラム言語のクラスでできることの一つが、情報を隠蔽してカプセル化できるということですね。
残念ながら、クラスが導入されてもJavaScriptのプロパティはオープンでpublicなままでした。
privateもprotectedもありません。
情報隠蔽は次の例のように、従来のクロージャ的な手法を継続して使用するしかないようです。
class a { constructor( v1 , v2 ) { const _name = v1 + " " + v2; this.getName = ()=>_name; } } const b = new a( "山田 " , "たろう" ); console.log( b.getName ); // 山田たろう
JavaScriptのクラスの正体
他の言語のクラスでできて、JavaScriptのクラスではできないことが非常に多いです。
こんなのはクラスではないという人が多いですが、それには理由があります。
JavaScriptのクラスはシンタックスシュガー
JavaScriptにクラスが導入されましたが、プログラムとしてできることは何も変わっていません。
それはクラスがシンタックスシュガー(糖衣構文)でしかないからです。
シンタックスシュガーは既存のプログラム上の手続きを、簡略化したりみやすくするために用意されたものです。
例えば、次のようなクラスがあるとします。
class a { constructor( ) { ...初期化処理 } func1(){ } // メンバメソッド static func2(){ } // 静的メソッド }
従来は次のように記述していました。
function a(){ ...初期化処理 }; a.prototype.func1 = function () {}; a.func2 = function () {};
どちらも(ほぼ)同じ関数オブジェクト(コンストラクター)が作成されます。
他の言語のクラスを使っていた人が、初めからclass構文を使用するのはおススメしません。
まずは従来の方法を学び、何ができて何ができないかを理解してから使用すべきです。
参考記事:
■【JavaScript】 コンストラクターとは?関数とは違うのか?
■【JavaScript】 プロトタイプとは?prototypeプロパティはプロトタイプではない件について
■【JavaScript】 new演算子は何をやっている?
クラスはstrictモードで動作する
クラス内のコードは強制的にstrictモードになります。
strictモードは変数の定義をしなくてもそれなりに動いてくれるなど少し適当だったJavaScriptを、ほんの少し厳格にするモードです。
デフォルトでは非strictモードで動作するので、strictモードを使用するときは"use strict";と記述する必要があります。
しかしクラスは、記述してもしなくてもstrictモードとなります。
非strictモードでプログラムを組んでいる人は、クラス内だけエラーとなり戸惑うかもしれませんね。
まとめ
JavaScriptのクラスは、内部的にはsuperキーワードが追加された以外は以前と変わっていません。
そのため、以前できなかったことは、今でもできません。
クラス定義を使えば、わかりやすいコードを作れるという意見もありますが、誰にとってわかりやすいかいまいち不明。
prototypeを隠蔽しているので、初心者がJavaScriptの本質を理解しにくくなる気もします。
class構文は内部でどんな置き換えをしているのか、ということを理解してからでないと、勉強にならないですね。
でも理解したら、わざわざclassを使う必要がない気がします。
使いたければ使えばいいんじゃない?と思います。
記事の内容について

説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
ご意見はこちら。
https://note.affi-sapo-sv.com/info.php
【お願い】

■このページのURL
■このページのタイトル
■リンクタグ