2021年10月16日

コンストラクタ、 new 演算子

通常の {...} 構文では、1つのオブジェクトを作成できます。しかし、複数のユーザやメニューアイテムなど、似たようなオブジェクトを多数作成する必要がある場合もあります。

このようなことは、コンストラクタ関数と "new" 演算子を使うことで実現できます。

コンストラクタ 関数

コンストラクタ関数は技術的には通常の関数です。それには2つの慣習があります:

  1. 名前は大文字で始めます。
  2. "new" 演算子を使ってのみ実行されるべきです。

例:

function User(name) {
  this.name = name;
  this.isAdmin = false;
}

let user = new User("Jack");

alert(user.name); // Jack
alert(user.isAdmin); // false

new 演算子を用いて関数が実行された場合、次のような処理が行われます:

  1. 新しい空のオブジェクトが作成され、 this に割り当てられます。
  2. 関数本体を実行します。通常は新しいプロパティを追加することで this に変更を加えます。
  3. this の値が返されます。

つまり、new User(...) は次のようなことを行います:

function User(name) {
  // this = {};  (暗黙)

  // this へプロパティを追加
  this.name = name;
  this.isAdmin = false;

  // return this;  (暗黙)
}

したがって、let user = new User("Jack") は、以下と同じ結果となります:

let user = {
  name: "Jack",
  isAdmin: false
};

もし他のユーザを作りたいのであれば、new User("Ann")new User("Alice") と言ったように呼び出すことができます。毎回リテラルを使うよりはるかに短く、また読みやすくなります。

再利用可能なオブジェクト作成のコードを実装すること、それがコンストラクタの主な目的です。

改めて留意しておきましょう。技術的にはどのような関数(this を持たないアロー関数を除く)でもコンストラクタとして使用できます。つまり、どの関数も new で実行することができ、上記のアルゴリズムが実行されることになります。“先頭が大文字” というのは、関数が new で実行されることを明確にするための共通の合意です。

new function() { … }

1つの複雑なオブジェクトを作成するためのコードがたくさんある場合、次のように即座に呼び出されるコンストラクタ関数でラップすることができます:

let user = new function() {
  this.name = "John";
  this.isAdmin = false;

  // ...ユーザ作成のための他のコード。
  // 複雑なロジック、文
  // ローカル変数などを持つかもしれません。
};

このコンストラクタはどこにも保存されておらず、単に作って呼び出されただけなので再び呼び出すことはできません。したがってこのトリックは、将来の再利用は考えず、単一のオブジェクトを構成するコードをカプセル化することを目的としています。

コンストラクタの呼び出しモードの確認: new.target

高度な内容

このセクションで登場する構文はめったに使われないため、すべてについて知りたいのでなれけばスキップしても構いません。

関数の中では、new.target という特別なプロパティを使うことで、new を使って呼び出されたのかそうでないのかを確認することができます。

このプロパティは、通常の呼び出しでは undefined であり、 new で呼び出された場合はその関数と等しくなります:

function User() {
  alert(new.target);
}

// new なし:
User(); // undefined

// new あり:
new User(); // function User { ... }

これを関数内で使用することにより、new を付けて「コンストラクタモード」で呼び出されたのか、あるいは new を付けずに「通常モード」で呼び出されたのかを知ることができます。

new の場合と、通常呼び出し両方の構文が同じように動作するようにするために使用することもできます:

function User(name) {
  if (!new.target) { // new なしで実行した場合
    return new User(name); // ...new を追加します
  }

  this.name = name;
}

let john = User("John"); // new User へのリダイレクト
alert(john.name); // John

このアプローチは、構文をより柔軟にするためにライブラリ中で使われることがあります。new の有無に関わらず同じ動作をするような、関数の呼び出しを可能とするためです。

new を省略すると何が起こっているのかが少しわかりにくくなるため、どこへでもこれを使うのは良いことではないでしょう。new があれば、新しいオブジェクトが作られることを知ることができます。

コンストラクタからの返却

通常、コンストラクタは return 文を持ちません。コンストラクタの仕事は、必要なものをすべて this の中に書き込むことであり、this が自動的に戻り値となります。

しかし、return 文がある場合のルールはシンプルです:

  • もし return がオブジェクトと一緒に呼ばれた場合、this の代わりにオブジェクトが返されます。
  • もし return がプリミティブと一緒に呼ばれた場合、それは無視されます。

言い換えると、オブエジェクトのreturn はそのオブジェクトを返し、それ以外のケースでは this が返されます。

例えば、ここで return はオブジェクトと共に呼ばれているため、this より優先してこのオブジェクトが返されます:

function BigUser() {

  this.name = "John";

  return { name: "Godzilla" };  // <-- オブジェクトを返す
}

alert( new BigUser().name );  // Godzilla, オブジェクトを取得 ^^

そして、これは空の return の例です(return の後にプリミティブを置いた場合も同じです)

