目次
class
クラス構文
class Test { // 静的フィールド static version = '1.0.0'; // 静的メソッド static info() { // 注意 staticコンテキストでのthis.nameはクラス名を指す(`#name`ではない) return `${this,name} class, version ${this.version}` } // privateフィールド #name = ''; // コンストラクタ constructor(v) { this.#name = v; } // メソッド getMessage() { return 'Hello from ${this.#name}' } // getter get name() { return this.#name; } // setter set name(v) { this.#name = v; } }
継承
class Test2 extends Test { // 拡張メソッド print() { console.log(this.getMessage()); } }
関数とnew
ここからはマニア向けのディープな話になる。JSの関数はコンストラクタとして機能し、new演算子によって新しいオブジェクトを生成できる
function Fuga(x) { this.x = x; } Fuga.prototype.inspect = function() { console.log(`x: ${this.x}`); } const fuga = new Fuga(1); fuga.inspect(); console.log(typeof Fuga); // => function // 普通の関数としても呼べる Fuga();
これはclassキーワードを使った以下のクラス定義とほぼ同等。
class Fuga { constructor(x) { this.x = x; } inspect() { console.log(`x: ${this.x}`); } } const fuga = new Fuga(1); fuga.inspect(); console.log(typeof Fuga); // => function // ただしclassで定義した関数は直接呼べないように制限がかかっている Fuga(); // TypeError: Class constructor Fuga cannot be invoked without 'new'
classキーワードで定義したクラスの正体は関数で、newキーワードとともに新しいオブジェクトを生成する関数をコンストラクタ関数と呼ぶ。コンストラクタ関数で生成されたオブジェクトはクラスのインスタンスであるかのように振る舞うが、実際にはクラスとは異なるprototype chainという仕組みで動作している。
なお、functionでコンストラクタ関数(クラス)を定義することは可能だが、あえてそうする理由はなくclassキーワードを使うべきである
constructorプロパティ
生成されたオブジェクトはconstructorというプロパティを持っている。これは自身を生成したコンストラクタ関数への参照である。
class Hoge { constructor(x) { this.x = x; } } const h = new Hoge(1) console.log(h,constructor === Hoge); // => true const h2 = new h.constructor(2); // newすることもできる
prototypeプロパティ
コンストラクタ関数はprototypeというプロパティを持っている
class Hoge { constructor(x) { this.x = x; } getNumber() { return this.x; } } console.log(Hoge.prototype);
classで定義したメソッドは、このprototypeプロパティに所属している。
console.log(Hoge.prototype,getNumber); // => [function getNumber]
コンストラクタ関数によって生成されたオブジェクトは__proto__
というプロパティを持っており、コンストラクタ関数のprototypeにアクセスできる。
console.log(h.__proto__.getNumber); // => [function getNumber]
h.__proto__
も__proto__
を持っている。このようなprototypeの連鎖をprototype chainと呼ぶ
console.log(h.__proto__); // => {} console.log(h.__proto__.__proto__); // => [Object: null prototype] {}
h.getNumber()
というメソッド呼び出しは、h.__proto__
に委譲することで実現している。そしてnew Hoge()
で生成されたオブジェクトはprototypeを共有することで同じメソッドを共有し、Hogeはクラスのように動作する。
prototype chainとメソッド呼び出し
JSのメソッドは関数が入ったプロパティである。よってプロパティに関数をセットすることでもメソッドを定義できる。
class Hoge { // プロパティの定義 name = ''; // コンストラクタ constructor(name) { this.name = name || ''; } // メソッド getName() { return this.name; } // プロパティに関数を入れてもメソッドになる(注意:これは実アプリでは避けるべき方法) getCount = function() { return this,name?.length || 0; } }
※ 通常のメソッドはprototypeにセットされて複数のHogeオブジェクト間で共有されるが、プロパティにセットしたgetCountはnewするたびに関数が作成されて共有されず各Hogeオブジェクトが個別に持つ。これはリソースの無駄なので実際のアプリ構築時には避けるべきコードである
h = new Hoge('example'); h,getCount(); // h自身がgetCountを持っている h,getName(); // h.__proto__がgetNameを持っている h,toString(); // h.__proto__.__proto__がtoStringを持っている
getCountはオブジェクト自身が持っているメソッドなので直接呼び出される。getNameはprototypeが持っているメソッドなのでprototypeに処理が委譲される。
オブジェクトがメソッドを持っていなければ、prototypeに処理が委譲され、prototypeも持っていなければprototypeのprototypeに処理が委譲される。JSはprototype chainを遡ってメソッドを検索し、見つかったらそのprototypeに処理を委譲する。こうしてクラスのような振る舞いを実現している。
したがって、getNameやtoStringの呼び出しは次のコードと概念的には同等である。
h.__proto__.getName.call(h); h.__proto__.__proto__.toString.call(h);
プロパティやメソッドの所有者はObject.hasOwn
で調べることができる
console.log(Object.hasOwn(h, "name")); // true console.log(Object.hasOwn(h, "getCount")); // true console.log(Object.hasOwn(h, "getName")); // false console.log(Object.hasOwn(h, "toString")); // false console.log(Object.hasOwn(h.__proto__, "getName")); // true console.log(Object.hasOwn(h.__proto__, "toString")); // false console.log(Object.hasOwn(h.__proto__.__proto__, "toString")); // true
prototype chainの末端まで探してもメソッドが見つからない場合はTypeErrorが発生する。何も継承していないクラスの場合prototype chainは2つで終わりだが、継承すると継承した回数だけprototype chainは長くなる。toStringやvalueOfなど定義しなくても存在しているメソッドはprototype chain末端が持っているメソッドである。
オーバーライド
オーバーライドはprototype chainのより近い位置に同名のメソッドを定義することで実現される
class Hoge { toString() { return super.toString() + "にゃん" } } const h = new Hoge(); // toStringはprototype chainの末端が持っている console.log(Object.hasOwn(h.__proto__.__proto__, "toString"); // => true // しかしオーバーライドしたのでHogeのprototypeも持っている console.log(Object.hasOwn(h.__proto__, "toString"); // => true // 呼び出されるのは、prototype chainのより近い側にあるメソッド console.log(h.toString()); // => [object Object]にゃん
prototypeを指定してオブジェクトを生成
Object.create
を使うと任意のオブジェクトをprototypeに設定して新しいオブジェクトを生成できる
const p = { name: 'proto', greet() { console.log("I am a " + this.name) } } // pをプロトタイプに設定してオブジェクトを生成 const h = Object.create(p); console.log(h.__proto__ === p); // => true // prototypeのnameが使われる h.greet(); // => I am a proto // hのnameを設定 h.name = "cat"; // h自身がnameを持っていれば、プロトタイプではなく自身のnameが使われる h.greet(); // => I am a cat