【JavaScript】prototypeとは?仕組みと使い方を初心者向けにわかりやすく解説
はじめに
JavaScriptを学び始めたばかりの段階では、prototypeという言葉を聞くと戸惑うことが多いかもしれません。
オブジェクト指向プログラミングを扱った言語はいくつかありますが、JavaScriptの継承モデルは独特で、クラスベースの言語に慣れた方にはややとっつきにくい印象を与えることもあるでしょう。
しかしながら、prototypeはJavaScriptにおけるオブジェクトの仕組みを理解するうえで、欠かせない概念です。
実務でJavaScriptを使う際には、クラス構文やライブラリを利用している場合でも、prototypeの知識があるとトラブルシューティングやコードリーディングがずっとラクになります。
特にチーム開発では、メンバーが書いたコードにprototype特有の記法や拡張が含まれているケースもあるので、基本を押さえておくと困りにくいです。
本記事では、prototypeの仕組みと使い方を、初心者にもわかるように丁寧に解説していきます。
「なぜJavaScriptにはクラスとは別にprototypeが存在するのか」「どう実務に活かすのか」といった視点も含めて整理しながら、学んでいきましょう。
そうすることで、普段何気なく使っているJavaScriptがどのように動いているのか、より深く理解できるようになるはずです。
この記事を読むとわかること
- prototypeの基本概念
- JavaScriptオブジェクトの構造とプロトタイプチェーン
- 関数コンストラクタからのオブジェクト生成や継承のしくみ
- prototypeを意識したコードが実務でどのように役立つのか
上記の内容を順序立てて解説していきます。
サンプルコードも示しますので、実際にコードを書きながら学んでみると理解が深まるでしょう。
prototypeが重要になる理由
JavaScriptでオブジェクトを扱う場面は非常に多いです。
配列や関数でさえ、内部的にはオブジェクトとして機能していることを踏まえると、ほとんどの要素が「オブジェクトでできている」と言っても過言ではありません。
そのうえで、prototypeという機構は、JavaScriptのオブジェクトがどのようにメソッドやプロパティを参照しているのかをコントロールしています。
例えば、あるオブジェクトのプロパティを参照したときに、それが見つからない場合はprototypeのなかを探しにいくという流れがあるのです。
これがプロトタイプチェーンと呼ばれる仕組みになります。
実務でJavaScriptを使っていると、コード量が増えるほどメソッドの共通化やオブジェクト間のプロパティ共有をどう行うかが重要になってきます。
そこでprototypeを上手に活用すると、処理を一元化したり、継承のしくみをシンプルにしたりできます。
初心者のうちは「なぜクラスのように書けるのに、prototypeがあるの?」と疑問を抱くかもしれませんが、JavaScript独特のこの仕組みに慣れておくと、コードを深く理解するうえでの大きな武器になるでしょう。
prototypeとは何かをざっくり理解する
prototypeは、あるオブジェクトが持つプロパティやメソッドを共有する仕組みを担う「土台」と考えるとイメージしやすいです。
JavaScriptでは、オブジェクト同士が**「プロトタイプ」というリンク**でつながっています。
何かのオブジェクトを生成すると、その背後には「生成元」となるオブジェクトの雛形があって、そこに共通の機能やデータが保持されているというわけです。
たとえば、function Foo() {}
のように関数コンストラクタを作ると、Foo.prototype
という特別なプロパティが自動的に生成されます。
ここにはFoo
から作られるインスタンス(オブジェクト)が共通で使うメソッドを格納する場所として機能させることができます。
また、オブジェクトリテラル {}
の場合も、暗黙的にObject.prototype
が参照されていて、そこからtoString()
などの基本的なメソッドを利用可能にしているのです。
慣れないうちは、クラスベースの言語と違う点に戸惑うかもしれませんが、ひとまず「すべてのオブジェクトには隠れた参照先(プロトタイプ)があって、ないプロパティをそこから探しに行く」と整理しておくと理解しやすいと思います。
プロトタイプチェーンのしくみ
オブジェクトがプロパティを参照する際には、まず自分自身のプロパティを探します。
見つからなければ、そのオブジェクトが参照するprototypeを探索し、そこにもなければ、さらにそのprototypeがもつprototypeへと探索を続けます。
このときの階層状のつながりをプロトタイプチェーンと呼びます。
実務では、「あるメソッドがどこで定義されているのかわからない」といった混乱が起きることがあります。
そんなとき、コードの中でObject.getPrototypeOf()
を利用してチェーンをたどってみると、どこに定義されているメソッドかがわかりやすくなります。
また、ブラウザの開発者ツールでオブジェクトを展開してみると、そのオブジェクトの__proto__
というプロパティを確認できるので、それをたどることでチェーンの全容を把握しやすいでしょう。
例えば、下記のような関数コンストラクタとオブジェクト生成を考えてみます。
これを見ると、apple
というオブジェクトがどこをたどってメソッドを見つけているのかがイメージしやすくなるはずです。
function Fruit(name) { this.name = name; } Fruit.prototype.getName = function() { return this.name; }; const apple = new Fruit("りんご"); console.log(apple.getName()); // "りんご"
ここでapple.getName()
を呼び出すと、apple
自身にはgetName
というプロパティが直接定義されていません。
しかし、apple
はFruit.prototype
を参照するプロトタイプをもっているので、そちらのgetName()
を呼び出すわけです。
そして、そこでも見つからなければObject.prototype
、最終的にはnull
へと続くチェーンをたどります。
関数コンストラクタとprototype
JavaScriptでは、関数コンストラクタと呼ばれる仕組みを使うと、クラスのような感覚で新しいオブジェクトを生成できます。
先ほどの例で使ったfunction Fruit(name)
も関数コンストラクタの一種です。
その際に自動生成されるのが、関数名に対応するFunctionName.prototype
というオブジェクトです。
このFunctionName.prototype
にメソッドやプロパティを追加すると、その関数コンストラクタから生成されたインスタンスが、共通でそれらを使えるようになります。
クラス構文を使わなくても、古くからあるJavaScriptの継承方式は、実はこのprototypeをベースに動いているのです。
実務では、すでに用意されているライブラリのソースコードを見ると、クラス構文ではなく関数コンストラクタとprototypeを使って記述されているケースも残っています。
そのため、prototypeベースのコードを読めるようになっておくと、レガシーなコードやクラシックな書き方にもスムーズに対応できるでしょう。
new演算子の動作とprototypeの関連
new
演算子で関数コンストラクタを呼び出すと、以下の処理が行われていると理解するとイメージしやすいです。
- 新しいオブジェクトを生成する
- そのオブジェクトの
__proto__
をコンストラクタ関数のprototype
プロパティと結びつける - コンストラクタ関数の内部で
this
を新しいオブジェクトに束縛した上で処理を実行する - 特に
return
文がない場合、その新しいオブジェクトを返す
この流れによって、結果としてインスタンスはprototype
に定義されているメソッドやプロパティを参照できるようになります。
クラス構文で書く場合も内部的には似たメカニズムで動いていますが、初心者が混乱しやすいのはやはりprototypeという存在を意識しにくい点かもしれません。
ここで仕組みを把握しておくと、後からコードの挙動を追いやすくなるでしょう。
実務での活用シーン
prototypeは、たとえば複数のオブジェクトで共通のメソッドを使いたい場合に役立ちます。
具体的には、関数コンストラクタで作られるインスタンスに同じメソッドを何個もコピーしたくない場合に、prototypeにまとめておけばコードが整理され、メモリの無駄遣いも防ぎやすくなります。
また、フレームワークやライブラリを自作する場面でも、prototypeを使ってクラスライクに機能を分割することが可能です。
大規模なシステムになればなるほど、繰り返し使われるメソッドをprototypeで一元管理するメリットが大きくなってきます。
たとえば、Vue.jsやReactなどの内部を深掘りしていくと、JavaScriptのprototypeを巧みに活用している部分があるかもしれません。
共通機能をprototypeに集約すると、コードベースの可読性や拡張性が高まりやすくなります。
もちろん、直接prototypeを書き換える場面が常にあるわけではありませんが、ライブラリの内部構造やカスタマイズを行うときに、その重要性がはっきり見えてくるはずです。
Object.create()を使った継承
関数コンストラクタだけでなく、Object.create()
を使ってオブジェクトのプロトタイプを指定する方法もあります。
たとえば、あるオブジェクトを別のオブジェクトのprototypeとして利用し、新たなオブジェクトを生成したい場合に便利です。
コード例は次のようになります。
const baseObj = { greet: function() { return "こんにちは"; } }; const derivedObj = Object.create(baseObj); derivedObj.sayName = function(name) { return "はじめまして、" + name + "です。"; }; console.log(derivedObj.greet()); // "こんにちは" console.log(derivedObj.sayName("田中")); // "はじめまして、田中です。"
ここではbaseObj
をprototypeとしてもつderivedObj
を生成し、独自メソッドsayName
を追加しています。
derivedObj
からgreet()
を呼び出すと、自分自身には定義がないのでbaseObj
へと検索が移り、そこからgreet()
を呼び出していることがわかります。
これもプロトタイプチェーンを利用した継承方法の一つです。
クラス構文との違い
現在では、class
キーワードを使ったクラス構文を利用してオブジェクト指向のコードを書くのが一般的になりつつあります。
しかし、その内部ではprototypeを用いた継承モデルが動いており、クラス構文はそれを見た目としてわかりやすくしたものにすぎません。
初学者がクラス構文を使うと、「JavaやC++に近い感覚で継承ができる」と思うかもしれませんが、実際にはJavaScript独特のprototypeベースの仕組みを使っている点は同じです。
たとえば、以下のクラス構文を使った例を見てみましょう。
class Animal { constructor(name) { this.name = name; } getName() { return this.name; } } const cat = new Animal("猫"); console.log(cat.getName()); // "猫"
このAnimal
クラスの下で定義されたgetName()
メソッドは、実際にはAnimal.prototype.getName
として登録されています。
したがって、cat
インスタンスからgetName()
を呼び出すときの動作は、prototypeを直接書いた関数コンストラクタの仕組みとほぼ同じなのです。
クラス構文の方が、ぱっと見で理解しやすいというメリットはあるでしょう。
とはいえ、もしトラブルが発生して内部構造まで追いたくなったときは、やはりprototypeを知っていると原因を掴みやすいです。
prototypeを上書きする時の注意点
prototypeの仕組みを使いこなすには、慎重に扱う必要もあります。
たとえば、Constructor.prototype = {...}
のようにprototypeをまるごと別のオブジェクトに置き換えると、元々設定されていたconstructor
プロパティが消えてしまう場合があります。
これはconstructor
プロパティが自動的に付与される仕様に依存しているだけで、手動で差し替えてしまうと予期しない振る舞いを引き起こすことがあるのです。
もし大規模なコードベースでprototypeを直接いじるなら、他の開発者が混乱しないように注意深くコメントを残す、あるいは拡張前後のオブジェクト構造を明確に把握しておくことが必要になります。
通常は、prototypeを付け替えるよりも、Constructor.prototype
に対してメソッドを追加・修正する書き方の方がわかりやすいでしょう。
クラス構文の場合も裏で同様の操作をしているので、同種の問題が起こり得ることは頭に入れておくといいかもしれません。
組み込みオブジェクトのprototype拡張
JavaScriptでは、Array.prototype
やString.prototype
など、組み込みオブジェクトのprototypeも書き換えることができます。
極端な例として、Array.prototype
に独自のメソッドを追加すれば、すべての配列からそのメソッドを利用可能になります。
しかし、これはチーム開発などで非常に大きな混乱を生む可能性が高い行為です。
実務においては、組み込みオブジェクトのprototypeを拡張するのは避けられるケースが多いです。
もしも独自メソッドを定義したい場合は、ユーティリティ関数として別途作るか、必要に応じてライブラリ内部で拡張する場合も注意深く行います。
それを知らずにコードを読む人がいた場合、「このメソッドはいつから存在しているんだ?」と戸惑うからです。
組み込みオブジェクトのprototype拡張は、チーム開発や長期運用でトラブルになりやすいので、慎重に考える必要があります。
コードを短くできるメリットがあるように見えても、将来の保守性を考えると大きなリスクを含む点に注意が必要です。
ただし、個人の学習プロジェクトなどでは、「この仕組みを理解する」という目的でやってみるのは良い練習になります。
オブジェクトの継承とプロトタイプチェーンの活用例
もう少し踏み込んだ例として、下記のように複数のコンストラクタ間で継承関係を作ってみます。
これによって、JavaScriptのプロトタイプチェーンがどのように活用できるのかを確認しましょう。
function Person(name) { this.name = name; } Person.prototype.introduce = function() { return "私の名前は" + this.name + "です。"; }; function Employee(name, position) { // Personコンストラクタを継承 Person.call(this, name); this.position = position; } // PersonのprototypeをEmployeeに引き継ぐ Employee.prototype = Object.create(Person.prototype); // constructorを明示的に設定 Employee.prototype.constructor = Employee; Employee.prototype.showPosition = function() { return "私の役職は" + this.position + "です。"; }; const emp = new Employee("鈴木", "開発チームリーダー"); console.log(emp.introduce()); // "私の名前は鈴木です。" console.log(emp.showPosition()); // "私の役職は開発チームリーダーです。"
ここではPerson
の機能をEmployee
が継承し、さらにEmployee
固有の機能を追加しています。
このコードでは、Object.create(Person.prototype)
を使ってEmployee.prototype
を生成し、constructor
プロパティをEmployee
に再設定している点が重要です。
そうしないと、Employee
インスタンスのconstructor
がPerson
を指すことになり、開発中にデバッグがやりにくくなる恐れがあります。
このように、複数のコンストラクタをつなげることで、クラス風の継承関係を表現することができます。
クラス構文を用いる場合も同様の仕組みが裏側で走っているので、prototypeチェーンがどのように形成されているのか意識しておくと、「インスタンスがどこからメソッドを引っ張ってくるのか」をしっかり追いかけられるようになるでしょう。
開発現場で気をつけたいポイント
prototypeを活用するうえで、開発現場で気をつけておきたいポイントをいくつか挙げます。
慣れないうちはトラブルの原因を作りがちなので、以下の点を頭の片隅に置いておくと安心です。
- コードリーディング: 他人の書いたコードを読む際、プロトタイプチェーンを意識するとメソッドの探索先を素早く特定できます。
- クラス構文との混在: プロジェクトによっては関数コンストラクタとクラス構文が混在している場合があります。どちらでも本質は同じなので、整理して把握するとよいでしょう。
- 組み込みオブジェクト拡張のリスク: 便利に思えても、チーム開発や長期的な保守を考えるとデメリットが大きいので、事前に合意した上で行うか、原則的に避けるのが無難です。
- prototypeの初期化:
Constructor.prototype = { ... }
のように大幅に書き換える場合は、constructor
プロパティを再設定するなど、想定外の挙動を防ぐ対策をすることが大切です。
これらを踏まえておくと、大規模プロジェクトでのトラブルシューティングや既存コードの改修もスムーズに進むのではないでしょうか。
「なぜこれで動くんだろう?」とわからなくなったときは、プロトタイプチェーンをたどってみるのも良い手です。
prototypeのデバッグ方法
JavaScriptのデバッグを行う際、prototypeのしくみを知っているとエラーの原因を早く見つけることができます。
オブジェクトのメソッドが呼べない、あるいは想定外の結果が返ってくる場合は、プロトタイプチェーンのどこかに問題があるのかもしれません。
- ブラウザ開発者ツール:
apple.__proto__
などをコンソールで入力すると、どのオブジェクトを参照しているか確認できます。 - Object.getPrototypeOf():
Object.getPrototypeOf(apple)
のように書いてプロトタイプを調べることができます。 - constructorの確認:
apple.constructor
で、どのコンストラクタを指しているのか確かめると、思わぬところで書き換えられていないかどうか発見しやすいです。
バグの原因がプロトタイプチェーンに潜んでいるケースは意外と多いので、基本的なデバッグ手順として覚えておくと便利です。
クラス構文で書かれているコードでも、同じように__proto__
やObject.getPrototypeOf()
を使って調べることができます。
まとめ
JavaScriptのprototypeは、初心者の方にとっては少しわかりにくい概念かもしれませんが、オブジェクト同士がどのようにメソッドやプロパティを共有しているのかを理解するうえで不可欠な要素です。
クラス構文で記述している場合も、根本的にはprototypeを用いた継承モデルが採用されていますので、コードの仕組みを深く把握するために知っておくと心強いでしょう。
オブジェクトの構造を把握しやすくなるだけでなく、共通のメソッドを一元管理したり、柔軟な継承関係を作り上げたりできるのがprototypeの特徴です。
ただし、組み込みオブジェクトのprototypeを上書きしたり、あまりにも複雑な継承関係を作りすぎたりすると、保守性が低下してしまうリスクもあるため、現場では扱い方に工夫が求められます。
実務では、クラス構文と関数コンストラクタの混在や、既存のコードで独自に拡張されたprototypeを読まなければならない場合もあるでしょう。
しかし、prototypeの基本的な仕組みを理解していれば、コードリーディングもずいぶん楽になるはずです。
ぜひ、今回紹介した内容を参考にしながら、自分のプロジェクトでのコードやライブラリの内部を観察してみてください。
そうすることで、JavaScriptの動きがより深いところで理解できるようになるのではないでしょうか。