ECMAScript クラスまたはオブジェクトの定義

プリ定義オブジェクトを使用することは、オブジェクト指向言語の能力の一部に過ぎませんが、実際には独自のクラスやオブジェクトを作成できることが真の強みです。

ECMAScriptはオブジェクトやクラスを生成するための多くの方法を持ちます。

ファクトリーメソッド

元の方法

オブジェクトの属性はオブジェクトが作成された後に動的に定義できるため、多くの開発者がJavaScriptが最初に導入されたときに以下のようなコードを書いていました:

var oCar = new Object;
oCar.color = "blue";
oCar.doors = 4;
oCar.mpg = 25;
oCar.showColor = function() {
  alert(this.color);
};

TIY

上記のコードでは、carオブジェクトを作成し、いくつかの属性を設定します:その色は青、4つのドアがあり、1ガロンあたり25マイルです。最後の属性は実際には関数へのポインタであり、そのため属性はメソッドです。このコードを実行すると、carオブジェクトを使用することができます。

ただし、ここに問題があります。複数のcarインスタンスを作成する必要があるかもしれません。

解決策:工場方式

この問題を解決するために、開発者は特定のタイプのオブジェクトを作成し返す工場関数(factory function)を作成しました。

例えば、createCar()関数は、前述のcarオブジェクトの作成操作を包装するために使用できます:

function createCar() {
  var oTempCar = new Object;
  oTempCar.color = "blue";
  oTempCar.doors = 4;
  oTempCar.mpg = 25;
  oTempCar.showColor = function() {
    alert(this.color);
  };
  return oTempCar;
}
var oCar1 = createCar();
var oCar2 = createCar();

TIY

ここでは、最初の例のすべてのコードがcreateCar()関数に含まれています。さらに、carオブジェクト(oTempCar)を関数の値として返す一行の追加コードがあります。この関数を呼び出すと、新しいオブジェクトが作成され、すべての必要な属性が割り当てられ、前述のcarオブジェクトをコピーして作成されます。したがって、この方法で、属性が完全に同じcarオブジェクトの二つのバージョン(oCar1とoCar2)を作成することができます。

関数に引数を渡す

createCar()関数を修正し、各属性のデフォルト値を渡すのではなく、デフォルト値を直接属性に割り当てるのではなく、デフォルト値を渡す方法を変更できます:

function createCar(sColor,iDoors,iMpg) {
  var oTempCar = new Object;
  oTempCar.color = sColor;
  oTempCar.doors = iDoors;
  oTempCar.mpg = iMpg;
  oTempCar.showColor = function() {
    alert(this.color);
  };
  return oTempCar;
}
var oCar1 = createCar("red",4,23);
var oCar2 = createCar("blue",3,25);
oCar1.showColor();		//"red"を出力します
oCar2.showColor();		//"blue"を出力します

TIY

createCar()関数に引数を追加することで、作成するcarオブジェクトのcolor、doors、mpg属性に値を割り当てることができます。これにより、属性が同じで属性値が異なる二つのオブジェクトが作成できます。

工場関数の外でオブジェクトのメソッドを定義します

ECMAScript がどんどん形式化される一方で、オブジェクトの作成方法は見捨てられており、その規格化は今でも反対されています。その理由の一つは文法的なものです(new演算子を用いた構造関数のように見えにくいからです)。もう一つは機能的な理由です。機能的な理由は、この方法でオブジェクトを作成する必要があることにあります。前の例では、createCar()関数を呼び出すたびに新しい関数showColor()が作成されるため、各オブジェクトには独自のshowColor()バージョンがあります。しかし実際には、各オブジェクトは同じ関数を共有しています。

一部の開発者は、工場関数の外でオブジェクトのメソッドを定義し、属性を通じてそのメソッドを指し示すことでこの問題を避けています:

function showColor() {
  alert(this.color);
}
function createCar(sColor,iDoors,iMpg) {
  var oTempCar = new Object;
  oTempCar.color = sColor;
  oTempCar.doors = iDoors;
  oTempCar.mpg = iMpg;
  oTempCar.showColor = showColor;
  return oTempCar;
}
var oCar1 = createCar("red",4,23);
var oCar2 = createCar("blue",3,25);
oCar1.showColor();		//"red"を出力します
oCar2.showColor();		//"blue"を出力します

TIY

上記の改変されたコードでは、createCar()関数の前にshowColor()関数が定義されています。createCar()内部で、既存のshowColor()関数へのポインタをオブジェクトに割り当てます。機能的には、この方法で関数オブジェクトの再作成を避けることができますが、意味的には、この関数はオブジェクトのメソッドのように見えません。

これらのすべての問題が引き起こしました。開発者によって定義されました。の登場です。

コンストラクタメソッド

