HTML Living Standard  第3部 Javascript 7 オブジェクト指向


 

7 オブジェクト指向

などのブラウザでは、他のオブジェクト指向プログラミング言語と同じような「クラス」が実装されています。

しかし、古いブラウザでは、「クラス」が存在しないものもあります。代わりに C++、Java などにはない prototype や C++、Java のとは全く異なる new 演算子や this が用意されています。


ここでは、class を使用した方法と class のないブラウザで使用される方法について説明します。また、他の方法についても「7.3 その他のオブジェクト指向プログラミング」にまとめて掲載しています。

7.1  class を使用した方法

新しいブラウザでは、他のオブジェクト指向プログラミング言語と同じような「クラス」が実装されています。

7.1.1  クラス定義

class を用いて「クラス」を定義します。

constructor は、インスタンス生成時に実行される関数です。インスタンス生成時に何も実行することがなければ記述する必要はありません。Java では、クラス名と同じ名前の関数として定義していました。

class クラス名 {
  constructor([引数]){     // コンストラクタ
    文;
  }
  // メンバ関数
}

例えば、Bird というクラスを定義するには次のようにします。

記述例
<script>
class Bird {
  constructor() { this.kind = "鳥"; }
  sing() { return "..."; }
  fly() { return "バサバサ"; }
}
</script>

this.kind は、メンバ変数です。this. を付ければ定義しなくても使用することができます。

7.1.2  インスタンスの作成

class を用いて定義されたクラスのインスタンスを作成する場合には new 演算子を使用します。

具体的には、constructor を実行します。return 文がなくてもインスタンスが返されます。

var 変数名 = new クラス名([引数[,引数[, ...,引数]]]);

次の例では、Bird() を実行して、インスタンスを生成します。

記述例
<div id="d1"></div>

<script>
class Bird {
  constructor() { this.kind = "鳥"; }
  sing() { return "..."; }
  fly() { return "バサバサ"; }
}

var bird = new Bird();        // インスタンスの作成
document.getElementById('d1').textContent = bird.kind + "が" + bird.fly() + "と羽ばたく";
</script>

constructor() が実行され、this.kind に "鳥" が設定されます。このときの this は、bird インスタンスが設定されています。

表示例

7.1.3  メンバ変数(プロパティ)

メンバ変数は次のように宣言します。

  this.メンバ変数名[ = 初期値];
記述例
<div id="d1"></div>
<div id="d2"></div>

<script>
class Bird {
  constructor() {
    this.kind = "鳥";
  }
}

var bird = new Bird();
var bird2 = new Bird();
document.getElementById('d1').textContent = bird.kind;
document.getElementById('d2').textContent = bird2.kind;
</script>

bird も bird2 もインスタンス生成時に constructor が実行されるので kind に "鳥" が代入されます。

表示例

なお、メンバ変数は当然のことながら値を変更することができます。

しかし、メンバ変数はインスタンスごと別の領域として確保されますので、bird.kind の変更が bird2.kind の変更になってしまうということはありません。

記述例
<div id="d1"></div>
<div id="d2"></div>

<script>
class Bird {
  constructor() {
    this.kind = "鳥";
  }
}

var bird = new Bird();
var bird2 = new Bird();
bird.kind = "ニワトリ";     // bird の kind を ニワトリ に変更
document.getElementById('d1').textContent = bird.kind;
document.getElementById('d2').textContent = bird2.kind;     // bird2 の kind は変更されない
</script>
表示例

その他に、メンバ変数に値を代入することでメンバ変数を定義することもできます。以下のようにして、bird という名前のインスタンスを作成し、bird.voice という名前のメンバ変数を追加します。

ただし、この方法で voice というメンバ変数が追加されるのはあくまで bird インスタンスです。つまり、Bird クラスには追加されていないので bird2 インスタンスを作成したとしても、voice というメンバ変数は存在していません。

また、文字列を キー とする連想配列のようにオブジェクトを使い、bird["voice"] という形式でも参照(もちろん代入も)することができます。

記述例
<div id="d1"></div>
<div id="d2"></div>
<div id="d3"></div>

<script>
class Bird {
  constructor() {
    this.kind = "鳥";
  }
}

var bird = new Bird();
bird.voice = "...";     // bird インスタンスに voice を追加
var bird2 = new Bird();
document.getElementById('d1').textContent = bird.kind + " " + bird.voice;
document.getElementById('d2').textContent = bird2.kind + " " + bird2.voice;     // bird2 インスタンスには voice はない
document.getElementById('d3').textContent = bird["kind"] + " " + bird["voice"]; // 連想配列のようにも指定できる
</script>

kind は、Bird の prototype を参照しているので、bird も bird2 も "鳥" と表示することはできますが、voice は bird2 には存在していないので、undefined と表示されます。

表示例

7.1.4  メンバ関数(メソッド)

メンバ関数は class 内に通常の関数のように宣言します。ただし、function キーワードを付けてはいけません

class クラス名 {
  constructor([引数]){     // コンストラクタ
    文;
  }
  // function キーワードは無し
  メンバ関数名([引数[,引数[, ...,引数]]]) {
    文;
  }
  // 関数式でも可能
  メンバ関数名 = function([引数[,引数[, ...,引数]]]) {
    文;
  }
}
記述例
<div id="d1"></div>

<script>
class Bird {
  constructor() {
    this.kind = "鳥";
  }
  sing() { return "..."; }
}

var bird = new Bird();
document.getElementById('d1').textContent = bird.kind + " " + bird.sing();
</script>
表示例

メンバ変数と同様、インスタンスにメンバ関数を定義することもできます。

やはり、この方法では fly() というメンバ関数が追加されるのはあくまで bird インスタンスです。つまり、Bird クラスには追加されていないので bird2 インスタンスを作成したとしても、fly() というメンバ関数は存在していません。

記述例
<div id="d1"></div>
<div id="d2"></div>

<script>
class Bird {
  constructor() {
    this.kind = "鳥";
  }
  sing() { return "..."; }
}

var bird = new Bird();
bird.fly = function() { return "バサバサ"; }     // bird インスタンスに fly() を追加
var bird2 = new Bird();
document.getElementById('d1').textContent = bird.kind + " " + bird.sing() + " " + bird.fly();
try {
  document.getElementById('d2').textContent = bird2.kind + " " + bird2.sing() + " " + bird2.fly();
}
catch(e) {
  document.getElementById('d2').textContent = e.message;     // bird2.fly() がないので例外が発生する
}
</script>
表示例

