【STEP.27】
JavaScriptのオブジェクト指向をマスターしよう!
~Part.1~

プロトタイプベースのオブジェクト指向とは

JavaScriptはれっきとしたオブジェクト指向言語です。

しかし、Java、C#、Rubyといったオブジェクト指向言語とは根本的に異なる点があります。

それは「『インスタンス化/インスタンス』という概念はあるものの、いわゆる『クラス』がなく、『プロトタイプ(ひな型)』という概念だけが存在する」という点です。

プロトタイプとは、「あるオブジェクトの元となるオブジェクト」のことです。

JavaScriptでは、(クラスの代わりに)プロトタイプを利用して、新たなオブジェクトを生成していくことになります。

このような性質から、JavaScriptのオブジェクト指向は、プロトタイプベースのオブジェクト指向と呼ばれることもあります。

プロトタイプとは、要は「より縛りの弱いクラスのようなもの」とも言えます。

このページでもプロトタイプのことを便宜的に「クラス」と称することがありますのでご了承ください。

このオブジェクト指向構文に関して、ES2015で重要な変更がありました。

それがclass構文の導入です。

そのためES2015以前の旧構文とES2015以降の新構文ではコードの見た目が異なります。

まずは、ES2015以前の旧構文でクラスを定義してみましょう。

クラスを定義する

ES2015以前の旧構文でクラスを定義する

ES2015以前の旧構文を利用して、中身を持たない、最もシンプルな「クラス」を定義した例を見てみましょう。

var Member = function() {};

「変数Memberに対して、空の関数リテラルを代入しているだけではないか」と思われるかもしれませんが、その通り、これがJavaScriptのクラスです。

実際、このMemberクラスは、以下のようにnew演算子でインスタンス化できます。

var mem = new Member();

繰り返しですが、JavaScriptの世界では「いわゆる厳密な意味でのクラス」という概念は存在しません。ここでは、

JavaScriptでは関数(Functionオブジェクト)にクラスとしての役割を与えている

ということを覚えておきましょう。

次は、ES2015以降の新構文でクラスを定義してみます。

ES2015以降の新構文でクラスを定義する

class命令を利用します。

class命令

class name {
  ...definitions...
}
name         クラス名
difinitions  クラス本体

たとえば以下は、Memberクラスの例です。

class Member {
			
}
let mem = new Member();

console.log(mem);
// 結果:Member{}

名前(クラス名)の規則や命名の留意点については、「変数に名前を付けるときのルール(命名規則)」を参照してください。

ただし、変数/関数と異なり、クラス名は大文字始まりとするのが一般的です。(Pascal記法)

また、クラスで扱うモノを端的に表す名詞で表現します。

この例では、まだ中身を持たないので、クラスとしての実質的な意味はありませんが、確かにnew演算子によって、インスタンスが生成されていることが確認できます。

MEMO
ークラスリテラルー
なお、以下のように「class{...}」の形式で、クラスリテラルを表すこともできます。

リテラルで、関数リテラルと同じく文の一部として表せます。

let Member = class {...クラスの中身...}

class命令で定義されたクラスは、内部的には関数です。

つまり、JavaScriptにいわゆるクラスが導入されたわけではなく、あくまで、「これまでFunctionオブジェクトで表現していたクラス(コンストラクター)」をより分かりやすく表現できるようになったにすぎないのです。

class命令は、プロトタイプベースのオブジェクト指向構文を覆い包むシンタックスシュガー(糖衣構文)といえます。

もっとも、class命令で定義されたクラスは、Functionオブジェクトによるそれと完全に等価ではない点もあるので、注意が必要です。

以下に、class命令で定義されたクラスと、Functionオブジェクトで定義されたクラスとの違いをまとめます。

class命令で定義されたクラスとFunctionオブジェクトで定義されたクラスの違い

定義前のクラスを呼び出すことはできない

次のコードのようにfunction命令で宣言された関数は、宣言場所に関わらず、コードのどこからでもアクセスできます。

function命令は静的な構造なので、定義前のクラスも呼び出すことができるということです。(ただし、関数リテラルによる宣言では不可です。)

var mem = new Member();

function Member(){};

一方、クラスはそうはならない点に注意が必要です。