クラスの作成は工場関数の作成と同様に簡単です。最初のステップはクラス名を選ぶこと、つまりコンストラクタの名前です。慣習に従って、この名前は大文字で始まることで、通常小文字で始まる変数名と区別されます。これ以外に、コンストラクタは工場関数に非常に似ています。以下の例を考えてみてください:

function Car(sColor,iDoors,iMpg) {
  this.color = sColor;
  this.doors = iDoors;
  this.mpg = iMpg;
  this.showColor = function() {
    alert(this.color);
  };
}
var oCar1 = new Car("red",4,23);
var oCar2 = new Car("blue",3,25);

TIY

上記のコードと工場メソッドの違いについて説明します。まず、コンストラクタ内でオブジェクトは作成されていません。代わりにthisキーワードを使用しています。new演算子を使用してコンストラクタを構築する際には、最初の行のコードを実行する前にオブジェクトを作成します。このオブジェクトにアクセスするにはthisを使用する必要があります。その後、this属性に直接値を割り当てることができます。デフォルトでは、それはコンストラクタの返値です(return演算子を使用する必要はありません)。

今やnew演算子とクラス名Carを使用してオブジェクトを作成すると、ECMAScript中の一般的なオブジェクトの作成方法にさらに似ています。

この方法が前の方法と同じ問題を管理する方法があるかどうかを尋ねるかもしれません。はい。

工場関数のように、構造関数は関数を繰り返し生成し、各オブジェクトに独立した関数バージョンを作成します。しかし、工場関数と同様に、外部関数を使用して構造関数を再書き込みすることもできます。これは、意味的には何も変わりません。これがプロトタイプ方式の利点です。

プロトタイプ方式

この方法はオブジェクトのprototype属性を使用しており、新たなオブジェクトを作成するためのプロトタイプとして考えられます。

ここでは、まず空の構造関数を使用してクラス名を設定します。その後、すべての属性とメソッドが直接prototype属性に割り当てられます。前の例を再現し、以下のようになります:

function Car() {
}
Car.prototype.color = "blue";
Car.prototype.doors = 4;
Car.prototype.mpg = 25;
Car.prototype.showColor = function() {
  alert(this.color);
};
var oCar1 = new Car();
var oCar2 = new Car();

TIY

このコードでは、まず構造関数(Car)を定義しますが、何もコードはありません。次の行で、Carのprototype属性に属性を追加して、Carオブジェクトの属性を定義します。new Car()を呼び出すと、プロトタイプのすべての属性がすぐに作成されるオブジェクトに割り当てられます。これは、すべてのCarインスタンスがshowColor()関数へのポインタを持つことを意味し、すべての属性が一つのオブジェクトに属しているように見えます。したがって、前の2つの方法の問題を解決しました。

さらに、この方法ではinstanceof演算子を使用して、与えられた変数が指すオブジェクトのタイプを確認できます。したがって、以下のコードはTRUEを出力します:

alert(oCar1 instanceof Car);	//「true」を出力

プロトタイプ方式の問題

プロトタイプ方式は良い解決策のようですが、残念ながら完全に満足のいくものではありません。

まず、この構造関数には引数がありません。プロトタイプ方式では、プロトタイプを通じて構造関数に引数を渡して属性の値を初期化することはできません。なぜなら、Car1とCar2のcolor属性はすべて"blue"、doors属性はすべて4、mpg属性はすべて25であるためです。これは、オブジェクトが作成された後に属性のデフォルト値を変更する必要があることを意味し、非常に不快です。しかし、それだけではありません。実際の問題は、属性が関数ではなくオブジェクトを指している場合に発生します。関数の共有は問題になりませんが、オブジェクトはほとんどが複数のインスタンスで共有されません。以下の例を考えてみてください:

function Car() {
}
Car.prototype.color = "blue";
Car.prototype.doors = 4;
Car.prototype.mpg = 25;
Car.prototype.drivers = new Array("Mike","John");
Car.prototype.showColor = function() {
  alert(this.color);
};
var oCar1 = new Car();
var oCar2 = new Car();
oCar1.drivers.push("Bill");
alert(oCar1.drivers);	//「Mike,John,Bill」を出力
alert(oCar2.drivers);	//「Mike,John,Bill」を出力

TIY

上記のコードでは、属性driversは配列オブジェクトへのポインタで、その配列には「Mike」と「John」の2つの名前が含まれています。driversは参照値であるため、Carの2つのインスタンスは同じ配列を指しています。これは、oCar1.driversに値「Bill」を追加すると、oCar2.driversでも見ることができることを意味します。これらのポインタのどちらを出力しても、文字列「Mike,John,Bill」が表示されます。

オブジェクトの生成には多くの問題があるため、合理的なオブジェクト生成方法があると考えてください。答えはあります。コンストラクタとプロトタイプ方式を組み合わせて使用する必要があります。