7.1.5  コンストラクタ

constructor 関数はクラスで作成されたインスタンスの生成と初期化のための特殊な関数です。クラス内に1つだけ記述することができます。JavaScript では new 演算子を使うことで constructor 関数が呼び出されます。

他のクラスを継承して作成した「サブクラス」の場合に、「スーパークラス」の constructor 関数を呼び出したければ、自クラスのコンストラクタ内に super 関数を記述します。詳しくは次の「7.1.6 継承」を参照してください。

独自の constructor 関数を記述しなかった場合は、既定の constructor 関数が実行されます。自クラスが他のクラスを継承していない「スーパークラス」の場合、コンストラクタでは何もしません。他のクラスを継承した「サブクラス」の場合は「スーパークラス」の constructor 関数が呼び出されます。


また new で生成されたインスタンスは、constructor というプロパティに、生成時に利用されたクラスへの参照を保持しています。これを利用してどのクラスのインスタンスかを判定することができます。

プロパティ
constructorインスタンスのコンストラクタ関数への参照
また、次のようなプロパティを持っている
プロパティ
length関数が期待する引数の数
nameクラス名
prototypeプロトタイプ

次の例では、new Bird() によって新しいインスタンスが生成されます。

記述例
<div id="d1"></div>
<!-- 以下略 -->

<script>
class Bird {
  constructor() {
    this.kind = "鳥";
  }
  sing() { return "..."; }
  fly() { return "バサバサ"; }
}

var bird = new Bird();
document.getElementById('d1').textContent = bird.constructor;
document.getElementById('d2').textContent = bird.constructor.length;
document.getElementById('d3').textContent = bird.constructor.name;
document.getElementById('d4').textContent = bird.constructor.prototype;
</script>
表示例

7.1.6  継承

クラスに「親子関係」を持たせることを「継承」といいます。子供のクラスは親クラスの性質を受け継ぐことができます。プログラミング言語の場合、引き継ぐ性質は定義されたメンバ変数 (プロパティ) やメンバ関数(メソッド)などになります。

つまり、既存のクラスの構造をベースに新しいクラスを定義する、クラスの全てを再利用する仕組みです。

クラスを継承する場合、その親になるクラスを「スーパークラス」と呼びます。そして、継承した子供のクラスを「サブクラス」と呼びます。ただし、この呼び方は言語によってまちまちで統一されていません。C++ の場合は、元になるクラスを「基本クラス」といい、継承するクラスを「派生クラス」ともいいます。

class を用いてクラスを作成した場合、extends を用いて、サブクラスを作成することができます。

サブクラスのコンストラクタ内で、super 関数を呼び出すとスーパークラスの constructor 関数を実行することができます。また、super.メンバ関数() でスーパークラスのメンバ関数の実行を、super.メンバ変数 でメンバ変数の参照をすることができます。

class サブクラス名 extends スーパークラス名 {
  constructor([引数]) {     // コンストラクタ
    super();   // スーパークラスの constructor() の呼び出し
    文;
  }
  関数名([引数]) {  // メンバ関数
    super.メンバ関数();   // スーパークラスのメンバ関数の呼び出し
    super.メンバ変数;     // スーパークラスのメンバ変数の参照
    文;
  }
}

また、サブクラスでスーパークラスのメンバー関数を置き換えることができます。これをオーバーライドといいます。

Cock で sing をオーバーライドしていますので、cock.sing() では Cock で定義された sing() が呼ばれます。しかし、Cock では fly は 定義されていませんので、cock.fly() では スーパークラス Bird の fly() が呼ばれます。

記述例
<div id="d1"></div>

<script>
class Bird {
  constructor() { this.kind = "鳥"; }
  sing() { return "..."; }
  fly() { return "バサバサ"; }
}

class Cock extends Bird {    // Bird を継承する
  constructor() {
    super();
    this.kind = "ニワトリ";
  }
  sing() { return "コケコッコー"; }   // sing をオーバーライドする
}

var cock = new Cock();
document.getElementById('d1').textContent = cock.kind + " " + cock.sing() + " " + cock.fly();
</script>
表示例

7.1.7  委譲

「委譲」とは、メンバ関数の処理を別のオブジェクトに委ねることで別のオブジェクトのメンバ関数を再利用する手法のことです。

再利用の仕組みとしては、先の継承がありますが、継承はスーパークラスの構造をすべて受け継ぎます。したがって、スーパークラスの変更はすべてのサブクラスに影響を与えてしまいます。それに対して、委譲はクラスの一部のメンバ関数のみを再利用します。したがって、委譲先のクラスの構造に関して知っている必要はなく、結果的に他のクラスの変更に影響を受けにくい手法となっています。


Penguin はペンギンなので飛べません。fly() をオーバーライドしています。

CockPenguin はニワトリの鳴き声をするペンギンです。sing() の処理は Cock クラスの sing() に委譲しています。

記述例
<div id="d1"></div>

<script>
class Bird {
  constructor() { this.kind = "鳥"; }
  sing() { return "..."; }
  fly() { return "バサバサ"; }
}

class Cock extends Bird {    // Bird を継承する
  constructor() {
    super();
    this.kind = "ニワトリ";
  }
  sing() { return "コケコッコー"; }   // sing をオーバーライドする
}

class Penguin extends Bird {    // Bird を継承する
  constructor() {
    super();
    this.kind = "ペンギン";
  }
  fly() { return "ドテッ";}   // fly をオーバーライドする
}

class CockPenguin extends Penguin {    // Penguin を継承する
  constructor() {
    super();
    this.kind = "ニワトリペンギン";
    this.cock = new Cock();
  }
  sing() { return this.cock.sing();}      // cock に処理を委譲する
}

var cockPenguin = new CockPenguin();
document.getElementById('d1').textContent = cockPenguin.kind + " " + cockPenguin.sing() + " " + cockPenguin.fly();
</script>
表示例

sing() を Cock に委譲してニワトリの鳴き声をさせることができました。ただし、これではカラスの鳴き声をさせたければ sing() を Crow に委譲した CrowPenguin を作成する必要があります。つまり、鳴き声の種類だけクラスを作らなければなりません。

