クモのようにコツコツと

フロントエンドエンジニア イイダリョウのブログ。略称「クモコツ」

【JS】newとプロトタイプとクラス

JSに慣れてくるとよく目にすることになるnew演算子。ライブラリやフレームワークのコードでもよく見るので、ここらでちゃんと理解しておきたく思います。
また、かつて「JSはクラスかプロトタイプか」という論争がありましたが、そのどちらもこのnewが登場します。比較してみましょう。

※目次:

※参考:JSの基本についてはこちら
【JSの基本-前編】書ける前に読む!HTML、CSS、JSの書式-4 - クモのようにコツコツと
【JSの基本-後編】書ける前に読む!HTML、CSS、JSの書式-5 - クモのようにコツコツと

newとはなんぞや、がイメージしにくい

そもそもnewってなんなのよ?

new 演算子は、コンストラクタ関数を持ったユーザ定義のオブジェクト型または内蔵オブジェクト型のインスタンスを生成します。

※参考:new 演算子 - JavaScript | MDN

うむ、わからん。何かを生成することはわかった。で、なんだって?コンストラクタ関数?

最初に、JavaScriptにはクラスはありません。  
コンストラクタからインスタンスを生成します。

もし「new」を忘れたら、関数呼び出しになってしまって一大事です。

なんだって?JSにはクラスがない?コンストラクタからインスタンスを生成?
関数を呼び出すことが一大事?うーむ。。

※参考:JavaScriptのクラス?コンストラクタ?? - Qiita

初めて触れたプログラミング言語がJS、ましてやjQueryerだったり自分としては、比較対象となるJS以外の言語はないわけで、まずこうした「オブジェクト指向」の言葉の概念を知るところから旅が始まります。*1

JSはプロトタイプベース?クラスベース?

JSの仕様であるES(EcmaScript)、今は年一改訂でES2018などの西暦がついておりますが、かつてはナンバリングでした。
そのES6(2015年)からついにJSにもクラスが加わりました。当時、様々な反応がありました。

  • JSにクラスがやってくる!ヤァ!ヤァ!ヤァ!
  • JSのクラスは純粋なクラスではない。所詮はプロトタイプの糖衣構文であーる。
  • いや、そもそもJSは純粋なプロトタイプとはいえない。プロトタイプとクラスの鬼っ子である。

(その頃の自分はjQueryerだったので「クラスってなにそれ、おいしいの?」状態でした)

クラスベースとプロトタイプベースの違いを端的にいうと下記のような概念のようです。

クラスベースとプロトタイプベースの根本的な違いは「ユーザ定義型のデータ構造が静的か動的か」という点にあると考えています。  
つまりクラスは静的なユーザ定義型データ構造、プロトタイプは動的なユーザ定義型データ構造ということです。

※参考:ES6はクラスベース?プロトタイプベース? - maru source

定義型(先ほどのコンストラクタにあたるか)が静的か動的かの違いであり、それを元にインスタンスを生成するという特徴は共通であると、どうやらそういうものらしい。

newを使わない危険性

さて、newを使わないとどんな一大事が起こるのかを見ていきたい。
まずはこんなコードを作りました。ボタンを押してみてください。

See the Pen JS's new 01 by イイダリョウ (@i_ryo) on CodePen.

「ザクは通常の1倍です」「シャアザクは通常の3倍です」「グフはザクとは違うのだよ」というアラートがでましたね。

まず、外側にあるのはクリックのイベントリスナです。
HTMLにある#btnのタグと紐づいています。

var btn = document.getElementById('btn');
btn.addEventListener('click', function(e){
  
//クリックで実行される処理

}, false);

では処理の中身を見ていきましょう。こんなです。

var ms = {
    "name" : "ザク"
};

alert(ms.name + "は通常の1倍です");

var shar = ms;
shar.name = "シャアザク";
alert(shar.name + "は通常の3倍です");

var gufu = ms;
gufu.name = "グフ";
alert(gufu.name +  "はザクとは違うのだよ");
  • 変数msの中に連想配列が入り、msをオブジェクト化しています。nameキーの中にはザクという文字列が入っています。
  • alert()メソッドの中でmsオブジェクトのnameプロパティを呼び出しているので「ザクは通常の1倍です」と表示されます。
  • 次に変数sharの中にmsオブジェクトを代入しオブジェクト化しています。sharオブジェクトのnameプロパティにシャアザクを入れます。
  • alert()メソッドの中でsharオブジェクトのnameプロパティを呼び出しているので「シャアザクは通常の3倍です」と表示されます。
  • 次に変数gufuの中にmsオブジェクトを代入しオブジェクト化しています。 gufuオブジェクトのnameプロパティにグフを入れます。
  • alert()メソッドの中でgufuオブジェクトのnameプロパティを呼び出しているので「グフはザクとは違うのだよ」と表示されます。

