メインコンテンツまでスキップ

クラス

クラスインスタンス

オブジェクトを使うと、複数の値をひとまとまりに扱うことができました。実世界においては、同じプロパティ (属性) を持つオブジェクトを多く扱う場合が多いです。例えば、学生をオブジェクトとして表すことを考えてみましょう。学生には必ず名前と年齢という属性があるはずなので、ひとまず nameageプロパティに持つとしましょう。

const tanaka = {
name: "田中",
age: 18,
};

同じ属性を持つオブジェクトを複数生成するときに役立つのがクラスです。クラスでは、オブジェクトプロパティを予め設定しておくだけでなく、下のメソッドの節で説明するように、プロパティを引数にもつような関数も設定しておくことができます。これにより、同じコードを何度も書く必要がなくなるというメリットがあります。クラスは、同じプロパティを持つオブジェクトを統一的に扱うための仕組みであり、オブジェクトの設計図と言えます。

次のコードでは、先ほど作った tanaka のように nameage というプロパティを持つオブジェクトの設計図として、クラス 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

演習

weightInTonscost をプロパティとして持ち、 weightInTons のデフォルト値が 1 であるクラス Car を作成し、 cost に好きな値を代入してみましょう。

解答例: Carクラスの定義
class Car {
weightInTons = 1;
cost;
}

const prius = new Car();
prius.cost = 2600000;

document.write(
`重さは${prius.weightInTons}トンで、値段は${prius.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 を定義して、今年の自己紹介と 1 年後の自己紹介を表示してみてください。田中さんの年齢は好きな値で構いません。

解答例: 年齢を増やすメソッド
class Student {
name;
age;

introduceSelf() {
document.write(`私の名前は${this.name}です。${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, birthYear, currentYear) {
// this.name は作成されたインスタンスのプロパティ
// name はコンストラクタに渡された引数
this.name = name;
this.age = currentYear - birthYear;
}

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;

constructor(name, age) {
this.name = name;
this.age = age;
}

introduceSelf() {
document.write(`私の名前は${this.name}です。${this.age}歳です。`);
}
}

class SeniorStudent extends Student {
researchQuestion;

constructor(name, age, researchQuestion) {
super(name, age);
this.researchQuestion = researchQuestion;
}

introduceSelf() {
super.introduceSelf();
document.write(`研究テーマは${this.researchQuestion}です。`);
}
}
const tanaka = new SeniorStudent("田中", 22, "量子力学");
tanaka.introduceSelf();

組み込みのクラス

JavaScript では、開発者が定義しなくても最初から使用可能なクラスが数多く用意されています。

例えば、Date クラスという、日付や時刻を扱うためのクラスがあります。

const myBirthDay = new Date("2014-05-06"); // Dateクラスをインスタンス化
document.write(myBirthDay.getFullYear()); // 2014

Date クラスコンストラクタは、引数として日時を表す文字列をひとつとります。省略された場合には現在の日時を用います。

getFullYear メソッドは、年となる数値を返すメソッドです。

また、DOM を利用して div 要素を作成または取得すると、HTMLDivElement クラスのインスタンスが得られます。

このクラスは HTMLElement クラスを継承しており、 HTMLElement クラスは Element クラスを、Element クラスは Node クラスを継承しています。

HTMLDivElementの継承関係

実は、DOM の節で使用した textContent プロパティは、この Node クラスで定義されています。

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 クラスを継承し、toString メソッドをオーバーライドしている
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

演習

Date クラス

Date クラスを使って、現在時刻を表示してみましょう。Date クラスのドキュメントを読んで、現在時刻を表示するのに必要なメソッドを探してみましょう。

ヒント

Date クラスには、時間、分、秒などを取得するためのメソッドが定義されています。

解答例: 現在時刻の表示
const currentTime = document.getElementById("current-time");

function getCurrentTime() {
const now = new Date();
const currentYear = now.getFullYear();
const currentMonth = now.getMonth() + 1;
const currentDate = now.getDate();
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
const currentSecond = now.getSeconds();

return `今は${currentYear}${currentMonth}${currentDate}${currentHour}${currentMinute}${currentSecond}秒です。`;
}

currentTime.textContent = getCurrentTime();

図形クラス

色を表す color プロパティを持つ Shape クラスを実装してみましょう。

そして、Shape クラスを継承し、面積を求める calculateArea メソッドを持つような、Rectangle (長方形) クラス、Square (正方形) クラス、Circle (円) クラスを実装してみましょう。また、この 3 つのクラスの間のどこで継承関係を作ればいいか考えてみましょう。

解答例: 図形クラス
class Shape {
color;
constructor(color) {
this.color = color;
}
}

class Rectangle extends Shape {
height;
width;
constructor(color, height, width) {
super(color);
this.height = height;
this.width = width;
}
calculateArea() {
return this.height * this.width;
}
}

class Square extends Rectangle {
constructor(color, sideLength) {
super(color, sideLength, sideLength);
}
}

class Circle extends Shape {
radius;
constructor(color, radius) {
super(color);
this.radius = radius;
}
calculateArea() {
return Math.PI * this.radius ** 2;
}
}