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()
メソッドを全部下に移動してますね。
引数の中身は変わりません。ms
、shar
、gufu
のオブジェクトを読み込んでいます。なのにname
プロパティはすべてグフ
になってしまいました。
どうやら、最後に書いたgufu.name = "グフ";
によってms
、shar
のname
プロパティも上書きされてしまったようです。
これが「一大事」。このような変数の上書きを「グローバル汚染」といいます。
もう一つ「一大事」の例を作ってみました。ボタンを押してみてください。
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
=Ms
のname
プロパティ、spec
プロパティに同名の2つの引数を代入する構造です。 prototype
プロパティでMs
のプロトタイプの機能を拡張しています。say()
メソッドを新たに作り、中身は上の2つのプロパティを読み込んだアラートです。- 変数
zaku
にnew
でMs
コンストラクタのインスタンスを生成。引数は「ザク」「通常の一倍」。 zaku
のsay
メソッドを実行。「ザクは通常の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:もっとマクロな視点で言えばオブジェクト指向以外のプログラム言語として、手続き型、関数型があります。オブジェクト指向の理解が先なのでまだ時ではない…