メインコンテンツまでスキップ

プロトタイプベース

ここではJavaScriptのプロトタイプベースの概要を説明します。JavaやPHPなどでクラスを使ったことがある方や、オブジェクト指向プログラミングに触れたことがある方を念頭に書いています。また、ここでは主に次の疑問に答えていきます。

  • プロトタイプベースとはどのような考え方なのか?
  • プロトタイプベースのJavaScriptは、クラスベースのPHPやJavaとどんなところが違う?
  • なぜJavaScriptはプロトタイプベースを採用したのか?
  • プロトタイプベースの利点は何か?

オブジェクトの生成

オブジェクト指向プログラミング(OOP)では、オブジェクトを扱います。オブジェクトを扱う以上は、オブジェクトを生成する必要があります。

しかし、オブジェクトの生成方式は、OOPで統一的な決まりはありません。言語によって異なるのです。言語によりオブジェクト生成の細部は異なりますが、生成方法は大きく分けて「クラスベース」と「プロトタイプベース」があります。

クラスベースとは

JavaやPHP、Ruby、Pythonなどはクラスベースに分類されます。クラスベースでのオブジェクト生成は、オブジェクトの設計図である「クラス」を用います。クラスに対してnew演算子を用いるなどして得られるのがオブジェクトであり、クラスベースの世界では、それを「インスタンス」と呼びます。

たとえば、ボタンのオブジェクトがほしいときは、まずその設計図となるボタンクラスを作ります。

js
class Button {
constructor(name) {
this.name = name;
}
}
js
class Button {
constructor(name) {
this.name = name;
}
}

その上で、ボタンクラスに対してnew演算子を用いると、ボタンオブジェクトが得られます。

js
const dangerousButton = new Button("絶対に押すなよ?");
js
const dangerousButton = new Button("絶対に押すなよ?");

このような言語がクラスベースと言われるのは、オブジェクトの素となるのがクラスだからです。

プロトタイプベースとは

一方のJavaScriptのオブジェクト生成はプロトタイプベースです。プロトタイプベースの特徴は、クラスのようなものが無いところです。(あったとしてもクラスもオブジェクトの一種だったりと特別扱いされていない)

クラスベースではオブジェクトの素となるものはクラスでした。プロトタイプベースには、クラスがありません。では、何を素にしてオブジェクトを生成するのでしょうか。答えは、「オブジェクトを素にして新しいオブジェクトを生成する」です。

たとえば、JavaScriptでは既存のオブジェクトに対して、Object.create()を実行すると新しいオブジェクトが得られます。

js
const button = {
name: "ボタン",
};
 
const dangerousButton = Object.create(button);
dangerousButton.name = "絶対に押すなよ?";
js
const button = {
name: "ボタン",
};
 
const dangerousButton = Object.create(button);
dangerousButton.name = "絶対に押すなよ?";

上の例のbuttondangerousButtonは異なるオブジェクトになります。その証拠に、それぞれのnameプロパティは値が異なります。

js
console.log(button.name);
"ボタン"
console.log(dangerousButton.name);
"絶対に押すなよ?"
js
console.log(button.name);
"ボタン"
console.log(dangerousButton.name);
"絶対に押すなよ?"

「プロトタイプ」とは日本語では「原型」のことです。プロトタイプベースは単純に言ってしまえば、原型となるオブジェクトを素にオブジェクトを生成するアプローチなのです。

info

コラム: プロトタイプベースは直感的でない?

この本の読者の多くは、PHPやJavaなどクラスベースの言語に馴染みが深いかと思います。その立場からすると、プロトタイプベースは直感的でないと感じるかもしれません。ところが、日常生活で私たちはプロトタイプベース的な活動をしていることがあります。ここでは、プロトタイプベースが少しでも身近に感じられるよう、ちょっとした例え話をしたいと思います。

仕事などで書類を作成することはないでしょうか。会議の議事録、テスト仕様書、報告書、経費精算書…。いろいろあると思います。中には定期的、または不定期に同じような書類を何度か作ることもあるでしょう。みなさんは繰り返しのペーパーワークをどうこなしていますか。

準備のいい人は、雛形を作っておくことでしょう。雛形とは、いつも変わらない部分は埋めておき、毎度内容が変わる部分は空欄にした文書のことです。いざ書類が必要になったときは、雛形をベースに穴埋めすれば書類ができます。このやり方はクラスベースに似ています。クラスはそのままでは使えませんが、インスタンス化すると使えます。書類の雛形もそのままでは提出できませんが、穴埋めすれば役立ちます。

