ユーザ用ツール

サイト用ツール


ecmascript:class

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
ecmascript/class.txt · 最終更新: by nullpon