[EffectiveJavaScript輪読会5]スーパークラスのコンストラクタは、サブクラスのコンストラクタから呼び出す

GAS

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

前回のおさらい

前回は、「インスタンスの状態は、インスタンスオブジェクトにだけ保存する」「thisの暗黙的な結合を理解しよう」を2本まとめてお届けしました。

今回は、「スーパークラスのコンストラクタは、サブクラスのコンストラクタから呼び出す」「スーパークラスのプロパティ名は、決して再利用しない」 を2本まとめてお届けします。

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

今日のアジェンダ

  • 完成したソースコード
  • インクリメント演算子の前置と後置
  • スーパークラスのプロパティ名は、決して再利用しない

完成したソースコード

書籍で紹介されていたクラスを、クラス構文に読み替えたものを置いておきます。

これまで、明示的なthis(callメソッドやObject.create()メソッド)は、クラス構文で解決してきました。

なので、今回も、その方向で読み解いていきます。

クラス構文の継承(extends、super())を、ぜひ復習しましょう。

加えて、問題の本質ではありませんので、ブラウザ上で動作するメソッドは、コンソールログ出力するなどして、読み替えてみました。

function myFunction05_38_01() {

  /** クラスScene */
  class Scene {

    constructor(context, width, height, images) {
      this.context = context;
      this.width = width;
      this.height = height;
      this.images = images;
      this.actors = [];
    }

    register(actor) {
      this.actors.push(actor);
    }

    unregister(actor) {
      const i = this.actors.indexOf(actor);
      if (i >= 0) this.actors.splice(i, 1);
    }

    draw() {
      // this.context.clearRect(0, 0, this.width, this.height);
      for (const a of this.actors) a.draw();
    }
  }


  /** クラスActor */
  class Actor {

    constructor(scene, x, y) {
      this.scene = scene;
      this.x = x;
      this.y = y;
      scene.register(this);
    }

    moveTo(x, y) {
      this.x = x;
      this.y = y;
      this.scene.draw();
    }

    exit() {
      this.scene.unregister(this);
      this.scene.draw();
    }

    draw() {
      const image = this.scene.images[this.type];
      console.log(`背景: ${image}とx: ${this.x}、y: ${this.y}にこのアクターをdrawしました`);
      //this.scene.context.drawImage(image, this.x, this.y);

    }

    width() {
      return this.scene.images[this.type].width;
    }

    height() {
      return this.scene.images[this.type].height;
    }

  }

  /** 
    * クラスSpaceShip
    * @extends Actor
    */
  class SpaceShip extends Actor {

    constructor(scene, x, y) {
      super(scene, x, y);
      this.points = 0;
      this.type = 'spaceShip';
    }

    scorePoint() {
      this.point++;
    }

    left() {
      this.moveTo(Math.max(this.x - 10, 0), this.y);
    }

    right() {
      const maxWidth = this.width - this.width();
      this.moveTo(Math.min(this.x + 10, maxWidth), this.y);
    }

  }

  const scene = new Scene('context', 1920, 1000, { spaceShip: 'image1' });
  const spaceShip = new SpaceShip(scene, 200, 100);
  
  scene.draw(); //背景: image1とx: 200、y: 100にこのアクターをdrawしました
  spaceShip.moveTo(10, 20); //背景: image1とx: 10、y: 20にこのアクターをdrawしました

}

インクリメント演算子の前置と後置

すこしばかり、初級講座の復習です。

インクリメント演算子には、前置と後置があります。

後置はよく使う記法で、変数の評価を先に行い、1を加えます。

一方、前置は、先に1を加えてから、評価を行います。

function myFunction5_39_00() {

  let a, b;

  //後置の場合、b=aをまず評価し、aに1を加える。
  a = 8;
  b;
  b = a++;
  console.log(a, b); //9,8

  a = 8;
  b;
  b = a;
  a = a + 1;
  console.log(a, b); //9,8

  //前置の場合、対象の変数を1だけ増加させて、b=aを評価する。
  a = 8;
  b;
  b = ++a;
  console.log(a, b); //9,9

  a = 8;
  b;
  a = a + 1;
  b = a;
  console.log(a, b); //9,9

}

