Q1class に書いた summary で、jsCourse.hasOwnProperty("summary") は?
プロトタイプ — class の裏側
class のメソッドが置かれる prototype と、名前を探す順番のプロトタイプチェーンを扱います。Object.getPrototypeOf、hasOwnProperty と in の違い、関数の足し方を学べます。
class から作った講座は summary() を呼べるのに、インスタンス自身はそのメソッドを持っていません。置き場所が分からないと、子クラスから親クラスのメソッドまで呼べる理由も説明できません。
この記事では、class のメソッドが入る prototype と、名前を探す順番の プロトタイプチェーン を扱います。
インスタンスに無いメソッドを呼ぶ — prototype
講座を 300 件作るとき、summary の本体もインスタンスごとに 300 個作られるのかが気になります。Object.keys(jsCourse) を表示しても summary は並ばず、呼べるメソッドがどこに置かれているのかは分かりません。
プロトタイプ(オブジェクトが継承する別オブジェクトへの参照)は Object.getPrototypeOf() で取り出せ、jsCourse では Course.prototype です。自分に無い名前はここから探します。
class の prototype プロパティも同じオブジェクトですが、jsCourse.prototype は undefined です。
class Course {
constructor(title, hours) {
this.title = title; // 講座名
this.hours = hours; // 学習時間
}
summary() { return `${this.title}(${this.hours} 時間)`; }
}
const jsCourse = new Course("JavaScript 入門", 12);
const sqlCourse = new Course("SQL 入門", 8);
// インスタンス自身のキーに summary は無い
console.log(Object.keys(jsCourse).join(", ")); // title, hours
// jsCourse のプロトタイプは Course.prototype(jsCourse.prototype ではない)
console.log(Object.getPrototypeOf(jsCourse) === Course.prototype); // true
console.log(jsCourse.prototype); // undefined
// 2 件とも、Course.prototype にある 1 つの summary を使う
console.log(jsCourse.summary === sqlCourse.summary); // true
console.log(sqlCourse.summary()); // SQL 入門(8 時間)
本体は 1 つでも、this には呼んだ行のドットの左が入るので、sqlCourse.summary() は sqlCourse の値で文字列を作ります。講座を 300 件作っても、増えるのは title と hours を持つインスタンスだけです。
null に着くまで順に探す — プロトタイプチェーン
ライブ配信の講座を class LiveCourse extends Course で作ると、liveCourse.summary() も動きます。ところが liveCourse のプロトタイプ LiveCourse.prototype に書いたメソッドは schedule だけです。
名前は、プロトタイプチェーン(プロトタイプのプロトタイプを null までたどった参照の列)をインスタンス側から探します。extends は LiveCourse.prototype の次を Course.prototype にし、その次は Object.prototype(チェーンの最後に入る組み込みのオブジェクト)です。
class Course {
constructor(title, hours) { this.title = title; this.hours = hours; }
summary() { return `${this.title}(${this.hours} 時間)`; }
}
class LiveCourse extends Course { schedule() { return "毎週火曜 20 時から"; } }
const liveCourse = new LiveCourse("React 実践", 10);
// プロトタイプを 1 段ずつ取り出す
const step1 = Object.getPrototypeOf(liveCourse);
const step2 = Object.getPrototypeOf(step1);
const step3 = Object.getPrototypeOf(step2);
console.log(step1 === LiveCourse.prototype); // true
console.log(step2 === Course.prototype); // true
console.log(step3 === Object.prototype); // true
console.log(Object.getPrototypeOf(step3)); // null(チェーンの終わり)
// summary は 2 段目の Course.prototype で見つかる
console.log(liveCourse.summary()); // React 実践(10 時間)
チェーンに入るのは Course.prototype で、Course そのものは入りません。static を付けたメソッドは Course に置かれるため、インスタンスから読むと null まで探しても見つからず undefined になり、呼ぶと TypeError で止まります。
インスタンスの同名キーが先に見つかる
liveCourse.summary = "準備中"; と代入すると、名前は liveCourse 自身で見つかり、プロトタイプまで探されません。liveCourse.summary() は TypeError: liveCourse.summary is not a function で止まります。
インスタンス自身のキーに絞る — hasOwnProperty
受け取った講座データで、summary がその講座だけに代入されて上書きされていないかを確かめたいとします。"summary" in jsCourse は、プロトタイプにあるメソッドでも true になるため、インスタンス自身に代入されたかを区別できません。
hasOwnProperty(オブジェクト自身がそのキーを持つときだけ true を返すメソッド)は、jsCourse.hasOwnProperty("title") のようにキーを文字列で渡します。このメソッド自体は Object.prototype にあり、チェーンで見つかります。
class Course {
constructor(title, hours) { this.title = title; this.hours = hours; }
summary() { return `${this.title}(${this.hours} 時間)`; }
}
const jsCourse = new Course("JavaScript 入門", 12);
const sqlCourse = new Course("SQL 入門", 8);
sqlCourse.summary = "準備中"; // この講座だけ上書きする
// in は、プロトタイプにある名前でも true
console.log("summary" in jsCourse); // true
console.log("summary" in sqlCourse); // true
// hasOwnProperty は、その講座自身が持つキーだけ true
console.log(jsCourse.hasOwnProperty("summary")); // false
console.log(sqlCourse.hasOwnProperty("summary")); // true(上書きされている)
console.log(Course.prototype.hasOwnProperty("summary")); // true
title—"JavaScript 入門"hours—12
summary()—inではtrue、jsCourse.hasOwnPropertyではfalse
hasOwnProperty()—jsCourseから呼べるのは、ここで見つかるため
代入で上書きした sqlCourse では hasOwnProperty("summary") が true に変わるので、上書きされた講座だけを 1 行で見つけられます。下の表は、ほかの調べ方で title と summary がどう扱われるかを並べたものです。
| 調べ方 | title(jsCourse 自身の値) | summary(Course.prototype のメソッド) |
|---|---|---|
| Object.keys(jsCourse) に並ぶか | 並ぶ | 並ばない |
| for...in で取り出されるか | 取り出される | 取り出されない |
| Course.prototype.hasOwnProperty("キー") | false | true |
外からメソッドを加える — prototype への代入
講座のインスタンスは別チームの共通コードが作って渡してくるため、class の本体も new の行も書き換えられないとします。画面用に「講座: 名前」を返す関数を 1 件ずつ代入すると、代入を忘れた講座だけが TypeError で止まります。
Course.prototype.label = function () { ... } と prototype に関数を代入すると、どのインスタンスもチェーンで同じ label を見つけます。本体を書き換えられる class なら、メソッドは class の本体に書きます。
class Course {
constructor(title, hours) { this.title = title; this.hours = hours; }
summary() { return `${this.title}(${this.hours} 時間)`; }
}
const jsCourse = new Course("JavaScript 入門", 12); // 先に 2 件作る
const sqlCourse = new Course("SQL 入門", 8);
// jsCourse だけに代入しても、sqlCourse には label が無い
jsCourse.label = function () { return `講座: ${this.title}`; };
console.log(typeof sqlCourse.label); // undefined(呼ぶと TypeError)
// Course.prototype に 1 回代入する(this を読むので function で書く)
Course.prototype.label = function () { return `講座: ${this.title}`; };
console.log(sqlCourse.label()); // 講座: SQL 入門
console.log(sqlCourse.hasOwnProperty("label")); // false
// for...in で取り出されるキーを集める
const keys = [];
for (const key in sqlCourse) keys.push(key);
console.log(keys.join(", ")); // title, hours, label
代入より前に作った sqlCourse でも見つかるのは、名前を呼んだ時点でチェーンを探すためです。代入で足した label は、class の本体に書いた summary と違って for...in にも並ぶので、キーを回す処理ではデータのキーと混ざります。
prototype に足す関数は function で書く
Course.prototype.label = () => this.title; と書くと、アロー関数は class の外の this を使います。このページではそれが undefined なので、sqlCourse.label() は TypeError で止まります。function で書きます。
理解度チェック
まずは1問ずつ答えてみましょう。
Q2LiveCourse extends Course のとき、LiveCourse.prototype のプロトタイプは?
Q3作成済みのインスタンスがある状態で、Course.prototype に function の label を代入すると?