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

特殊メソッド — toString / toJSON / Symbol.iterator

決まった場面で JavaScript が呼ぶ toString・toJSON・Symbol.iterator を扱います。表示の形の決め方、JSON に出す値の選び方、class を for...of で回す書き方を学べます。

注文をテンプレートリテラルに入れると [object Object] と表示され、JSON.stringify では getter の合計が入らず、for...of では TypeError で止まります。

この記事では、特殊メソッド(JavaScript が決まった場面で名前を探して呼ぶメソッド)のうち、表示・JSON への変換・繰り返しで呼ばれる 3 つを扱います。

文字列に入れたときの表示を決める — toString

通知文に注文を入れようとして、受付: ${order} と書いたとします。表示されるのは注文番号でも金額でもなく [object Object] です。画面ごとに order.idorder.total を並べ直すと、同じ組み立てがあちこちに増えます。

toString(値を文字列にする場面で JavaScript が呼び、戻り値をその文字列として使うメソッド)は、${}String()・配列の join で呼ばれます。class に書かないと、チェーンの先の Object.prototype にある既定の toString[object Object] を返します。

// toString を書いていない class
class DraftOrder {
  constructor(id, total) { this.id = id; this.total = total; }
}

// toString を書いた class
class Order {
  constructor(id, total) { this.id = id; this.total = total; }
  toString() { return `注文 ${this.id}${this.total} 円)`; }
}
const draft = new DraftOrder("A-101", 3200);
const order = new Order("A-102", 4800);

// テンプレートリテラルに入れると、toString が呼ばれる
console.log(`受付: ${draft}`);           // 受付: [object Object]
console.log(`受付: ${order}`);           // 受付: 注文 A-102(4800 円)

// String() と配列の join も、同じ toString を使う
console.log(String(order));               // 注文 A-102(4800 円)
console.log([order, new Order("A-103", 1500)].join(" / "));   // 注文 A-102(4800 円) / 注文 A-103(1500 円)
${} が呼ぶ toString の行き先
受付: ${draft}DraftOrder.prototypeに toString は無いObject.prototypeで見つかる受付:[object Object]受付: ${order}Order.prototypeで見つかるOrder の toStringを呼ぶ受付: 注文 A-102(4800 円)
どちらも自身は toString を持たず、チェーンを探します。class に書くと、Object.prototype より先に見つかります

呼ぶ側の ${} は、2 つの段で同じです。表示の形を変えるときは class の toString を 1 か所直せば、埋め込んだすべての行に反映されます。console.log(order) のようにインスタンスをそのまま渡したときは、toString は呼ばれません。

駅前の案内表示に、発車するバスを並べます。BusDeparture と departures は宣言済みです。

① 「42 系統 [2 番のりば] 市立病院行き」の形で返すメソッドを書いてください。

② departures[0] を ${} に入れ、「まもなく発車: 」に続けて表示してください。

③ departures をそのまま「 / 」でつないで表示してください。

④ 2 件目の発車のりばを 3 番にし、${} に入れて表示してください。

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

JavaScript / TypeScript エディタ

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

保存する JSON に合計金額を含める — toJSON

注文をサーバーに送るため、JSON.stringify(order) で文字列にしたとします。明細を #lines に持ち、合計を getter の total で計算する class では、どちらもプロパティのキーに並ばないため、送られるのは {"id":"A-102"} だけです。

toJSONJSON.stringify が値を文字列にする前に呼び、その戻り値を代わりに文字列にするメソッド)を書くと、送りたい値を並べたオブジェクトを返せます。Date が JSON で日時の文字列になるのも、Date がこのメソッドを持つためです。

