[JavaScript講座] コンストラクタ関数と class 構文

当ページのリンクには広告が含まれています。
目次

コンストラクタ関数と 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) を実行すると:

  1. 新しい空オブジェクト {} を作る
  2. そのオブジェクトの [[Prototype]]User.prototype に設定する
  3. そのオブジェクトを this として User 関数を実行する
  4. 何も 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 モードでは、thisundefined
  • 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 由来)

やっていること:

  1. Animal.call(this, name) で「親のコンストラクタ処理」を流用
    name プロパティなどを初期化
  2. Dog.prototype = Object.create(Animal.prototype)
    Dog のインスタンスからプロトタイプチェーンをたどると、Animal.prototype にたどり着くようにする
  3. 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 function
  • static を付けたメソッドは、「インスタンスではなくクラスにぶら下がる」メソッド
  • ユーティリティ関数や、インスタンス不要のヘルパー処理によく使う

裏側では、だいたいこんなイメージ:

function MathUtil() {}
MathUtil.add = function (a, b) {
  return a + b;
};

<<前へ(オブジェクトとプロトタイプ)

>>次へ(クラスフィールドとプライベートフィールド)

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次