クラス
クラスとインスタンス
オブジェクトを使うと、複数の値をひとまとまりに扱うことができました。実世界においては、同じプロパティ (属性) を持つオブジェクトを多く扱う場合が多いです。例えば、学生をオブジェクトとして表すことを考えてみましょう。学生には必ず名前と年齢という属性があるはずなので、ひとまず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
確認問題
weightInTons
とcost
をプロパティとして持ち、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
クラスを継承しています。
実は、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
演習問題1
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();
演習問題2
色を表す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;
}
}