順番に読み進めながら学べます

インスタンスメンバーと static — class が持つ値と処理

インスタンスではなく class そのものに値や処理を持たせる static を扱います。作った件数を数える static フィールド、JSON から作るファクトリメソッド、共通の定数、インスタンスメンバーとの使い分けを学べます。

配送伝票に 1 から順に番号を振るには、作った枚数をどこかに覚えておく必要があります。宛名は 1 枚ごとに違いますが、作った枚数や重さの上限は全伝票で 1 つあれば足ります。

この記事では、インスタンスではなく class そのものに値やメソッドを持たせる static を扱います。

伝票の枚数を数える — static フィールド

配送伝票に、作った順の番号を振るとします。クラスフィールドに count = 0; と書いて constructorthis.count += 1 と増やしてから番号に使っても、フィールドは new のたびに 0 から入り直すため、どの伝票の番号も 1 になります。

static フィールドstatic を付けて class の中に書き、class そのものが 1 つだけ持つプロパティ)は、Parcel.count のように class 名から読み書きします。

これに対し、this.number のようにインスタンスごとに持つプロパティと、static を付けずに書くメソッドをインスタンスメンバーと呼びます。

class Parcel {
  // class に 1 つだけある、これまでに作った伝票の枚数
  static count = 0;

  constructor(recipient) {
    Parcel.count += 1;               // class の count を 1 増やす
    this.number = Parcel.count;      // その時点の枚数を伝票番号にする
    this.recipient = recipient;
  }
}

// 2 枚作る
const first = new Parcel("青木 様");
const second = new Parcel("石田 様");

console.log(first.number, second.number);   // 1 2
console.log(Parcel.count);                  // 2
console.log(first.count);                   // undefined
count と伝票番号の置き場所
Parcel(class そのもの)
  • static count — 1 枚目の new で 1、2 枚目の new で 2
  • 読み書きは Parcel.count と書く
new で作ったインスタンス
first
  • number — 1(作った時点の count)
  • recipient — 「青木 様」
  • count は無い — first.countundefined
second
  • number — 2(作った時点の count)
  • recipient — 「石田 様」
count は class の枠に 1 つだけで、インスタンスの枠にはありません。インスタンスから読むと undefined です

2 回の new は、どちらも class の枠にある同じ count を 1 増やし、その時点の値を自分の number に写しています。後から count が 2 に増えても、first.number は作った時点の 1 のままです。

アンケートに届いた順で回答番号を付けます。answerTexts は宣言済みです。

① 回答内容を受け取り、全回答で 1 つの回答数を 1 増やして番号にする SurveyAnswer を定義してください。

② 3 件を SurveyAnswer にし、answers に入れてください。

③ 全件を「A-001 回答内容」の形で表示してください。

④ 「回答数: ◯」と「1 件目の番号: ◯」を表示してください。

(正しく実行できれば解説が表示されます)

JavaScript / TypeScript エディタ

コードを実行してください

JSON から伝票を作る — static メソッド

保存しておいた伝票を読み込むと、届くのは宛名と重さを並べた JSON 文字列です。読み込む画面ごとに JSON.parsenew Parcel(...) を書くと、JSON のキー名が変わったときに、その画面をすべて直すことになります。

JSON から伝票を作る時点では、まだインスタンスがありません。static メソッドstatic を付けて定義し、インスタンスを作らずに class 名から呼ぶメソッド)なら、Parcel.fromJSON(text) の形で呼べます。fromJSON のように、インスタンスを作って返す static メソッドをファクトリメソッドと呼びます。

class Parcel {
  constructor(recipient, weightKg) {
    this.recipient = recipient;
    this.weightKg = weightKg;
  }

  label() {
    return `${this.recipient}${this.weightKg} kg)`;
  }

  // JSON 文字列を受け取り、Parcel のインスタンスを作って返す
  static fromJSON(text) {
    const data = JSON.parse(text);
    return new Parcel(data.recipient, data.weightKg);
  }
}

const text = '{"recipient":"青木 様","weightKg":2}';
const parcel = Parcel.fromJSON(text);        // class 名から呼ぶ
console.log(parcel.label());                 // 青木 様(2 kg)
JSON 文字列が Parcel になるまで
Parcel.fromJSON(text) を呼ぶJSON.parse(text)で読むnew Parcel(…)で作って返すtext はただの文字列プロパティはあるがlabel() は無いlabel() を呼べるインスタンス
JSON.parse の段では、プロパティはあってもメソッドはありません。new を通った段で、label() を呼べるインスタンスになります

fromJSON の最後の行は new Parcel(...) なので、constructor の本体も普段どおり動きます。constructorthrow の検査を書いておけば、JSON から読み戻した伝票も、画面で入力した伝票と同じ検査を通ります。

fromJSON はインスタンスから呼べない

インスタンスから parcel.fromJSON(text) と呼ぶと、TypeError: parcel.fromJSON is not a function で止まります。label() と違い、static を付けたメソッドは class 名からしか呼べません。仕組みはプロトタイプの記事で扱います。

カートに保存した商品を、JSON 文字列から読み戻します。CartItem と savedTexts は宣言済みです。

① JSON 文字列から CartItem のインスタンスを作って返すメソッド fromJSON を追加してください。

② ① を使い、savedTexts の 2 件を CartItem にしてください。

③ 各商品の「商品名 × 数量 = 小計 円」を表示してください。

④ 小計の合計を「合計: ◯ 円」で表示してください。

JavaScript / TypeScript エディタ

コードを実行してください

