同一の処理を行う場合、2つの異なる関数インスタンスを定義する必要はありません。さらに、thisオブジェクトを使用することで、関数とオブジェクトの結びつきを実行時に遅延させることが可能です。
この問題を解決するには、関数定義をコンストラクタの外部に移動します:
function createPerson(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.displayName = displayName;
}
function displayName() {
console.log(this.name);
}
let employee1 = new createPerson("田中", 35, "エンジニア");
let employee2 = new createPerson("佐藤", 42, "デザイナー");
employee1.displayName(); // 田中
employee2.displayName(); // 佐藤
この例では、displayName()がコンストラクタの外部で定義されています。コンストラクタ内部では、displayNameプロパティがグローバルなdisplayName()関数を指します。今回はdisplayNameプロパティに外部関数へのポインタのみが含まれるため、employee1とemployee2はグローバルスコープで定義されたdisplayName()関数を共有します。
これにより、同一ロジックの関数を重複定義する問題は解決されますが、グローバルスコープが汚染されるという新たな問題が生じます。なぜなら、その関数は実際には特定のオブジェクトでのみ使用されるからです。オブジェクトに複数のメソッドが必要な場合、グローバルスコープに複数の関数を定義することになります。これにより、カスタム型の参照コードがまとまって配置されにくくなります。この新たな問題はプロトタイプパターンで解決できます。
プロトタイプパターン
すべての関数はprototypeプロパティを作成します。このプロパティは、特定の参照型のインスタンスで共有されるべき属性とメソッドを含むオブジェクトです。実際、このオブジェクトはコンストラクタを呼び出して作成されたオブジェクトのプロトタイプです。プロトタイプオブジェクトを使用する利点は、そこで定義された属性とメソッドをオブジェクトインスタンスで共有できる点です。以前はコンストラクタ内で直接オブジェクトインスタンスに割り当てていた値を、直接そのプロトタイプに割り当てることができます:
function Employee() {}
Employee.prototype.name = "鈴木";
Employee.prototype.age = 31;
Employee.prototype.position = "マネージャー";
Employee.prototype.showName = function() {
console.log(this.name);
};
let staff1 = new Employee();
staff1.showName(); // "鈴木"
let staff2 = new Employee();
staff2.showName(); // "鈴木"
console.log(staff1.showName == staff2.showName); // true
関数式を使用しても同様の結果が得られます:
const Employee = function() {};
Employee.prototype.name = "山田";
Employee.prototype.age = 28;
Employee.prototype.position = "アナリスト";
Employee.prototype.showName = function() {
console.log(this.name);
};
let member1 = new Employee();
member1.showName(); // "山田"
let member2 = new Employee();
member2.showName(); // "山田"
console.log(member1.showName == member2.showName); // true
ここでは、すべての属性とshowName()メソッドがEmployeeのprototypeプロパティに直接追加されており、コンストラクタ本体には何もありません。しかし、このように定義すると、コンストラクタを呼び出して作成された新しいオブジェクトは依然として対応する属性とメソッドを持ちます。コンストラクタパターンとは異なり、このプロトタイプパターンで定義された属性とメソッドはすべてのインスタンスで共有されます。したがって、member1とmember2は同じ属性と同じshowName()関数にアクセスします。このプロセスを理解するには、ECMAScriptにおけるプロトタイプの本質を理解する必要があります。
プロトタイプの理解
関数を作成する際、常に特定のルールに従ってその関数のprototypeプロパティ(プロトタイプオブジェクトを指す)が作成されます。デフォルトでは、すべてのプロトタイプオブジェクトは自動的にconstructorという名前のプロパティを取得し、それに関連付けられたコンストラクタを指します。前の例では、Employee.prototype.constructorはEmployeeを指します。その後、コンストラクタに応じて、プロトotypeオブジェクトに他の属性やメソッドが追加されることがあります。
カスタムコンストラクタを定義する場合、プロトタイプオブジェクトはデフォルトでconstructorプロパティのみを取得し、他のすべてのメソッドはObjectから継承します。コンストラクタを呼び出して新しいインスタンスを作成するたび、そのインスタンスの内部[[Prototype]]ポインタはコンストラクタのプロトタイプオブジェクトに割り当てられます。スクリプトでこの[[Prototype]]特性にアクセスする標準的な方法はありませんが、Firefox、Safari、Chromeは各オブジェクトで__proto__プロパティを公開しており、このプロパティを通じてオブジェクトのプロトタイプにアクセスできます。他の実装では、この特性は完全に隠されています。重要な点は、インスタンスとコンストラクタのプロトタイプの間には直接の関係があるが、インスタンスとコンストラクタの間には直接の関係がないということを理解することです。この関係は視覚化しにくいですが、次のコードでプロトタイプの動作を理解できます:
// コンストラクタは関数式でも関数宣言でも可能
// 以下の2つの形式のどちらでも使用できます:
// function Employee() {}
// let Employee = function() {}
function Employee() {}
// 宣言後、コンストラクタには関連する
// プロトタイプオブジェクトがあります:
console.log(typeof Employee.prototype);
console.log(Employee.prototype);
// {
// constructor: f Employee(),
// __proto__: Object
// }