Q1file.preview() を呼ぶループがある一覧に PdfFile を足すとき、書き足す場所は?
ポリモーフィズム — 同じ呼び出し、違う振る舞い
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: 再生ボタンを表示
1 つ目の console.log が「プレビューなし」になったのは、previewOf にまだ "video" の枝が無いためです。class の書き方では、動画を足す人は preview という名前をそろえるだけで、kind に入る文字列を覚えておく必要もありません。
混ざった種類をまとめて呼ぶ — 配列と 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: プレビューなし
3 周目の memo.txt は Attachment から作ったので、Attachment に書いた preview がそのまま動きます。子クラスで preview を書き忘れた種類もエラーにならずにこの表示になるため、種類を足したら一覧で表示を確かめます。
その種類だけの処理を呼ぶ — instanceof で分ける
一覧のうち画像の行にだけ、切り抜きボタンを添えたいとします。ボタンを返す cropButton は ImageFile にしか書かないため、ループで全件に 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: プレビューなし
上の段で表示されるのは TypeError: file.cropButton is not a function です。preview まで instanceof で分けると previewOf と同じく種類ごとに判定が増えるので、分けるのは一部の class だけが持つメソッドに絞ります。
! と instanceof は括弧でまとめる
!file instanceof ImageFile は、先に !file が false になり、false instanceof ImageFile を判定するため、結果はいつも false です。「ImageFile ではない」は !(file instanceof ImageFile) と括弧で囲んで書きます。
継承していない値も並べる — ダックタイピング
一覧にフォルダのリンクも並べたいとします。リンクは 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: 表示できません
}
}
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 で関数かまで確かめます。
理解度チェック
まずは1問ずつ答えてみましょう。
Q2Attachment から作った memo で、!memo instanceof ImageFile の結果は?
Q3preview() を持つオブジェクトリテラル link の instanceof Attachment と typeof link.preview は?