はじめに
こんにちは!!
現在、京都のWeb制作会社でフロントエンジニアに従事しております。
Web系の開発会社に転職するため、React×TypeScriptを学習しています。
今回、JavaScriptにおけるオブジェクト指向について学んだことの備忘録を残したいと思い、記事を書くことにしました。
なお、本記事の作成にあたって
JavaScript本格入門
を参考にしています。
この記事の読者対象
- JavaScriptの基礎学習を終えている方
- JavaやC言語など多言語でオブジェクト指向を学んだことがある方
- JavaScript特有のプロトタイプベースのオブジェクト指向がよく分からないという方
尚、今回オブジェクト指向について詳しくは書かないのでご了承ください。
開発環境
mac OS: Ventura13
エディタ: Visual Studio Code(またはお使いのエディタ)
ブラウザ: Chrome 112.0.5615.137(Official Build)(x86_64)
Node.js: v18
オブジェクト指向の種類
クラスベースのオブジェクト指向
クラスベースのオブジェクト指向は、多くのプログラミング言語(Java、C++、C#など)で採用されているアプローチです。クラスベースのオブジェクト指向では、以下の特徴があります。
- クラス: オブジェクトの設計図としての役割を持ち、データ(プロパティ)と振る舞い(メソッド)を定義します。
- インスタンス: クラスから生成されるオブジェクトで、クラスで定義されたプロパティとメソッドを持ちます。
- 継承: 既存のクラスから新たなクラスを派生させることができ、親クラスのプロパティやメソッドを子クラスが引き継ぎます。
プロトタイプベースのオブジェクト指向
プロトタイプベースのオブジェクト指向は、JavaScriptで採用されているアプローチで、以下の特徴があります。
- プロトタイプ: オブジェクトが他のオブジェクトからプロパティやメソッドを継承するための仕組みです。プロトタイプはオブジェクト間の暗黙的なリンク(プロトタイプチェーン)を作ります。
- インスタンス: コンストラクタ関数を使って生成されるオブジェクトで、他のオブジェクトのプロトタイプを継承します。
- 継承: オブジェクトが他のオブジェクトのプロトタイプを継承し、そのプロパティやメソッドを利用できるようになります。
クラスベースのオブジェクト指向は抽象クラスから具象オブジェクトを生成するのに対して、プロトタイプベースでは抽象クラスが存在せず実態のあるオブジェクトがオブジェクトを直接継承します。その、継承元になったオブジェクトをプロトタイプと言います。
プロトタイプベースのオブジェクト指向では、同じプロトタイプを元に作成されたオブジェクトは同じプロパティとメソッドを共有します。しかし、プロパティとメソッドを直接オブジェクトに定義することができるため、同じプロトタイプを元にしていても、個々のオブジェクトが持つプロパティやメソッドは異なることがあります。
そのため、「より縛りの弱いクラス」と表現されます。これにより、動的なオブジェクト指向を実現することができます。ただし、この特性はコードの可読性や保守性を損なう可能性があるため、慎重に使う必要があります。
プロトタイプオブジェクトを利用するメリット
1. メモリの使用量を節減できる
プロトタイプオブジェクトは、オブジェクト間で共有されるため、メモリの使用量を節約できます。それぞれのインスタンスが独自のメソッドを持つ代わりに、すべてのインスタンスが同じプロトタイプオブジェクト上のメソッドを共有することができます。
例えば、プロトタイプベースのオブジェクト指向で、あるクラスに1000個のインスタンスがある場合、それぞれのインスタンスが同じメソッドを持っていると、メモリ上に1000個の同じメソッドが存在します。しかし、プロトタイプオブジェクトを使用すると、すべてのインスタンスは同じプロトタイプオブジェクト上のメソッドを共有するため、メモリ上には1つのメソッドしか存在しません。
2. メンバーの追加や変更をインスタンスがリアルタイムに認識できる
プロトタイプオブジェクトを使用すると、メンバー(メソッドやプロパティ)の追加や変更がインスタンスにリアルタイムで反映されます。これは、すべてのインスタンスが同じプロトタイプオブジェクトを参照しているためです。
例えば、既存のインスタンスに新しいメソッドが必要になった場合、プロトタイプオブジェクトにそのメソッドを追加するだけで、すべてのインスタンスがそのメソッドを使用できるようになります。これにより、メンバーの追加や変更が容易になり、コードのメンテナンス性が向上します。
また、プロトタイプオブジェクトを利用することで、継承やオブジェクトの拡張が容易になります。プロトタイプチェーンを通じてオブジェクト間でプロパティやメソッドを共有できるため、コードの再利用性が向上し、開発効率が向上します。
JavaScriptのES2015以降の最大の変更点
ES2015以前とそれ以降ではオブジェクト指向構文はclass構文が導入されたことで、大きく変化しました。
ES2015以前のオブジェクト指向
ES2015以前のJavaScriptでは、オブジェクト指向の機能はプロトタイプベースで提供されていました。以下は、その例です。
// コンストラクタ関数
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
// メソッドをプロトタイプに追加
Person.prototype.getName = function() {
return this.lastName + ' ' + this.firstName;
}
// インスタンス生成
var person1 = new Person('Taro', 'Yamada');
console.log(person1.getName());
ES2015以前は、プロトタイプベースのオブジェクト指向言語でした。クラスを作成するにはコンストラクタ関数を定義し、その関数のprototypeプロパティにメソッドを定義することで、オブジェクト指向的なコードを記述していました。
ES2015以降のオブジェクト指向
ES2015では、classキーワードが導入され、より簡潔で分かりやすいオブジェクト指向の構文が提供されるようになりました。
// クラス定義
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
// メソッド定義
getName() {
return this.lastName + ' ' + this.firstName;
}
}
// インスタンス生成
const person1 = new Person('Taro', 'Yamada');
console.log(person1.getName());
ES2015以降のオブジェクト指向構文は、ES2015以前のプロトタイプベースの構文を裏で利用していますが、classキーワードを使ってよりわかりやすい形で表現できます。また、class構文では継承やアクセサメソッドなどの機能が簡潔に表現できるようになっています。
また、letやconstなどの新しい変数宣言キーワード、アロー関数などの新しい構文が追加され、開発の効率性が向上しました。
アクセサーメソッド
アクセサーメソッドは、JavaScriptにおけるオブジェクト指向のプロパティアクセス方法の一つです。オブジェクトのプロパティに直接アクセスするのではなく、アクセサーメソッドを通してプロパティにアクセスします。
参照用のメソッドをゲッターメソッド、設定用のメソッドをセッターメソッドという場合もあります。
Getterは、プロパティ値を取得するためのメソッドです。オブジェクトのプロパティを読み込むと、自動的にGetterメソッドが呼び出され、その戻り値が返されます。
Setterは、プロパティ値を設定するためのメソッドです。オブジェクトのプロパティに値を代入すると、自動的にSetterメソッドが呼び出され、引数の値がプロパティに設定されます。
SetterとGetterを使用することで、オブジェクトのプロパティの値を制御することができます。例えば、プロパティの値が不正な場合にはエラーをスローするように設定することができます。また、プロパティの値が変更された場合には、自動的に他のプロパティの値を更新するようにすることもできます。
以下のコードは、クラスを定義し、SetterとGetterを用いて、firstNameとlastNameというプロパティを設定しています。Setterはプロパティに値を設定する際に使用され、Getterはプロパティの値を取得する際に使用されます。また、コンストラクターとメソッドを定義し、getNameメソッドでオブジェクトの名前を取得しています。最後に、インスタンスを作成し、getNameメソッドを呼び出しています。ただし、引数に空文字列や文字列以外が渡された場合、エラーがスローされます。
class Member {
// コンストラクター
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
// firstNameプロパティ
get firstName() {
return this._firstName;
}
set firstName(value) {
if (typeof value === 'string' && value.length > 0) {
this._firstName = value;
} else {
throw new Error('firstName must be a non-empty string');
}
}
// lastNameプロパティ
get lastName() {
return this._lastName;
}
set lastName(value) {
if (typeof value === 'string' && value.length > 0) {
this._lastName = value;
} else {
throw new Error('lastName must be a non-empty string');
}
}
// メソッド
getName() {
return this.lastName + ' ' + this.firstName;
}
}
let m = new Member('Takeshi', 'Arihori');
/* let m = new Member(123, ''); // 空文字または文字列以外ならエラーをスローする */
console.log(m.getName());
継承 ~プロトタイプチェーン~
このコードは、Member という親クラスと BusinessMember という子クラスを定義し、BusinessMember クラスが Member クラスを継承しています。
class Member{
// コンストラクター
// javaScriptのクラスは全てがpublic
constructor(firstName, lastName){
this.firstName = firstName;
this.lastName = lastName;
}
// メソッド
getName(){
return this.lastName + ' ' + this.firstName;
}
}
class BusinessMember extends Member{
work(){
return this.getName() + 'は働いています!!';
}
}
let bm = new BusinessMember('Taro', 'Yamada');
console.log(bm.getName()); // "Yamada Taro" が出力される
console.log(bm.work()); // "Taro Yamadaは働いています!!" が出力される
Member クラスには、firstName と lastName を受け取り、それを利用して名前を返す getName メソッドが定義されています。
BusinessMember クラスは Member クラスを継承しており、work メソッドが定義されています。work メソッドは getName メソッドを呼び出し、それに「は働いています!!」という文字列を追加して返します。
最後に、BusinessMember クラスのインスタンス bm を生成し、getName メソッドと work メソッドを呼び出しています。
これによって、BusinessMember クラスは Member クラスのメソッドを継承して利用しており、また、BusinessMember クラス自身が定義した work メソッドを呼び出すこともできます。
このように、JavaScriptのクラスでは継承がプロトタイプチェーンによって行われるため、親クラスのメソッドを呼び出した場合でも、自身で定義したメソッドが呼び出されることがわかります。
継承関係は動的に変更が可能
この例では、DogクラスがAnimalクラスを継承しています。Dogクラスのインスタンスdは、barks()メソッドを持ちます。しかし、プロトタイプオブジェクトを変更することで、barks()メソッドの振る舞いをhowls()メソッドに変更しました。そのため、d.speak()を呼び出すと、"Mitzie howls."という出力がされます。
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(this.name + " makes a noise.");
}
}
class Dog extends Animal {
speak() {
console.log(this.name + " barks.");
}
}
let d = new Dog("Mitzie");
d.speak(); // => "Mitzie barks."
// プロトタイプオブジェクトを変更することで、Dogクラスのインスタンスdの振る舞いを変更できる
Dog.prototype.speak = function() {
console.log(this.name + " howls.");
}
d.speak(); // => "Mitzie howls."
クラスベースのオブジェクト指向言語においては、継承関係は静的に定義されます。そのため、継承関係を動的に変更することはできません。
一方、プロトタイプベースのオブジェクト指向言語においては、継承関係は動的に変更することができます。プロトタイプチェーンによってオブジェクト同士の関連性が定義されるため、オブジェクトを動的に変更することで、そのオブジェクトを継承した新たなオブジェクトを生成することもできます。
このように、プロトタイプオブジェクトを動的に変更することで、既存のインスタンスに対しても影響を与えることができます。
ただし、このような動的な変更は、コードの保守性を下げることにもつながります。適切に使い分けることが重要です。