シンプルな内容なので、イメージがしやすいかと思います。
さて、これを少しいじってみましょう。こんなです。またボタンを押してみてください。

See the Pen JS's new 02 by イイダリョウ (@i_ryo) on CodePen.

あらら?今度は「グフは通常の1倍です」「グフは通常の3倍です」「グフはザクとは違うのだよ」というアラートがでました。
全部「グフ」になっちゃいましたね。前の2つは間違った情報です。これは「一大事」です。いったいどうしてこうなった?

var ms = {
    "name" : "ザク"
};

var shar = ms;
shar.name = "シャアザク";

var gufu = ms;
gufu.name = "グフ";

//下に移動
alert(ms.name + "は通常の1倍です");
alert(shar.name + "は通常の3倍です");
alert(gufu.name +  "はザクとは違うのだよ");

先ほどとの違いはなんでしょうか。そう、alert()メソッドを全部下に移動してますね。
引数の中身は変わりません。msshargufuのオブジェクトを読み込んでいます。なのにnameプロパティはすべてグフになってしまいました。
どうやら、最後に書いたgufu.name = "グフ";によってmssharnameプロパティも上書きされてしまったようです。
これが「一大事」。このような変数の上書きを「グローバル汚染」といいます。

もう一つ「一大事」の例を作ってみました。ボタンを押してみてください。

See the Pen JS's new 03 by イイダリョウ (@i_ryo) on CodePen.

「ザクの名は シャアザクの名は グフの名は」
映画「君の名は」を意識してみましたが、偶然にも五七五になりました。日本人だねぇ。コードを見てみましょう。

var ms = {
    "name" : "ザク"
};

var zaku = function() {
alert(ms.name + "の名は");
}

zaku();

var shar = ms;
shar.name = "シャアザク";

zaku();

var gufu = ms;
gufu.name = "グフ";

zaku();

変数zakuに無名関数を入れてメソッド化しています。中身はmsオブジェクトのnameプロパティを読み込むので「ザクの名は」になるはずです。
このzaku()メソッドをsharオブジェクト、gufuオブジェクトの下で実行すると、先ほどの五七五になりました。
やはり、msオブジェクトのnameプロパティが上書きされていることがわかります。

プロトタイプベースのnew

さて、こうしたグローバル汚染がおこらないために、定義されたコンストラクタ(元の関数)からインスタンス(別の関数)を生成して、コンストラクタを汚染から守るのがnew演算子です。
コンストラクタが静的(クラスタイプベース)か動的(プロトタイプベース)かの違いはありますが、ともあれ、newが付いた書き方を見ていきましょう。
まずはプロトタイプから…

「ザクは通常の1倍です」「シャアザクは通常の3倍です」「グフはザクとは違うのだよ」アラートは最初と同じですね。
こんなコードです。

//コンストラクタ関数
function Ms (name, spec) {
  this.name = name;
  this.spec = spec;
}
//プロトタイプ拡張 
Ms.prototype.say = function() {
    alert(this.name + "は" + this.spec);
}

var zaku = new Ms("ザク", "通常の一倍");
zaku.say();
 
var shar = new Ms("シャアザク", "通常の三倍");
shar.say();
  
var gufu = new Ms("グフ", "ザクとは違う");
gufu.say();
  • 最初にコンストラクタ関数Msが定義されています。中はthis=Msnameプロパティ、specプロパティに同名の2つの引数を代入する構造です。
  • prototypeプロパティでMsのプロトタイプの機能を拡張しています。say()メソッドを新たに作り、中身は上の2つのプロパティを読み込んだアラートです。
  • 変数zakunewMsコンストラクタのインスタンスを生成。引数は「ザク」「通常の一倍」。
  • zakusayメソッドを実行。「ザクは通常の1倍です」
  • starも同様で「シャアザクは通常の3倍です」
  • gufuも同様で「グフはザクとは違うのだよ」

thisというのが新たにでてきましたが、jQueryでもよく出てくるのでjQueryerには馴染深いのでは?実行している関数自身を呼び出すこと多いです。

※参考:JavaScriptの「this」は「4種類」?? - Qiita

先ほどと同じくメソッドを下に移動するとどうなるでしょうか。先ほどは「グフ」に上書きされましたね。

