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

プロトタイプ — 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.prototypeundefined です。

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 時間)
class とインスタンスが指す先
new Course("JavaScript 入門", 12)class Coursenew Course("SQL 入門", 8)jsCourseCourse.prototypesummary を持つsqlCourse自分のキーはtitle, hourssummary の本体は 1 つだけ自分に無いのでCourse.prototype へjsCourse.prototypeは undefined2 件の summary を比べると truesummary() はSQL 入門(8 時間)
2 件のプロトタイプは、class の prototype と同じです。自分に無い summary は、そこで見つかります

本体は 1 つでも、this には呼んだ行のドットの左が入るので、sqlCourse.summary()sqlCourse の値で文字列を作ります。講座を 300 件作っても、増えるのは titlehours を持つインスタンスだけです。

null に着くまで順に探す — プロトタイプチェーン

ライブ配信の講座を class LiveCourse extends Course で作ると、liveCourse.summary() も動きます。ところが liveCourse のプロトタイプ LiveCourse.prototype に書いたメソッドは schedule だけです。

名前は、プロトタイプチェーン(プロトタイプのプロトタイプを null までたどった参照の列)をインスタンス側から探します。extendsLiveCourse.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 時間)
summary を探すチェーンの順番
liveCourse(インスタンス)title・hours を持つsummary は無いLiveCourse.prototypeschedule を持つsummary は無いCourse.prototypesummary を持つここで見つかるObject.prototype組み込みのメソッドを持つ(次は null)
summary は上から探し、Course.prototype で見つかります。Object.prototype の次の null で終わります

チェーンに入るのは Course.prototype で、Course そのものは入りません。static を付けたメソッドは Course に置かれるため、インスタンスから読むと null まで探しても見つからず undefined になり、呼ぶと TypeError で止まります。

インスタンスの同名キーが先に見つかる

liveCourse.summary = "準備中"; と代入すると、名前は liveCourse 自身で見つかり、プロトタイプまで探されません。liveCourse.summary()TypeError: liveCourse.summary is not a function で止まります。

車の種類ごとにメソッドの探索順を表示します。Car・ElectricCar・compactCar・ev・prototypeNames は宣言済みです。

① ev のプロトタイプが ElectricCar.prototype かを表示してください。

② 受け取った車のプロトタイプから null の前まで名前をつなぐ chainOf を定義してください。

③ compactCar と ev で結果を表示してください。

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

JavaScript / TypeScript エディタ

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

インスタンス自身のキーに絞る — 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
in と hasOwnProperty が探す範囲
"summary" in jsCourse が探す範囲(チェーン全体)
jsCourse 自身(jsCourse.hasOwnProperty が見る枠)
  • title"JavaScript 入門"
  • hours12
Course.prototype
  • summary()in では truejsCourse.hasOwnProperty では false
Object.prototype
  • hasOwnProperty()jsCourse から呼べるのは、ここで見つかるため
in はチェーン全体、hasOwnProperty は jsCourse の枠だけを見ます。summary は jsCourse の枠にありません

代入で上書きした sqlCourse では hasOwnProperty("summary")true に変わるので、上書きされた講座だけを 1 行で見つけられます。下の表は、ほかの調べ方で titlesummary がどう扱われるかを並べたものです。

調べ方title(jsCourse 自身の値)summary(Course.prototype のメソッド)
Object.keys(jsCourse) に並ぶか並ぶ並ばない
for...in で取り出されるか取り出される取り出されない
Course.prototype.hasOwnProperty("キー")falsetrue

外からメソッドを加える — 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
label の代入先で分かれる結果
jsCourse だけにlabel を代入sqlCourse 自身にlabel は無いチェーンにも無くundefinedsqlCourse.label()は TypeErrorCourse.prototype.label に代入sqlCourse 自身にlabel は無いCourse.prototypeで見つかる講座: SQL 入門
どちらも sqlCourse 自身は label を持ちません。Course.prototype に代入したときだけ、チェーンで見つかります

代入より前に作った sqlCourse でも見つかるのは、名前を呼んだ時点でチェーンを探すためです。代入で足した label は、class の本体に書いた summary と違って for...in にも並ぶので、キーを回す処理ではデータのキーと混ざります。

prototype に足す関数は function で書く

Course.prototype.label = () => this.title; と書くと、アロー関数は class の外の this を使います。このページではそれが undefined なので、sqlCourse.label()TypeError で止まります。function で書きます。

動画の作品一覧に、Video の本体を書き換えずに再生時間の表示を足します。Video と videos は宣言済みです。

① 全作品で呼べる、「◯ 時間 ◯ 分」を返す durationLabel を足してください。

② 作品ごとに「作品名: ◯ 時間 ◯ 分」を表示してください。

③ 1 件目の作品が durationLabel を自身で持つかを表示してください。

④ 1 件目を for...in で回し、作品自身が持たないキーを表示してください。

JavaScript / TypeScript エディタ

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

理解度チェック

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

Q1class に書いた summary で、jsCourse.hasOwnProperty("summary") は?

Q2LiveCourse extends Course のとき、LiveCourse.prototype のプロトタイプは?

Q3作成済みのインスタンスがある状態で、Course.prototype に function の label を代入すると?