Skip to content

Latest commit

 

History

History
472 lines (347 loc) · 19.2 KB

File metadata and controls

472 lines (347 loc) · 19.2 KB
title クラス

クラスとインスタンス

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

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

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

次のコードでは、先ほど作ったtanakaのようにnameageというプロパティを持つオブジェクトの設計図として、クラスStudentを定義しています。クラスでは、この例のageプロパティのように、デフォルトの値を設定することができます。

class Student {
  name; // nameプロパティを作成する
  age = 18; // ageプロパティのデフォルト値として`18`を使用する
}

:::info

{/* prettier-ignore */} クラスの名前は、通常のキャメルケースの最初の文字を大文字にしたパスカルケースで記述するのが普通です。

:::

new演算子をクラスに対して適用すると、設計図に基づいてオブジェクトが作成されます。こうしてできたオブジェクトを、もとになったクラスのインスタンスと呼びます。今回のageプロパティのように、クラスのプロパティにデフォルトの値が設定されている場合、新たな値を代入するまではデフォルト値が入ります。もちろん、プロパティに新たな値を代入してデフォルト値を書き換えることもできます。

const tanaka = new Student(); // Studentクラスをもとにオブジェクトを作成する

tanaka.name = "田中"; // nameプロパティに代入
document.write(tanaka.age); // ageプロパティのデフォルト値は18

クラスとインスタンス

:::tip[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に好きな値を代入してみましょう。

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}歳です。`);
  }
}

{/* prettier-ignore */} クラス自体は単なる設計図でしかないため、実際のオブジェクトが存在するわけではありません。そこで、メソッド内では、設計図から作成されたインスタンス自身を指す特殊な変数thisが使用できます。

{/* prettier-ignore */} メソッドを使用するには、プロパティへのアクセス時と同じく、インスタンスに対して.(ドット)記号を用います。

const tanaka = new Student();
tanaka.name = "田中";
tanaka.age = 18;

// introduceSelfメソッド内ではthisはtanakaに格納されたオブジェクトになる
tanaka.introduceSelf();

:::tip[メソッドやプロパティの表記と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();

コンストラクタ

{/* prettier-ignore */} コンストラクタは、インスタンスを作成するタイミング(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();

{/* prettier-ignore */} クラスとコンストラクタのメリットを理解するために、クラスのインスタンスを複数生成する場合を考えましょう。例えば、田中さん、鈴木さん、佐藤さんが続けて自己紹介する場合、クラスを使わないでコードを書くと以下のようになります。

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クラスで定義されています。

:::tip[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クラスのドキュメントを読んで、現在時刻を表示するのに必要なメソッドを探してみましょう。

:::tip 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;
  }
}