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

ポリモーフィズム — 同じ呼び出し、違う振る舞い

class ごとに同じ名前のメソッドを書き、呼ぶ側を 1 つの書き方にそろえるポリモーフィズムを扱います。種類の混ざった配列での一括呼び出し、instanceof で分ける場面、継承していない値も扱うダックタイピングを学べます。

問い合わせフォームの添付一覧で、画像なら縮小画像、動画なら再生ボタンを表示するとします。種類を文字列で持って表示する関数の if で分けていると、種類を足すたびにその関数を開いて書き足すことになります。

この記事では、同じ名前のメソッドで class ごとに違う処理を動かす ポリモーフィズム と、継承の関係を問わない ダックタイピング を扱います。

表示の違いを class に書く — 同じ名前のメソッド

添付ファイルを { kind: "video", fileName: "demo.mp4" } のようなオブジェクトで持ち、previewOf という関数の中で kind を比べているとします。動画を足すには、画像の枝が入っている previewOf を開いて書き足します。

ポリモーフィズム(別々の class に同じ名前のメソッドを持たせ、呼ぶ側は同じ書き方のまま、インスタンスを作った class の処理を動かすこと)では、種類ごとの表示を子クラスの preview に書きます。

子クラスごとに親の preview をオーバーライドしておくと、呼ぶ側はどの子クラスかを確かめずに preview() と書くだけです。

// 種類を文字列で持ち、表示する関数の if で分ける
function previewOf(file) {
  if (file.kind === "image") return `${file.fileName}: 縮小画像を表示`;
  return `${file.fileName}: プレビューなし`;
}
console.log(previewOf({ kind: "video", fileName: "demo.mp4" }));   // demo.mp4: プレビューなし

// 種類ごとの class に、同じ名前の preview を書く
class Attachment {
  constructor(fileName) { this.fileName = fileName; }
  preview() { return `${this.fileName}: プレビューなし`; }
}
class ImageFile extends Attachment {
  preview() { return `${this.fileName}: 縮小画像を表示`; }
}
class VideoFile extends Attachment {
  preview() { return `${this.fileName}: 再生ボタンを表示`; }
}
console.log(new ImageFile("logo.png").preview());   // logo.png: 縮小画像を表示
console.log(new VideoFile("demo.mp4").preview());   // demo.mp4: 再生ボタンを表示
動画の表示を足すときに開くコード
動画の表示を足すpreviewOfを開く画像の枝と同じ関数を編集画像の表示も書き換えうる動画の表示を足すclass VideoFileを書くImageFile は開かない画像の表示はそのまま
上の段は、画像の枝と同じ previewOf を書き換えます。下の段は VideoFile を書くだけで、ImageFile は開きません

1 つ目の console.log が「プレビューなし」になったのは、previewOf にまだ "video" の枝が無いためです。class の書き方では、動画を足す人は preview という名前をそろえるだけで、kind に入る文字列を覚えておく必要もありません。

決済画面で、支払い方法ごとに手数料を含めた支払額を出します。PaymentMethod は宣言済みです。

① PaymentMethod を引き継ぐ、コンビニ払いの class を定義してください。

② 同じく、代金引換の class を定義してください。

③ クレジットカード・コンビニ払い・代金引換を作り、4980 円の支払額を表示してください。

④ 代金引換で、12800 円の支払額を表示してください。

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

JavaScript / TypeScript エディタ

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

混ざった種類をまとめて呼ぶ — 配列と for...of

一覧には、画像や PDF、種類を判定できないファイルが 1 つの配列に混ざって届きます。1 件ずつ変数に入れて preview() を書き並べると、添付の件数が変わるたびに、呼ぶ行を増やしたり減らしたりすることになります。

種類の違うインスタンスも、1 つの配列にそのまま入れられます。for...of で回すと、本体の file.preview() は周ごとに別の class のインスタンスに対して動き、呼ぶ行は 1 行のままです。

PDF 用の PdfFile を足すときも、ループは書き換えません。

// Attachment と ImageFile は 1 つ目のサンプルと同じ定義
// 後から足した種類。ループの側は書き換えない
class PdfFile extends Attachment {
  preview() { return `${this.fileName}: 1 ページ目を表示`; }
}

const attachments = [
  new ImageFile("logo.png"),
  new PdfFile("manual.pdf"),
  new Attachment("memo.txt"),
];

// 本体は 1 行。file に入る値は、周ごとに違う class のインスタンス
for (const file of attachments) {
  console.log(file.preview());
}
// logo.png: 縮小画像を表示
// manual.pdf: 1 ページ目を表示
// memo.txt: プレビューなし
周ごとに動く preview の本体
for...of の本体file.preview()1 周目logo.png2 周目manual.pdf3 周目memo.txtImageFile のpreviewPdfFile のpreviewAttachment のpreviewlogo.png:縮小画像を表示manual.pdf:1 ページ目を表示memo.txt:プレビューなし
ループに書いた呼び出しは 1 行でも、周ごとに別の本体が動きます。動く preview は、その周の file を作った class で決まります

3 周目の memo.txtAttachment から作ったので、Attachment に書いた preview がそのまま動きます。子クラスで preview を書き忘れた種類もエラーにならずにこの表示になるため、種類を足したら一覧で表示を確かめます。

注文確認画面で、代金ごとに支払額が最も安い支払い方法を案内します。PaymentMethod・BankTransfer・PayLater と orderTotals は宣言済みです。

① 代金の 3% を手数料として足す、分割払いの class を定義してください。

② 3 つの支払い方法を 1 つの配列にまとめてください。

③ 代金ごとに、支払額が最も安い支払い方法を選んでください。

④ 選んだ支払い方法と支払額を表示してください。

