class がないのに new するってどいういうこと?
JavaScript 書いてますか?
JavaScript を書いているあなたはご存じかと思いますが、
あの言語には class がありません。
いや、正確には ES6 (最近の書き方) には class があります。
が、こちらを読んでいただいたら分かるように、
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Classes
Java にあるような、 class という考え方が追加されたわけではないです。
なので、 JavaScript の new は、class からインスタンスをつくるやつではないのです。
…と言う感じの話を聞かされて、特に Java に親しんでこられた方は
「new って class からインスタンス作るやつちゃうんか!? class ないのに new があるとかどういうことや!」
って、これまでなってた訳です。
分かりますよ。その気持ち。
今回は、そんな気持ちが解消できるように、
JavaScript の new について見ていこうじゃありませんか。
Java の new について
とりあえず、JavaScript の new についてお話してもいいのですが、
その前に、割となじみが深いと思われるクラスベースの言語における new から考えていきましょう。
クラスベースの言語で、よく使われている言語の一つが Java ですね。
import java.util.*;
class Hoge {
private String fuga;
public Hoge(String fuga) {
this.fuga = fuga;
}
public String getFuga() {
return this.fuga;
}
}
public class Main {
public static void main(String[] args) throws Exception {
Hoge hoge1 = new Hoge(”ふがふが”);
// ふがふがと出力される
System.out.println(hoge1.getFuga());
}
}
サンプルとしてシンプルにクラスを使ったらこんな感じですかね?
new をつかっているのは
Hoge hoge1 = new Hoge(”ふがふが”);
この部分。
イメージとしては、class はひな形であり、
new を使ってその実体をつくる。
これによって、同じ機能を持つ(今回の場合は fuga という変数を持ち、それを出力する機能)存在を複数作成することができるようになります。
このあたりは、プログラミングをやる上でよく見るクラスベースの考え方ですね。
JavaScript の new
先ほどと同じ事を JavaScript でやろうとすると、こんな感じ。
var Hoge = function (fuga) {
this.fuga = fuga;
};
Hoge.prototype.getFuga = function() {
return this.fuga;
};
var hoge = new Hoge(’ふがふが’);
// ふがふがと出力される
console.log(hoge.getFuga());
サンプルなんかでよく見る形になりましたね。
ここで大切なのは、
var hoge = new Hoge(’ふがふが’);
この部分。
ちょっと待って!
確か Hoge は最初に「function」を定義した変数では?
var Hoge = function (fuga) {
this.fuga = fuga;
};
そうなんです。
JavaScript では class はありませんで、function に対して new を使います。
さて、この処理の流れを具体的に見ていきましょう。
まず、気をつけなければならないのは、 new Hoge(’ふがふが’) は
new 演算子 の計算と、 Hoge(’ふがふが’) の関数実行に分けられる点です。
例えば 3 + 5 * 2 という式があれば 5 * 2 = 10 を先にして 10 + 3 = 13 となるように、
計算は必ず順序を考えなければなりません。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#Table
このページを参照してみると、
- new (引数を伴う): 優先順位 19
- 関数呼び出し: 優先順位 18
ということで、 new の方が優先順位が高いですね!
つまり、この処理では関数の呼び出しは置いておいて、 new Hoge を実行しなければなりません。
では new Hoge は何をしているのか?
ここは Java とほぼ同じです。
すなわち、 Hoge 関数をひな形として、実体(オブジェクト)を生成します。
Hoge の実体というと難しいですが、これは以下と ほぼ 同じです。
{}
何?と思われましたか?そうです。空のオブジェクトですね。
(ほぼ、というのは、実際には空ではないからです。その理由は後ほど。)
空のオブジェクトであればひな形も無いのでは?普通に空のオブジェクトつくれば良いじゃん。と思われるかと思いますが、
ここからが new の能力を発揮するところです。
new の能力 1: 指定された関数で初期化する
new は関数 Hoge を対象としてとっていましたね?
アレには意味がありまして、new は生成したオブジェクトの中で、指定された関数を実行します。
サンプルでは確か
var Hoge = function (fuga) {
this.fuga = fuga;
};
こうでしたね。
今注目するのは、この this の部分。
先ほど生成されたオブジェクトの中でこの関数を実行するので、
この this は先ほどの空のオブジェクトを指します。
つまり、イメージとしては
// 空オブジェクトを生成して
var test = {};
// そのオブジェクトに対して fuga を設定
test.fuga = fuga;
これと一緒です。
なぜって、 this = 先ほどの空のオブジェクトですからね。
つまり、一連の流れを整理すると new Hoge(’ふがふが’) が生成したものは
var test = {};
test.fuga = ’ふがふが’
こうなので、
{
fuga: ’ふがふが’
}
こんなオブジェクトができたことになります。
new の能力 2: プロトタイプを継承する
さて、new が何を作ったのかは分かりました。
でもまだ一つ解決していない問題がありますよね?
// ふがふがと出力される console.log(hoge.getFuga());
これで ふがふが と表示されると言うことがまだ説明できていません。
作られたオブジェクトが本当に fuga しか持っていないのであれば、 getFuga なんて関数はありません。
あのコードが正しく動くためには、
{
fuga: ’ふがふが’,
getFuga: function() { return this.fuga; }
}
こんなかんじで、getFuga 関数が同じオブジェクト内にいなければいけませんよね?
var Hoge = function (fuga) {
this.fuga = fuga;
};
Hoge.prototype.getFuga = function() {
return this.fuga;
};
var hoge = new Hoge(’ふがふが’);
console.log(hoge);
ためしに、最後の log 出力で hoge の中身そのものを見てみましょう。
Google Chrome のコンソールで動作を見てみると…
Hoge の中身に注目すると、一見 fuga プロパティしかないように見えます。
が、しかし __proto__ というよく分からないプロパティの中に、
getFuga 関数が格納されていることが確認できますね!
これが new したときの二つ目の能力です!
注目したいのはここ
Hoge.prototype.getFuga = function() {
return this.fuga;
};
prototype というプロパティの下に、 getFuga を定義していますね。
この prototype というプロパティは特殊で、
new で空オブジェクトがつくられた時、この配下の内容は新しく作られたオブジェクトに継承されます。
継承ですから、 Java でクラスを継承するように、
あたかも自分のオブジェクトに存在するかのように呼び出すことができます。
実際の所、今回のように getFuga() を呼び出した際、JavaScript の内部では
まず、自分自身に getFuga が定義されているかを確認し、存在すれば実行して終了
{
fuga: ’ふがふが’,
getFuga: function() { return this.fuga; }
}
getFuga が無ければ、 __proto__ 内に getFuga がないか確認、存在すれば実行して終了
{
fuga: ’ふがふが’,
__proto__: {
getFuga: function() { return this.fuga; }
}
}
さらに無ければ、 __proto__ 内の __proto__ 内に getFuga がないか確認、存在すれば実行して終了
{
fuga: ’ふがふが’,
__proto__: {
__proto__: {
getFuga: function() { return this.fuga; }
}
}
}
… これを繰り返していき、 proto がたどれない所(一番大本のオブジェクト)までたどり着いても getFuga が見つからなければ、未定義としてエラーになる
というような挙動で動いているのですね。
ちなみに、このように __proto__ をたどっていく挙動を、鎖をつなげていく様子に見立てて、「プロトタイプチェーン」と呼んでいます。
もしかしたら、この言葉についてはどこかで聞いたことがある方もいらっしゃるかもしれませんね。
このプロトタイプのおかげで、晴れて ”ふがふが” が出力されることになるのですね。
まとめ
どうですか?
Java 脳で考えるとちょっと難しく感じる JavaScript の new ですが、
どちらの new もやりたいことは、ひな形から新しい実態を生成するだけ…と考えても良いのではないでしょうか。
プロトタイプベースの考え方はぱっと見では難しそうですが、
順に紐解いていけば何と言うことはなく、クラスベースな考え方とちょこっとアプローチが異なるだけ、
と言うことを感じてもらえましたでしょうか。
ちょっと特殊な挙動も多い JavaScript ですが、
怖じ気づかず一つずつ理解していき、うまく使いこなせるようになりたいですね。