たとえば以下のコードは「Cannot access 'Member' before initialization」のようなエラーになります。

let mem = new Member();
		
class Member{

}

class命令は関数としての呼び出しはできない

function命令で定義されたクラスは、以下のように関数として呼び出すことができます。

function Member(){};

var mem = Member();

しかし、class命令で定義されたMemberクラスを、以下のようなコードで呼び出すと「class constructors must be invoked with 'new'」のようなエラーとなります。

class Member{
		
}
let mem = Member();

コンストラクターを定義する

コンストラクターとは、「インスタンス(オブジェクト)を生成する際に、オブジェクトを初期化するための処理を記述するための特殊なメソッド(関数)」のことです。

一般的には、オブジェクトで利用できるプロパティ(メンバー変数)を準備するために利用します。

たとえば以下は、Memberクラスにコンストラクター経由でfirstNamelastNameプロパティを追加する例です。

class Member {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
}
let mem = new Member ('流星','横浜');
console.log(mem.firstName);
// 結果:流星

コンストラクターの構文は、以下の通りです。

コンストラクター

constructor(arguments,...) {
  ...statements...
}
arguments   引数
statements  初期化のためのコード

コンストラクターの名前はconstructorで固定で、クラスに1つしか定義できません。

コンストラクター配下のthisは、コンストラクターによって生成されるインスタンス(つまり、自分自身)を表します。

thisキーワードに対して変数を設定することで、インスタンスのプロパティを設定できます。

プロパティの設定

this.property = value
property  プロパティ名
value     値

なお、コンストラクターでは、自動的にthisが示すオブジェクトを返すので、戻り値は不要です。

明示的に戻り値を返した場合には、その値がnew演算子の値となります。

その場合、thisへのプロパティ設定などは無視されるので、注意が必要です。

コンストラクターでのプロパティの初期化をよりシンプルに記述する

Object.assignメソッド、プロパティ簡易構文を組み合わせることで、コンストラクターでのプロパティの初期化をよりシンプルに記述できます。

具体例を見てみましょう。

class Member {
  constructor(firstName, lastName) {
    Object.assign(this, {firstName, lastName});
  }
}

コンストラクター配下のthisは、現在のインスタンスを意味します。

そのため、「Object.assign(this, {firstName, lastName});」は、「現在のインスタンスに対して、引数オブジェクトをまとめてマージする」という意味になります。

{firstName, lastName}」はプロパティ簡易構文で

{
  firstName: firstName,
  lastName: lastName
}

と同じ意味です。

このような記法を利用することで、初期化すべきプロパティの数が増えた場合にも、「this.プロパティ = 値」を列記しなくて済むので、コードがシンプルになります。

文脈で変わる変数this

thisキーワードは、コンストラクターによって生成されるインスタンス(つまり、自分自身)を表すものです。

厳密には「コンストラクターという文脈では」という条件が頭につきます。

thisは、スクリプトのどこからでも参照できる特別な変数です。

そして、呼び出す場所、または呼び出しの方法(文脈)によって中身が変化する、不思議な変数でもあります。

thisキーワードの参照先

thisキーワードの参照先は、以下の条件で変化します。

【変数thisが指すもの】
文脈thisの指すもの
関数グローバルオブジェクト(Strictモードではundefined)
call/apply引数で指定したオブジェクト
イベントリスナーイベントの発生元
コンストラクター生成するインスタンス
メソッド呼び出し元のオブジェクト(=レシーバー)

上の表のcallapplyメソッドは、いずれも関数(Functionオブジェクト)が提供するメンバーで、その関数を呼び出します。

callapplyメソッドの違いは、実行すべきfuncに渡す引数の指定方法だけです。

callは個々の値で指定するのに対して、applyメソッドは配列として渡します。

call/applyメソッド

func.call(that [,arg1 [,arg2 [,...]]])
func.apply(that [,args])
that          関数の中でthisキーワードが指すもの
arg1,arg2...  関数に渡す引数
args          関数に渡す引数(配列)

ここで注目していただきたいのは、引数thatです。

callapplyメソッドでは、引数thatを引き渡すことで、関数オブジェクト配下のthisキーワードが示すオブジェクトを切り替えることができます。

以下の具体例を見てみましょう。