そこで、どのような鳴き声でもさせることができる、SingingPenguin を作成することにします。これは SingingPenguin のコンストラクタで受け取った Bird のサブクラスに sing() の処理を委譲しています。

スーパークラス型の変数にサブクラスのオブジェクトを代入しメンバ関数を呼び出すとサブクラスのメンバ関数が実行されるという手法はポリモーフィズムとして知られています。この例でもポリモーフィズムを実現しているのですが、Javascript では変数に型がないので実際にはどのようなクラスのメソッドでも呼び出せます。

記述例
<div id="d1"></div>
<div id="d2"></div>

<script>
class Bird {
  constructor() { this.kind = "鳥"; }
  sing() { return "..."; }
  fly() { return "バサバサ"; }
}

class Cock extends Bird {    // Bird を継承する
  constructor() {
    super();
    this.kind = "ニワトリ";
  }
  sing() { return "コケコッコー"; }   // sing をオーバーライドする
}

class Crow extends Bird {    // Bird を継承する
  constructor() {
    super();
    this.kind = "カラス";
  }
  sing() { return "カーカー";}   // sing をオーバーライドする
}

class Penguin extends Bird {    // Bird を継承する
  constructor() {
    super();
    this.kind = "ペンギン";
  }
  fly() { return "ドテッ";}   // fly をオーバーライドする
}

class SingingPenguin extends Penguin {
  constructor(bird) {
    super();
    this.kind = "シンギングペンギン";
    this.bird = bird;
  }
  sing() { return this.bird.sing();}      // 引数の bird に処理を委譲する
}

var singingPenguin;
singingPenguin = new SingingPenguin(new Cock());
document.getElementById('d1').textContent = singingPenguin.kind + " " + singingPenguin.sing()
        + " " + singingPenguin.fly();
singingPenguin = new SingingPenguin(new Crow());
document.getElementById('d2').textContent = singingPenguin.kind + " " + singingPenguin.sing()
        + " " + singingPenguin.fly();
</script>
表示例

7.2  function を使用した方法

古いブラウザなどでは、「クラス」が存在しませんが、function を用いて「クラス」を定義することができます。

また、この方法は新しいブラウザを含めたほとんどのブラウザで動作します。

7.2.1  クラス定義

function を用いて「クラス」を定義します。これは通常の関数を定義する構文と同一です。

そのため、クラスとして定義した場合は慣習として1文字目は大文字にして、普通の関数と区別します。

function クラス名([引数[,引数[, ...,引数]]]) {}

例えば、Bird というクラスを定義するには次のようにします。

記述例
function Bird() {}

クラスのメンバ変数やメンバ関数は、クラス名.prototype に定義します。

prototype は、すべてのクラスが持っているプロパティです。クラスを定義した直後は空のインスタンスを参照していますが、別のインスタンスを代入したり、新たなプロパティを設定したりすることも可能です。

プロパティ
prototypeプロトタイプオブジェクトへの参照
記述例
function Bird() {}
Bird.prototype.kind = "鳥";                                    // メンバ変数
Bird.prototype.fly = function() { return "バサバサ"; }         // メンバ関数

上記の例では次のような状態になっています。

Bird prototype prototype kind fly

なお、メンバ変数やメンバ関数の定義を、クラス定義の中で行うことも可能ですが、クラス定義の内部で行うとインスタンスの生成のたびにメンバ変数やメンバ関数の定義が実行されてしまいます。そのため、通常はクラス定義の外に記述します。

7.2.2  インスタンスの作成

function を用いて定義されたクラスのインスタンスを作成する場合には new 演算子を使用します。

具体的には、クラス名に記述された関数を実行します。return 文がなくてもインスタンスが返されます。

var 変数名 = new クラス名([引数[,引数[, ...,引数]]]);

次の例では、Bird() を実行して、インスタンスを生成します。

記述例
<div id="d1"></div>

<script>
function Bird() {}
Bird.prototype.kind = "鳥" ;
Bird.prototype.fly = function() { return "バサバサ"; }

var bird = new Bird();        // インスタンスの作成
document.getElementById('d1').textContent = bird.kind + "が" + bird.fly() + "と羽ばたく";
</script>
表示例

7.2.3  メンバ変数(プロパティ)

メンバ変数は次のように宣言します。

  クラス名.prototype.メンバ変数名[ = 初期値];
記述例
<div id="d1"></div>
<div id="d2"></div>

<script>
function Bird() {}
Bird.prototype.kind = "鳥";

var bird = new Bird();
var bird2 = new Bird();
document.getElementById('d1').textContent = bird.kind;
document.getElementById('d2').textContent = bird2.kind;
</script>

kind は、Bird の prototype を参照しているので、bird も bird2 も "鳥" と表示します。

表示例

bird prototype bird2 prototype prototype kind(鳥)

なお、メンバ変数は当然のことながら値を変更することができます。

しかし、new を実行して生成された bird や bird2 が共に同じ prototype への参照を持つのであれば、例えば bird.kind の変更は bird2.kind の変更になってしまうような気がします。

しかし、prototype は確かに bird と bird2 で共有されていますが、実際には書き換えられてしまうということはありません。なぜなら、bird.kind を変更した時点で、bird 内に kind が新たに生成され、そこが書き換えられるからです。

記述例
<div id="d1"></div>
<div id="d2"></div>

<script>
function Bird() {}
Bird.prototype.kind = "鳥";

var bird = new Bird();
var bird2 = new Bird();
bird.kind = "ニワトリ";     // bird の kind を ニワトリ に変更
document.getElementById('d1').textContent = bird.kind;
document.getElementById('d2').textContent = bird2.kind;     // bird2 の kind は変更されない
</script>
bird prototype kind(ニワトリ) bird2 prototype prototype kind(鳥)
表示例

その他に、メンバ変数に値を代入することでメンバ変数を定義することもできます。以下のようにして、bird という名前のインスタンスを作成し、bird.voice という名前のメンバ変数を追加します。

ただし、この方法で sing というメンバ変数が追加されるのはあくまで bird インスタンスです。つまり、Bird クラスには追加されていないので bird2 インスタンスを作成したとしても、sing というメンバ変数は存在していません。

また、文字列を キー とする連想配列のようにオブジェクトを使い、bird["sing"] という形式でも参照(もちろん代入も)することができます。