JavaScript / TypeScript エディタ

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

その種類だけの処理を呼ぶ — instanceof で分ける

一覧のうち画像の行にだけ、切り抜きボタンを添えたいとします。ボタンを返す cropButtonImageFile にしか書かないため、ループで全件に file.cropButton() を呼ぶと、memo.txt の周で TypeError になって止まります。

instanceof は、左の値が右の class かその子クラスから作られていれば true を返します。file instanceof ImageFile を確かめて、true の周でだけ cropButton を呼びます。

どの class も持つ preview は、判定せずにそのまま呼びます。

class Attachment {
  constructor(fileName) { this.fileName = fileName; }
  preview() { return `${this.fileName}: プレビューなし`; }
}
class ImageFile extends Attachment {
  preview() { return `${this.fileName}: 縮小画像を表示`; }
  cropButton() { return `${this.fileName}: 切り抜きボタンを表示`; }   // ImageFile だけが持つ
}

const attachments = [new ImageFile("logo.png"), new Attachment("memo.txt")];

for (const file of attachments) {
  console.log(file.preview());          // どの class も持つので、判定せずに呼ぶ
  if (file instanceof ImageFile) {
    console.log(file.cropButton());     // ImageFile の周でだけ呼ぶ
  }
}
// logo.png: 縮小画像を表示
// logo.png: 切り抜きボタンを表示
// memo.txt: プレビューなし
cropButton を呼ぶ周の決め方
全件でfile.cropButton()2 周目の file はmemo.txtAttachment にcropButton が無いTypeError で止まるinstanceofImageFile で分けるmemo.txt の周はfalsecropButton を呼ばずに次へlogo.png だけボタンも表示
上の段は、cropButton の無い memo.txt の周で止まります。下の段は ImageFile の周でだけ呼びます

上の段で表示されるのは TypeError: file.cropButton is not a function です。preview まで instanceof で分けると previewOf と同じく種類ごとに判定が増えるので、分けるのは一部の class だけが持つメソッドに絞ります。

! と instanceof は括弧でまとめる

!file instanceof ImageFile は、先に !filefalse になり、false instanceof ImageFile を判定するため、結果はいつも false です。「ImageFile ではない」は !(file instanceof ImageFile) と括弧で囲んで書きます。

ポイント払いを選べる注文で、ポイント払いの行にだけ残りのポイントを添えます。PaymentMethod・PointPayment・methods と cartTotal は宣言済みです。

① 支払い方法を順に処理し、支払い方法ごとの支払額を表示してください。

② ポイント払いの行だけ、使った後に残るポイントを末尾に付けてください。

③ ポイント払い以外の支払い方法の数を表示してください。

JavaScript / TypeScript エディタ

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

継承していない値も並べる — ダックタイピング

一覧にフォルダのリンクも並べたいとします。リンクは preview() を持つオブジェクトリテラルで、Attachment を継承していません。preview の無い古いデータ { fileName: "old.doc" } を除こうと instanceof Attachment で確かめると、リンクまで false になり、古いデータと同じ「表示できません」側に回ります。

ダックタイピング(class や継承の関係は確かめず、必要なメソッドを持っているかだけで扱い方を決める書き方)では、typeof item.preview === "function" で判定します。

メソッドの呼び出しは、ドットの左の値がその名前の関数を持っていれば、class を問わずに動きます。

class Attachment {
  constructor(fileName) { this.fileName = fileName; }
  preview() { return `${this.fileName}: プレビューなし`; }
}

// class を使わずに作ったリンク。Attachment は引き継いでいない
const folderLink = {
  fileName: "見積書フォルダ",
  preview() { return `${this.fileName}: リンクを開く`; },
};
console.log(folderLink instanceof Attachment);   // false

// class ではなく、preview を関数として持っているかで決める
for (const item of [new Attachment("memo.txt"), folderLink, { fileName: "old.doc" }]) {
  if (typeof item.preview === "function") {
    console.log(item.preview());                   // memo.txt: プレビューなし / 見積書フォルダ: リンクを開く
  } else {
    console.log(`${item.fileName}: 表示できません`);   // old.doc: 表示できません
  }
}
folderLink が一覧に並ぶ条件
instanceofAttachmentclass から作っていないinstanceof はfalse見積書フォルダ:表示できませんtypeofitem.previewpreview 関数を持っているtypeof は"function"見積書フォルダ:リンクを開く
同じ folderLink でも、確かめ方で表示が分かれます。typeof は class を見ず、preview が関数かだけを見ます

instanceof で確かめないぶん、preview という名前で文字列を返さない関数を持つ値も通ります。一覧に並べる値は、preview() が表示用の文字列を返す形にそろえます。下の表は、この記事で使った 3 つの呼び方を並べたものです。

呼び方確かめているもの使う場面
item.preview() をそのまま呼ぶ何も確かめない配列の全件が preview を持つと決まっているとき
item instanceof ImageFile で分ける値を作った class が ImageFile かその子クラスかその class だけが持つメソッドを呼ぶとき
typeof item.preview === "function" で分けるpreview という名前の関数を持っているかclass を問わず、preview を持つ値を受け付けるとき

in はプロパティが関数でなくても true

"preview" in item は、{ preview: "準備中" } のように値が文字列でも true です。その値の item.preview()TypeError: item.preview is not a function で止まるので、呼ぶ前は typeof で関数かまで確かめます。

QUIZ

理解度チェック

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

Q1file.preview() を呼ぶループがある一覧に PdfFile を足すとき、書き足す場所は?

Q2Attachment から作った memo で、!memo instanceof ImageFile の結果は?

Q3preview() を持つオブジェクトリテラル linkinstanceof Attachmenttypeof link.preview は?