クラス
クラスとインスタンス
オブジェクトを使うと、複数の値をひとまとまりに扱うことができました。実世界においては、同じプロパティ(属性)を持つオブジェクトを多く扱う場合が多いです。例えば、学生をオブジェクトとして表すことを考えてみましょう。学生には必ず名前と年齢という属性があるはずなので、ひとまず name
と age
をプロパティに持つとしましょう。
const tanaka = {
name: "田中",
age: 18,
};
同じ属性を持つオブジェクトを複数生成するときに役立つのが クラス です。クラスでは、オブジェクトのプロパティを予め設定しておくだけでなく、下のメソッドの節で説明するように、プロパティを引数にもつような関数も設定しておくことができます。これにより、同じコードを何度も書く必要がなくなるというメリットがあります。クラスは、同じプロパティを持つオブジェクトを統一的に扱うための仕組みであり、オブジェクトの設計図と言えます。
次のコードでは、先ほど作った tanaka
のように name
や age
というプロパティを持つオブジェクトの設計図として、クラス Student
を定義しています。クラスでは、この例の age
プロパティのように、デフォルトの値を設定することができます。
class Student {
name; // name プロパティを作成する
age = 18; // age プロパティのデフォルト値として `18` を使用する
}
new
演算子をクラスに対して適用すると、設計図に基づいてオブジェクトが作成されます。こうしてできたオブジェクトを、もとになったクラスの インスタンス と呼びます。今回の age
プロパティのように、クラスのプロパティにデフォルトの値が設定されている場合、新たな値を代入するまではデフォルト値が入ります。もちろん、プロパティに新たな値を代入してデフォルト値を書き換えることもできます。
const tanaka = new Student(); // Student クラスをもとにオブジェクトを作成する
tanaka.name = "田中"; // name プロパティに代入
document.write(tanaka.age); // age プロパティのデフォルト値は 18
undefined
という値上で定義した Student
クラスには、デフォルト値の指定されていないプロパティ name
が存在します。new Student
をした直後のオブジェクトの name
プロパティの値はどうなっているのでしょうか。
実は、JavaScript には、未定義であることを表す特殊な値 undefined
が存在しています。これまで、JavaScript の値には数値、文字列、論理値、オブジェクトがあるとしてきましたが、これらとはまた別の値です。
存在しないプロパティの値、値を返さない関数の戻り値などは、すべて undefined
となります。
const emptyObject = {};
function emptyFunction() {}
document.write(emptyObject.unknownProperty); // 存在しないプロパティは undefined
document.write(emptyFunction()); // 値を返さない関数の戻り値は undefined
課題
weight
と cost
をプロパティとして持ち、 weight
のデフォルト値が "1t"
であるクラス Car
を作成し、 cost
に好きな値を代入してみましょう。
メソッド
同じプロパティを持つオブジェクトに対しては、同じような処理を行うことが多いです。例えば、学生はたいてい最初の授業で自己紹介をします。そこで、 Student
クラスに、自己紹介をする関数 introduceSelf()
を設定してみましょう。
オブジェクトに対して定義されている関数を メソッド と呼びます。メソッドの定義はクラス定義の中で行われますが、関数と異なり、function
キーワードを必要としません。
class Student {
name;
age;
// メソッド introduceSelf を定義する
introduceSelf() {
// this は作成されたインスタンスを指す
document.write(`私の名前は${this.name}です。${this.age}歳です。`);
}
}
クラス自体は単なる設計図でしかないため、実際のオブジェクトが存在するわけではありません。そこで、メソッド内では、設計図から作成されたインスタンス自身を指す特殊な変数 this
が使用できます。
メソッドを使用するには、プロパティへのアクセス時と同じく、インスタンスに対して .
(ドット)記号を用います。
const tanaka = new Student();
tanaka.name = "田中";
tanaka.age = 18;
// introduceSelf メソッド内では this は tanaka に格納されたオブジェクトになる
tanaka.introduceSelf();
prototype
多くの言語で、クラス Class
のメソッドやプロパティ method
を、#
記号を用いて Class#method
と表記します。本資料では他言語の慣習に習い、この表記を用いるものとします。たとえば、上の例で定義されているメソッドは Student#introduceSelf
メソッドです。
ただし、JavaScript においては prototype
という語を用いて Class.prototype.method
とされる場合があります。これはより厳密な表記です。外部の資料を読む場合は注意してください。
課題
自分自身の年齢を 1 増やすメソッド incrementAge
を定義して、実行してみてください。
解答
class Student {
name;
age = 18;
introduceSelf() {
document.write(`私の名前は${this.name}です。`);
document.write(`${this.age}歳です`);
}
incrementAge() {
this.age += 1;
}
}
const tanaka = new Student();
tanaka.name = "田中";
tanaka.age = 19;
tanaka.introduceSelf();
tanaka.incrementAge();
tanaka.introduceSelf();
コンストラクタ
コンストラクタは、インスタンスを作成するタイミング(new
演算子をクラスに適用するタイミング)で実行される特殊なメソッドです。コンストラクタとなるメソッドは constructor
という名前で定義する必要があります。コンストラクタを定義すると、new Student
を実行してインスタンスを生成するときにプロパティの設定も同時に行うことができます。
class Student {
name;
age;
// コンストラクタを定義する
constructor(name, yearOfBirth, currentYear) {
// this.name は作成されたインスタンスのプロパティ
// name はインスタンス生成時に代入する値
this.name = name;
this.age = currentYear - yearOfBirth;
}
introduceSelf() {
document.write(`私の名前は${this.name}です。${this.age}歳です。`);
}
}
const tanaka = new Student("田中", 2004, 2022);
tanaka.introduceSelf();
const tanaka = {
name: "田中",
age: 18,
introduceSelf() {
document.write(`<p>私の名前は${tanaka.name}です。${tanaka.age}歳です。<p>`);
},
};
const suzuki = {
name: "鈴木",
age: 20,
introduceSelf() {
document.write(`<p>私の名前は${suzuki.name}です。${suzuki.age}歳です。<p>`);
},
};
const sato = {
name: "佐藤",
age: 20,
introduceSelf() {
document.write(`<p>私の名前は${sato.name}です。${sato.age}歳です。<p>`);
},
};
tanaka.introduceSelf();
suzuki.introduceSelf();
sato.introduceSelf();
オブジェクトの定義が長くなり、書くのも読むのも大変です。さらに人数が増えると、コードはどんどん長くなってしまいます。また、introduceSelf()
関数の定義はほとんど同じコードが 3 回繰り返されています。
では、クラスとコンストラクタを用いるとどうでしょうか。
class Student {
name;
age;
// コンストラクタを定義する
constructor(name, age) {
// this は作成されたインスタンスを指す
this.name = name;
this.age = age;
}
// メソッド introduceSelf を定義する
introduceSelf() {
document.write(`<p>私の名前は${this.name}です。${this.age}歳です。<p>`);
}
}
const tanaka = new Student("田中", 18);
const suzuki = new Student("鈴木", 20);
const sato = new Student("佐藤", 20);
tanaka.introduceSelf();
suzuki.introduceSelf();
sato.introduceSelf();
クラスの定義自体はやや長いものの、1 つのオブジェクトの定義はたった 1 行で済みます。これならオブジェクトの数が増えても安心です。introduceSelf()
関数の定義を繰り返す必要もなくなり、読みやすく編集しやすいコードになりました。
継承
クラス定義の際に extends
キーワードを用いて別のクラスを指定すると、指定されたクラスのプロパティとメソッドを全て受け継いだ新たなクラスを定義することができます。
class Student {
name;
age;
constructor(name, age) {
this.name = name;
this.age = age;
}
introduceSelf() {
document.write(`私の名前は${this.name}です。${this.age}歳です。`);
}
}
// Student を継承したクラス FreshmanStudent を定義
class FreshmanStudent extends Student {
selectedLanguage;
constructor(name, age, selectedLanguage) {
// コンストラクタ内では super キーワードで親クラスのコンストラクタを呼ぶ必要がある
super(name, age);
this.selectedLanguage = selectedLanguage;
}
// 継承元のクラスと同じ名前のメソッドを定義(オーバーライド)すると、継承元のクラスのメソッドは覆い隠されてしまう
introduceSelf() {
// super キーワードを使えば覆い隠された同名のメソッドを呼び出せる
super.introduceSelf();
document.write(`${this.selectedLanguage}選択です。`);
}
}
const tanaka = new FreshmanStudent("田中", "18", "ドイツ語");
tanaka.introduceSelf(); // 私の名前は田中です。18歳です。ドイツ語選択です。
課題
Student
クラスを継承して SeniorStudent
クラスを作ってみましょう。SeniorStudent
クラスのインスタンスは researchQuestion
プロパティを持ち、introduceSelf
メソッドを実行すると自分の名前を出力した後に自分の研究内容を紹介するようにしてみましょう。
解答
class Student {
name;
age;
introduceSelf() {
document.write(`私の名前は${this.name}です。${this.age}歳です。`);
}
}
class SeniorStudent extends Student {
researchQuestion;
introduceSelf() {
super.introduceSelf();
document.write(`研究テーマは${this.researchQuestion}です。`);
}
}
const tanaka = new SeniorStudent();
tanaka.age = 22;
tanaka.name = "田中";
tanaka.researchQuestion = "量子力学";
tanaka.introduceSelf();
Date
クラス
Date
クラスは、JavaScript に標準で用意されている、日付や時刻を扱うためのクラスです。このように、JavaScript では、開発者が定義しなくても最初から使用可能なクラスが数多く用意されています。
const myBirthDay = new Date("2014-05-06"); // Dateクラスをインスタンス化
document.write(myBirthDay.getFullYear()); // 2014
Date
クラスのコンストラクタは、引数として日時を表す文字列をひとつとります。省略された場合には現在の日時を用います。
getFullYear
メソッドは、年となる数値を返すメソッドです。
Object
クラスJavaScript では、全てのオブジェクトはObject
クラスを自動的に継承します。このため、全てのオブジェクトは Object
クラスのメソッドを使用することができます。また、プリミティブな値でも、メソッドを呼び出すと自動的にオブジェクトに変換されます。
toString
メソッドはその一つで、オブジェクトの文字列表記を返します。このメソッドはオーバーライド可能で、たとえば Date
クラスではこのメソッドがオーバーライドされています。
// 通常のオブジェクトの toString メソッドは "[object Object]" を返す
document.write({ name: "田中" }.toString()); // [object Object]
// Date クラスは toString メソッドをオーバーライドしている
document.write(new Date().toString()); // Fri Apr 01 2022 10:00:00 GMT+0900 (Japan Standard Time)
// 関数もオブジェクトの一種なのでやはり Object クラスを継承している
function add(a, b) {
return a + b;
}
document.write(add.toString()); // function add(a, b) { return a + b; }
// 数値や文字列、論理値はメソッドを呼び出すときに自動的にオブジェクトに変換される
document.write((123).toString()); // 123
document.write("Hello World!".toString()); // Hello World!
document.write(false.toString()); // false
課題
document.getElementById
関数で div
要素を取得すると、HTMLDivElement
クラスのインスタンスが返されます。このクラスは HTMLElement
クラス を継承しており、さらに HTMLElement
クラスは Element
クラスを、Element
クラスは Node
クラスを継承しています。
実は、DOM の節で使用した textContent
プロパティは、この Node
クラスで定義されています。
HTMLDivElement
クラスを自分でインスタンス化し、textContent
プロパティに適当な値を代入して、document.body.appendChild
関数を用いて、作成した div
要素を body
要素の中に追加しましょう。
※ HTMLDivElement
クラスをインスタンス化する際には new HTMLDivElement
ではなく document.createElement("div")
とします。
(発展) document.body
は何のクラスのインスタンスなのでしょうか。appendChild
メソッドはどのクラスに定義されているのでしょうか。