var data = 'Global data';
let obj1 = { data: 'obj1 data'};
let obj2 = { data: 'obj2 data'};
	
function hoge() {
  console.log(this.data);
}
hoge.call(null);
// 結果:Global data
hoge.call(obj1);
// 結果:obj1 data
hoge.call(obj2);
// 結果:obj2 data
hoge.apply(null);
// 結果:Global data
hoge.apply(obj1);
// 結果:obj1 data
hoge.apply(obj2);
// 結果:obj1 data

引数thatにそれぞれ異なるオブジェクトを渡すことで、hoge関数配下のthisの内容(ここでは出力されるthis.dataの値)が変化していることを確認できます。

なお、引数thatnullを渡した場合、暗黙的にグローバルオブジェクトが渡されたものと見なされます。

callメソッドによるthisの変化

プロパティのゲッター/セッターを定義する

プロパティを取得/設定するときに呼び出される特別なメソッドをゲッター/セッターといいます。

プロパティは、コンストラクターの中で「this.プロパティ名 = 値」の形式で準備する以外にもゲッター/セッターを介して準備することもできます。

コンストラクターを介して準備する方法はこれはこれでシンプルで便利なのですが、実践的なアプリでは、ゲッター/セッターを介することが通常です。

というのも、「this.プロパティ名」で用意したプロパティは単なる変数(器)です。

そのため、値をただ受け渡しすることしかできません。

しかし、ゲッター/セッターの実体はメソッドです。

よって、「値を取得する際にデータを加工したい」「設定時に値の妥当性を検証したい」といった場合にも、自由に処理を加えることが可能です。

また、ゲッターだけを準備し、セッターを除けば、読み取り専用のプロパティを実装することも可能です。(ゲッターだけを省略すれば、書き込み専用のプロパティとなります。)

ゲッター/セッターを介することで、プロパティをより安全に操作できるというわけです。

ゲッター/セッターは、それぞれgetset命令で定義します。

get/set命令

get name() {...get_statements...}
set name(value) {...set_statements...}
name            プロパティ名
get_statements  値を取得するコード
value           プロパティへの設定値を受け取る変数名
set_statements  値を設定するコード

具体例を見てみましょう。

class Member {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
  get firstName() {
    return this._firstName;
  }
  set firstName(value) {
    this._firstName = value;
  }
  get lastName() {
    return this._lastName;
  }
  set lastName(value) {
    this._lastName = value;
  }
}
let mem = new Member('流星','横浜');
mem.firstName = 'りゅうせい';
console.log(mem.firstName);
// 結果:りゅうせい

getsetは、あくまでも値を取得/設定するための特殊なメソッドです。

実際に値を保持するのは、この例であれば、this._firstNamethis._lastNameです。

getsetブロックでも、このthis._firstNamethis._lastNameから値を取得/設定している点に注目です。