記述例
<div id="d1"></div>
<div id="d2"></div>
<div id="d3"></div>

<script>
function Bird() {}
Bird.prototype.kind = "鳥";

var bird = new Bird();
bird.voice = "...";     // bird インスタンスに voice を追加
var bird2 = new Bird();
document.getElementById('d1').textContent = bird.kind + " " + bird.voice;
document.getElementById('d2').textContent = bird2.kind + " " + bird2.voice;     // bird2 インスタンスには voice はない
document.getElementById('d3').textContent = bird["kind"] + " " + bird["voice"]; // 連想配列のようにも指定できる
</script>

kind は、Bird の prototype を参照しているので、bird も bird2 も "鳥" と表示することはできますが、voice は bird2 には存在していないので、undefined と表示されます。

表示例

7.2.4  メンバ関数(メソッド)

メンバ関数は次のように宣言します。

  クラス名.prototype.メンバ関数名 = function([引数[,引数[, ...,引数]]]) {
    文;
  }
記述例
<div id="d1"></div>

<script>
function Bird() {}
Bird.prototype.kind = "鳥" ;
Bird.prototype.sing = function() { return "..."; }

var bird = new Bird();
document.getElementById('d1').textContent = bird.kind + " " + bird.sing();
</script>
表示例

メンバ変数と同様、インスタンスにメンバ関数を定義することもできます。

やはり、この方法では fly() というメンバ関数が追加されるのはあくまで bird インスタンスです。つまり、Bird クラスには追加されていないので bird2 インスタンスを作成したとしても、fly() というメンバ関数は存在していません。

記述例
<div id="d1"></div>
<div id="d2"></div>

<script>
function Bird() {}
Bird.prototype.kind = "鳥" ;
Bird.prototype.sing = function() { return "..."; }

var bird = new Bird();
bird.fly = function() { return "バサバサ"; }     // bird インスタンスに fly() を追加
var bird2 = new Bird();
document.getElementById('d1').textContent = bird.kind + " " + bird.sing() + " " + bird.fly();
try {
  document.getElementById('d2').textContent = bird2.kind + " " + bird2.sing() + " " + bird2.fly();
}
catch(e) {
  document.getElementById('d2').textContent = e.message;     // bird2.fly() がないので例外が発生する
}
</script>
表示例

7.2.5  コンストラクタ

JavaScript では new 演算子を使うことで関数を「コンストラクタ」として利用することができます。

また new で生成されたインスタンスは、constructor というプロパティに、生成時に利用されたクラスへの参照を保持しています。これを利用してどのクラスのインスタンスかを判定することができます。

プロパティ
constructorインスタンスのコンストラクタ関数への参照
また、次のようなプロパティを持っている
プロパティ
length関数が期待する引数の数
nameクラス名
prototypeプロトタイプ

次の例では、new Bird() によって新しいインスタンスが生成されます。

記述例
<div id="d1"></div>
<!-- 以下略 -->

<script>
function Bird() {}
Bird.prototype.kind = "鳥" ;
Bird.prototype.sing = function() { return "..."; }
Bird.prototype.fly = function() { return "バサバサ"; }

var bird = new Bird();
document.getElementById('d1').textContent = bird.constructor;
document.getElementById('d2').textContent = bird.constructor.length;
document.getElementById('d3').textContent = bird.constructor.name;
document.getElementById('d4').textContent = bird.constructor.prototype;
</script>
表示例

7.2.6  継承

クラスに「親子関係」を持たせることを「継承」といいます。子供のクラスは親クラスの性質を受け継ぐことができます。プログラミング言語の場合、引き継ぐ性質は定義されたメンバ変数 (プロパティ) やメンバ関数(メソッド)などになります。

つまり、既存のクラスの構造をベースに新しいクラスを定義する、クラスの全てを再利用する仕組みです。

クラスを継承する場合、その親になるクラスを「スーパークラス」と呼びます。そして、継承した子供のクラスを「サブクラス」と呼びます。ただし、この呼び方は言語によってまちまちで統一されていません。C++ の場合は、元になるクラスを「基本クラス」といい、継承するクラスを「派生クラス」ともいいます。

JavaScript の場合、Object.setPrototypeOf() を用いて、サブクラスを作成することができます。

記述例
  Object.setPrototypeOf(サブクラス.prototype, スーパークラス.prototype);   // prototype をコピーしてスーパークラスを継承

また、サブクラスでスーパークラスのメンバー関数を置き換えることができます。これをオーバーライドといいます。

Cock で sing をオーバーライドしていますので、cock.sing() では Cock で定義された sing() が呼ばれます。しかし、Cock では fly は 定義されていませんので、cock.fly() では setPrototypeOf によって Bird から Cock にコピーされた fly() が呼ばれます。

記述例
<div id="d1"></div>

<script>
function Bird() {}
Bird.prototype.kind = "鳥";
Bird.prototype.sing = function() { return "..."; }
Bird.prototype.fly = function() { return "バサバサ"; }

function Cock() {}
Object.setPrototypeOf(Cock.prototype, Bird.prototype);    // Bird を継承する
Cock.prototype.kind = "ニワトリ";
Cock.prototype.sing = function() { return "コケコッコー"; }   // sing をオーバーライドする

var cock = new Cock();
document.getElementById('d1').textContent = cock.kind + " " + cock.sing() + " " + cock.fly();
</script>
表示例

7.2.7  委譲

「委譲」とは、メンバ関数の処理を別のオブジェクトに委ねることで別のオブジェクトのメンバ関数を再利用する手法のことです。

再利用の仕組みとしては、先の継承がありますが、継承はスーパークラスの構造をすべて受け継ぎます。したがって、スーパークラスの変更はすべてのサブクラスに影響を与えてしまいます。それに対して、委譲はクラスの一部のメンバ関数のみを再利用します。したがって、委譲先のクラスの構造に関して知っている必要はなく、結果的に他のクラスの変更に影響を受けにくい手法となっています。


Penguin はペンギンなので飛べません。fly() をオーバーライドしています。

CockPenguin はニワトリの鳴き声をするペンギンです。sing() の処理は Cock クラスの sing() に委譲しています。

記述例
<div id="d1"></div>