スーパークラスのプロパティ名は、決して再利用しない

ActorクラスのインスタンスにIDを付与する

それでは、インスタンスにIDを付与する機能を実装してみましょう。

まず、Actorクラスにプロパティidを付与します。

プロパティidには、Actor.nextIDに前置でインクリメントした値を代入します。

  /** クラスActor  */
  class Actor {

    constructor(scene, x, y) {
      this.scene = scene;
      this.x = x;
      this.y = y;
      this.id = ++Actor.nextID;
      scene.register(this);
    }

  }

グローバル領域では、ActorオブジェクトにnextIDプロパティを定義し、初期化しています。

Actor.nextID = 0;

インスタンスを生成するごとに、IDが付与されます。

  const scene = new Scene('context', 1920, 1000, { spaceShip: 'image1' });

  const spaceShip1 = new SpaceShip(scene, 200, 100);
  console.log(spaceShip1.id); //1

  const spaceShip2 = new SpaceShip(scene, 200, 100);
  console.log(spaceShip2.id); //2

サブクラスのインスタンスにIDを付与する

さらに、サブクラスのインスタンスにもIDを付与してみましょう。

今回は、Actorクラスのサブクラスとして、Alienクラスを作成します。

  /** 
    * クラスAlien
    * @extends Actor
    */
  class Alien extends Actor {

    constructor(scene, x, y, direction, speed, strength) {
      super(scene, x, y);
      this.direction = direction;
      this.speed = speed;
      this.strength = strength;
      this.damage = 0;
      this.id = ++Alien.nextID;
    }

  }
  Alien.nextID = 0;

旧方式のクラスでは、ActorIDとAlienIDが重複してしまい、{id:1~4}ということで、IDが増えてしまいます。

なので、スーパークラスで使っているプロパティ名は、サブクラスでは使わないようにしましょう。

今回は、クラス構文で、スーパークラスでも、actorIdというユニークなプロパティ名にしました。

function myFunction5_39_02() {

  /** クラスScene */
  class Scene {

    constructor(context, width, height, images) {
      this.context = context;
      this.width = width;
      this.height = height;
      this.images = images;
      this.actors = [];
    }

    register(actor) {
      this.actors.push(actor);
    }

  }


  /** クラスActor  */
  class Actor {

    constructor(scene, x, y) {
      this.scene = scene;
      this.x = x;
      this.y = y;
      this.actorId = ++Actor.nextID;
      scene.register(this);
    }

  }
  Actor.nextID = 0;


  /** 
    * クラスSpaceShip
    * @extends Actor
    */
  class SpaceShip extends Actor {

    constructor(scene, x, y) {
      super(scene, x, y);
      this.points = 0;
      this.type = 'spaceShip';
    }

  }

  /** 
    * クラスAlien
    * @extends Actor
    */
  class Alien extends Actor {

    constructor(scene, x, y, direction, speed, strength) {
      super(scene, x, y);
      this.direction = direction;
      this.speed = speed;
      this.strength = strength;
      this.damage = 0;
      this.alienId = ++Alien.nextID;
    }

  }

  Alien.nextID = 0;


  const scene = new Scene('context', 1920, 1000, { spaceShip: 'image1' });

  const spaceShip1 = new SpaceShip(scene, 200, 100);
  console.log(spaceShip1.actorId); //1

  const spaceShip2 = new SpaceShip(scene, 200, 100);
  console.log(spaceShip2.actorId); //2


  const alien1 = new Alien(scene, 200, 100);
  console.log(alien1.alienId); //1

}

まとめ

以上で、「スーパークラスのコンストラクタは、サブクラスのコンストラクタから呼び出す」「スーパークラスのプロパティ名は、決して再利用しない」を2本まとめてお届けしました。

第5回目の輪読会は、ここまでです。

次回輪読会をお楽しみに。

このシリーズの目次

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