一方で、書類の準備の時間がないときや、準備のモチベーションが上がらないときは、雛形までは作らないかもしれません。それでも、前回使った書類があれば、それを複製して今回必要になることに合わせて内容を加筆したり、置き換えたりして仕上げてしまうことはありませんでしょうか。このアプローチはプロトタイプベースに似ています。プロトタイプとなるオブジェクトはそれ自身も使えますし、それを素にした新しいオブジェクトももちろん使えます。前回使った書類はそれ自身で役に立っていますが、それを複製して作った新しい書類も役に立ちます。

継承

継承についても、クラスベースとプロトタイプベースでは異なる特徴があります。クラスベースでは、継承するときはextendsキーワードなどを用いてクラスからクラスを派生させ、派生クラスからオブジェクトを生成する手順を踏みます。

では上の手順を具体的なコードで確認してみましょう。ここにCounterクラスがあります。

js
class Counter {
constructor() {
this.count = 0;
}
 
countUp() {
this.count++;
}
}
js
class Counter {
constructor() {
this.count = 0;
}
 
countUp() {
this.count++;
}
}

このクラスは数とそれをカウントアップする振る舞いを持っています。このCounterクラスを継承して、リセット機能を持った派生クラスは次のResettableCounterクラスになります。

js
class ResettableCounter extends Counter {
reset() {
this.count = 0;
}
}
js
class ResettableCounter extends Counter {
reset() {
this.count = 0;
}
}

このResettableCounterクラスを使うには、このクラスに対してnew演算子でオブジェクトを生成します。

js
counter = new ResettableCounter();
counter.countUp();
counter.reset();
js
counter = new ResettableCounter();
counter.countUp();
counter.reset();

以上の例でもわかるとおり、クラスベースでの継承とオブジェクトの生成はextendsnewといった異なる言語機能になっていることが多いです。

一方、プロトタイプベースのJavaScriptでは、継承もオブジェクトの生成と同じプロセスで行います。次の例は、counterオブジェクトを継承したresettableCounterオブジェクトを作っています。

js
const counter = {
count: 0,
countUp() {
this.count++;
},
};
 
const resettableCounter = Object.create(counter);
resettableCounter.reset = function () {
this.count = 0;
};
js
const counter = {
count: 0,
countUp() {
this.count++;
},
};
 
const resettableCounter = Object.create(counter);
resettableCounter.reset = function () {
this.count = 0;
};

継承と言ってもプロトタイプベースでは、クラスベースのextendsのような特別な仕掛けがあるわけではなく、「既存のオブジェクトから新しいオブジェクトを作る」というプロトタイプベースの仕組みを継承に応用しているにすぎません。

クラスベース風にも書けるJavaScript

ここまでの説明で、クラスベースに慣れ親しんだ読者の中には「JavaScriptでオブジェクト指向プログラミングをしようとすると、随分と独特な書き方になるんだな」と思った方がいるかもしれません。ここで誤解して欲しくないのが、プロトタイプベースのJavaScriptでもクラスのような書き方ができるようになっていることです。

古いJavaScriptには確かにクラスの構文がなく独特の書き方がありましたが、ES2015にclassextends構文が導入されたため、近年のJavaScriptではクラスベース風の書き方が容易にできるようになっています。なので、クラスベースの他言語から来た開発者にも、JavaScriptコードは理解しやすいものになってきています。次のコードはクラスベースの説明の際に提示したものですが、実はこれはJavaScriptでした。

js
class Counter {
constructor() {
this.count = 0;
}
 
countUp() {
this.count++;
}
}
js
class Counter {
constructor() {
this.count = 0;
}
 
countUp() {
this.count++;
}
}

class構文が使える近年のJavaScript開発では、Object.createを多用したり、無理にプロトタイプベースを意識したコードにする必要もそうそう無いので心配しないでください。ただ、class構文があると言っても、JavaScriptがクラスベースに転向したのではなく、クラスベース風の書き方ができるにすぎません。かくいうclass構文もプロトタイプベースの仕組みの上に成り立っており、JavaScriptのオブジェクトモデルはプロトタイプベースなので、この点は頭の片隅に入れておく必要があります。