全伝票で変えない値を置く — static 定数

重さの上限 30 kg は、伝票を作る前の入力フォームの案内にも、伝票ごとの残り重量の計算にも使います。上限をインスタンスのフィールドに置くと、フォームに案内を出すためだけに、宛名も重さも無い伝票を 1 枚作ることになります。

static 定数(途中で変えない値を持たせた static フィールド)は、static MAX_WEIGHT_KG = 30; のように名前をすべて大文字にし、単語を _ でつなぎます。class の外でもメソッドの中でも、Parcel.MAX_WEIGHT_KG と class 名を付けて読みます。

class Parcel {
  // 全伝票で共通の上限は、static で class に 1 つだけ置く
  static MAX_WEIGHT_KG = 30;

  constructor(weightKg) {
    this.weightKg = weightKg;
  }

  // 上限まであと何 kg 入れられるか
  remainingKg() {
    return Parcel.MAX_WEIGHT_KG - this.weightKg;
    // this.MAX_WEIGHT_KG - this.weightKg と書くと NaN が返る
  }
}

// インスタンスを作る前から読める
console.log(`1 個 ${Parcel.MAX_WEIGHT_KG} kg まで`);   // 1 個 30 kg まで
const parcel = new Parcel(12);
console.log(parcel.remainingKg());                      // 18
上限の読み方で結果が分かれる
static MAX_WEIGHT_KG= 30フォームの案内でParcel.MAX_WEIGHT_KGremainingKg の中でParcel.MAX_WEIGHT_KGremainingKg の中でthis.MAX_WEIGHT_KGインスタンス無しで30 を読める30 - 12を計算するインスタンスには無く undefined1 個 30 kg までと表示18 が返るundefined - 12で NaN が返る
左と中央の列は、インスタンスの有無に関係なく同じ 30 を読みます。this から読む右の列だけが NaN になります

右の列が NaN になるのは、メソッドの中の this がインスタンスを指し、static 定数はインスタンスの側に無いためです。undefined - 12 はエラーにならないので、表示された残り重量がおかしくなって初めて気づきます。

大文字の名前でも代入は止まらない

大文字の名前は「変えない値」を示す慣習で、Parcel.MAX_WEIGHT_KG = 50; と代入すると書き換わります。static get MAX_WEIGHT_KG() { return 30; } と getter だけを書けば、代入は TypeError で止まります(getter は次の記事で扱います)。

ユーザー名を文字数の規則で点検します。registeredNames は宣言済みです。

① Account に、全員共通の最小文字数 3 と最大文字数 12 を持たせてください。

② 名前の長さが範囲内かを返す isValidName を追加してください。

③ 全員を Account にし、「名前: OK」か「名前: NG」を表示してください。

④ 画面側のコードに最大文字数を 20 にする代入が紛れ込んだとして、③ と同じ形でもう一度表示してください。

JavaScript / TypeScript エディタ

コードを実行してください

値の置き場所を決める — static とインスタンス

static を覚えると、宛名のような値も class に置けば Parcel.recipient の 1 か所から読めるように見えます。伝票を 1 枚しか作らない動作確認では、static に置いても this に置いても同じ宛名が表示されるため、違いに気づけません。

置き場所は、値が 1 件ごとに違うかで決めます。this に入れた値は new のたびに作られるインスタンスごとに別々にあり、static の値は何枚作っても class に 1 つだけです。

// 宛名を static に置く
class SharedParcel {
  static recipient = "";
  constructor(recipient) { SharedParcel.recipient = recipient; }
  label() { return `宛名: ${SharedParcel.recipient}`; }
}

// 宛名をインスタンスに置く
class Parcel {
  constructor(recipient) { this.recipient = recipient; }
  label() { return `宛名: ${this.recipient}`; }
}

const sharedFirst = new SharedParcel("青木 様");
new SharedParcel("石田 様");
console.log(sharedFirst.label());   // 宛名: 石田 様

const first = new Parcel("青木 様");
new Parcel("石田 様");
console.log(first.label());         // 宛名: 青木 様
2 枚目の new が 1 枚目の宛名に届くか
static recipientに宛名を入れる2 枚目の new が石田 様 を代入1 つの recipientが上書きされるsharedFirst.label()は 宛名: 石田 様this.recipientに宛名を入れる2 枚目の new は別のインスタンス1 枚目の recipientは 青木 様 のままfirst.label()は 宛名: 青木 様
上の段では、2 枚目の宛名が class にある 1 つの値を上書きします。1 件ごとに違う値は、this に入れてインスタンスに持たせます

sharedFirst は自分の宛名を持たず、label() は呼んだ時点の SharedParcel.recipient を読むため、最後に代入された「石田 様」が出ます。下の表は、この記事の伝票で使ったメンバーの書き方と使い方を並べたものです。

持たせるもの書き方と使い方この記事の例
1 件ごとに違う値this に入れ、インスタンスから読むnumber / recipient / weightKg
1 件ごとの値を読むメソッドstatic を付けずに書き、インスタンスから呼ぶlabel() / remainingKg()
全件で 1 つの値static を付けて書き、class 名から読むcount / MAX_WEIGHT_KG
インスタンスが無くても呼ぶ処理static を付けて書き、class 名から呼ぶfromJSON(text)
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1static count = 0;Parcel から作った firstfirst.count を読むと?

Q2インスタンス parcel から static メソッドを parcel.fromJSON(text) と呼ぶと?

Q3宛名を static に置く class で「青木 様」「石田 様」の順に作ると、1 枚目の label() の宛名は?