[EffectiveJavaScript輪読会5]newに依存しないコンストラクタの作り方

GAS

どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。

前回のシリーズでは、第3章終わりと、第4章始めの考察をお届けしました。

引き続き、第4章をお送りします。

目次と日程

第4章後半は「オブジェクトとプロトタイプ」です。クラスベースではなく、プロトタイプベースを採用した、JavaScriptのアイデンティティを追及する日々です。

  • 第1章 JavaScriptに慣れ親しむ
  • 第2章 変数のスコープ
  • 第3章 関数の扱い
  • 第4章 オブジェクトとプロトタイプ
  • 第5章 配列とディクショナリ
  • 第6章 ライブラリとAPI設計
  • 第7章 並行処理

LT大会は2021年10月31日です。がんばりましょう。

今日はさっそく1回目で、「newに依存しないコンストラクタの作り方」 をお届けします。

テキスト第4章「オブジェクトとプロトタイプ」の項目33に対応しています。

今日のアジェンダ

  • new演算子を付けわすれると
  • new演算子を忘れてもいいコードにする
  • 可変長引数に対応する

new演算子を付けわすれると

おさらいもかねて、コンストラクタであるUser関数を定義して、呼び出します。

コンストラクタ関数は、new演算子で呼び出すことを前提としています。

作成したオブジェクト(インスタンス)に、名前とパスワードハッシュをプロパティとして格納します。

コンストラクタ関数Userのレシーバは、新しく生成されたオブジェクトです。

function myFunction5_33_01() {

  /**
    * コンストラクタ 
    * @param {string} name 名前
    * @param {string} passwordHash ハッシュ
   */
  function User(name, passwordHash) {
    this.name = name;
    this.passwordHash = passwordHash;
  }

  const u = new User('tsujike', 'hogehoge');
  console.log(u); //{ name: 'tsujike', passwordHash: 'hogehoge' }
  console.log(u.name); //tsujike

}

しかし、コンストラクタ関数に、new演算子をつけ忘れると、レシーバがグローバルオブジェクトになります。

これは、単に関数Userが実行され、関数User内に記述されているthisが、グローバルオブジェクトを示すからです。

グローバルオブジェクトに、nameとpasswodHashというプロパティを追加してしまう、最悪なグローバル汚染になります。

function myFunction5_33_02() {

  /**
    * コンストラクタ 
    * @param {string} name 名前
    * @param {string} passwordHash ハッシュ
   */
  function User(name, passwordHash) {
    this.name = name;
    this.passwordHash = passwordHash;
  }

  const u = User('tsujike', 'hogehoge');
  console.log(u); //undefined
  console.log(this.name); //tsujike

}

use strictモードでは、グローバルオブジェクトへの結合を禁止する(undefinedを結合する)はずですが、new演算子を付けないコンストラクタ関数の呼び出しは、例外をはくようです。

function myFunction5_33_03() {

'use strict'

  /**
    * コンストラクタ 
    * @param {string} name 名前
    * @param {string} passwordHash ハッシュ
   */
  function User(name, passwordHash) {
    this.name = name;
    this.passwordHash = passwordHash;
  }

  const u = User('tsujike', 'hogehoge');
  //TypeError: Cannot set property 'name' of undefined

}

new演算子を忘れてもいいコードにする

まぁ、コンストラクタ関数を呼び出すときは、new演算子を忘れないでねという話です。

しかし、忘れることもあるので、new演算子を忘れても動くようにしてみよう、というのが書籍の狙いです。

方法として、「レシーバの値が、Userのインスタンスとして正しいものかチェックする」というアプローチです。

一致しなければ、newしようというテクニックです。

function myFunction5_33_04() {

  /**
    * コンストラクタ 
    * @param {string} name 名前
    * @param {string} passwordHash ハッシュ
   */
  function User(name, passwordHash) {

    if (!(this instanceof User)) {
      return new User(name, passwordHash);
    }

    this.name = name;
    this.passwordHash = passwordHash;

  }

  const u = User('tsujike', 'hogehoge');
  console.log(u); //undefined
  console.log(this.name); //undefined
  console.log(u.name); //tsujike

}

このような書き方をすれば、newをつけても(コンストラクタとして呼び出しても)つけ忘れても(普通の関数として呼び出しても)、必ずUser.prototypeを継承したオブジェクトになります。

function myFunction5_33_05() {

  /**
    * コンストラクタ 
    * @param {string} name 名前
    * @param {string} passwordHash ハッシュ
   */
  function User(name, passwordHash) {

    if (!(this instanceof User)) {
      return new User(name, passwordHash);
    }

    this.name = name;
    this.passwordHash = passwordHash;

  }

  const funcU = User('tsujike', 'hogehoge');
  const constructorU = new User('tsujike', 'hogehoge');

  console.log(funcU instanceof User); //true
  console.log(constructorU instanceof User); //true

}

可変長引数に対応する

この、newを忘れてもいいよverのコンストラクタ関数は、可変長引数に対応できません。(returnに記述しているnew演算子付きの関数呼び出しの引数の話です)

レシーバを指定するapplyメソッドに代わるものが無いからです。

なので、ES5から実装された、Object.create()メソッドを使うテクニックがあります。

function myFunction5_33_06() {

  /**
    * コンストラクタ 
    * @param {string} name 名前
    * @param {string} passwordHash ハッシュ
   */
  function User(name, passwordHash) {

    const self = this instanceof User
      ? this
      : Object.create(User.prototype);

    self.name = name;
    self.passwordHash = passwordHash;
    return self;
  }

  const funcU = User('tsujike', 'hogehoge');
  const constructorU = new User('tsujike', 'hogehoge');

  console.log(funcU); //{ name: 'tsujike', passwordHash: 'hogehoge' }
  console.log(constructorU); //{ name: 'tsujike', passwordHash: 'hogehoge' }

}

書籍では、このように書かれていました。

コンストラクタはnewを忘れてもいいような書き方で待ち受けておこう new演算子で呼ばれるべきなら、ドキュメントなどで注意を促そう

が、しかし、クラス構文で書くことと、「new演算子は必ず付ける!」という、コーディングガイドラインでいいと感じました。。。

まとめ

以上で、「newに依存しないコンストラクタの作り方」をお届けしました。

次回は、「メソッドをプロトタイプに格納しよう」「プライベートデータの格納にはクロージャを使おう」 の2本をお届けします。

このシリーズの目次

タイトルとURLをコピーしました