function SmallUser() {

  this.name = "John";

  return; // <-- returns this
}

alert( new SmallUser().name );  // John

通常、コンストラクタは return 文を持ちません。ここで、オブジェクトを返すときの特別な動作について言及しているのは、主に網羅性のためです。

丸括弧の省略

ところで、もし引数を持たない場合は、new の後の丸括弧を省略することもできます。

let user = new User; // <-- 括弧なし
// これと同じ
let user = new User();

丸括弧の省略は “良いスタイル” ではありませんが、この構文は仕様で認められています。

コンストラクタの中のメソッド

オブジェクトを作るとき、コンストラクタ関数を使用することで高い柔軟性を得ることができます。コンストラクタ関数はオブジェクトがどのように組み立てられるか、その中に何を置くかを定義するパラメータを持っています。

もちろん、this にはプロパティだけではなく、同様にメソッドも追加することができます。

例えば、下の new User(name) は、namesayHi メソッドを持つオブジェクトを作ります:

function User(name) {
  this.name = name;

  this.sayHi = function() {
    alert( "My name is: " + this.name );
  };
}

let john = new User("John");

john.sayHi(); // My name is: John

/*
john = {
   name: "John",
   sayHi: function() { ... }
}
*/

複雑なオブジェクトを作るために、より高度な構文 classes があります。これに関しては後ほど説明します。

サマリ

  • コンストラクタ関数(もしくは簡潔にコンストラクタ)は通常の関数ですが、大文字から始まる名前を持つと言う共通の合意があります。
  • コンストラクタ関数は new を使ってのみ呼び出されるべきです。この呼び出しは、最初に空の this を作成し、必要な処理が行われた this を最後に返すことを意味します。

複数の似たようなオブジェクトを作るために、コンストラクタ関数を使うことができます。

JavaScript には、日付を表す Date や集合を表す Set、そしてこのあと私たちが学ぶ予定のものなど、多くの組み込みオブジェクトに対するコンストラクタ関数が用意されています。

オブジェクト、我々は戻ってきます!

このチャプターでは、オブジェクトとコンストラクタについての基礎のみを説明しています。これらは、次の章でデータ型と関数についてより深く学ぶために不可欠です。

それらを学んだあとオブジェクトに戻り、プロトタイプ, 継承クラス の各章で詳しく説明します。

タスク

重要性: 2

new A()==new B() のような関数 AB を作ることはできるでしょうか?

function A() { ... }
function B() { ... }

let a = new A;
let b = new B;

alert( a == b ); // true

もしできるなら、そのコード例を書いてみてください。

はい、可能です。

もし関数がオブジェクトを返す場合、newthis の代わりにそれを返します。

なので、例えば、同じ外部で定義されたオブジェクト obj を返すことで実現できます。:

let obj = {};

function A() { return obj; }
function B() { return obj; }

alert( new A() == new B() ); // true
重要性: 5

3つのメソッドをもつオブジェクトを作るコンストラクタ関数 Calculator を作りなさい:

  • read()prompt を使って2つの値を訪ね、オブジェクトプロパティの中でそれを覚えます。
  • sum() はそれらのプロパティの合計を返します。
  • mul() はそれらのプロパティの掛け算結果を返します。

例:

let calculator = new Calculator();
calculator.read();

alert( "Sum=" + calculator.sum() );
alert( "Mul=" + calculator.mul() );

デモを実行

テストと一緒にサンドボックスを開く

function Calculator() {

  this.read = function() {
    this.a = +prompt('a?', 0);
    this.b = +prompt('b?', 0);
  };

  this.sum = function() {
    return this.a + this.b;
  };

  this.mul = function() {
    return this.a * this.b;
  };
}

let calculator = new Calculator();
calculator.read();

alert( "Sum=" + calculator.sum() );
alert( "Mul=" + calculator.mul() );

サンドボックスでテストと一緒に解答を開く

重要性: 5

コンストラクタ関数 Accumulator(startingValue) を作りなさい。

作成するオブジェクトは:

  • “現在の値” をプロパティ value に格納します。開始値はコンストラクタ startingValue の引数がセットされます。
  • read() メソッドは prompt を使って新しい値を読み込み、value に加算します。

つまり、value プロパティーはユーザーが入力したすべての値と初期値 startingValue の合計です。

これはそのコードのデモです:

let accumulator = new Accumulator(1); // 初期値 1
accumulator.read(); // ユーザの入力値の加算
accumulator.read(); // ユーザの入力値の加算
alert(accumulator.value); // それらの値の合計を表示

デモを実行

テストと一緒にサンドボックスを開く

function Accumulator(startingValue) {
  this.value = startingValue;

  this.read = function() {
    this.value += +prompt('How much to add?', 0);
  };

}

let accumulator = new Accumulator(1);
accumulator.read();
accumulator.read();
alert(accumulator.value);

サンドボックスでテストと一緒に解答を開く

チュートリアルマップ