このカリキュラムでは、JavaScriptでオブジェクト指向プログラミング(OOP)の基礎から応用までを学びます。
クラスやオブジェクトの概念、継承、カプセル化、多態性など、OOPの重要な概念を実践的に習得し、小規模なアプリを設計・実装できる力を養います。
1. オブジェクトとクラスの概念
オブジェクトとは何か?
オブジェクトは、データ(プロパティ)と操作(メソッド)を一つにまとめたエンティティです。JavaScriptでは、オブジェクトはキーと値のペアで構成されています。
特徴:
- プロパティ:
オブジェクトの特徴を表すデータ。
例: 名前、年齢、色など。 - メソッド:
オブジェクトに関連する動作や操作を表す関数。
例: 車が動く(start)、停止する(stop)。
サンプルコード:
// オブジェクトの例
const car = {
make: 'Toyota', // プロパティ
model: 'Corolla',
start: function() { // メソッド
console.log('The car is starting.');
}
};
// プロパティにアクセス
console.log(car.make); // Output: Toyota
// メソッドを呼び出す
car.start(); // Output: The car is starting.
クラスとオブジェクトの違い
クラスはオブジェクトの設計図であり、オブジェクトはその設計図をもとに作成された実体です。
- クラス:
• オブジェクトの構造(プロパティやメソッド)を定義します。
• 再利用可能で一貫性のあるコードを作成できます。 - オブジェクト:
• クラスをインスタンス化(実体化)して作られるものです。
• 実際のデータを持つ具現化されたものです。
サンプルコード(クラスとオブジェクト):
// クラスの定義
class Car {
constructor(make, model) { // コンストラクタ
this.make = make;
this.model = model;
}
start() { // メソッド
console.log(`${this.make} ${this.model} is starting.`);
}
}
// クラスからオブジェクトを作成
const car1 = new Car('Toyota', 'Corolla'); // インスタンス
car1.start(); // Output: Toyota Corolla is starting.
const car2 = new Car('Honda', 'Civic');
car2.start(); // Output: Honda Civic is starting.
リアルワールドのオブジェクトをプログラムに抽象化する方法
プログラミングでオブジェクトを作成する際、現実世界の対象を「抽象化」します。
抽象化とは、対象の本質的な特徴だけを取り出し、それ以外の詳細を省略することです。
抽象化のプロセス:
- 対象を特定する:
何を表現するか決めます(例: 車)。 - 特徴を抽出する:
対象の重要な属性を選びます。
• プロパティ:
メーカー、モデル、色、価格など。
• メソッド:
動作や振る舞い(例: 動く、止まる)。 - 構造化する:
クラスを使ってプロパティとメソッドを整理します。
例: 動物を抽象化する
現実世界の「動物」をプログラムで表現します。
// 動物クラスの定義
class Animal {
constructor(name, species) {
this.name = name; // 名前
this.species = species; // 種類
}
makeSound() {
console.log(`${this.name} is making a sound.`);
}
}
// 動物オブジェクトの作成
const dog = new Animal('Dog', 'Canine');
const cat = new Animal('Cat', 'Feline');
// メソッドの使用
dog.makeSound(); // Output: Dog is making a sound.
cat.makeSound(); // Output: Cat is making a sound.
2. ES6クラスの基本
ES6クラスの特徴
ES6クラスは、JavaScriptにおけるオブジェクト指向プログラミングのための構文的な糖衣(syntactic sugar)です。
プロトタイプをベースにした構造を内部で使用していますが、より簡潔で読みやすい記述を可能にします。
主な特徴
- クラス構文の簡潔さ
• プロトタイプベースの記述に比べ、コードがシンプルでわかりやすい。
• クラス定義とそのメソッドを1か所で記述可能。 - constructorメソッド
• クラスのインスタンスを初期化するための特別なメソッド。
• プロパティの初期化が容易にできる。 - extendsキーワードによる継承
• クラス間の継承を簡単に実現。
• サブクラスが親クラスのプロパティやメソッドを利用できる。 - 静的メソッド(static)のサポート
• インスタンスではなく、クラス自体に紐づくメソッドを簡単に定義可能。
ES6クラスの特徴
ES6クラスは、JavaScriptにおけるオブジェクト指向プログラミングのための構文的な糖衣(syntactic sugar)です。プロトタイプをベースにした構造を内部で使用していますが、より簡潔で読みやすい記述を可能にします。
主な特徴
- クラス構文の簡潔さ
• プロトタイプベースの記述に比べ、コードがシンプルでわかりやすい。
• クラス定義とそのメソッドを1か所で記述可能。 - constructorメソッド
• クラスのインスタンスを初期化するための特別なメソッド。
• プロパティの初期化が容易にできる。 - extendsキーワードによる継承
• クラス間の継承を簡単に実現。
• サブクラスが親クラスのプロパティやメソッドを利用できる。 - 静的メソッド(static)のサポート
• インスタンスではなく、クラス自体に紐づくメソッドを簡単に定義可能。
サンプルコード:
// クラスの基本構造
class Animal {
constructor(name, species) {
this.name = name; // プロパティの定義
this.species = species;
}
// インスタンスメソッド
makeSound() {
console.log(`${this.name} makes a sound.`);
}
}
// クラスのインスタンス化
const dog = new Animal('Dog', 'Canine');
dog.makeSound(); // Output: Dog makes a sound.
class構文 vs プロトタイプ
JavaScriptはもともとプロトタイプベースの言語ですが、ES6クラス構文の導入により、オブジェクト指向プログラミングの記述が洗練されました。ここでは、両者の違いを比較します。
プロトタイプの特徴:
- 記述が冗長:
プロパティやメソッドを別々に定義する必要がある。 - 可読性が低い:
記述が複雑になりやすく、大規模なコードでは混乱しやすい。
// プロトタイプベースの記述
function Animal(name, species) {
this.name = name;
this.species = species;
}
Animal.prototype.makeSound = function() {
console.log(`${this.name} makes a sound.`);
};
// インスタンス作成
const cat = new Animal('Cat', 'Feline');
cat.makeSound(); // Output: Cat makes a sound.
class構文の特徴:
- 簡潔でわかりやすい:
メソッドやプロパティを1か所で定義可能。 - 継承が容易:
extendsを使用して親クラスを継承できる。
// ES6クラス構文
class Animal {
constructor(name, species) {
this.name = name;
this.species = species;
}
makeSound() {
console.log(`${this.name} makes a sound.`);
}
}
const bird = new Animal('Bird', 'Avian');
bird.makeSound(); // Output: Bird makes a sound.
静的メソッドと動的メソッドの違い
動的メソッド:
- クラスのインスタンスに関連付けられ、インスタンスごとに動作します。
- インスタンスを作成しないと呼び出せません。
class MathOperations {
multiply(a, b) {
return a * b;
}
}
const math = new MathOperations();
console.log(math.multiply(2, 3)); // Output: 6
静的メソッド:
- クラス自体に紐づけられ、インスタンスを作成せずに呼び出せます。
- 通常、ユーティリティ関数やグローバルな動作を定義するのに適しています。
class MathOperations {
static multiply(a, b) {
return a * b;
}
}
// インスタンス化不要
console.log(MathOperations.multiply(2, 3)); // Output: 6
3. クラスの作成 (class文)
クラスの基本構造
JavaScriptのクラスは、classキーワードを使って定義します。
クラスの中には以下の要素を含めることができます。
- constructorメソッド: クラスのインスタンスを作成する際に実行される特別なメソッド。
- インスタンスメソッド: クラスのインスタンスで呼び出すメソッド。
- 静的メソッド(必要に応じて): クラス自体で呼び出すメソッド。
基本構造の例:
class Person {
constructor(name, age) { // コンストラクタ
this.name = name; // プロパティを初期化
this.age = age;
}
greet() { // インスタンスメソッド
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
}
インスタンス化とプロパティの初期化
インスタンス化とは、クラスの設計図からオブジェクトを作成することを指します。
newキーワードを使い、コンストラクタに引数を渡してインスタンスを初期化します。
インスタンス化の例:
const person1 = new Person('Alice', 30);
const person2 = new Person('Bob', 25);
// プロパティにアクセス
console.log(person1.name); // Output: Alice
console.log(person2.age); // Output: 25
メソッドの定義と呼び出し
クラス内で定義されたメソッドは、インスタンスを通じて呼び出します。
メソッドには以下のような種類があります。
1. インスタンスメソッド
- クラスのインスタンスごとに動作するメソッド。
- クラスの内部プロパティ(thisを使う)にアクセス可能。
// メソッドの呼び出し
person1.greet(); // Output: Hello, my name is Alice and I am 30 years old.
2. 静的メソッド
- インスタンスを必要とせず、クラス自体に関連付けられる。
- 通常、ユーティリティ関数として利用。
class MathUtil {
static add(a, b) {
return a + b;
}
}
// 静的メソッドの呼び出し
console.log(MathUtil.add(2, 3)); // Output: 5
4. コンストラクタとメソッド
1. constructorメソッドの役割
constructorメソッドは、クラスのインスタンスを初期化するための特別なメソッドです。
クラスをインスタンス化する際に自動的に呼び出されます。
主な役割:
- 初期プロパティの設定: インスタンスのプロパティを定義し、初期値を設定します。
- 初期化ロジックの実行: 必要に応じて、インスタンス生成時のカスタム処理を実行します。
基本構文:
class ClassName {
constructor(引数1, 引数2, ...) {
// プロパティの初期化
this.property1 = 引数1;
this.property2 = 引数2;
}
}
2. インスタンスメソッドと静的メソッド
インスタンスメソッド:
- クラスのインスタンスごとに動作し、thisを使用してインスタンスのプロパティや他のメソッドにアクセスできます。
- インスタンスを作成しないと呼び出せません。
静的メソッド:
- クラス自体に紐づけられるメソッドで、インスタンスを作成しなくても呼び出せます。
- インスタンスのプロパティにはアクセスできませんが、クラスレベルで共有される機能(ユーティリティ関数など)を提供します。
- 定義時にstaticキーワードを使用します。
3. getterとsetterでプロパティを管理する方法
getterとsetterは、クラスのプロパティに対するアクセスや変更を制御するために使用される特別なメソッドです。
getterの役割:
- プロパティの値を取得する際に特定の処理を追加できます。
- プロパティを取得する際に、関数呼び出しのように見えない記述が可能です。
setterの役割:
- プロパティの値を設定する際に特定の処理を追加できます。
- 入力値のバリデーションなどを実装可能です。
メリット:
- プロパティの直接アクセスを制限し、必要に応じて追加ロジックを挿入可能。
- クラスの利用者にはシンプルなインターフェイスを提供。
5. 継承とプロトタイプチェーン
1. extendsキーワードによる継承
extendsキーワードを使用すると、あるクラス(親クラス)を基に新しいクラス(サブクラス)を作成できます。
これにより、親クラスのプロパティやメソッドをそのまま活用しつつ、サブクラス独自の機能を追加できます。
基本構文:
class ParentClass {
// 親クラスの定義
}
class ChildClass extends ParentClass {
// サブクラスの定義
}
2. サブクラスの定義と親クラスからのプロパティ・メソッドの継承
親クラスからの継承の流れ:
- サブクラスは、親クラスのプロパティとメソッドをそのまま受け継ぎます。
- サブクラスは、新しいプロパティやメソッドを追加できます。
- 必要に応じて、親クラスのメソッドをオーバーライドしてカスタマイズできます。
例: オーバーライド:
class Animal {
constructor(name) {
this.name = name;
}
makeSound() {
console.log(`${this.name}は音を出します。`);
}
}
class Cat extends Animal {
makeSound() {
console.log(`${this.name}はニャーと鳴きます。`); // オーバーライド
}
}
const myCat = new Cat('ミケ');
myCat.makeSound(); // Output: ミケはニャーと鳴きます。
サブクラスにメソッドを追加:
class Fish extends Animal {
swim() {
console.log(`${this.name}は泳ぎます。`);
}
}
const myFish = new Fish('金魚');
myFish.makeSound(); // Output: 金魚は音を出します。
myFish.swim(); // Output: 金魚は泳ぎます。
3. プロトタイプチェーンの詳細と挙動
JavaScriptでは、すべてのオブジェクトは内部的にプロトタイプ(prototype)と呼ばれるもう一つのオブジェクトを参照します。
この仕組みを利用して継承が行われます。
プロトタイプチェーンとは?
- オブジェクトのプロパティやメソッドを参照する際、最初にそのオブジェクト自体を検索します。
- 該当がなければ、そのオブジェクトのプロトタイプ(親オブジェクト)を検索します。
- この探索の連鎖を「プロトタイプチェーン」と呼びます。
例: プロトタイプチェーンの動作:
class A {
methodA() {
console.log('Aのメソッド');
}
}
class B extends A {
methodB() {
console.log('Bのメソッド');
}
}
const b = new B();
b.methodB(); // Output: Bのメソッド
b.methodA(); // Output: Aのメソッド(親クラスから継承)
ここでb.methodA()が呼び出されると、インスタンスbが直接methodAを持たないため、親クラスAのプロトタイプを参照します。
プロトタイプチェーンを確認:
console.log(Object.getPrototypeOf(b)); // Bのプロトタイプ(A)
console.log(Object.getPrototypeOf(Object.getPrototypeOf(b))); // Aのプロトタイプ(Object)
6. カプセル化、継承、多態性の実践
1. カプセル化の概念と#記法(プライベートフィールド)
カプセル化とは?
カプセル化は、オブジェクトの内部状態(プロパティ)を隠し、外部からの直接アクセスを制御する概念です。
これにより、不正な値の設定や、オブジェクト内部のデータが誤って変更されることを防ぎます。
プライベートフィールド(#記法)
JavaScriptでは、プライベートフィールドを定義するために#記法が使われます。これにより、クラス外部から直接アクセスできない変数を定義できます。
例: プライベートフィールドを使った銀行口座クラス
class BankAccount {
#balance; // プライベートフィールド
constructor(owner, initialBalance) {
this.owner = owner;
this.#balance = initialBalance; // プライベートフィールドを初期化
}
// プライベートフィールドにアクセスするためのgetter
getBalance() {
return this.#balance;
}
// プライベートフィールドを変更するためのsetter
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
console.log(`${amount}円を預け入れました。`);
} else {
console.log('無効な金額です。');
}
}
withdraw(amount) {
if (amount > this.#balance) {
console.log('残高不足です。');
} else {
this.#balance -= amount;
console.log(`${amount}円を引き出しました。`);
}
}
}
// 使用例
const myAccount = new BankAccount('田中太郎', 5000);
console.log(myAccount.getBalance()); // Output: 5000
myAccount.deposit(2000); // Output: 2000円を預け入れました。
myAccount.withdraw(1000); // Output: 1000円を引き出しました。
console.log(myAccount.#balance); // Error: プライベートフィールドには直接アクセスできません
2. 継承の活用例(親クラスを拡張する子クラス)
継承は、親クラスのプロパティやメソッドを子クラスが受け継ぐ仕組みです。
これにより、コードの再利用性が向上し、共通の機能を親クラスにまとめることができます。
例: 図形クラスを拡張する継承の活用
// 親クラス
class Shape {
constructor(name) {
this.name = name;
}
draw() {
console.log(`${this.name}を描画します。`);
}
}
// 子クラス: Circle
class Circle extends Shape {
constructor(radius) {
super('円'); // 親クラスのコンストラクタを呼び出す
this.radius = radius;
}
calculateArea() {
return Math.PI * this.radius ** 2;
}
}
// 子クラス: Rectangle
class Rectangle extends Shape {
constructor(width, height) {
super('四角形');
this.width = width;
this.height = height;
}
calculateArea() {
return this.width * this.height;
}
}
// 使用例
const circle = new Circle(5);
circle.draw(); // Output: 円を描画します。
console.log(`面積: ${circle.calculateArea()}`); // Output: 面積: 78.53981633974483
const rectangle = new Rectangle(4, 7);
rectangle.draw(); // Output: 四角形を描画します。
console.log(`面積: ${rectangle.calculateArea()}`); // Output: 面積: 28
3. 多態性の実装例(オーバーライド、抽象化)
多態性とは?
多態性(ポリモーフィズム)とは、同じインターフェース(メソッド名やプロパティ名)で異なる動作を実現する仕組みです。主に以下の方法で実現されます。
- オーバーライド:
親クラスのメソッドを子クラスで上書き。 - 抽象化:
共通のインターフェースを定義し、具体的な動作はサブクラスに任せる。
オーバーライドの例
親クラスのメソッドを子クラスでカスタマイズして動作を変更します。
class Animal {
speak() {
console.log('動物が音を出します。');
}
}
class Dog extends Animal {
speak() {
console.log('犬がワンワンと吠えます。'); // オーバーライド
}
}
class Cat extends Animal {
speak() {
console.log('猫がニャーと鳴きます。'); // オーバーライド
}
}
// 使用例
const animals = [new Dog(), new Cat()];
animals.forEach(animal => animal.speak());
// Output:
// 犬がワンワンと吠えます。
// 猫がニャーと鳴きます。
抽象化の例
抽象化は、共通のインターフェースを定義し、具体的な実装はサブクラスに委ねる設計パターンです。
JavaScriptでは、厳密な抽象クラスはありませんが、抽象的な親クラスを作成することで実現できます。
class Animal {
constructor(name) {
if (this.constructor === Animal) {
throw new Error('このクラスは直接インスタンス化できません。');
}
this.name = name;
}
speak() {
throw new Error('speakメソッドはサブクラスで実装してください。');
}
}
class Dog extends Animal {
speak() {
console.log(`${this.name}がワンワンと吠えます。`);
}
}
class Cat extends Animal {
speak() {
console.log(`${this.name}がニャーと鳴きます。`);
}
}
// 使用例
const dog = new Dog('ポチ');
dog.speak(); // Output: ポチがワンワンと吠えます。
const cat = new Cat('ミケ');
cat.speak(); // Output: ミケがニャーと鳴きます。
まとめ
オブジェクト指向プログラミング(OOP)は、複雑なプログラムを効率的に設計・管理するための考え方です。
本カリキュラムでは、OOPの核となる概念であるカプセル化、継承、多態性をJavaScriptで実践する方法を学びました。
OOPを活用することで、データとロジックを適切に分離し、再利用可能で保守性の高いコードを作成できます。
特に、カプセル化によるデータの保護、継承によるコードの再利用、多態性を使った柔軟な動作の定義は、効率的なプログラム開発に不可欠なスキルです。
これらの技術を駆使すれば、小規模なアプリから大規模なシステムまで、さまざまな規模の開発に対応できるでしょう。
OOPの考え方は、単なるプログラミング手法にとどまらず、設計力や問題解決能力を養う土台となります。
今回学んだ内容をさらに深め、実践を重ねることで、応用力の高いエンジニアリングスキルを身につけることができます。