自主的20%るぅる

各々が自主的に好き勝手書くゆるふわ会社ブログ

さすがにそろそろ JavaScript の new (あと継承も)について理解したいと思っているあなたに。

class がないのに new するってどいういうこと?

JavaScript 書いてますか?

JavaScript を書いているあなたはご存じかと思いますが、
あの言語には class がありません。

いや、正確には ES6 (最近の書き方) には class があります。
が、こちらを読んでいただいたら分かるように、

クラス

ECMAScript 2015 で導入された JavaScript クラスは、JavaScript にすでにあるプロトタイプベース継承の糖衣構文です。クラス構文は、新しいオブジェクト指向継承モデルを JavaScript に導入しているわけでは ありません 。 クラスは実は「特別な 関数」であり、 関数式と 関数宣言の 2 通りで関数が定義できるように、クラス構文には クラス式と クラス宣言 という 2 つの定義方法があります。 クラスを定義するひとつの方法は、 クラス宣言を使うことです。クラスを宣言するには、クラス名 (この例では “Rectangle”) 付きで class キーワードを使います。 class Rectangle { constructor(height, width) { this.height = height; this.width = width; } } ホイスティング(巻き上げ) 関数宣言と クラス宣言の重要な違いは、関数宣言では Hoisting されるのに対し、クラス宣言ではされないことです。クラスにアクセスする前に、そのクラスを宣言する必要があります。そうしないと、ReferenceError がスローされます: const p

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 となるように、
計算は必ず順序を考えなければなりません。

演算子の優先順位

演算子の優先順位は、演算子が評価される順番を決定します。より高い優先順位の演算子は最初に評価されます。 よくある例: 3 + 4 * 5 // 23 を返す 乗算演算子 (“*”) は加算演算子 (“+”) より高い優先順位を持つので、最初に評価されます。 結合性は、同じ優先順位の演算子を処理する順番を決定します。例えば、次のような式を考えてみましょう。 a OP b OP c 左結合性 (左から右) とはこれを (a OP b) OP c というように処理するという意味であり、対して右結合性 (右から左) はこれを a OP (b OP c) というように解釈するという意味です。代入演算子は右結合性なので、 a = b = 5; と書くことで、 a と b が値 5 を得るという期待通りの結果を得ることができます。これは代入演算子が代入した値を返すためです。まず b に 5 がセットされます。そして a にも、代入演算子の右オペランドである b = 5 が返す 5 がセットされるのです。 以下の表は優先順位の最も高いもの (20) から最も低いもの (0) の順に並べられています。

このページを参照してみると、

  • 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 ですが、
怖じ気づかず一つずつ理解していき、うまく使いこなせるようになりたいですね。

Let’s share this article!

{ 関連記事 }

{ この記事を書いた人 }

takato_ezaki
記事一覧