class Order {
  #lines;                                        // 明細(外からは読めない)
  constructor(id, lines) { this.id = id; this.#lines = lines; }
  get total() { return this.#lines.reduce((sum, line) => sum + line.price * line.quantity, 0); }

  // JSON.stringify のときに呼ばれ、戻り値が文字列になる
  toJSON() { return { id: this.id, itemCount: this.#lines.length, total: this.total }; }
}
const order = new Order("A-102", [{ price: 1200, quantity: 2 }, { price: 2400, quantity: 1 }]);
const other = new Order("A-103", [{ price: 1500, quantity: 1 }]);

// インスタンス自身のキーは id だけ
console.log(Object.keys(order).join(", "));   // id

// toJSON の戻り値が文字列になる
console.log(JSON.stringify(order));           // {"id":"A-102","itemCount":2,"total":4800}

// 配列に入れると、要素ごとに toJSON が呼ばれる
console.log(JSON.stringify([order, other]));
// [{"id":"A-102","itemCount":2,"total":4800},{"id":"A-103","itemCount":1,"total":1500}]
toJSON の有無と配列で変わる結果
JSON.stringifyに渡すtoJSON なしの ordertoJSON ありの order[order, other](toJSON あり)キーに並ぶid だけを読むtoJSON() の戻り値を読む要素ごとにtoJSON() を呼ぶ{"id":"A-102"}{"id":"A-102",…"total":4800}[{…"total":4800},{…"total":1500}]
toJSON が無いと、キーに並ぶ id しか読まれません。toJSON があると戻り値が文字列になり、配列の要素にも 1 件ずつ呼ばれます

#lines は外から読めないままで、JSON に出す値だけを class の中で選んでいます。itemCount のように、インスタンスがプロパティとして持っていない値も、戻り値のオブジェクトに足せば JSON に出せます。JSON.parse で戻すと、この形の普通のオブジェクトになり、Order のインスタンスには戻りません。

toJSON が文字列を返すと二重に変換される

return JSON.stringify({ id: this.id, total: this.total }); のように文字列を返すと、その文字列がもう一度 JSON に変換されます。JSON.parse で 1 回戻しても文字列のままで、.totalundefined です。オブジェクトを返します。

会員情報をサーバーに送る JSON から、カード番号を外します。Member・member・sentAt は宣言済みです。

① JSON にするとき、会員番号と名前だけを返すメソッドを書いてください。

② member をそのまま JSON 文字列にして表示してください。

③ sentAt と member を 1 つのオブジェクトにまとめ、JSON 文字列にして表示してください。

④ ③ の文字列にカード番号が含まれるかを表示してください。

JavaScript / TypeScript エディタ

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

明細を for...of で回す — Symbol.iterator

注文の明細を 1 行ずつ画面に並べたいとします。明細は #lines に隠してあるので外からは読めず、for (const line of order) と書くと TypeError: order is not iterable(反復可能ではない)で止まります。明細を公開の配列で持っていても、インスタンスそのものは回せません。

Symbol.iteratorfor...of が呼ぶメソッドの名前に使う、組み込みの重複しないキーの値)の名前でメソッドを書くと、for...of は戻り値の next() で 1 件ずつ受け取ります。

class では function* 名前()*名前() と書きます。名前を [Symbol.iterator] と角括弧で書くと、その値が名前です。

class Order {
  #lines;                                        // 明細(外からは読めない)
  constructor(id, lines) { this.id = id; this.#lines = lines; }
  // for...of が呼ぶメソッド。yield した明細が 1 件ずつ変数に入る
  *[Symbol.iterator]() {
    for (const line of this.#lines) yield line;
  }
}
const order = new Order("A-102", [{ name: "ドリップコーヒー", quantity: 2 }, { name: "マグカップ", quantity: 1 }]);

// class の外から、明細を 1 件ずつ受け取る
for (const line of order) {
  console.log(`${line.name} × ${line.quantity}`);      // ドリップコーヒー × 2 / マグカップ × 1 の 2 行
}

// 展開も、書くたびにメソッドを呼ぶので 2 回目も 2 件
console.log([...order].length, [...order].length);   // 2 2
書くたびに作られるジェネレータ
for (const lineof order)[Symbol.iterator]()を呼び 2 行を表示[...order]1 回目また呼ぶので2 件[...order]2 回目また呼ぶので2 回目も 2 件
3 つの書き方とも、その行で [Symbol.iterator]() を呼びます。2 回目の展開も、最初から 2 件です

読み切ったジェネレータが空になる点は、イテレータとジェネレータの記事のとおりです。呼ぶ側に渡るのは yield された明細だけで、#lines の配列そのものは渡らないので、外から明細を足したり消したりはできません。

配列をそのまま返すと for...of で止まる

return this.#lines; と配列を返すと、配列には next() が無いので for...ofTypeError: undefined is not a function で止まります。配列は反復可能ですが、イテレータではありません。* を付けて要素ごとに yield します。

カートを、中の配列を出さずに回せるようにします。Cart と cart は宣言済みです。

① for...of で商品を 1 件ずつ受け取れるようにしてください。

② cart をそのまま for...of に渡し、数量が 1 以上の商品を「商品名 × 数量」で表示してください。

③ cart をそのまま配列に展開し、合計金額を「合計: ◯ 円」で表示してください。

④ cart を分割代入して先頭の商品を取り出し、「先頭: 商品名」で表示してください。

JavaScript / TypeScript エディタ

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

書き忘れを結果の形から探す — 3 つのメソッド

表示や JSON が思った形と違っても、${order}JSON.stringify(order) の行には、呼ばれるメソッドの名前が書かれていません。どのメソッドを書き忘れたかは、呼ぶ側の書き方と結果の形から逆にたどって決めます。

3 つとも書いていない class のインスタンスでも、呼ぶ側の書き方は、それぞれ決まった名前をプロトタイプチェーンで探します。見つかるのは Object.prototype にある既定の toString だけで、toJSON[Symbol.iterator]null まで探しても見つかりません

// toString・toJSON・[Symbol.iterator] を 1 つも書いていない class
class DraftOrder {
  #lines;
  constructor(id, lines) { this.id = id; this.#lines = lines; }
  get total() { return this.#lines.reduce((sum, line) => sum + line.price * line.quantity, 0); }
}
const draft = new DraftOrder("A-101", [{ name: "ドリップコーヒー", price: 1200, quantity: 2 }]);

// toString は Object.prototype で見つかり、toJSON は無いので自身のキーだけを並べる
console.log(`${draft}`);              // [object Object]
console.log(JSON.stringify(draft));   // {"id":"A-101"}

// [Symbol.iterator] はどこにも無いので止まる
try {
  console.log([...draft]);
} catch (error) {
  console.log(error.message);         // draft is not iterable
}
何も書いていない draft の結果
${draft}JSON.stringify(draft)[...draft]toString を探すtoJSON を探す[Symbol.iterator]を探すObject.prototypeで見つかるnull まで探して見つからないnull まで探して見つからない[object Object]{"id":"A-101"}(total が出ない)TypeError: draftis not iterable
3 つの書き方は別々の名前をチェーンで探します。止まるのは [Symbol.iterator] だけで、残り 2 つは既定の結果を返します

JSON は止まらずに {"id":"A-101"} を返すので、#lines の明細と total が抜けたまま送っても、受け取った側で初めて気づきます。下の表は、同じ名前を探すほかの書き方と、書いていないときの結果を並べたものです。

呼ぶ側の書き方探すメソッド書いていないときの結果
JSON.stringify([draft])toJSON(要素ごと)[{"id":"A-101"}]
for (const line of draft)[Symbol.iterator]TypeError: draft is not iterable
const [firstLine] = draft[Symbol.iterator]TypeError: draft is not iterable
QUIZ

理解度チェック

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

Q13 つとも書いていない class の draft で、const [firstLine] = draft; を実行すると?

Q2toJSON{ id: this.id } を返す注文 2 件の配列を JSON.stringify すると?

Q3*[Symbol.iterator]() を持つ cart[...cart] を 2 回書くと、2 回目の長さは?