🦖

いまさらJavaScriptのclassとprototypeを勉強する

2024/01/25に公開

はじめに

JavaScriptのプロトタイプ理解してますか?
classはプロトタイプのシンタックスシュガーであるみたいな記述を見たり、MDNのドキュメントでメソッドを調べるとprototypeが現れたり (e.g. Array.prototype.map) でなんとなく目にしますが、典型的なアプリケーション開発をしている分にはプロトタイプやclassの裏側を知らずに乗り切れてしまいます。
しかしいざ必要になって[1][2]勉強してみたところ面白かったのですが、どこから勉強すればいいのか前後することになりました。[3]
そこで、順番に読めるようドキュメントをまとめ、私の理解を残しておきます。

ドキュメント

以下がクラスとプロトタイプの関係を理解するまでに読んだMDN web docsです。

  1. this
  2. オブジェクトのプロトタイプ
  3. new 演算子
  4. クラス
  5. extends
  6. instanceof
  7. 継承とプロトタイプチェーン (これは重かったので読み切っていない)

次のセクションでは各ドキュメントに対する要約と私の理解をまとめています。

各ドキュメントの要約

this

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/this

function内のthisは関数を定義時ではなく呼び出し時のコンテキストに依存します。

function getName() {
  return this.name;
}

const obj = { name: "foo", getName: getName };

console.log(obj1.getName()); // foo

ラムダ式で定義された関数の場合は、定義時のコンテキストに依存します。

オブジェクトのプロトタイプ

https://developer.mozilla.org/ja/docs/Learn/JavaScript/Objects/Object_prototypes

すべてのオブジェクトはプロトタイプと呼ばれるオブジェクトを持ち、Object.getPrototypeOfで取得できます。
プロトタイプは「aのプロトタイプ→aのプロトタイプのプロトタイプ→…」のようにチェインになっており、最後はnullになります。

プロトタイプはプロパティアクセスに対して継承のような機能を提供します。
obj.foo のようにプロパティにアクセスしたとき、objのプロトタイプチェーンをたどってfooという名前のプロパティを探します。

const prototype = {
  name: "foo",
};

// prototypeをプロトタイプとして持つオブジェクトを作成する
const obj = Object.create(prototype);

console.log(obj); // {}
console.log(obj.name); // foo

関数はprototypeというプロパティを持ちます。これはその関数自身のプロトタイプではなく[4]、その関数をコンストラクタとして使ったときに生成されるオブジェクトのプロトタイプになります。

function Foo() {}
Foo.prototype.name = "foo";

Object.getPrototypeOf(Foo) !== Foo.prototype; // true (Foo.prototypeはFooのプロトタイプではない)

const obj = new Foo();

console.log(Object.getPrototypeOf(obj) === Foo.prototype); // true
console.log(obj.name); // foo

このようにnewはクラスに限らず関数に対して使用することができます。
newの詳細は次のnew 演算子で説明されます。

new 演算子

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/new

new演算子は以下のように関数をコンストラクタとして扱ってオブジェクトを作成します。

function MyClass(foo, bar) {
  this.foo = foo;
  this.bar = bar;
}

const obj = new MyClass("value1", "value2");

console.log(obj); // MyClass { foo: 'value1', bar: 'value2' }

前のオブジェクトのプロトタイプで説明したように、new MyClassで作成されるオブジェクトのプロトタイプはMyClass.prototypeになるという話と合わせると、newは概ね以下のようなコードと等価になります。

// new MyClass("value1", "value2") はほぼ以下のコードと同じ

// MyClass.prototypeをプロトタイプとして持つオブジェクトを作成する
const obj = Object.create(MyClass.prototype);
// オブジェクトをthisとしてバインドしてMyClass関数を呼び出す
MyClass.call(obj, "value1", "value2");

prototype にメソッドを定義して、コンストラクタでオブジェクトにフィールドとしてのプロパティをセットすることで class っぽいことが実現できます。

クラス

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Classes

クラス定義の構文は以下のようになります。

class MyClass {
  constructor(foo, bar) {
    this.foo = foo;
    this.bar = bar;
  }

  static baz = "value3";

  fooBar() {
    return this.foo + this.bar;
  }  
}

上記のクラスは概ね以下のような関数を定義することと等価です。

function MyClass(foo, bar) {
  this.foo = foo;
  this.bar = bar;
}

MyClass.baz = "value3";

MyClass.prototype.fooBar = function() {
  return this.foo + this.bar;
};

TypeScriptにおいてはMyClassを値として使うときは上記のMyClassそのものを指し、MyClassを型として使うときはMyClassのインスタンスの型を指すということになります。

classfunctionの単純なシンタックスシュガーとして完全に提供できるわけではなく、例えばプライベートプロパティを提供します。

extends

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Classes/extends

ChildClassParentClassを継承している場合、ChildClassのプロトタイプとしてParentClassが設定され、
ChildClass.prototypeのプロトタイプとしてParentClass.prototypeが設定されます。

instanceof

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/instanceof

obj instanceof ClassobjClassのインスタンスであるかどうかを、objのプロトタイプチェーンにClass.prototypeと一致するものが存在するかどうかによって判定します。

まとめ

以上でクラスとプロトタイプを概ね理解できたと思います。
ちょっと可能性が広がった感じがしていろいろ試してみたくなったのではないでしょうか。[5]

この記事は同じことを勉強する必要が現れた人がスムーズに勉強できるようにしたい気持ち半分、私が味わった楽しさを共有したい気持ち半分で書きました。
どちらも楽しくスムーズに勉強できていれば非常に嬉しいです!!!

脚注
  1. JSのイケてるWebフレームワークであるHonoに入った最適化PR (解説記事: HonoのNode.jsアダプタが2.7倍速くなりました) を読もうとしたときに必要になりました ↩︎

  2. 実はけっこう前に『JavaScriptのカスタムエラーはこれでOK』を理解する上でも必要になっていたのですが、なんとなく雰囲気でわかったつもりになっていました ↩︎

  3. こういうのをyak shavingというらしいです ↩︎

  4. 何もわかってなかったのでこれは衝撃でした ↩︎

  5. 私はKotlinのスコープ関数っぽいものをTypeScriptで書いてみました: スクラップ ↩︎

GitHubで編集を提案
Aidemy Tech Blog

Discussion