前回の記事 では、JavaScriptのオブジェクトについて、基本的な操作からMap/Setなどの新しいデータ構造までを紹介しました。今回は、ES2015で導入されたクラス構文とオブジェクト指向プログラミングについて、従来のプロトタイプベースのアプローチとの違いを含めて紹介します。
クラスの基本構文
クラスは、オブジェクトの設計図やひな形として機能します。ES2015以前はプロトタイプベースで実現していたオブジェクト指向プログラミングを、より直感的で理解しやすい構文で書けるようになりました。
基本的なクラス定義
// ES2015以降のクラス構文
class Person {
// コンストラクタ(初期化処理)
constructor(name, age) {
this.name = name;
this.age = age;
}
// インスタンスメソッド
greet() {
return `こんにちは、私は${this.name}です。${this.age}歳です。`;
}
// 別のメソッド
introduce() {
console.log(this.greet());
}
}
// インスタンスの作成
const person1 = new Person("田中太郎", 30);
const person2 = new Person("佐藤花子", 25);
console.log(person1.greet()); // "こんにちは、私は田中太郎です。30歳です。"
person2.introduce(); // "こんにちは、私は佐藤花子です。25歳です。"
ES5プロトタイプベースとの比較
ES2015のクラス構文は、内部的にはプロトタイプベースの仕組みを使っていますが、より読みやすい構文を提供します。
// ES5以前のプロトタイプベース
function PersonOld(name, age) {
this.name = name;
this.age = age;
}
PersonOld.prototype.greet = function() {
return `こんにちは、私は${this.name}です。${this.age}歳です。`;
};
PersonOld.prototype.introduce = function() {
console.log(this.greet());
};
// ES2015以降のクラス構文(上記と同等の機能)
class PersonNew {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `こんにちは、私は${this.name}です。${this.age}歳です。`;
}
introduce() {
console.log(this.greet());
}
}
// どちらも同じように使える
const oldPerson = new PersonOld("山田次郎", 40);
const newPerson = new PersonNew("鈴木美香", 35);
コンストラクタとインスタンス生成
コンストラクタの役割
コンストラクタは、クラスからインスタンスを作成する際に自動的に実行される特別なメソッドです。
class Book {
constructor(title, author, pages = 100) {
// 引数の検証
if (!title || !author) {
throw new Error("タイトルと著者は必須です");
}
this.title = title;
this.author = author;
this.pages = pages;
this.isRead = false;
this.createdAt = new Date();
}
// ページ数の妥当性チェック
setPages(pages) {
if (pages <= 0) {
throw new Error("ページ数は1以上である必要があります");
}
this.pages = pages;
}
// 読書状況の更新
markAsRead() {
this.isRead = true;
this.readAt = new Date();
}
// 本の情報表示
getInfo() {
const status = this.isRead ? "読了" : "未読";
return `『${this.title}』${this.author}著(${this.pages}ページ)- ${status}`;
}
}
// インスタンス生成の例
try {
const book1 = new Book("JavaScript入門", "山田太郎", 200);
const book2 = new Book("プログラミング基礎", "佐藤花子"); // デフォルト値使用
console.log(book1.getInfo()); // 『JavaScript入門』山田太郎著(200ページ)- 未読
book1.markAsRead();
console.log(book1.getInfo()); // 『JavaScript入門』山田太郎著(200ページ)- 読了
// エラーになる例
// const invalidBook = new Book("", "著者名"); // エラー
} catch (error) {
console.error(error.message);
}
継承
継承を使うことで、既存のクラスの機能を受け継ぎながら、新しい機能を追加できます。
extendsを使ったサブクラス化
// 基底クラス(スーパークラス)
class Animal {
constructor(name, species) {
this.name = name;
this.species = species;
this.energy = 100;
}
eat(food) {
console.log(`${this.name}が${food}を食べています`);
this.energy += 10;
}
sleep() {
console.log(`${this.name}が眠っています`);
this.energy += 20;
}
getStatus() {
return `${this.name}(${this.species})- エネルギー:${this.energy}`;
}
}
// 派生クラス(サブクラス)
class Dog extends Animal {
constructor(name, breed) {
// 親クラスのコンストラクタを呼び出し
super(name, "犬");
this.breed = breed;
this.loyalty = 100;
}
// 犬特有のメソッド
bark() {
console.log(`${this.name}が吠えています:ワンワン!`);
this.energy -= 5;
}
// 親クラスのメソッドをオーバーライド
eat(food) {
if (food === "ドッグフード") {
console.log(`${this.name}が${food}を美味しそうに食べています`);
this.energy += 15; // 犬用フードでより多くエネルギー回復
this.loyalty += 5;
} else {
super.eat(food); // 親クラスのメソッドを呼び出し
}
}
// 犬特有の情報表示
getStatus() {
return `${super.getStatus()}、犬種:${this.breed}、忠誠度:${this.loyalty}`;
}
}
class Cat extends Animal {
constructor(name, color) {
super(name, "猫");
this.color = color;
this.independence = 80;
}
meow() {
console.log(`${this.name}が鳴いています:ニャーン`);
this.energy -= 3;
}
hunt() {
console.log(`${this.name}が狩りをしています`);
this.energy -= 15;
this.independence += 10;
}
getStatus() {
return `${super.getStatus()}、毛色:${this.color}、独立性:${this.independence}`;
}
}
// 使用例
const dog = new Dog("ポチ", "柴犬");
const cat = new Cat("タマ", "三毛");
console.log(dog.getStatus()); // ポチ(犬)- エネルギー:100、犬種:柴犬、忠誠度:100
console.log(cat.getStatus()); // タマ(猫)- エネルギー:100、毛色:三毛、独立性:80
dog.bark(); // ポチが吠えています:ワンワン!
dog.eat("ドッグフード"); // ポチがドッグフードを美味しそうに食べています
console.log(dog.getStatus()); // ポチ(犬)- エネルギー:115、犬種:柴犬、忠誠度:105
cat.meow(); // タマが鳴いています:ニャーン
cat.hunt(); // タマが狩りをしています
console.log(cat.getStatus()); // タマ(猫)- エネルギー:85、毛色:三毛、独立性:90
staticメソッド
staticメソッドは、インスタンスではなくクラス自体に属するメソッドです。ユーティリティ関数や、インスタンスを作成せずに使える機能を提供する際に使用します。
インスタンスメソッドとの違い
class MathUtils {
constructor(initialValue = 0) {
this.value = initialValue;
}
// インスタンスメソッド
addValue(num) {
this.value += num;
return this;
}
getValue() {
return this.value;
}
// staticメソッド - クラスから直接呼び出し
static multiply(a, b) {
return a * b;
}
static factorial(n) {
if (n <= 1) return 1;
return n * MathUtils.factorial(n - 1);
}
static isEven(num) {
return num % 2 === 0;
}
// staticメソッドでインスタンスを作成するファクトリーパターン
static createCounter(start = 0) {
return new MathUtils(start);
}
}
// staticメソッドの使用例
console.log(MathUtils.multiply(5, 3)); // 15
console.log(MathUtils.factorial(5)); // 120
console.log(MathUtils.isEven(4)); // true
// インスタンスメソッドの使用例
const calculator = new MathUtils(10);
console.log(calculator.addValue(5).getValue()); // 15
// ファクトリーメソッドの使用例
const counter = MathUtils.createCounter(100);
console.log(counter.addValue(25).getValue()); // 125
// 注意:staticメソッドはインスタンスからは呼び出せない
// console.log(calculator.multiply(2, 3)); // エラー
別のstaticメソッドの例
class DateHelper {
// 現在の日時を特定のフォーマットで取得
static getCurrentDateString(format = "YYYY-MM-DD") {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, "0");
const date = String(now.getDate()).padStart(2, "0");
switch (format) {
case "YYYY-MM-DD":
return `${year}-${month}-${date}`;
case "DD/MM/YYYY":
return `${date}/${month}/${year}`;
default:
return `${year}/${month}/${date}`;
}
// 2つの日付の差を計算
static daysBetween(date1, date2) {
const oneDay = 24 * 60 * 60 * 1000;
const firstDate = new Date(date1);
const secondDate = new Date(date2);
return Math.round(Math.abs((firstDate - secondDate) / oneDay));
}
// 年齢計算
static calculateAge(birthDate) {
const today = new Date();
const birth = new Date(birthDate);
let age = today.getFullYear() - birth.getFullYear();
const monthDiff = today.getMonth() - birth.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
age--;
}
return age;
}
}
// 使用例
console.log(DateHelper.getCurrentDateString()); // "2025/01/14"
console.log(DateHelper.getCurrentDateString("DD/MM/YYYY")); // "14/01/2025"
console.log(DateHelper.daysBetween("2025-01-01", "2025-01-14")); // 13
console.log(DateHelper.calculateAge("1990-05-15")); // 35(2025年6月時点での年齢)
プロトタイプチェーンの基礎
JavaScriptのクラス構文は、内部的にプロトタイプチェーンを使用しています。これを理解することで、より深くJavaScriptのオブジェクト指向を理解できます。
プロトタイプチェーンの仕組み
class Vehicle {
constructor(type) {
this.type = type;
}
start() {
console.log(`${this.type}が開始されました`);
}
}
class Car extends Vehicle {
constructor(brand, model) {
super("自動車");
this.brand = brand;
this.model = model;
}
honk() {
console.log("クラクションが鳴りました:プップー");
}
}
const myCar = new Car("トノタ", "プルウス");
// プロトタイプチェーンの確認
console.log(myCar instanceof Car); // true
console.log(myCar instanceof Vehicle); // true
console.log(myCar instanceof Object); // true
// プロトタイプの確認
console.log(Object.getPrototypeOf(myCar) === Car.prototype); // true
console.log(Object.getPrototypeOf(Car.prototype) === Vehicle.prototype); // true
// プロパティの検索順序
// 1. myCar自身のプロパティ
// 2. Car.prototypeのプロパティ
// 3. Vehicle.prototypeのプロパティ
// 4. Object.prototypeのプロパティ
プロトタイプメソッドの動的追加
// クラス定義後にメソッドを追加
Vehicle.prototype.stop = function() {
console.log(`${this.type}が停止しました`);
};
// 既存のインスタンスでも新しいメソッドが使える
myCar.stop(); // "自動車が停止しました"
// プロトタイプチェーンの確認用メソッド
Vehicle.prototype.getPrototypeChain = function() {
const chain = [];
let current = this;
while (current) {
chain.push(current.constructor.name);
current = Object.getPrototypeOf(current);
}
return chain;
};
console.log(myCar.getPrototypeChain()); // ["Car", "Vehicle", "Object"]
実践的なコード例
1. データモデルの作成
// ユーザー管理システムの例
class User {
constructor(id, email, name) {
this.id = id;
this.email = email;
this.name = name;
this.createdAt = new Date();
this.isActive = true;
}
static validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
updateEmail(newEmail) {
if (!User.validateEmail(newEmail)) {
throw new Error("無効なメールアドレスです");
}
this.email = newEmail;
}
deactivate() {
this.isActive = false;
this.deactivatedAt = new Date();
}
getProfile() {
return {
id: this.id,
email: this.email,
name: this.name,
isActive: this.isActive
};
}
}
class AdminUser extends User {
constructor(id, email, name, permissions = []) {
super(id, email, name);
this.permissions = permissions;
this.role = "admin";
}
addPermission(permission) {
if (!this.permissions.includes(permission)) {
this.permissions.push(permission);
}
}
hasPermission(permission) {
return this.permissions.includes(permission);
}
getProfile() {
return {
...super.getProfile(),
role: this.role,
permissions: this.permissions
};
}
}
// 使用例
const user = new User(1, "user@example.com", "一般ユーザー");
const admin = new AdminUser(2, "admin@example.com", "管理者", ["read", "write"]);
admin.addPermission("delete");
console.log(admin.getProfile()); // { id: 2, email: "admin@example.com", name: "管理者", isActive: true, role: "admin", permissions: ["read", "write", "delete"] }
console.log(admin.hasPermission("write")); // true
2. ゲームキャラクター管理システム
class GameCharacter {
constructor(name, hp = 100, mp = 50) {
this.name = name;
this.maxHp = hp;
this.hp = hp;
this.maxMp = mp;
this.mp = mp;
this.level = 1;
this.experience = 0;
}
// ダメージを受ける
takeDamage(damage) {
this.hp = Math.max(0, this.hp - damage);
console.log(`${this.name}が${damage}のダメージを受けました(残りHP: ${this.hp})`);
if (this.hp === 0) {
console.log(`${this.name}が倒れました`);
}
}
// 回復
healHp(amount) {
const healed = Math.min(amount, this.maxHp - this.hp);
this.hp += healed;
console.log(`${this.name}が${healed}回復しました(現在HP: ${this.hp})`);
}
// 経験値獲得
gainExperience(exp) {
this.experience += exp;
const newLevel = Math.floor(this.experience / 100) + 1;
if (newLevel > this.level) {
this.levelUp(newLevel);
}
}
levelUp(newLevel) {
console.log(`${this.name}がレベル${newLevel}に上がりました!`);
this.level = newLevel;
this.maxHp += 20;
this.maxMp += 10;
this.hp = this.maxHp; // レベルアップ時に全回復
this.mp = this.maxMp;
}
getStatus() {
return `${this.name} Lv.${this.level} HP:${this.hp}/${this.maxHp} MP:${this.mp}/${this.maxMp}`;
}
}
class Warrior extends GameCharacter {
constructor(name) {
super(name, 120, 30); // 戦士は高HP、低MP
this.attackPower = 25;
}
attack(target) {
console.log(`${this.name}が${target.name}を攻撃!`);
target.takeDamage(this.attackPower);
}
powerAttack(target) {
if (this.mp < 10) {
console.log("MPが足りません");
return;
}
this.mp -= 10;
const damage = this.attackPower * 1.5;
console.log(`${this.name}がパワーアタック!`);
target.takeDamage(Math.floor(damage));
}
heal() {
if (this.mp < 12) {
console.log("MPが足りません");
return;
}
this.mp -= 12;
console.log(`${this.name}がヒールを詠唱!`);
this.healHp(25);
}
}
class Mage extends GameCharacter {
constructor(name) {
super(name, 80, 100); // 魔法使いは低HP、高MP
this.magicPower = 30;
}
fireball(target) {
if (this.mp < 15) {
console.log("MPが足りません");
return;
}
this.mp -= 15;
console.log(`${this.name}がファイアボールを詠唱!`);
target.takeDamage(this.magicPower);
}
}
// ゲームの例
const warrior = new Warrior("勇者");
const mage = new Mage("魔法使い");
console.log(warrior.getStatus()); // 勇者 Lv.1 HP:120/120 MP:30/30
console.log(mage.getStatus()); // 魔法使い Lv.1 HP:80/80 MP:100/100
warrior.attack(mage); // 勇者が魔法使いを攻撃!魔法使いが25のダメージを受けました(残りHP: 55)
mage.fireball(warrior); // 魔法使いがファイアボールを詠唱!勇者が30のダメージを受けました(残りHP: 90)
warrior.heal(); // 勇者がヒールを詠唱!勇者が25回復しました(現在HP: 115)
warrior.attack(mage); // 勇者が魔法使いを攻撃!魔法使いが25のダメージを受けました(残りHP: 30)
mage.fireball(warrior); // 魔法使いがファイアボールを詠唱!勇者が30のダメージを受けました(残りHP: 85)
warrior.powerAttack(mage); // 勇者がパワーアタック!魔法使いが37のダメージを受けました(残りHP: 0)
// 魔法使いが倒れました
warrior.gainExperience(150); // 勇者がレベル2に上がりました!
console.log(warrior.getStatus()); // 勇者 Lv.2 HP:140/140 MP:40/40
復習
基本問題
1. 以下のクラスに足りない部分を補完し、正しく動作させる
class Rectangle {
constructor(/* 引数を追加 */) {
// プロパティを設定
}
// 面積を計算するメソッド
// 周囲を計算するメソッド
// 正方形かどうかを判定するメソッド
}
// テスト用コード
const rect = new Rectangle(5, 3);
console.log(rect.getArea()); // 15
console.log(rect.getPerimeter()); // 16
console.log(rect.isSquare()); // false
const square = new Rectangle(4, 4);
console.log(square.isSquare()); // true
2. 次のコードの実行結果を、理由を考えながら予想する
class Parent {
constructor(name) {
this.name = name;
}
greet() {
return `こんにちは、${this.name}です`;
}
}
class Child extends Parent {
constructor(name, age) {
super(name);
this.age = age;
}
greet() {
return `${super.greet()}。${this.age}歳です`;
}
}
const child = new Child("太郎", 10);
console.log(child.greet());
console.log(child instanceof Parent);
console.log(child instanceof Child);
3. staticメソッドとインスタンスメソッドの使い分けを考える
以下のクラスで、どのメソッドをstaticにすべきか、理由を考えながら判断してください。
class Temperature {
constructor(celsius) {
this.celsius = celsius;
}
// 摂氏を華氏に変換
toFahrenheit() {
return this.celsius * 9/5 + 32;
}
// 摂氏から華氏への変換(汎用関数)
celsiusToFahrenheit(celsius) {
return celsius * 9/5 + 32;
}
// 華氏から摂氏への変換(汎用関数)
fahrenheitToCelsius(fahrenheit) {
return (fahrenheit - 32) * 5/9;
}
// 温度が氷点下かどうか
isFreezing() {
return this.celsius < 0;
}
}
実践問題
4. 図書管理システムを作成する
以下の仕様に従って、図書管理システムのクラスを作成してください。
Book クラス
- プロパティ:title, author, isbn, isAvailable
- メソッド:borrow(), return(), getInfo()
Library クラス
- プロパティ:books(配列), name
- メソッド:addBook(), findBookByIsbn(), listAvailableBooks(), borrowBook()
// 使用例
const library = new Library("中央図書館");
const book1 = new Book("JavaScript入門", "山田太郎", "978-1234567890");
const book2 = new Book("Python基礎", "佐藤花子", "978-0987654321");
library.addBook(book1);
library.addBook(book2);
console.log(library.listAvailableBooks());
library.borrowBook("978-1234567890");
console.log(library.listAvailableBooks());
5. 継承を使った動物園管理システム
基底クラス Animal
と、それを継承した Mammal
, Bird
クラスを作成し、さらに具体的な動物クラス(Lion
, Eagle
など)を作成してください。各クラスには適切なプロパティとメソッドを追加してください。
解答例
問題1
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
getPerimeter() {
return 2 * (this.width + this.height);
}
isSquare() {
return this.width === this.height;
}
}
問題2
// 実行結果:
// "こんにちは、太郎です。10歳です"
// true
// true
// 理由:
// 1. Child.greet()がParent.greet()をオーバーライドしている
// 2. super.greet()で親クラスのメソッドを呼び出し、その結果に年齢を追加
// 3. childはChildクラスのインスタンスであり、Parentクラスからもinstanceofでtrue
問題3
class Temperature {
constructor(celsius) {
this.celsius = celsius;
}
// インスタンスメソッド - このインスタンスの温度を変換
toFahrenheit() {
return this.celsius * 9/5 + 32;
}
// staticメソッド - 汎用的な変換機能
static celsiusToFahrenheit(celsius) {
return celsius * 9/5 + 32;
}
static fahrenheitToCelsius(fahrenheit) {
return (fahrenheit - 32) * 5/9;
}
// インスタンスメソッド - このインスタンスの状態をチェック
isFreezing() {
return this.celsius < 0;
}
}
問題4
class Book {
constructor(title, author, isbn) {
this.title = title;
this.author = author;
this.isbn = isbn;
this.isAvailable = true;; // 初期状態は貸出可能
}
borrow() {
if (this.isAvailable) {
this.isAvailable = false;
return true; // 貸出成功
}
return false; // 貸出不可
}
return() {
this.isAvailable = true;
}
getInfo() {
const status = this.isAvailable ? "貸出可能" : "貸出中";
return `タイトル: ${this.title}, 著者: ${this.author}, ISBN: ${this.isbn}, 貸出可能: ${status}`;
}
}
class Library {
constructor(name) {
this.name = name;
this.books = [];
}
addBook(book) {
this.books.push(book);
}
findBookByIsbn(isbn) {
return this.books.find(book => book.isbn === isbn);
}
listAvailableBooks() {
return this.books
.filter(book => book.isAvailable)
.map(book => book.getInfo());
}
borrowBook(isbn) {
const book = this.findBookByIsbn(isbn);
if (book && book.borrow()) {
console.log(`${book.title}を貸し出しました`);
return true;
}
console.log("貸出できませんでした");
return false;
}
}
問題5
class Animal {
constructor(name, species, age) {
this.name = name;
this.species = species;
this.age = age;
this.energy = 100;
}
eat() {
console.log(`${this.name}が食事をしています`);
this.energy += 20;
}
sleep() {
console.log(`${this.name}が眠っています`);
this.energy += 30;
}
move() {
console.log(`${this.name}が移動しています`);
this.energy -= 10;
}
}
class Mammal extends Animal {
constructor(name, species, age, furColor) {
super(name, species, age);
this.furColor = furColor;
this.bodyTemperature = 37; // 恒温動物
}
giveBirth() {
console.log(`${this.name}が出産しました`);
}
}
class Bird extends Animal {
constructor(name, species, age, wingspan) {
super(name, species, age);
this.wingspan = wingspan;
this.canFly = true;
}
fly() {
if (this.canFly) {
console.log(`${this.name}が飛んでいます`);
this.energy -= 15;
}
}
buildNest() {
console.log(`${this.name}が巣を作っています`);
}
}
class Lion extends Mammal {
constructor(name, age) {
super(name, "ライオン", age, "金茶色");
this.pride = null; // 群れ
}
roar() {
console.log(`${this.name}が吠えています:ガオー!`);
}
hunt() {
console.log(`${this.name}が狩りをしています`);
this.energy -= 25;
}
}
class Eagle extends Bird {
constructor(name, age) {
super(name, "イヌワシ", age, 200);
this.sharpVision = true;
}
dive() {
console.log(`${this.name}が急降下しています`);
this.energy -= 20;
}
catchPrey() {
console.log(`${this.name}が獲物を捕まえました`);
}
}
// 使用例
const lion = new Lion("シンバ", 5);
const eagle = new Eagle("アクィラ", 3);
lion.roar();
lion.hunt();
console.log(`${lion.name}のエネルギー: ${lion.energy}`);
eagle.fly();
eagle.dive();
console.log(`${eagle.name}のエネルギー: ${eagle.energy}`);
まとめ
JavaScriptのクラス構文とオブジェクト指向プログラミングについて、基本的な概念から実践的な活用方法まで紹介しました。
技術的メリット
-
コードの再利用性
クラスを使うことで、似たような機能を持つオブジェクトを効率的に作成できる -
保守性の向上
関連するデータとメソッドをまとめることで、コードの理解と修正が容易になる -
継承による拡張性
既存のクラスをもとに新しい機能を追加する柔軟性を持たせられる -
カプセル化
データと操作をまとめることで、安全で使いやすいインターフェースを提供できる
実践的な活用場面
-
データモデル
ユーザー, 商品, 注文などのビジネスロジックを表現する -
UI コンポーネント
再利用可能な画面部品を作成する -
ゲーム開発
キャラクター、アイテム、ステージなどを管理する -
ライブラリ開発
機能をクラスとして提供し、利用者が拡張可能な設計を実現する
注意すべきポイント
クラス構文は内部的にプロトタイプベースの仕組みを使用しているため、JavaScriptの基本的な動作原理を理解しておくことが重要です。また、すべてをクラスで解決する必要はなく、シンプルな処理には関数やオブジェクトリテラルを使う方が適切な場合もあります。
継承の使用は慎重に行ない、「is-a関係」が明確な場合にのみ使用することをお勧めします。複雑な継承階層は保守性を低下させる可能性があるため、コンポジション(has-a関係)を考慮することも重要です。
次回は、JavaScriptの組み込みオブジェクトとユーティリティ機能について、よく使用される便利なメソッドや機能を中心に紹介します。