<script>
function Bird() {}
Bird.prototype.kind = "鳥";
Bird.prototype.sing = function() { return "..."; }
Bird.prototype.fly = function() { return "バサバサ"; }

function Cock() {}
Object.setPrototypeOf(Cock.prototype, Bird.prototype);    // Bird を継承する
Cock.prototype.kind = "ニワトリ";
Cock.prototype.sing = function() { return "コケコッコー"; }   // sing をオーバーライドする

function Penguin() {}
Object.setPrototypeOf(Penguin.prototype, Bird.prototype);    // Bird を継承する
Penguin.prototype.kind = "ペンギン";
Penguin.prototype.fly = function() { return "ドテッ"; }   // fly をオーバーライドする

function CockPenguin() {
  this.cock = new Cock();
}
Object.setPrototypeOf(CockPenguin.prototype, Penguin.prototype);    // Penguin を継承する
CockPenguin.prototype.kind = "ニワトリペンギン";
CockPenguin.prototype.sing = function() { return this.cock.sing(); }  // Cock に処理を委譲する

var cockPenguin = new CockPenguin();
document.getElementById('d1').textContent = cockPenguin.kind + " " + cockPenguin.sing() + " " + cockPenguin.fly();
</script>
表示例

sing() を Cock に委譲してニワトリの鳴き声をさせることができました。ただし、これではカラスの鳴き声をさせたければ sing() を Crow に委譲した CrowPenguin を作成する必要があります。つまり、鳴き声の種類だけクラスを作らなければなりません。

そこで、どのような鳴き声でもさせることができる、SingingPenguin を作成することにします。これは SingingPenguin のコンストラクタで受け取った Bird のサブクラスに sing() の処理を委譲しています。

スーパークラス型の変数にサブクラスのオブジェクトを代入しメンバ関数を呼び出すとサブクラスのメンバ関数が実行されるという手法はポリモーフィズムとして知られています。この例でもポリモーフィズムを実現しているのですが、Javascript では変数に型がないので実際にはどのようなクラスのメソッドでも呼び出せます。

記述例
<div id="d1"></div>
<div id="d2"></div>

<script>
function Bird() {}
Bird.prototype.kind = "鳥";
Bird.prototype.sing = function() { return "..."; }
Bird.prototype.fly = function() { return "バサバサ"; }

function Cock() {}
Object.setPrototypeOf(Cock.prototype, Bird.prototype);    // Bird を継承する
Cock.prototype.kind = "ニワトリ";
Cock.prototype.sing = function() { return "コケコッコー"; }   // sing をオーバーライドする

function Crow() {}
Object.setPrototypeOf(Crow.prototype, Bird.prototype);    // Bird を継承する
Crow.prototype.kind = "カラス";
Crow.prototype.sing = function() { return "カーカー"; }   // sing をオーバーライドする

function Penguin() {}
Object.setPrototypeOf(Penguin.prototype, Bird.prototype);    // Bird を継承する
Penguin.prototype.kind = "ペンギン";
Penguin.prototype.fly = function() { return "ドテッ"; }   // fly をオーバーライドする

function SingingPenguin(bird) {
  this.bird = bird;
}
Object.setPrototypeOf(CockPenguin.prototype, Penguin.prototype);    // Penguin を継承する
CockPenguin.prototype.kind = "シンギングペンギン";
CockPenguin.prototype.sing = function() { return this.bird.sing(); }  // 引数の bird に処理を委譲する

var singingPenguin;
singingPenguin = new SingingPenguin(new Cock());
document.getElementById('d1').textContent = singingPenguin.kind + " " + singingPenguin.sing()
        + " " + singingPenguin.fly();
singingPenguin = new SingingPenguin(new Crow());
document.getElementById('d2').textContent = singingPenguin.kind + " " + singingPenguin.sing()
        + " " + singingPenguin.fly();
</script>
表示例

7.3  その他のオブジェクト指向プログラミング

この章の最初にも記述しましたが、class や prototype を使用した上記の方法以外にも JavaScript ではいろいろな方法でクラスの継承関係を作成することができます。

7.3.1  call を使用した方法

call() や apply() を用いても、サブクラスを作成することができます。引数固定の場合は call()、可変個引数の場合は apply() を用います。どちらも第一引数は、関数内の this に設定される値です。もし、第一引数がオブジェクトでない場合は、例えば 10 ならば new Number(10)、"文字列" ならば new String("文字列") のように、暗黙的にオブジェクトに変換されます。ただし、null や undefined の場合は 関数内の this にはグローバルオブジェクトである Window オブジェクトが設定されます。

なお、これらのメソッドは古くからある仕様なのでたいていのブラウザで動作します。

関数.call(this,[引数[,引数[, ...,引数]]])

this の値と、個々にあたえた引数をわたして、関数を呼び出す。継承の場合は関数にスーパークラスを指定する。

引数 this関数内の this に設定される値
引数関数に渡される引数
戻り値関数値

関数.apply(this,[引数])

this の値と、個々にあたえた引数のリストをわたして、関数を呼び出す。継承の場合は関数にスーパークラスを指定する。

引数 this関数内の this に設定される値
引数関数に渡される引数のリスト
戻り値関数値

スーパークラスを継承してサブクラスを作成する場合には次のように宣言します。

記述例
function サブクラス名([引数[,引数[, ...,引数]]]) {
  スーパークラス.call(this,[引数[,引数[, ...,引数]]]);        // スーパークラスを継承
  文;
}

あるいは、

function サブクラス名() {
  スーパークラス.apply(this,arguments);        // スーパークラスを継承
  文;
}

new を用いてオブジェクトを生成する際に、call や apply を用いることで Java のようにコンストラクタによる初期化を連鎖させることができます。

下の例では new Cock() を実行したときに Cock 内の Bird.call が実行され、Cock 内の this と "ニワトリ" を引数に Bird() が呼ばれます。よって、そのときの Bird() 内の this は、Bird.call したときの第一引数、つまり Cock 内の this(Cock のオブジェクト)になります。

そのため、Bird() 内の代入式の this.kind や this.sing、this.fly の this が Cock オブジェクトなので、kind や sing、fly は Cock オブジェクトに作られることになります。そして、その結果、Cock には定義されていない fly を利用できるようになるわけです。

記述例
<div id="d1"></div>
<div id="d2"></div>