混合のコンストラクタ/プロトタイプ方式

コンストラクタとプロトタイプ方式を組み合わせることで、他のプログラミング言語のようにオブジェクトを作成することができます。この概念は非常にシンプルで、非関数属性はコンストラクタで定義し、関数属性(メソッド)はプロトタイプ方式で定義します。結果として、すべての関数は一度だけ生成され、各オブジェクトには独自のオブジェクト属性インスタンスがあります。

前の例を修正し、以下のようになります:

function Car(sColor,iDoors,iMpg) {
  this.color = sColor;
  this.doors = iDoors;
  this.mpg = iMpg;
  this.drivers = new Array("Mike","John");
}
Car.prototype.showColor = function() {
  alert(this.color);
};
var oCar1 = new Car("red",4,23);
var oCar2 = new Car("blue",3,25);
oCar1.drivers.push("Bill");
alert(oCar1.drivers);	//「Mike,John,Bill」を出力
alert(oCar2.drivers);	//「Mike,John」を出力

TIY

現在は一般的なオブジェクトを生成するのと同じようです。すべての非関数属性はコンストラクタ内で生成され、これによりコンストラクタの引数を使って属性にデフォルト値を設定することができます。showColor()関数のインスタンスは1つだけ生成されるため、メモリの無駄がありません。また、oCar1のdrivers配列に"Bill"を追加することで、oCar2の配列には影響を与えません。したがって、これらの配列の値を出力すると、oCar1.driversは"Mike,John,Bill"、oCar2.driversは"Mike,John"が表示されます。プロトタイプ方式を使用しているため、instanceof演算子を使ってオブジェクトのタイプを判断することができます。

これは ECMAScript が採用する主な方法であり、他の方法の特性を持ちつつ、その副作用を持たない方法です。しかし、一部の開発者は、この方法が完璧ではないと感じることがあります。

動的プロトタイプメソッド

他の言語を使用する習慣がある開発者にとっては、混在の構築関数/プロトタイプ方式は那么に調和していないと感じることがあります。なぜなら、クラスを定義する際には、ほとんどの面向オブジェクト言語が属性とメソッドを視覚的にエンケードするからです。以下の Java クラスを考えてみてください:

class Car {
  public String color = "blue";
  public int doors = 4;
  public int mpg = 25;
  public Car(String color, int doors, int mpg) {
    this.color = color;
    this.doors = doors;
    this.mpg = mpg;
  }
  public void showColor() {
    System.out.println(color);
  }
}

Java は Car クラスのすべての属性とメソッドを非常にパッケージ化しており、このコードを見ると、何を実行するのかがすぐにわかります。これは、オブジェクトの情報を定義しています。混在の構築関数/プロトタイプ方式を批判する人は、属性を構築関数内で見つけ、メソッドをその外で見つけるという方法が論理的でないと考えています。したがって、彼らはよりフレンドリーなコーディングスタイルを提供するために動的プロトタイプメソッドを設計しました。

動的プロトタイプメソッドの基本的なアイデアは、混在の構築関数/プロトタイプ方式と同じで、構築関数内で非関数属性を定義し、関数属性はプロトタイプ属性を使用して定義されます。唯一の違いは、オブジェクトにメソッドを割り当てる位置です。以下は、動的プロトタイプメソッドで書き直された Car クラスです:

function Car(sColor,iDoors,iMpg) {
  this.color = sColor;
  this.doors = iDoors;
  this.mpg = iMpg;
  this.drivers = new Array("Mike","John");
  if (typeof Car._initialized == "undefined") {
    Car.prototype.showColor = function() {
      alert(this.color);
    };
    Car._initialized = true;
  }
}

TIY

typeof Car._initialized が "undefined" に等しいかどうかを確認するまで、この構造関数は変更されません。これは動的プロトタイプメソッドの中で最も重要な部分です。この値が未定義の場合、構造関数はプロトタイプ方式でオブジェクトのメソッドを定義し、Car._initialized を true に設定します。この値が定義されている場合(その値が true の場合、typeof の値が Boolean)、そのメソッドは再定義されません。要するに、このメソッドは(_initialized)フラグを使用して、プロトタイプにメソッドが追加されているかどうかを判断します。このメソッドは一度だけ作成および設定され、伝統的な OOP 開発者はこのコードが他の言語のクラス定義のように見えることに喜びを感じるでしょう。

混在ファクトリーワイズ

この方法は、前述の方法が適用できない場合の代替手段として一般的に使用されます。その目的は、仮の構造関数を作成し、別のオブジェクトの新しいインスタンスを返すことです。

このコードはファクトリーファンクションに非常に似ています:

function Car() {
  var oTempCar = new Object;
  oTempCar.color = "blue";
  oTempCar.doors = 4;
  oTempCar.mpg = 25;
  oTempCar.showColor = function() {
    alert(this.color);
  };
  return oTempCar;
}

