いまさらJavaScriptのclassとprototypeを勉強する
はじめに
JavaScriptのプロトタイプ理解してますか?
class
はプロトタイプのシンタックスシュガーであるみたいな記述を見たり、MDNのドキュメントでメソッドを調べるとprototypeが現れたり (e.g. Array.prototype.map) でなんとなく目にしますが、典型的なアプリケーション開発をしている分にはプロトタイプやclass
の裏側を知らずに乗り切れてしまいます。
しかしいざ必要になって[1][2]勉強してみたところ面白かったのですが、どこから勉強すればいいのか前後することになりました。[3]
そこで、順番に読めるようドキュメントをまとめ、私の理解を残しておきます。
ドキュメント
以下がクラスとプロトタイプの関係を理解するまでに読んだMDN web docsです。
- this
- オブジェクトのプロトタイプ
- new 演算子
- クラス
- extends
- instanceof
- 継承とプロトタイプチェーン (これは重かったので読み切っていない)
次のセクションでは各ドキュメントに対する要約と私の理解をまとめています。
各ドキュメントの要約
this
function
内のthis
は関数を定義時ではなく呼び出し時のコンテキストに依存します。
function getName() {
return this.name;
}
const obj = { name: "foo", getName: getName };
console.log(obj1.getName()); // foo
ラムダ式で定義された関数の場合は、定義時のコンテキストに依存します。
オブジェクトのプロトタイプ
すべてのオブジェクトはプロトタイプと呼ばれるオブジェクトを持ち、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 演算子
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
っぽいことが実現できます。
クラス
クラス定義の構文は以下のようになります。
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
のインスタンスの型を指すということになります。
class
はfunction
の単純なシンタックスシュガーとして完全に提供できるわけではなく、例えばプライベートプロパティを提供します。
extends
ChildClass
がParentClass
を継承している場合、ChildClass
のプロトタイプとしてParentClass
が設定され、
ChildClass.prototype
のプロトタイプとしてParentClass.prototype
が設定されます。
instanceof
obj instanceof Class
はobj
がClass
のインスタンスであるかどうかを、obj
のプロトタイプチェーンにClass.prototype
と一致するものが存在するかどうかによって判定します。
まとめ
以上でクラスとプロトタイプを概ね理解できたと思います。
ちょっと可能性が広がった感じがしていろいろ試してみたくなったのではないでしょうか。[5]
この記事は同じことを勉強する必要が現れた人がスムーズに勉強できるようにしたい気持ち半分、私が味わった楽しさを共有したい気持ち半分で書きました。
どちらも楽しくスムーズに勉強できていれば非常に嬉しいです!!!
-
JSのイケてるWebフレームワークであるHonoに入った最適化PR (解説記事: HonoのNode.jsアダプタが2.7倍速くなりました) を読もうとしたときに必要になりました ↩︎
-
実はけっこう前に『JavaScriptのカスタムエラーはこれでOK』を理解する上でも必要になっていたのですが、なんとなく雰囲気でわかったつもりになっていました ↩︎
-
こういうのをyak shavingというらしいです ↩︎
-
何もわかってなかったのでこれは衝撃でした ↩︎
Discussion