<script>
function Bird() {
  this.kind = (arguments[0] == undefined) ? "鳥" : arguments[0];    // 引数があれば設定する
  this.sing = function() { return "..."; }
  this.fly = function() { return "バサバサ"; }
}
function Cock() {
  Bird.call(this, "ニワトリ");   // Bird を継承する(Cock の this と "ニワトリ" を引数に Bird() を呼び出す)
  this.sing = function() { return "コケコッコー"; }// sing をオーバーライドする(sing の実行内容を換える)
}
function Crow() {
  Bird.call(this, "カラス");              // Bird を継承する
  this.sing = function() { return "カーカー"; }        // sing をオーバーライドする
}

function Penguin() {
  var kind = (arguments[0] == undefined) ? "ペンギン" : arguments[0];
  Bird.call(this, kind);   // Bird を継承する
  this.fly = function() { return "ドテッ"; }           // fly をオーバーライドする
}
function SingingPenguin(bird) {
  Penguin.call(this, "シンギングペンギン");   // Penguin を継承する
  this.sing = function() { return bird.sing(); }      // 引数の bird に処理を委譲する
}

var singingPenguin;
singingPenguin = new SingingPenguin(new Cock());      // Cock のオブジェクト
document.getElementById('d1').textContent = singingPenguin.kind + " " + singingPenguin.sing()
            + " " + singingPenguin.fly();
singingPenguin = new SingingPenguin(new Crow());      // Crow のオブジェクト
document.getElementById('d2').textContent = singingPenguin.kind + " " + singingPenguin.sing()
            + " " + singingPenguin.fly();
</script>
表示例

7.3.2  Object.create を使用した方法

Object.create でスーパークラスのプロトタイプに新たなプロパティを追加したり、既存のプロパティを置き換えたりして、サブクラスを生成します。

記述例
<div id="d1"></div>
<div id="d2"></div>

<script>
var Bird = Object.create({}, {
  kind : {value:"鳥"},
  sing : {get:function() { return "..."; }},
  fly : {get:function() { return "バサバサ"; }}
});

var Cock = Object.create(Bird, { kind:{value:"ニワトリ"}, sing:{get:function() { return "コケコッコー"; }} });
var Crow = Object.create(Bird, { kind:{value:"カラス"}, sing:{get:function() { return "カーカー"; }} });
var Penguin = Object.create(Bird, { kind:{value:"ペンギン"}, fly:{get:function() { return "ドテッ"; }} });
var SingingPenguin = Object.create(Penguin, { kind:{value:"シンギングペンギン"},
        singer:{set:function(b) { this.bird = b; }},
        sing:{get:function() { return this.bird.sing; }} 
    });

var singingPenguin = Object.create(SingingPenguin);
singingPenguin.singer = Cock;      // Cock のオブジェクト
document.getElementById('d1').textContent = singingPenguin.kind + " " + singingPenguin.sing
        + " " + singingPenguin.fly;
singingPenguin.singer = Crow;      // Crow のオブジェクト
document.getElementById('d2').textContent = singingPenguin.kind + " " + singingPenguin.sing
        + " " + singingPenguin.fly;
</script>
表示例

7.4  静的変数、静的メソッド

通常のメンバ変数はオブジェクトごとに作成されるオブジェクト固有のものです。動的変数とも言います。それに対して、静的変数はオブジェクト間で共通で、クラスに1つしか存在しません。よって、あるオブジェクトが静的変数を変更すると他のオブジェクトから見ても値が変わってしまうことになります。

また、動的変数を参照するには動的メソッドとも呼ばれる通常のメンバ関数である必要がありますし、静的変数には静的メソッドが必要になります。


これもまた、いろいろな方法で作成することができます。

7.4.1  static を使用した方法

class が使用できるのならば、静的変数や静的メソッドは Java と同じように static 宣言をして定義します。呼び出すには、クラス名.静的変数名、クラス名.静的メソッド名() のように指定します。

また、静的変数は、クラス外に クラス名.静的変数名のような形式で宣言することもできます。

class クラス名 {
  static 静的変数名 = 初期値;     // 静的変数の宣言
  static 静的メソッド名() {
    return クラス名.静的変数名
  }
}
クラス名.静的変数名 = 初期値;     // この方法でも静的変数の宣言ができる
記述例
<div id="d1"></div>

<script>
class Bird {
  constructor(kind) { this.kind = (arguments[0] == undefined) ? "鳥" : arguments[0]; }
  sing() { return "..."; }
  fly() { return "バサバサ"; }
}
class Sparrow extends Bird {
  static count = 0;     // 静的変数の宣言
  constructor() {
    super("スズメ");
    Sparrow.count++;
  }
  static getCount() { return Sparrow.count; }
}

var list = [new Sparrow(), new Sparrow(), new Sparrow(), new Sparrow()]; // Sparrow オブジェクトを4つ生成する
document.getElementById('d1').textContent = Sparrow.getCount();      // Sparrow オブジェクトの数を得る
</script>
表示例

7.4.2  callee を使用した方法

callee はすべての関数オブジェクト内で利用できるローカル変数 arguments のプロパティであり、自分自身を参照することができます。

function クラス名() {
  arguments.callee.静的変数名 = 初期値;
  
  arguments.callee.静的メソッド名 = function() {
    return クラス名.静的変数名
  }
}

Sparrow は、オブジェクトが生成されるたびにその数を数えています。クラスに共通の領域でないとそれまで生成された数を覚えていられないので、静的変数にして数を数えます。またその値を返す関数も、静的メソッドである必要があります。

記述例
<div id="d1"></div>

