オブジェクト指向まとめ — class 構文と this の早見表

このカテゴリで扱った class の書き方を、メンバーの置き場所、名前を探す順、this の決まり方、機能の足し方、エラーの止まる時点で整理する早見表です。

タスク管理のコードでは、static# のメンバー、extends した子クラス、取り出したメソッドが 1 つのファイルに混ざります。違いを整理していないと、undefinedTypeError の原因をたどれません。

この記事では、class 構文this を中心に、このカテゴリの書き方を早見表で扱います。

置き場所でメンバーを読む — class 構文の早見表

ほかの人が書いた Task の class を読むとします。static#get の付いた行が同じ波括弧に並び、task.fromJSON(text)TypeError になる理由や、JSON.stringify(task) に完了の状態が出ない理由は、1 行ずつ読んでも分かりません。

class の本体に書いたものは、書き方で 置き場所(インスタンス自身・全インスタンスが共有するメソッドを置く Task.prototype・class そのもののどれか)が決まります。名前を読むときは、インスタンス自身から Task.prototypeObject.prototype の順にたどります。

class Task {
  static count = 0;                              // Task そのものが 1 つだけ持つ
  priority = "通常";                             // new のたびに task 自身に入る
  #done = false;                                 // task に入るが、class の外からは読めない
  constructor(title) {
    this.title = title;
    Task.count += 1;
  }
  complete() { this.#done = true; }              // Task.prototype に置かれ、全インスタンスで共有する
  get status() { return this.#done ? "完了" : "未着手"; }
  static fromJSON(text) { return new Task(JSON.parse(text).title); }
}

// インスタンスから読める名前と、読めない名前を確かめる
const task = Task.fromJSON('{"title":"見積書を送る"}');
task.complete();
console.log(task.status, Task.count);                                 // 完了 1
console.log(Object.keys(task).join(", "));                            // priority, title
console.log(JSON.stringify(task));                                    // {"priority":"通常","title":"見積書を送る"}
console.log(task.hasOwnProperty("complete"), typeof task.fromJSON);   // false undefined
task から探せる名前の範囲
task.名前 で探す範囲
  • task 自身 → Task.prototype → Object.prototype の順に探す
task 自身
  • titlepriority — キーに並び、JSON に出る
Task.prototype(全インスタンスで共有)
  • complete()get status() — 探すと見つかる
task.名前 では探せない private フィールド
  • #done — task に保存されるが、class の中の this.#done でしか読めない
Task そのもの(static)
  • countfromJSON() — 探す範囲の外にある
  • task.fromJSONundefined
# の名前と static は、task から探す範囲の外です。task.fromJSON は見つからないので、class 名から呼びます

statusTask.prototype にあり、#done は task にあってもキーに並ばないため、どちらも Object.keysJSON.stringify に出ません。JSON に含めたいときは toJSON で出す値を返します。下の表は、このカテゴリの書き方を置き場所で並べたものです。

書き方置き場所task から読むと
constructor の this.titletask 自身キーと JSON に出る
クラスフィールド prioritytask 自身キーと JSON に出る
private フィールド #donetask 自身(class 内だけ)キーにも JSON にも出ない
メソッド complete()Task.prototype呼べる。キーには出ない
get / set statusTask.prototype括弧なしで読む
static count / fromJSON()Task そのものundefined

どの定義が動くかをたどる — extends と super

毎週くり返すタスクを class RecurringTask extends Task で作ったとします。weekly.describe() の行を見ても、動くのが子クラスと親のどちらの describe か、子クラスに書いていない complete がなぜ呼べるのかは、その行からは読み取れません。

名前は プロトタイプチェーン を自身から順に探し、最初に見つけた定義が動きます。子クラスのオーバーライドは親より先に見つかり、super.describe() は親から探し始めます。ポリモーフィズムで種類ごとに書き分けたメソッドも、この順で見つかります。

class Task {
  constructor(title) { this.title = title; }
  describe() { return `タスク: ${this.title}`; }
  complete() { return `${this.title} を完了`; }
}

// 子クラス: super() で親の constructor を呼び、describe だけを書き直す
class RecurringTask extends Task {
  constructor(title, weekday) { super(title); this.weekday = weekday; }
  describe() { return `${super.describe()}(毎週${this.weekday})`; }
}

// どの定義が動くかを、呼んで確かめる
const weekly = new RecurringTask("週報を出す", "金曜");
console.log(weekly.describe());                              // タスク: 週報を出す(毎週金曜)
console.log(weekly.complete());                              // 週報を出す を完了
console.log(`${weekly}`);                                    // [object Object]
console.log(weekly instanceof Task, typeof weekly.archive);  // true undefined
weekly から名前を探す段の順番
weekly(インスタンス)title・weekdayを持つRecurringTask.prototypedescribe が見つかるweekly.describe() の先Task.prototypecomplete が見つかるsuper.describe() の先Object.prototypetoString が見つかる${weekly} の先
describe は 2 段目、complete は 3 段目で見つかります。親の describe は、super. を付けたときだけ動きます

archive のようにどの段にも無い名前は undefined で、呼ぶと TypeError です。${weekly}[object Object] なのは、最後の段にある toString の既定の定義が動くためです。

呼び方で this を見分ける — this の早見表

タスクの完了処理を、変数に入れて後から呼んだり、setTimeout に渡したりするとします。task.complete() では動くメソッドも、渡し方によって thistask ではなくなり、TypeError で止まったり、this.titleundefined になったりします。

this に入る値は、関数の書き方で 決まる時点 が違います。class に書いた通常のメソッドは呼んだ行のドットの左で決まり、アロー関数のフィールドは new でインスタンスを作るときに決まります。bind の戻り値は、bind に渡した値に固定された別の関数です。

class Task {
  onComplete = () => `${this.title} を完了`;       // this は new のときの task
  constructor(title) { this.title = title; }
  complete() { return `${this.title} を完了`; }   // this は呼んだ行で決まる
}
const task = new Task("請求書を送る");

// ドットの左に task を書いて呼ぶ
console.log(task.complete());   // 請求書を送る を完了

// 変数に取り出してから呼ぶ
const runMethod = task.complete;
const runArrow = task.onComplete;
const runBound = task.complete.bind(task);
console.log(runArrow());        // 請求書を送る を完了
console.log(runBound());        // 請求書を送る を完了
// runMethod();                 // TypeError: Cannot read properties of undefined (reading 'title')
取り出した関数の this の決まり方
const runMethod= task.complete通常のメソッド呼んだ行で決まるrunMethod() はドットの左が無いthis は undefinedTypeErrorconst runArrow= task.onCompleteアロー関数のフィールドnew のときに決まるrunArrow() でもthis は task のまま請求書を送るを完了
runMethod() の this は undefined です。取り出して渡す関数は、下の段の書き方なら task を失いません

コードの runBound も、取り出した後で task のまま動きます。setTimeout(task.complete, 100) ではブラウザが thiswindow を入れるので、アロー関数で包みます。下の表は、呼び方ごとに this が決まる時点を並べたものです。

呼び方this が決まる時点取り出して渡すと
通常のメソッド呼んだ行のドットの左task を失い TypeError
bind の戻り値bind を呼んだときtask のまま動く
アロー関数のフィールドnew のときtask のまま動く
() => task.complete() で包む包んだ中で task.complete() を呼んだときtask のまま動く
Object.assign で足したメソッド呼んだ行のドットの左書き込み先を失う

子クラスに書いた同名メソッドが動かない

class RecurringTask extends Task { onComplete() { … } } と書いても、親 Task のアロー関数のフィールドが weekly 自身に入って先に見つかり、weekly.onComplete() は親の関数を動かします。書き直す関数は親もメソッドで書きます。

機能を足す場所を選ぶ — 継承・prototype・合成

動いているタスク管理に、あとから機能を足すとします。1 件だけにピン留めを付けたいのか、作成済みの全タスクにラベル表示を付けたいのか、チームのタスクにだけ担当者を付けたいのかで書く場所を誤ると、付けたつもりのタスクで TypeError になります。

このカテゴリで扱った足し方は、機能の 書き込む先(インスタンス自身・Task.prototypeextends でつなぐ新しい段)が違います。書き込む先が決まると、その名前をチェーンで見つけられるインスタンスの範囲も決まります。

class Task {
  constructor(title) { this.title = title; }
}
const first = new Task("見積書を送る");        // 先に 2 件作っておく
const second = new Task("議事録をまとめる");

// 1 件だけに書き込む
Object.assign(first, { pin() { return `${this.title} をピン留め`; } });
// Task.prototype に代入する
Task.prototype.label = function () { return `タスク: ${this.title}`; };
// ミックスインで段を重ねた class を作る
const Assignable = (Base) => class extends Base {
  assign(name) { return `${this.title}${name} さんに割り当て`; }
};
class TeamTask extends Assignable(Task) {}

console.log(typeof second.pin, second.label());          // undefined タスク: 議事録をまとめる
console.log(typeof first.assign);                        // undefined
console.log(new TeamTask("週報を出す").assign("佐藤"));   // 週報を出す を 佐藤 さんに割り当て
書き込む先で変わる機能の届く範囲
Task から作ったfirst と secondObject.assign(first, 部品)Task.prototype.label = functionextendsAssignable(Task)first 自身のキーに入るTask.prototypeに 1 つ入るTeamTask の親に段が 1 つ増えるfirst だけsecond は undefined作成済みの2 件とも呼べるnew TeamTask だけfirst・second には無い
first 自身なら 1 件だけ、Task.prototype なら作成済みの 2 件に届きます。ミックスインは新しい class の分だけです

作成済みの second にも label が届くのは、second がチェーンで Task.prototype を探すためです。prototype への代入は、本体を書き換えられない class に使います。子クラスに書いたメソッドは、ミックスインと同じく子クラスで作ったものにだけ届きます。

1 件ずつ足した機能は typeof で確かめる

Object.assignfirst にだけ pin を書き込んでも、firstsecondinstanceof Tasktrue です。class では見分けられないので、呼ぶ前に typeof task.pin === "function" で持っているかを確かめます。

止まる時点で原因を探す — エラーの早見表

class のコードでエラーが出たとき、止まった行と直す行が離れていることがあります。子クラスの constructor の書き間違いは new を実行したときに中の行で止まり、class の外に書いた # の名前では、その行より上にある console.log の表示まで出ません。

エラーは、止まる 時点(コードを読み込む段階・newconstructor が動くとき・メソッドを呼んだとき)で 3 つに分けられます。読み込む段階の SyntaxError では 1 行も実行されず、ほかの 2 つはエラーの行より前の行が動いてから止まります。

class Task {
  #done = false;
  constructor(title) { this.title = title; }
  complete() { return `${this.title} を完了`; }
}
class RecurringTask extends Task {
  constructor(title, weekday) {
    this.weekday = weekday;   // super() より前に this を書いた
    super(title);
  }
}
console.log("確認を始めます");   // ② と ③ では表示され、① の行があると表示されない

// ① 読み込む段階: console.log(new Task("請求書を送る").#done);
//    SyntaxError: Private field '#done' must be declared in an enclosing class
// ② new のとき: new RecurringTask("週報を出す", "金曜");
//    ReferenceError: Must call super constructor in derived class before accessing 'this' …
// ③ 呼んだとき: const run = new Task("請求書を送る").complete; run();
//    TypeError: Cannot read properties of undefined (reading 'title')
エラーで止まる 3 つの時点
① コードを読み込む段階SyntaxError1 行も動かない② new でconstructor が動くReferenceErrornew より上の行は動く③ メソッドを呼んだときTypeError呼ぶより上の行は動く
上の段ほど早く止まり、① では確認の表示も出ません。表示が 1 つも出ないときは、読み込む段階の SyntaxError を疑います

② と ③ は直前の表示まで出てから止まるので、最後に出た表示の後ろから原因の行を探せます。下の表は、このカテゴリで出てきたエラーを、止まる時点の早い順に並べたものです。

エラー文原因の書き方直し方
SyntaxError: Private fieldclass の外で task.#donegetter を通して読む
SyntaxError: … export nameddefault export を { Cart } で読む波括弧を外す
ReferenceError: Must call supersuper() より前に thissuper() を先に呼ぶ
TypeError: … without 'new'class を new なしで呼ぶnew を付ける
TypeError: Cannot read properties取り出したメソッドを run() と呼ぶbind かアロー関数で包む
RangeError: Maximum call stacksetter で同名に代入/super. を省く別名に入れる/super. を付ける
QUIZ

理解度チェック

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

Q1RecurringTask に書いていない weekly.complete() の定義が見つかる段は?

Q2アロー関数のフィールドを const run = task.onComplete; run(); と呼ぶと、this は?

Q3console.log の後の newReferenceError が出たとき、その表示は?