なぜJavaScriptはプロトタイプベースなのか?

JavaScriptが採用しているプロトタイプベースがどのようなものなのか見てきました。では、なぜJavaScriptはクラスベースではなくプロトタイプベースを選んだのでしょうか。プロトタイプベースにした狙いとは何だったのでしょうか。

JavaScriptの開発には次のような要件がありました。ブラウザで動く言語で、構文はJava風に。しかし、Javaほど大掛かりでないようにと。そして、開発期間はというと、10日と逼迫したものでした。

クラスベースの言語を作るのは、プロトタイプベースの言語を作るより難しいと言われています。JavaScriptを作るのに与えられた時間は非常に少ないものでしたから、工数削減にもプロトタイプベースは一役買ったことでしょう。

Javaに似せよと言われて作られたJavaScript。Javaはクラスベースですが、JavaScriptはプロトタイプベースです。では、JavaScriptは泣く泣くクラスベースを諦めたのでしょうか。実はそうではありません。JavaScriptの作者であるBrendan Eich氏は後のインタビューで次のように語っています。

Seibel: So you wanted to be like Java, but not too much.
(Javaのようにしたいけれど、大掛かりはしたくなかったわけですね。)

Eich: Not too much. If I put classes in, I'd be in big trouble. Not that I really had time to, but that would've been a no-no.
(そうですね。もしクラスを取り入れていたら、大変なことになっていたでしょう。時間がなかったのは確かですが、時間があったとしてもクラスはいやですね。)

JavaScriptはクラスベースにするつもりはハナからなかったわけです。Eich氏はJavaScriptを設計するにあたって、できるだけ言語をシンプルにしたいと考えていたようです。JavaScriptはプリミティブ型の種類が少なかったり、プリミティブ型もオブジェクトのようにメソッドが使えるようになっていてプリミティブとオブジェクトの間に大きな隔たりが無かったりします。こうした言語設計もシンプルさを目指したからだそうです。

JavaScriptの開発にあたり、Selfという言語の影響があったとEich氏は言います。Selfは1990年に発表されたプロトタイプベースのオブジェクト指向言語です。Selfの発表論文に掲げられたタイトルは「The Power of Simplicity」つまり「シンプルさの力」です。Selfはクラスを用いたオブジェクト指向プログラミングよりも、プロトタイプベースのほうが言語が単純化されると同時に柔軟になると主張しました。Selfはクラスだけでなく、関数と値の区別や、メソッドとフィールドの区別も撤廃したシンプルさを追求した言語です。言語は単純になると、言語の説明も簡単になり学びやすくもなります。シンプルにするために継承やクラスを諦めたかというとそうではなく、逆に柔軟さが生まれるので、クラスのようなものや継承もプロトタイプを応用すれば実現できるとSelfは主張しています。

これはあくまでSelfの意見でJavaScriptが明言したわけではありませんが、歴史の文脈から読み取るに、JavaScriptもSelfの考え方に共感してプロトタイプベースを採用したのは明らかです。JavaScriptのプロトタイプベース採用の背景には、言語をシンプルで柔軟なものにしたいという考えが根底にあったわけです。

JavaScriptがプロトタイプベースを採用したことで、実際に柔軟なプログラミングが行えるようになっています。その一例として、プロトタイプを応用してクラス風のオブジェクト指向を実現するイディオムが生まれ、それがclass構文として言語仕様に取り込まれたり、プロトタイプをプログラマが拡張することで古い実行環境でも最新バージョンのJavaScriptのメソッドが使えるようにするポリフィルが誕生してきました。

まとめ

  • クラスベースは、クラスをもとに新しいオブジェクトを生成するスタイル。JavaやPHPなどが該当。
  • プロトタイプベースは、既存のオブジェクトから新しいオブジェクトを生成するスタイル。JavaScriptが該当。
  • プロトタイプベースでの継承は、特別な操作ではなく、オブジェクト生成とまったく同じプロセスである。
  • JavaScriptでもclass構文を使えばクラスベース風のプログラミングが可能。
  • JavaScriptがプロトタイプベースを採用したのは、言語をシンプルで柔軟なものにするのが狙い。
  • 質問する ─ 読んでも分からなかったこと、TypeScriptで分からないこと、お気軽にGitHubまで🙂
  • 問題を報告する ─ 文章やサンプルコードなどの誤植はお知らせください。