<script>
function Bird() {
Bird.prototype.kind = "鳥";
Bird.prototype.sing = function() { return "..."; }
Bird.prototype.fly = function() { return "バサバサ"; }

function Sparrow() {
  if (arguments.callee.count == undefined)    // count が未定義のとき
    arguments.callee.count = 0;               // 値 0 で、count を定義する
  arguments.callee.count++;                   // オブジェクト生成のたびに count を加算する

  arguments.callee.getCount = function() {
    return Sparrow.count;
  }
}
Object.setPrototypeOf(Sparrow.prototype, Bird.prototype);    // Bird を継承する
Sparrow.prototype.kind = "スズメ";

var list = [new Sparrow(), new Sparrow(), new Sparrow(), new Sparrow()]; // Sparrow オブジェクトを4つ生成する
document.getElementById('d1').textContent = Sparrow.getCount();      // Sparrow オブジェクトの数を得る
</script>

Safari は、setPrototypeOf が機能しないので表示されませんが、callee 自体は動作します。

表示例

7.5  this

JavaScript の this は同じソース上でも呼び出し元次第で意味が違います。


なお、以下の例では this の内容を表示するために dispThis という関数を作成しています。その関数は、constructor.name を表示していますが、 では、constructor.name が未定義なので constructor を表示しています。

7.5.1  グローバルコンテクスト

トップレベル(どの関数にも属さない、すべての関数の外側)で使用された場合は、グローバルオブジェクトを指します。

Javascript では、Window オブジェクトがグローバルオブジェクトです。

記述例
<div id="d1"></div>
<div id="d2"></div>

<script>
var value = "Global";
document.getElementById('d1').textContent = dispThis(this);       // this は Window オブジェクト
document.getElementById('d2').textContent = this.value;

function dispThis(obj) {
  var name = obj.constructor.name;
  if (name != undefined && name != "")
    return name;
  return obj.constructor;
}
</script>
表示例

7.5.2  関数コンテクスト

関数内では、this の値は関数の呼び出され方によって異なります。

なお、以下の例で使用されている dispThis という関数は、「7.5.1 グローバルコンテクスト」の例を参照してください。

(1) 単純な関数呼び出し

オブジェクトのメソッドとしてではなく、単なる関数として呼び出されている場合は、this の値は呼び出しによって設定されません。this の値は常にグローバルオブジェクト、つまり Window オブジェクトです。

記述例
<div id="d1"></div>
<div id="d2"></div>

<script>
this.value = "Global";       // window.value と記述しても同じ
function func(){
  var value = "local";       // ここで this.value とすると window.value の意味になる
  return this;       // this は Window オブジェクト(= window)
}
document.getElementById('d1').textContent = dispThis(func());
document.getElementById('d2').textContent = func().value;
</script>
表示例

(2) オブジェクトのメソッド

オブジェクトのメソッドとして呼び出されている場合は、this は、生成されるオブジェクト(この例では Obj)への参照になります。

記述例
<div id="d1"></div>
<div id="d2"></div>

<script>
this.value = "Global";        // window.value
var Obj = function() {
  this.value = "local";       // Obj オブジェクト の value
  this.func = function(){
    return this;       // this は Obj オブジェクト
  }
}
var obj = new Obj();
document.getElementById('d1').textContent = dispThis(obj);
document.getElementById('d2').textContent = obj.func().value;
</script>
表示例

(3) オブジェクト内の関数呼び出し

オブジェクトのメソッドから呼び出された関数の場合は、グローバルオブジェクトである Window オブジェクトです。つまり、Obj 内の this.value での this は、Obj オブジェクトですが、sub() で return している this は、Window オブジェクトです。

記述例
<div id="d1"></div>
<div id="d2"></div>

<script>
this.value = "Global";       // this は Window オブジェクト
var Obj = function() {
  this.value = "local";      // this は Obj オブジェクト
  this.func = function(){
    var sub = function(){
      return this;      // this は Window オブジェクト
    }
    return sub();       // Window オブジェクトが返る
  }
}
var obj = new Obj();
document.getElementById('d1').textContent = dispThis(obj.func());
document.getElementById('d2').textContent = obj.func().value;
</script>

obj.func() は Window オブジェクトを返すので、obj.func().value は Global になります。

表示例

obj.func().value が Obj オブジェクト内の value の値である local を返すようにするには、次のように this を別の変数に保存し、その変数を参照します。変数名としては、「self」、「that」、「_this」などが使われることが多いようです。

記述例
<div id="d1"></div>
<div id="d2"></div>

<script>
this.value = "Global";       // this は Window オブジェクト (var value = "Global"; としても同じ意味)
var Obj = function() {
  var self = this;      // Obj オブジェクトを保存
  this.value = "local";      // this は Obj オブジェクト
  this.func = function() {
    var sub = function(){
      return self;      // self は Obj オブジェクト
    }
    return sub();
  }
}
var obj = new Obj();
document.getElementById('d1').textContent = dispThis(obj.func());
document.getElementById('d2').textContent = obj.func().value;
</script>
表示例

(4) apply, call呼び出し

callapply では、this を外部から設定できます。したがって、this は call や apply されたときの引数の持つ値になります。

この例では、Obj1.call(this) によって、Obj1内の this に Obj2 のオブジェクトが設定され、Obj1 が実行されます。その結果、func 関数が Obj2 内に生成されます。そして、Obj2 オブジェクトの func 関数を呼び出せば、それが返す戻り値である this は Obj2 になります。

記述例
<div id="d1"></div>
<div id="d2"></div>

<script>
function Obj1() {
  this.value = "local 1",
  this.func = function() {
    return this;
  }
}
function Obj2() {
  Obj1.call(this);
  this.value = "local 2";
}

var obj = new Obj2();   // obj.func() は Obj2 オブジェクト
document.getElementById('d1').textContent = dispThis(obj.func());
document.getElementById('d2').textContent = obj.func().value;
</script>
表示例

(5) プロトタイプメソッド

プロトタイプでは、this は、生成されるオブジェクト(この例では Obj)への参照になります。

記述例
<div id="d1"></div>
<div id="d2"></div>

<script>
this.value = "Global";       // this は Window オブジェクト
function Obj() {}
Obj.prototype.value = "local";
Obj.prototype.func = function() {
  return this;     // this は Obj オブジェクト
};

var obj = new Obj();
document.getElementById('d1').textContent = dispThis(obj.func());
document.getElementById('d2').textContent = obj.func().value;
</script>
表示例

(6) class 内

class 内で使用される this は、生成されるオブジェクト(この例では Obj)への参照になります。

記述例
<div id="d1"></div>
<div id="d2"></div>

<script>
this.value = "Global";       // this は Window オブジェクト
class Obj {
  constructor() { this.value = "local"; }
  func() {
    return this;     // this は Obj オブジェクト
  }
}

var obj = new Obj();
document.getElementById('d1').textContent = dispThis(obj.func());
document.getElementById('d2').textContent = obj.func().value;
</script>
表示例

また、サブクラスのオブジェクト(この例では obj2)から参照されたスーパークラス(この例では Obj)内の this は、サブクラス(この例では Obj2)のオブジェクトへの参照になります。

ただし、この例の Obj のようにスーパークラスとして記述されていても、そのクラスのオブジェクトを生成してそのオブジェクトから参照された場合の this は、そのクラス(この例では Obj)のオブジェクトへの参照になります。これは、上の例そのものです。

記述例
<div id="d1"></div>
<div id="d2"></div>

<script>
this.value = "Global";       // this は Window オブジェクト
class Obj {
  constructor() { this.value = "local"; }
  func() {
    return this;     // obj2 から参照された場合の this は Obj2 オブジェクト
  }
}
class Obj2 extends Obj {     // Obj を継承
  constructor() {
    super();
    this.value = "local2";     // this は Obj2 オブジェクト
  }
}

var obj2 = new Obj2();
document.getElementById('d1').textContent = dispThis(obj2.func());
document.getElementById('d2').textContent = obj2.func().value;
</script>
表示例

(7) アロー関数

アロー関数 では、this は常に Window オブジェクトです。

記述例
<span id="d11"></span>  <span id="d12"></span><br>
<span id="d21"></span>  <span id="d22"></span>

<script>
var value = "Global";
var func1 = () => { return this; };       // this は Window オブジェクト

var Obj = {
  value: "local",
  func2: () => { return this; }           // this は Window オブジェクト
};

document.getElementById('d11').textContent = dispThis(func1());
document.getElementById('d12').textContent = func1().value;
document.getElementById('d21').textContent = dispThis(Obj.func2());
document.getElementById('d22').textContent = Obj.func2().value;
</script>
表示例

(8) コールバック関数

コールバック関数では、this は常に Window オブジェクトです。

オブジェクトのメソッドも、オブジェクトのメソッドとして呼び出されている場合は、this は、生成されるオブジェクト(この例では Obj)への参照になりますが、コールバック関数として使用された場合は Window オブジェクトです。

記述例
<span id="d11"></span>  <span id="d12"></span><br>
<span id="d21"></span>  <span id="d22"></span>

<script>
var value = "Global";
function func1(f) { return f(); }

function func2() {
  var value = "local";
  return this;           // コールバックされた場合 this は Window オブジェクト
}

var Obj = function() {
  this.value = "local";
  this.func = function() {
    return this;         // コールバックされた場合 this は Window オブジェクト
  }
}
var obj = new Obj();

document.getElementById('d11').textContent = dispThis(func1(func2));
document.getElementById('d12').textContent = func1(func2).value;
document.getElementById('d21').textContent = dispThis(func1(obj.func));
document.getElementById('d22').textContent = func1(obj.func).value;
</script>
表示例

7.5.3  イベントハンドラ

イベントハンドラは、イベントを検出し、イベントに処理を与えるのに使用します。 発生したイベントごとに指定された関数が呼び出され、処理を実行させることができます。

(1) インラインイベントハンドラ

HTML の要素に、on~ 属性でイベントハンドラを記述したときには、その属性値内の this は、記述した要素のオブジェクトになります。

記述例
<input id="b" type="button" value="input" onclick="func(this)">   <!-- this は input 要素のオブジェクト -->
<div id="d1"></div>
<div id="d2"></div>

<script>
var value = "Global";
var element = document.getElementById('b');

function func(obj) {
  var value = "local";
  document.getElementById('d1').textContent = dispThis(obj);   // obj(==this) は input 要素のオブジェクト
  document.getElementById('d2').textContent = obj.value;
}
</script>

[input]ボタンをクリックしてください。

この場合の this は、クリックした input 要素であるボタンのオブジェクトになります。したがって、その this を受け取った obj の メンバーである obj.value は、input 要素の value 属性、つまり、"input" になります。

表示例

(2) イベントハンドラ

HTML の要素に、on~ 属性でイベントハンドラとして設定した関数においては、その関数内の this にはグローバルオブジェクトである Window オブジェクトが設定されます。

次の例は、ボタンをクリックしたときに呼び出されるイベントハンドラ(func)内の this は、Window オブジェクトを持っています。

記述例
<input type="button" value="input" onclick="func()">
<div id="d1"></div>
<div id="d2"></div>

<script>
var value = "Global";

function func() {
  var value = "local";
  document.getElementById('d1').textContent = dispThis(this);   // this は Window オブジェクト
  document.getElementById('d2').textContent = this.value;
}
</script>

「input」ボタンをクリックしてください。

この場合の this は、Window オブジェクトになります。

表示例

(3) Javascript

要素オブジェクト.on~ に関数を代入して、イベントハンドラとして設定した場合は、イベントハンドラ内の this には要素オブジェクトが設定されます。

次の例は、ボタンをクリックしたときに呼び出されるイベントハンドラ(func)内の this は、クリックした input 要素のオブジェクトです。

記述例
<input id="b" type="button" value="input">
<div id="d1"></div>
<div id="d2"></div>

<script>
var value = "Global";
var element = document.getElementById("b");
element.onclick = func;

function func() {
  var value = "local";
  document.getElementById('d1').textContent = dispThis(this);   // this は input 要素のオブジェクト
  document.getElementById('d2').textContent = this.value;
}
</script>

[input]ボタンをクリックしてください。

この場合の this は、クリックした input 要素であるボタンのオブジェクトになります。したがって、this.value は、input 要素の value 属性、つまり、"input" になります。

表示例

(4) DOM イベントハンドラ

addEventListener メソッドで、イベントハンドラとして要素に付加した関数においては、イベントハンドラ内の this にはイベントを発生させた要素が設定されます。

次の例は、ボタンをクリックしたときに呼び出されるイベントハンドラ(func)内の this は、クリックした input 要素のオブジェクトです。

記述例
<input id="b" type="button" value="input">
<div id="d1"></div>
<div id="d2"></div>

<script>
var value = "Global";
var element = document.getElementById('b');
element.addEventListener('click', func, false);

function func() {
  var value = "local";
  document.getElementById('d1').textContent = dispThis(this);   // this は input 要素のオブジェクト
  document.getElementById('d2').textContent = this.value;
}
</script>

[input]ボタンをクリックしてください。

この場合の this は、クリックした input 要素のであるボタンのオブジェクトになります。したがって、this.value は、input 要素の value 属性、つまり、"input" になります。

表示例