プロパティへの設定値は、setブロックの引数(ここではvalue)に暗黙的に渡されます。(set firstName(value) { this._firstName = value; }

また、このように定義されたゲッター/セッターも、利用者からはメソッドではなくあくまでも変数(プロパティ)として見えている点に注目です。(「mem.lastName('りゅうせい')」ではなく、「mem.lastName = 'りゅうせい'」でアクセスできます。)

ゲッター/セッターを利用することで、「見た目は変数、中身はメソッド」のプロパティを定義できるというわけです。

MEMO
ー「this_title」の意味ー
プロパティを保管する変数this._firstName、this._lastNameがアンダースコア始まりであるのは、これがプライベート変数(=クラスの外から利用できない)であることを表すためです。

JavaScriptのクラスではpublic/privateのようなアクセス修飾子を持ちません。

そこで、このような命名規則でもって、プライベート変数であることを表すのが慣例です。

もちろん、あくまで「外からアクセスしてほしくない」ことを意思表明しているだけなので、実際には「クラスの外から利用すべきでない変数」を宣言していることになります。(つまり、利用者が意図して「a_firstName」のようなコードを書くことは可能です。)

クラス定数を定義する

classブロック配下で表せるのはメソッド(getsetブロックを含む)だけで、以下のようなプロパティは宣言できません。

class MyClass {
  let hoge = 'foo';
  const piyo = 'bar';
}

プロパティを宣言するにはコンストラクターの中で「this.プロパティ名 = 値;」とするか、getsetブロックを利用します。

そして、クラス定数(らしきもの)を定義するには、staticgetブロックを利用します。

class MyUtil {
  // 読み取り専用のtaxプロパティ
  static get tax() {
  return 1.1;
  }
}
console.log(MyUtil.tax);
// 結果:1.1

正確には読み取り専用のプロパティですが、見た目は「クラス名.定数」(console.log(MyUtil.tax);)の形式でアクセスできる、いわゆるクラス定数が準備できます。

メソッドを定義する

メソッドは、以下の構文で定義できます。

メソッド

method(parameters, ...) {
  ...statements
}
method      メソッド名
parameters  引数
statements  メソッドの本体

たとえば以下は、Memberクラスにオブジェクトを文字列化するgetNameメソッドを追加する例です。

class Member {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
  getName() {
    return this.lastName + this.firstName;
  }
}

let mem = new Member('流星','横浜');
console.log(mem.getName());
// 結果:横浜流星

コンストラクターと同じく、メソッドの配下では「this.~」で準備済みのプロパティにアクセスできます。

MEMO
ーアクセス修飾子はないー
Java/C#などの言語に慣れている人は、メソッド/コンストラクターの定義に際して、public/protected/privateのようなアクセス修飾子がJavaScriptでは利用できない点に注意が必要です。

アクセス修飾子とは、メソッド/プロパティがどこからアクセスできるかを表す情報です。

JavaScriptのクラスでは、すべてのメンバーはpublic(どこからでもアクセス可能)です。

静的メソッドを定義する

静的メソッドとは、インスタンスを生成しなくても呼び出せる(=「クラス.メソッド(...)」で呼び出せる)メソッドのことです。

静的メソッドを定義するには、メソッド定義にstaticキーワードを付与するだけです。

たとえば以下は、Figureクラスに静的メソッドgetSquareAreaを追加する例です。

class Figure {
  static getSquareArea(base, height) {
    return base * height;
  }
}
console.log(Figure.getSquareArea(5,3));
// 結果:15

ちなみに、静的メソッドをインスタンスから呼び出すことはできません。

たとえば以下のようなコードは「fig.getSquareArea is not a function」のようなエラーとなります。

let fig = new Figure();
console.log(fig.getSquareArea(30,5));
MEMO
ーインスタンスメソッドー
クラス経由ではなく、インスタンスから呼び出すメソッド(staticなしのメソッド)のことをインスタンスメソッドといいます。

メソッドをあとから追加する

メソッドは、コンストラクターで定義するばかりではありません。

new演算子でいったんインスタンス化したオブジェクトに対して、あとからメソッドを追加することもできます。

たとえば以下は、Memberクラスに対して、あとからオブジェクトを文字列化するgetNameメソッドを追加する例です。

class Member {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
}

let mem = new Member('流星','横浜');

mem.getName = function () {
  return this.lastName + this.firstName;
};
console.log(mem.getName());
// 結果:横浜流星

メソッドをあとから追加する場合の注意点

ただし、インスタンスに対して直接メンバー(プロパティやメソッド)を追加した場合には、注意すべき点もあります。

次の例を見てみましょう。

class Member {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
}

let mem = new Member('流星','横浜');

mem.getName = function () {
  return this.lastName + this.firstName;
};
console.log(mem.getName());
// 結果:横浜流星
let mem2 = new Member ('匠海','北村');
console.log(mem2.getName());

追加したのはソースコードの15~16行目の部分です。

新たに生成したインスタンスmem2から、動的に追加したgetNameメソッドを呼び出そうとすると、「mem2.getName is not a function」(mem2.getNameは関数でありません。)というメッセージが返されるはずです。

要は、ソースコード10~12行目では、Memberクラスそのものではなく、「生成されたインスタンスに対してメソッドが追加されている」ということです。

Javaのようなクラスベースのオブジェクト指向ならば、「同一のクラスを元に生成されたインスタンスは同一のメンバーも持つ」のが常識ですが、プロトタイプベースのオブジェクト指向(JavaScript)の世界では、

同一のクラスを元に生成されたインスタンスであっても、それぞれが持つメンバーは同一であるとは限らない

ということです。

ここでは、新たにメンバーを追加しているだけですが、delete演算子でインスタンスから既存のメンバーを削除することもできます。

このようなゆるさが、プロトタイプが「より縛りの弱いクラスのようなもの」といわれる所以でもあります。

プロトタイプは「より縛りの弱いいクラス」

コンストラクターの問題点とプロトタイプ

上述のようにインスタンス共通のメソッドを定義するには、少なくともコンストラクターでメソッドを定義する必要があります。

しかし、実はコンストラクターによるメソッドの追加には、

メソッドの数に比例して、無駄なメモリを消費する

という問題があります。

コンストラクターはインスタンスを生成するたびに、それぞれのインスタンスのためにメモリを確保します。

以下の例であれば、Memberクラスに属するfirstNamelastNameプロパティ、getNameメソッドをインスタンスのためにコピーするわけです。

class Member {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
  getName() {
    return this.lastName + this.firstName;
  }
}
let mem = new Member ('流星','横浜');
console.log(mem.getName());
// 結果:横浜流星

しかし、getNameメソッドのようなメソッド(関数)は、すべてインスタンスで中身が同じなので、インスタンス単位でメモリを確保するのは無駄なことです。

コンストラクターでメソッドを定義することの問題

ここではgetNameメソッド1つだけですから、さほど問題にはならないかもしれません。

しかし、これがもっと複雑なメソッドを10も20も持つようなクラスであったらどうでしょうか。

そのようなクラスをいくつもインスタンス化するようなコードは、そのすべてのメソッドを、いちいちインスタンス化のたびにコピーするのは無駄なことです。

そこでJavaScriptでは、オブジェクトにメンバーを追加するために、prototypeというプロパティを用意しています。

prototypeプロパティは、デフォルトで空のオブジェクトを参照していますが、これにプロパティやメソッドを追加することができます。

そして、このprototypeプロパティに格納されたメンバーは、インスタンス化された先のオブジェクトに引き継がれる、もっといえば、prototypeプロパティに対して追加されたメンバーは、そのクラス(コンストラクター)を元に生成されたすべてのインスタンスから利用できるというわけです。やや難しげな言い方をするならば、

オブジェクトをインスタンス化した場合、インスタンスは元となるオブジェクトに属するprototypeオブジェクトに対して、暗黙的な参照を持つことになる

ということです。

プロトタイプオブジェクト

具体的なコードを見てみましょう。

Memberオブジェクトに含まれるgetNameメソッドを、プロトタイプとして定義した例です。

class Member {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
}
Member.prototype.getName = function (){
  return this.lastName + this.firstName;
};
let mem = new Member ('流星','横浜');
console.log(mem.getName());
// 結果:横浜流星

プロトタイプオブジェクト(prototypeプロパティが参照するオブジェクト)に追加されたgetNameメソッドが、Memberクラスのインスタンス(変数mem)からも正しく参照できていることが確認できるはずです。

「プロトタイプ(ひな型)ベースのオブジェクト指向」というと、特に「クラスベースのオブジェクト指向」に慣れた方にはとっつきづらく感じるかもしれませんが、要は「クラスという抽象的な設計図が存在しないのがJavaScriptの世界である」と考えれば良いでしょう。

JavaScriptの世界にあるのは、あくまで実体化されたオブジェクトだけで、新しいオブジェクトを生成するにも(クラスではなく)オブジェクトが元になっています。

そして、新しいオブジェクトを作るための原型(ひな型)を表すのが、それぞれのオブジェクトに属する「プロトタイプ」という特別なオブジェクトであるわけです。

プロトタイプオブジェクトを利用することの2つの利点

このように、プロトタイプオブジェクトを介してメソッドを定義することには、以下の2つの利点があります。

メモリの使用量を節減できる

繰り返しですが、プロトタイプオブジェクトの内容は、あくまで「インスタンスから暗黙的に参照される」のみで、インスタンスに対してコピーされるわけではありません。

つまり、JavaScriptでは、オブジェクトのメンバーが呼び出された時に、以下の流れでメンバーを取得することとなります。

  • インスタンス側に要求されたメンバーが存在しないかを確認
  • 存在しない場合は、暗黙の参照をたどってプロトタイプオブジェクトを検索

暗黙の参照

これによって、コンストラクター経由でメソッドを定義する場合に起こる「メモリを無駄に消費する」という問題が回避できるわけです。

メンバーの追加や変更をインスタンスがリアルタイムに認識できる

「インスタンスにメンバーをコピーしない」ということは「プロトタイプオブジェクトへの変更(追加や削除)を、インスタンス側で動的に認識できる」ということでもあります。

プロトタイプへの変更もリアルタイムに認識

試しに、以下のようなコードで実際の挙動を確認してみましょう。

class Member {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
}

let mem = new Member('流星','横浜');

Member.prototype.getName = function () {
  return this.lastName + this.firstName;
};
console.log(mem.getName());
// 結果:横浜流星

getNameメソッドを、new演算子によってインスタンスを生成した後に追加しているという点です。

この場合でも、何ら問題なくメソッドを認識できることが確認できます。

プロトタイプオブジェクトの不思議(プロパティの設定)

プロトタイプオブジェクトで、プロパティを宣言したらどうなるのでしょうか。

「暗黙の参照」という考え方からすれば、プロパティ値はすべてのインスタンスで共有されるように思われます。

あるインスタンスでプロパティの値を変更したら、すべてのインスタンスにその変更が反映されてしまうのでしょうか。

実際の動作を確認してみましょう。

class Member {

}
Member.prototype.sex = '男';
let mem1 = new Member();
let mem2 = new Member();
console.log(mem1.sex + '|' + mem2.sex);
// 結果:男|男
mem2.sex = '女';
console.log(mem1.sex + '|' + mem2.sex);
// 結果:男|女

注目していただきたいのは、ソースコードの11行目の部分です。

mem2.sex = '女';」で、インスタンスmem2からsexプロパティを変更しているので、大本のプロトタイプオブジェクトも書き換えられ、ソースコードの11行目は双方とも「女」になるように思えます。

しかし、結果は、インスタンスmem1の内容はそのままに、インスタンスmem2の内容だけが書き換えられています。

これは、プロトタイプオブジェクトが利用しているのは「値の参照時だけ」だからです。

値の設定時は、常にインスタンスに対して行われます。

プロパティ設定/参照の内部的な挙動を、もう少し詳しく見てみることにしましょう。

暗黙の参照(プロパティの設定)

まず、ソースコードの8行目の時点で、インスタンスmem1mem2はいずれもsexプロパティを持っていないので、プロトタイプオブジェクトがもつsexプロパティを「暗黙的に参照」します。

ところが、「mem2.sex = '女';」の時点で、インスタンスmem2sexプロパティが書き換えられると、「インスタンスmem2自身がsexプロパティを持つようになる」ので、インスタンスmem2はプロトタイプを参照する必要がなくなります。

結果、インスタンスmem2に設定されているsexプロパティが、参照されるわけです。(これを「インスタンスのsexプロパティが、プロトタイプのnameプロパティを隠蔽する」といいます。)

もちろん、この時点でもインスタンスmem1は、依然としてsexプロパティを持っていないので、そのまま暗黙の参照をたどって、プロトタイプオブジェクトで保有するsexプロパティを参照することになります。

このように、プロパティをプロトタイプで定義しても、動作上は「インスタンス個別にプロパティを保有しているように見える」ので、問題はありません。

ただし、本来、インスタンス単位で値が異なるはずのプロパティを、プロトタイプオブジェクトで宣言する意味はありません。

通常は、以下のように使い分けてください。

  • プロパティの宣言 コンストラクターで定義する
  • メソッドの宣言 プロトタイプで定義する

プロトタイプオブジェクトの不思議(プロパティの削除)

プロトタイプでメンバーを削除する場合はどうなるのでしょうか。

まずは実際の動作を見てみましょう。

class Member {
	
}

Member.prototype.sex = '男';

let mem1 = new Member();
let mem2 = new Member();

console.log(mem1.sex + '|' + mem2.sex);
// 結果:男|男
mem2.sex = '女';
console.log(mem1.sex + '|' + mem2.sex);
// 結果:男|女

delete mem1.sex;
delete mem2.sex;
console.log(mem1.sex + '|' + mem2.sex);
// 結果:男|男

この場合の結果(ソースコードの19行目)に注目です。

まずソースコードの16行目で、delete演算子は「インスタンスmem1の」sexプロパティを削除しようとします。

しかし、インスタンスmem1sexプロパティを持たないので、delete演算子は何も行いません。(=プロトタイプまで削除されることはありません。)

結果、 ソースコードの19行目でインスタンスmem1は、暗黙の参照をたどって、プロトタイプオブジェクトのsexプロパティを返します。

つまり、結果は「男」となります。

一方、ソースコードの17行目ではどうでしょうか。

今度は、インスタンスmem2が自分自身でsexプロパティを持つので、delete演算子はこれを削除します。

結果、ソースコードの19行目でインスタンスmem2は、(独自のプロパティ値を持たなくなったので)再び暗黙の参照をたどって、プロトタイプオブジェクトのsexプロパティ(つまり、「男」)を返すことになるわけです。

暗黙の参照(プロパティの削除)

繰り返しになりますが、

インスタンス側でメンバーの追加や削除といった操作が、プロトタイプオブジェクトにまで影響を及ぼすことはない

ということを、改めて押さえておきましょう。

インスタンス単位でプロトタイプのメンバーを削除する

もっとも、厳密には以下のように記述することで、プロトタイプオブジェクトのメンバーを削除することもできます。

delete Member.prototype.sex

ただし、このようなコードは、このプロトタイプを参照しているすべてのインスタンスに影響を及ぼす(=すべてのインスタンスのsexプロパティが削除されてします)点に注意してください。

もしも、プロトタイプで定義されたメンバーを「インスタンス単位で」削除したい場合、ややトリッキーではありますが、定数undefinedを用いる方法があります。

class Member {
	
}

Member.prototype.sex = '男';

let mem1 = new Member();
let mem2 = new Member();

console.log(mem1.sex + '|' + mem2.sex);
// 結果:男|男
mem2.sex = undefined;
console.log(mem1.sex + '|' + mem2.sex);
// 結果:男|undefined

ここでは、sexプロパティの値を定数undefinedで上書きすることで、疑似的にメンバーを無効化しているわけです。

ただし、この方法はあくまで「メンバーの存在自体はそのままに、値を強制的に未定義としている」にすぎません。

厳密にはメンバーを削除しているわけではないため、for…inループでオブジェクト内のメンバーを列挙した場合には、sexプロパティは依然として存在するものとして表示されることになります。

class Member {

}

Member.prototype.sex = '男';

let mem = new Member();
mem.sex = undefined;

for (let key in mem) {
  console.log(key + ':' + mem[key]);
  // 結果:sex:undefined
}

オブジェクトリテラルでプロトタイプを定義する

ドット演算子(.)を使って、プロトタイプにメンバーを追加するのは、構文的には正しい書き方です。

しかし、メンバー数が多くなってきた場合、コードが冗長にならざるを得ず、好ましい書き方とは言えません。

毎回「Member.prototype.~」という記述を繰り返すのは面倒ですし、そもそもオブジェクト名(ここではMember)が変更になった場合、すべての定義箇所を書き換えなければならないので、うれしくありません。

また、個々のメンバー定義が独立したブロックで記述されていることから、「どこからどこまでが同じオブジェクトのメンバー定義であるのか、一見して見えにく」という、可読性の問題もあります。

そこで登場するのが、オブジェクトのリテラル表現です。

// ドット演算子を使った場合
class Member {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
}

Member.prototype.getName = function () {
  return this.lastName + this.firstName;
};
Member.prototype.toString = function () {
  return this.lastName + this.firstName;
};
// リテラル表現を使った場合
class Member {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
}
Member.prototype = {
  getName : function () {
    return this.lastName + this.firstName;
  },
  toString : function () {
    return this.lastName + this.firstName;
  }
};

このように、オブジェクトリテラルを利用することで、

  • 「Member.prototype.~」のような記述を最低限に抑えられる
  • 結果、オブジェクト名に変更があった場合にも影響範囲を限定できる
  • 同一オブジェクトのメンバー定義が1つのブロックに納められているため、コードの可読性が向上する

といった効果があります。

バラバラに記述していたものに比べると、ずいぶんコードがすっきりしたことが確認できます。

通常、プロトタイプを定義する場合には、このようにリテラル表現を利用することをお勧めします。

ドット演算子とリテラル表現


コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です