目次
コンストラクタ関数と new の基本
普通の関数との違い
function createUser(name, age) {
return {
name,
age,
};
}
const u1 = createUser("Taro", 20);↑ これはただの「関数」。
一方、コンストラクタ関数は new とセットで呼ぶことを前提にした関数 です。
コンストラクタ関数の例
function User(name, age) {
// this は「これから作られるインスタンス」を指す
this.name = name;
this.age = age;
}
const user1 = new User("Taro", 20);
const user2 = new User("Hanako", 25);
console.log(user1.name); // "Taro"
console.log(user2.name); // "Hanako"ポイント:
- コンストラクタ関数は 先頭を大文字(
User,Personなど)にする慣習 new User(...)と呼び出すことで、「新しいオブジェクト」が作られる
new をつけると内部で何が起きているか
new User("Taro", 20) を実行すると:
- 新しい空オブジェクト
{}を作る - そのオブジェクトの
[[Prototype]]をUser.prototypeに設定する - そのオブジェクトを
thisとしてUser関数を実行する - 何も
returnしなければ、そのthisを返す
という処理が行われています。
なので、User の中で
this.name = name;
this.age = age;とすると、「新しく作られたインスタンス」にプロパティが付く、というわけです。
new を付け忘れると?
"use strict";
function User(name) {
this.name = name;
}
const u = User("Taro"); // ← new なし
console.log(u); // undefined- strict モードでは、
thisはundefined this.name = ...を実行しようとしてTypeErrorになる
→ コンストラクタ関数を作るときは、
- 名前を大文字で始める
- 必ず
newで呼ぶ
という 2 点セットで覚えておくのが安全です。
prototype とインスタンス共有メソッド
インスタンスごとに同じ関数を作ると無駄
function User(name) {
this.name = name;
this.sayHello = function () {
console.log(`こんにちは、${this.name}です`);
};
}
const u1 = new User("Taro");
const u2 = new User("Hanako");
console.log(u1.sayHello === u2.sayHello); // false(別々の関数)- 各インスタンスごとに 毎回新しい関数オブジェクト を生成している
- インスタンスが大量にあると、メモリ的に無駄
メソッドは prototype に定義する
function User(name) {
this.name = name;
}
// ここがポイント:prototype にメソッドを定義
User.prototype.sayHello = function () {
console.log(`こんにちは、${this.name}です`);
};
const u1 = new User("Taro");
const u2 = new User("Hanako");
u1.sayHello(); // こんにちは、Taroです
u2.sayHello(); // こんにちは、Hanakoです
console.log(u1.sayHello === u2.sayHello); // true(同じ関数を共有)User.prototypeにあるメソッドは、
インスタンス(u1,u2)からプロトタイプチェーンを通じて見える- なので、全インスタンスが同じ関数オブジェクトを共有できる
イメージ:
User.prototype ----> { sayHello: [Function] }
↑
├─ [[Prototype]] of u1
└─ [[Prototype]] of u2インスタンスごとの状態 vs 共有メソッド
this.nameのような「個々のデータ」 → コンストラクタ内でthis.xxx = ...- 共有したい「メソッド(振る舞い)」 →
User.prototype.xxx = function () { ... }
という分担が基本パターンです。
コンストラクタ+prototype での継承
「古い書き方での継承」について簡単に解説します。
(class で書けるようになれば十分なので、細かいパターンは覚える必要なし)
function Animal(name) {
this.name = name;
}
Animal.prototype.sayName = function () {
console.log(`I am ${this.name}`);
};
function Dog(name, breed) {
// 親コンストラクタを呼ぶ(this を継承先インスタンスにバインド)
Animal.call(this, name);
this.breed = breed;
}
// プロトタイプチェーンを繋げる
Dog.prototype = Object.create(Animal.prototype);
// constructor を修正しておくのが定石
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function () {
console.log("わん!");
};
const d = new Dog("Pochi", "Shiba");
d.sayName(); // I am Pochi (Animal.prototype 由来)
d.bark(); // わん! (Dog.prototype 由来)やっていること:
Animal.call(this, name)で「親のコンストラクタ処理」を流用
→nameプロパティなどを初期化Dog.prototype = Object.create(Animal.prototype)
→Dogのインスタンスからプロトタイプチェーンをたどると、Animal.prototypeにたどり着くようにするDog.prototype.constructor = Dog;はお約束のお片付け
この書き方は正直かなり冗長で覚えにくいので、ES2015 の class 構文 が導入されました。
class 構文の基本
コンストラクタ関数の class 版
さっきの User を class で書き直すとこうなります:
class User {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(`こんにちは、${this.name}です`);
}
}
const u1 = new User("Taro");
const u2 = new User("Hanako");
u1.sayHello(); // こんにちは、Taroです
u2.sayHello(); // こんにちは、Hanakoです
console.log(u1 instanceof User); // trueポイント:
class User { ... }は シンタックスシュガー(コンストラクタ+prototype を包んだ文法)constructorメソッドが、コンストラクタ関数本体に相当sayHello()のようなメソッド定義は、内部的にはUser.prototype.sayHelloに追加される
つまり、これは本質的に次と同じ構造になっています:
function User(name) {
this.name = name;
}
User.prototype.sayHello = function () {
console.log(`こんにちは、${this.name}です`);
};メソッドの定義場所
class の中に書いたメソッドは、全部 prototype 上のメソッドになります。
class User {
constructor(name) {
this.name = name; // インスタンスごとの状態
}
sayHello() {
// prototype メソッド
console.log(`こんにちは、${this.name}です`);
}
rename(newName) {
this.name = newName;
}
}- インスタンスプロパティ:
this.xxx = ...(constructor 内) - インスタンスメソッド:クラス本体に
method() { ... }と書く
extends と super による継承
基本例:Animal → Dog
class Animal {
constructor(name) {
this.name = name;
}
sayName() {
console.log(`I am ${this.name}`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // 親クラスの constructor を呼ぶ
this.breed = breed;
}
bark() {
console.log("わん!");
}
}
const d = new Dog("Pochi", "Shiba");
d.sayName(); // I am Pochi (Animal のメソッド)
d.bark(); // わん! (Dog のメソッド)extends の意味:
DogクラスのプロトタイプチェーンをAnimalに自動で繋いでくれる- さっきの
Dog.prototype = Object.create(Animal.prototype)をやってくれるイメージ
super(name) の意味:
- 親クラスの
constructorを呼ぶ特別な構文 super(...)を呼ぶまでthisはまだ使えない(仕様)
class Dog extends Animal {
constructor(name, breed) {
// この行より前で this を使うとエラー
super(name);
this.breed = breed;
}
}メソッドのオーバーライドと super.xxx()
class Animal {
constructor(name) {
this.name = name;
}
say() {
console.log("...");
}
}
class Dog extends Animal {
say() {
// 親クラスの say を呼びたければ super.say()
super.say();
console.log("わん!");
}
}
const d = new Dog("Pochi");
d.say();
// ...
// わん!- 同じ名前のメソッドを子クラスで定義すると、オーバーライドになる
- 親の実装を活かしつつ追加する場合は、
super.xxx()で呼び出す
static メソッド(クラスに属する関数)
インスタンスではなく「クラス自体」のメソッド
class MathUtil {
static add(a, b) {
return a + b;
}
}
console.log(MathUtil.add(2, 3)); // 5
const m = new MathUtil();
// m.add(2, 3); // TypeError: m.add is not a functionstaticを付けたメソッドは、「インスタンスではなくクラスにぶら下がる」メソッド- ユーティリティ関数や、インスタンス不要のヘルパー処理によく使う
裏側では、だいたいこんなイメージ:
function MathUtil() {}
MathUtil.add = function (a, b) {
return a + b;
};
コメント