「ザクは通常の1倍です」「シャアザクは通常の3倍です」「グフはザクとは違うのだよ」正しくでましたね!

//コンストラクタ関数
function Ms (name, spec) {
  this.name = name;
  this.spec = spec;
}
//プロトタイプ拡張 
Ms.prototype.say = function() {
    alert(this.name + "は" + this.spec);
}

var zaku = new Ms("ザク", "通常の一倍");
 
var shar = new Ms("シャアザク", "通常の三倍");
  
var gufu = new Ms("グフ", "ザクとは違う");

//下に移動
zaku.say();
shar.say(); 
gufu.say();

newで作られたインスタンスのMsは上のコンストラクタのMsの分身、複製コピーのようなものなので、コンストラクタは上書きされないわけです。

最後に、敢えてnewをつけ忘れてみるとどうなるでしょうか。

……シーン。返事がない。ただのしかばねのようだ。

//コンストラクタ関数
function Ms (name, spec) {
  this.name = name;
  this.spec = spec;
}
//プロトタイプ拡張 
Ms.prototype.say = function() {
    alert(this.name + "は" + this.spec);
}

//newをつけ忘れる
var zaku = Ms("ザク", "通常の一倍");
 
var shar = Ms("シャアザク", "通常の三倍");
  
var gufu = Ms("グフ", "ザクとは違う");

//下に移動
zaku.say();
shar.say(); 
gufu.say();

newがないと、一大事とまではいかなくとも、ちゃんと動かないようです。。

クラスベースのnew

さて、次はプロトタイプベースからクラスベースに書き換えてみましょう。ヤァ!ヤァ!ヤァ!

「ザクは通常の1倍です」「シャアザクは通常の3倍です」「グフはザクとは違うのだよ」挙動は同じですね。
コードは下記のようになります。

class Ms {
  //コンストラクタ
   constructor(name, spec) {
    this.name = name;
    this.spec = spec;
  }
  //メソッド 
  say() {
    alert(this.name + "は" + this.spec);
  }
}

var zaku = new Ms("ザク", "通常の一倍");
zaku.say();
 
var shar = new Ms("シャアザク", "通常の三倍");
shar.say();
  
var gufu = new Ms("グフ", "ザクとは違う");
gufu.say();
  • 全体をclassで包む。クラス名はMS
  • プロトタイプの時のコンストラクタ関数だったところはconstructor()メソッドになる。
  • その下のプロトタイプ拡張Ms.prototype.sayだったところは随分とすっきりしたsay()メソッド(このメソッド名は任意で付けられる)。
  • その下のnewインスタンス生成のところは変わらない。

こうしてみると書き方が少し変わって見やすくなった感じ。まだ静的、動的な違いの真髄は垣間見れない。

またメソッドを一番下に移動しましょう。

「ザクは通常の1倍です」「シャアザクは通常の3倍です」「グフはザクとは違うのだよ」大丈夫、上書きされません。

class Ms {
  //コンストラクタ
   constructor(name, spec) {
    this.name = name;
    this.spec = spec;
  }
  //メソッド 
  say() {
    alert(this.name + "は" + this.spec);
  }
}

var zaku = new Ms("ザク", "通常の一倍");
 
var shar = new Ms("シャアザク", "通常の三倍");
  
var gufu = new Ms("グフ", "ザクとは違う");

//下に移動
zaku.say();
shar.say();
gufu.say();

最後に、駄目元でnewを消してみる。

……シーン。返事がない。ただのしかばねのようだ。

class Ms {
  //コンストラクタ
   constructor(name, spec) {
    this.name = name;
    this.spec = spec;
  }
  //メソッド 
  say() {
    alert(this.name + "は" + this.spec);
  }
}

//newをつけ忘れ
var zaku = Ms("ザク", "通常の一倍");
 
var shar = Ms("シャアザク", "通常の三倍");
  
var gufu = Ms("グフ", "ザクとは違う");

//下に移動
zaku.say();
shar.say();
gufu.say();

すべてプロトタイプと同じ挙動でしたね。

まとめ

ということで、JSの中級者向けコードによくでてくるnewを理解すべく、グローバル汚染とその対策としてのプロトタイプ、クラス構文をいろいろ試してみました。大元のコンストラクタからインスタンスに継承することで、安全性だけでなく再利用性も高まるように思います。読み書きしながらだんだんと慣れていきましょう。


※参考:ネイティブJSやってみたシリーズ
qiita.com

*1:もっとマクロな視点で言えばオブジェクト指向以外のプログラム言語として、手続き型、関数型があります。オブジェクト指向の理解が先なのでまだ時ではない…