TIY

古典的な方法とは異なり、この方法は new演算子を使用し、実際の構造関数のように見えます:

var car = new Car();

Car() 構造関数内で new演算子が呼ばれるため、構造関数の外に位置する第二个 new演算子は無視され、構造関数内で作成されたオブジェクトが car 変数に渡されます。

この方法は、オブジェクトメソッドの内部管理において古典的な方法と同じ問題があります。強くお勧めします:万が一の場合を除き、この方法は避けましょう。

どの方法を選ぶか

前述の通り、現在最も広く使用されているのは、混在した構造関数/プロトタイプ方式です。また、動的原始方法も人気があり、構造関数/プロトタイプ方式と同様の機能を持っています。これらのいずれかを選択することができます。ただし、古典的な構造関数やプロトタイプ方式を単独で使用することは避けましょう。なぜなら、これによりコードに問題が生じる可能性があるからです。

オブジェクトが興味深い点の一つはそれらを使って問題を解決する方法です。ECMAScriptで最も一般的な問題の一つは文字列結合のパフォーマンスです。他の言語と同様に、ECMAScriptの文字列は変更不可で、すなわちその値を変更することはできません。以下のコードを考えてみてください:

var str = "hello ";
str += "world";

実際には、このコードが背後で実行する手順は以下の通りです:

  1. "hello " を格納する文字列を作成します。
  2. "world" を格納する文字列を作成します。
  3. 結合結果を格納する文字列を作成します。
  4. strの現在の内容を結果にコピーします。
  5. "world" を結果にコピーします。
  6. strを更新し、結果を指すようにします。

文字列の結合が完了するたびに、ステップ 2 から 6 が実行され、この操作は非常にリソースを消費します。このプロセスを数百回、または数千回繰り返すと、パフォーマンス問題が発生します。解決策として、文字列を Array オブジェクトに格納し、最終的な文字列を作成するために join() メソッド(空文字列を引数に)を使用します。以下のコードで前のコードを置き換えることを想像してみてください:

var arr = new Array();
arr[0] = "hello ";
arr[1] = "world";
var str = arr.join(

このように、配列にどれだけ多くの文字列が追加されたとしても問題ありません。なぜなら、join() メソッドが呼び出される際にのみ結合操作が行われるからです。その時の実行手順は以下の通りです:

  1. 結果を格納する文字列を作成します
  2. 各文字列を結果に適切な位置にコピーします

このソリューションは良いですが、もっと良い方法があります。問題は、このコードはその意図を正確に反映していないことです。より理解しやすくするために、StringBuffer クラスにその機能をパッケージ化することができます:

function StringBuffer () {
  this._strings_ = new Array();
}
StringBuffer.prototype.append = function(str) {
  this._strings_.push(str);
};
StringBuffer.prototype.toString = function() {
  return this._strings_.join(
};

このコードではまず注意すべきは strings 属性で、本意はプライベート属性です。append() メソッドと toString() メソッドの2つのメソッドしかありません。append() メソッドには1つの引数があり、その引数を文字列配列に追加します。toString() メソッドは配列の join メソッドを呼び出し、実際に結合された文字列を返します。StringBuffer オブジェクトを使用して文字列を結合するには以下のコードを使用できます:

var buffer = new StringBuffer();
buffer.append("hello ");
buffer.append("world");
var result = buffer.toString();

TIY

以下のコードを使用して StringBuffer オブジェクトと伝統的な文字列結合メソッドのパフォーマンスをテストできます:

var d1 = new Date();
var str = "";
for (var i=0; i < 10000; i++) {
    str += "text";
}
var d2 = new Date();
document.write("プラス記号での結合: ")
 + (d2.getTime() - d1.getTime()) + "ミリ秒");
var buffer = new StringBuffer();
d1 = new Date();
for (var i=0; i < 10000; i++) {
    buffer.append("text");
}
var result = buffer.toString();
d2 = new Date();
document.write("<br />StringBuffer での結合: ")
 + (d2.getTime() - d1.getTime()) + "ミリ秒");

TIY

このコードは文字列結合に対して2つのテストを行います。1つはプラス記号を使用し、もう1つは StringBuffer クラスを使用します。各操作は10000個の文字列を結合します。日付の値 d1 と d2 は操作が完了するまでの時間を判断するために使用されます。Date オブジェクトを作成する際にパラメータが指定されていない場合、現在の日付と時間がオブジェクトに割り当てられます。結合操作がどれくらいの時間がかかるかを計算するには、日付のミリ秒表示(getTime メソッドの返値を使用して)を相減します。これは JavaScript のパフォーマンスを測定する一般的な方法です。このテストの結果は、StringBuffer クラスとプラス記号の使用の効率の違いを比較するのに役立ちます。