Google流JavaScriptにおけるクラス定義の実現方法(ES6以前)

目次

2019年追記

この記事ではclassが導入されたES6以前のJavaScriptでどのようにクラスに相当するものを実現していたかを解説しています。 ES6でクラスが導入されほとんどのブラウザがサポートしている2019年現在、ここで書かれている手法を直接使用することはないでしょう。 ただし、この記事で説明するようにJavaScriptはprototypeベースの言語であり、それは今後もJavaScriptが下位互換性を維持する限り変わることはありません。 MDNにも書かれているようにJavaScriptのclass文法はあくまでプロトタイプを使って他の言語のクラスに相当することを実現するためのシンタックスシュガー(糖衣構文)に過ぎません。

classが導入された今、prototypeベースの言語であるJavaScriptでどのようにクラスが実現されているかは理解していなくてもJavaScriptで最低限の仕事はできてしまうのは事実でしょう。今後はprototypeを聞いたこともないJavaScriptエンジニアがあらわれても驚きません。しかしMDNのクラスの最初に書かれている

ECMAScript 2015 で導入された JavaScript クラスは、JavaScript にすでにあるプロトタイプベース継承の糖衣構文です。クラス構文は、新しいオブジェクト指向継承モデルを JavaScript に導入しているわけではありません。

の意味がわからなくては、なんとなくJavaScriptを書いていたかつての僕のように、永遠にJavaScript初心者のままでしょう。 今後もJavaScriptがJavaScriptである限り、ここでまとめてある知識は中級者以上には不可欠な知識であると思います。

はじめに

他のメジャーなオブジェクト指向プログラミング言語と異なり (ES6以前のオリジナルの)JavaScript には「クラス」が存在しません。 代わりに C++, Java などにはない prototype や C++, Java のとは全く異なる new 演算子や this が用意されています。 これらの機能は一見するとどれもかなり奇妙な仕様をしています。 そのため、それぞれの機能の仕様を 1 つ 1 つ勉強しても一体全体何のためにそんな機能が用意されていて、 どのようにその機能を活用してプログラムを作ればよいのか全く理解できないと思います。

そのため C++, Java, Python などの「まともな」オブジェクト指向プログラミング言語の経験のあるプログラマが JavaScript で大規模なプログラミングを書こうとすると クラスがないのにプログラムをどうやってモジュール化したらよいのか分からないし、 代わりにある this とか prototype とかは何に使えばよいか分からないしで途方に暮れてしまうと思います。

しかし JavaScript には「クラス」という言語仕様は用意されていないものの、this, prototype などを一定のルールに基づいて利用すれば 他の言語のクラスほぼ同等のことは実現可能です。 つまり他のクラスで行うようなクラスを使ったカプセル化、ポリモーフィズム、継承などを JavaScript でも実現することができます。 このドキュメントでは Google が公開している JavaScript のオープンソースライブラリ Google Closure Library を参考にしてどのように JavaScript でクラスを実現すればよいかを学びます。

クラス実現のために必要な JavaScript の言語仕様

JavaScript でのクラスの実現方法を理解するためには this, new, prototype などの JavaScript の特殊な言語仕様を理解している必要があります。 まずはそうした JavaScript の言語仕様から復習しておきます。

function

JavaScript では function で関数を定義します。

var sum = function(a, b) {
  return a + b;
};

JavaScript における関数の定義方法を知らない場合はクラスの実現方法を学ぶ前に、まず JavaScript の入門書や 関数と関数スコープ などを読んだほうがよいかと思います。ここでは詳細は省略します。

this

JavaScript では this という特殊な変数が関数の中で利用可能です。 JavaScript の this は Java, C++ の this とは全く挙動が異なる ので注意してください。 JavaScript の this はある関数が呼び出された際にその関数を格納していた object を指します。 例えば

var sayHelloShared = function() {
  console.log("Hello, I'm " + this.name);
};

という関数があり、それが alice, bob というオブジェクトの sayHello として登録されていたとします。

var alice = {
  sayHello: sayHelloShared,
  name: "Alice"
};

var bob = {
  sayHello: sayHelloShared,
  name: "Bob",
  child: alice
};

これを

alice.sayHello(); // Hello, I'm Alice
bob.sayHello(); // Hello, I'm Bob

のように呼び出すと前者の場合では thisalice を, 後者の場合では bob を指すので それぞれの実行で I'm AliceI'm Bob が表示されます。 また下の例のように . が複数存在する場合は this はその関数を直接格納していたオブジェクト child を参照します。

bob.child.sayHello(); // Hello, I'm Alice

なおクラスを実現する上ではあまり重要なことではないですが、 関数を単に method(); という形で単体で実行した場合には thiswindow を指します。

call

関数呼び出しの際に this の明示的に指定することも可能です。 それには call を利用します。 call は全ての関数が暗黙的に持っているプロパティで、関数として呼び出すことができます。 call を呼ぶと call の第一引数として渡されたオブジェクトが this にセットされて元の関数が呼び出されます。 第二引数移行は元の関数の引数として利用されます。

sayHelloShared.call(alice); // Hello, I'm Alice
sayHelloShared.call(bob); // Hello, I'm Bob

new 演算子

JavaScript にも new 演算子が存在します。 ただし JavaScript の new も Java や C++ でクラスのインスタンス化を行う new とは全く動きが異なります。 C++, Java では new はクラスと共に利用しますが JavaScript の new は任意の関数と一緒に呼び出します。

new <関数>(<引数>);

new と一緒に関数を呼び出すと、まず新しい空のオブジェクト (つまり {}) が生成されます。 次に関数が呼び出されますが、その際に関数内の this が生成されたオブジェクトを指すようになります。 関数が実行された後、生成されたオブジェクトが new の実行結果として返されます。

var Person = function(name, age) {
  this.name = name;
  this.age = age;
};

var alice = new Person("Alice", 7);

例えばこの例では、new Person... によって新しいオブジェクトが生成され、それが this に格納されて Person が実行され、 name, age がオブジェクトにセットされます。そして生成されたオブジェクトは alice に代入されます。 そのため、alice.name, alice.agePerson に渡された引数 name, age になります。

console.log(alice.name); // Alice
console.log(alice.age); // 7

もうお気づきのように、new 演算子を使うことで JavaScript では関数を「コンストラクタ」として利用することができます。 実際 new で生成されたオブジェクトは constructor というプロパティで生成時に利用された関数への参照を保持しています。

console.log(alice.constructor == Person); // true

prototype チェーン

JavaScript のオブジェクトは基本的には key と value のペアを保持する単なるマップ (連想配列) です。 obj['prop'] = value; あるいは obj.prop = value; のようにキーと値のペアを代入するとオブジェクトが内部的に保持しているマップにキーと値が保存されます。

var alice = {
  name: "Alice" // 'name': 'Alice' と同義
};
alice.age = 7; // alice['age'] = 7; と同義

登録した値は obj['prop'] あるいは obj.prop のように参照できます。参照されたキーが存在しない場合は undefined が返されます。

// alice['name'] と alice.name は同義
console.log(alice.name); // Alice
console.log(alice.age); // 7
console.log(alice.address); // undefined

これがオブジェクトの基本動作です。しかし実は参照されたプロパティをオブジェクトが持っていなかった場合に、 他のオブジェクトからプロパティを探してきて参照するための仕組みが JavaScript には用意されています。 それがプロトタイプチェーン (prototype chain) と呼ばれるものです。

JavaScript のオブジェクトは他のオブジェクトを プロトタイプ として利用することができます。 オブジェクのプロパティが参照された際、そのプロパティをオブジェクト自身が保持していない場合には代わりにプロトタイプのオブジェクトのプロパティが参照されます。 またプロトタイプのオブジェクトがそのプロパティを保持していない場合には、さらにプロトタイプのプロトタイプを参照します。 このようにプロトタイプとしてオブジェクトが鎖のように繋がれて、それが順々に参照されることからこの仕組は「プロトタイプチェーン」と呼ばれます。

なお JavaScript の仕様書 ではこの プロトタイプ を表す内部的なプロパティを obj[[Prototype]] のように記述します。 ただしこれは仕様書の中でのみ現れる表現であって、JavaScript のコードの中では利用できません。 JavaScript で obj のプロトタイプを参照するには Object.getPrototypeOf(obj) を使用します。 逆に obj のプロトタイプとして proto を設定するには Object.setPrototypeOf(obj, proto) を利用します。

一部の JavaScript エンジンでは [[Prototype]] に相当する __proto__という特殊なプロパティが用意されていて、 このプロパティを参照、設定することでオブジェクトのプロトタイプを参照、設定することができます。 ただしこれは非標準の機能であり廃止される予定なので今後はあまり利用しないほうがよいでしょう。 ES6 でprotoが標準になりました。ただ下位互換性の観点からあまり推奨されないことには変わりないかと思います。

プロパティ: prototype

プロトタイプの設定の方法はもう 1 つ存在します。それが関数の prototype プロパティを使う方法です。 実は function で作られた関数オブジェクトには prototype というプロパティが存在し、空のオブジェクトが格納されています。 そしてその関数が new 演算子とともにコンストラクタとして実行された際に、new で作成されたオブジェクト(つまり関数内では this が表すオブジェクト) のプロトタイプとして関数の prototype プロパティのオブジェクトが設定されます。

名前が非常に紛らわしいですが、 prototype はオブジェクトのプロトタイプを表すプロパティではありませんprototype プロパティは「そのオブジェクトがコンストラクタとして利用された際に作成される新しいオブジェクト」のプロトタイプを決めるものです。 オブジェクトのプロトタイプを表すプロパティは __proto__ あるいは言語仕様書で [[Prototype]] と表されるもので prototype プロパティとは異なります。 ここを勘違いしてしまうと混乱のもとになるので自分で図を書いたりコードを実行してよく違いを理解しておいて下さい。

さてこの prototype というプロパティ、オブジェクトの直接のプロトタイプを表さないので一見非常に使いにくように思えます。 しかしこの特殊な仕様が JavaScript でクラスを実現するにはとても重要になります。 実際、JavaScript でプロトタイプを利用する場合 setPrototypeOf よりもこちらを使うのが一般的です。

var Constructor = function() {};
Constructor.prototype.a = "Apple";
Constructor.prototype.b = "Banana";

var instance = new Constructor();

console.log(Object.getPrototypeOf(instance) == Constructor.prototype); // true
console.log(instance.a); // 'Apple';
console.log(instance.b); // 'Banana';

Google Closure 流のクラスの実現方法の概要

まずクラスの実現方法の例を示して、それから各要素について解説します。 クラスの定義は次のような形になります。

// クラスとコンストラクタは関数を使って定義します
Person = function(name, age) {
  // this はインスタンスを表します。
  this.name = name;
  this.age = age;
};

// メソッドはコンストラクタの prototype プロパティに定義します
Person.prototype.getName = function() {
  // メンバ変数の定義・参照は this.<メンバ変数> を使います。
  // C++, Java と違い this は省略できません。
  return this.name;
};

Person.prototype.sayHello = function() {
  // メソッド内から他のメソッドを呼ぶ場合も this.<メソッド> を使います。
  // C++, Java と違い this は省略できません。
  console.log("Hello I'm " + this.getName());
};

クラスをインスタンス化する際には new を使います。

var alice = new Person("Alice", 7);
alice.sayHello();

継承は inherits という関数を用意して次のように行います。

var inherits = function(childCtor, parentCtor) {
  // 子クラスの prototype のプロトタイプとして 親クラスの
  // prototype を指定することで継承が実現される
  Object.setPrototypeOf(childCtor.prototype, parentCtor.prototype);
};

// 子クラスのコンストラクタ
var Employee = function(name, age, salary) {
  // 親クラスのコンストラクタの呼び出しには call を使用
  Person.call(this, name, age);
  this.salary = salary;
};

// inherits を使って親子関係を明示する
inherits(Employee, Person);

// 子クラスのメソッド
Employee.prototype.getSalary = function() {
  return this.salary;
};

// 同じ名前のメソッドを子クラスで定義すればオーバーライドになる。
Employee.prototype.sayHello = function() {
  // 親クラスのメソッドを呼び出す場合は親クラスの prototype に
  // 定義されているメソッドを call を使って呼び出す。
  Person.prototype.sayHello.call(this);
  console.log("Salary is " + this.salary);
};

ではそれぞれの要素について解説していきましょう。

クラスの宣言とコンストラクタの定義

上で述べたように new 演算子をつかうと関数をクラスのコンストラクタのように利用することができます。 そのため JavaScript では関数を使ってクラスとコンストラクタを同時に定義します。 クラスのインスタンスの生成とコンストラクタの呼び出しには new 演算子を使います。 上述したように JavaScript の new と C++/Java の new の仕様は大きく異なりますが、結果的には似たような使い方をすることになります。

// クラス Person とそのコンストラクタを定義。インスタンス変数の設定にはコンストラクタ中で `this.` を使う。
var Person = function(name, age) {
  // コンストラクタの中身
};

var alice = new Person("Alice", 7);

メンバ変数 (インスタンス変数)

上の例で出てきているように、クラスの内部でメンバ変数を定義・参照するには this.<プロパティ名> を使います。 JavaScript では Java や C++ と違い this を省略することは不可能 なので注意してください。 Python を知っている人は thisself に相当するものだと思うと分かりやすいかと思います。 インスタンス変数やメソッド呼び出しの際に Python では self を付けなければならないように JavaScript では this が必ず必要です。

メソッド定義と呼び出し

JavaScript でメソッドを定義するときにはコンストラクタ関数の prototype オブジェクトに関数を定義します。 またメソッド内から他のメソッドの呼び出しを行う場合は this.<メソッド名>(引数) を使います。 メンバ変数の場合と同様に、メソッド呼び出しの際に this を省略することは不可能 なので気をつけて下さい。

Person.prototype.sayHello = function() {
  console.log("Hello, I'm " + this.getName());
};

Person.prototype.getName = function() {
  return this.name;
};

var alice = new Person("Alice", 7);
alice.sayHello();

まず上述したように alice のコンストラクタ Personprototype プロパティ Person.prototype が alice のプロトタイプとなります。 つまり alice に存在しないプロパティがアクセスされた場合、JavaScript は Person.prototype から同名のプロパティを探してきます。 そのため、 alice.sayHelloPerson.prototype.sayHello になります(プロトタイプチェーン)。 さらに JavaScript では this は関数が呼び出された際にその関数を保持していたオブジェクトがセットされるので、 alice.sayHello(); という形で sayHello を呼び出した際には thisalice となります。

このように prototypethis の単体だと何のためにあるのか分からない奇妙な仕様がこのように。

上のメソッド定義の例をみると thisprototype を指すのではないか?心配になるかもしれませんが前節で述べたように JavaScript の this は関数が呼び出された際にその関数をプロパティ保持していたオブジェクトを指します。そのため、 alice.sayHello(); という形で sayHello を呼び出した場合は thisalice を指すことになるのです。

private, protected

JavaScript でクラスを実現する場合、メンバ変数やメソッドを privateprotected にすることはできません。 ただし名前規約で private なものを名前でわかりやすくして間違えてアクセスしないようにすることはできます。 Google の JavaScript のスタイルガイド では private なメソッド, メンバ変数は名前の末尾に _ をつけることが求められています。

継承

プロトタイプチェーンを利用してメソッドを親クラスから引き継ぐ

子クラスから親クラスのメソッドが引き継がれるようにするには、 子クラスの prototype にメソッドが見つからなかった場合に、親クラスの prototype に定義されてるメソッドが参照されれば良いので、 親クラスの prototype が子クラスの prototype のプロトタイプ (__proto__, あるいは [[Prototype]])になるようにします。

<figure>

前述したように、setPrototypeOf であるオブジェクトを他のオブジェクトのプロトタイプに設定できるので、次のような継承用の関数を事前に用意しておきます。 (Google Closure の実際の inherits は互換性のためにもう少し複雑です)

var inherits = function(childCtor, parentCtor) {
  Object.setPrototypeOf(childCtor.prototype, parentCtor.prototype);
};

子クラスのコンストラクタを定義した後に、 inherits(Child, Parent); のように呼び出して使います。

var Parent = function(arg) {
// Parent のコンストラクタ実装
};

var Parent.prototype.method0 = function() {
console.log('Parent.method0');
}

var Child = function(arg) {
// Child のコンストラクタの実装
};

inherits(Child, Parent);

Child.prototype.method1 = function() {
console.log('Child.method1');
}

var child = new Child();
child.method0();
child.method1();

親クラスのコンストラクタの呼び出し

上の例ではコンストラクタが空だったので問題ありませんでしたが、 現実のプログラムでは初期化を正しく行うためには子クラスのコンストラクタから 親クラスのコンストラクタを呼びださなくてはなりません。

親クラスのコンストラクタを呼び出す際には、親クラスのコンストラクタ内の this が子クラスのコンストラクタ内の this (つまり new で生成された初期化対象のインスタンス) になるようにしなくてはなりません。this を明示的に指定して関数を呼び出すには前述したように call を使います。 そのため親クラスのコンストラクタの呼び出しは Parent.call(this, 引数...) のように行います。

var Child = function(arg) {
  Parent.call(this, arg);
};

メソッドオーバーライドと親クラスのメソッドの呼び出し

前述したように、child.method0() のようにメソッド呼び出しが行われると、 JavaScript はまず child のプロトタイプである Child.prototype から method を探します。 Child.prototypemethod が見つからない場合は、さらにプロトタイプチェーンをたどって Parent.prototype から method を探してきて呼び出します。

仕組み上、C++ のようにオーバーライドする関数に virtual などの特殊な修飾子を付ける必要はありません。 また Java のようにメソッドに final をつけてオーバーライドを禁止することもできません。 子クラスで同名のメソッドを定義されてしまえば問答無用でオーバーライドされてしまいます。 これは大規模なプログラムでは問題になってしまいますが、純粋な JavaScript では解決する手段がありません。

また親クラスのメソッドを明示的に呼び出すには、親クラスのコンストラクタの呼び出しの場合と同様に call を使用します。

Parent.prototype.sayHello(this);

多重継承

プロトタイプチェーンを利用している仕組み上、多重継承はできません。

abstract, interface

JavaScript には interfaceabstract に相当する言語仕様は用意されていません。 Java, C++ の抽象メソッド (abstract method) はメソッドの実体は定義せずに、 子クラスあるいはインターフェースを実装するクラスが実装しなくてはならないメソッドを宣言するものです。 Java, C++ は静的型チェックを行うのでそのような仕組みが必須です。 しかし JavaScript は型は動的にチェックされるので抽象メソッドはなくてもプログラムを書くことは可能です。

ただ現実にはプログラムの可読性を高めるために子クラスが実装しなくてはならないメソッドを明示的に書きたいと思うことも多いでしょう。 JavaScript ではそのような場合、単純に例外を投げるだけの関数を定義してしまいます。これは Python でも同じです。 Java の interface に相当することをしたい場合はすべてのメソッドが例外を投げるだけのクラスを作成すればよいでしょう。

Person.prototype.sayHello() = function() {
  throw new Error('Not Implemented');
};

inherits の実際のコード

前述した継承用の関数 inherits は非常にシンプルでしたが、実際には setPrototypeOf が利用できない古いブラウザの互換性のために、 同じことをもう少し複雑なコードで行う必要があります。 Google Closure の base.js に定義されている goog.inherits はブラウザ互換性のために setPrototypeOf を使わず下のような少し複雑なコードになっています。

goog.inherits = function(childCtor, parentCtor) {
/** @constructor \*/
function tempCtor() {};
tempCtor.prototype = parentCtor.prototype;
childCtor.superClass\_ = parentCtor.prototype;
childCtor.prototype = new tempCtor();
/** @override \*/
childCtor.prototype.constructor = childCtor;
};

良くないクラス実現方法

メソッドの定義方法として、

var Person = function() {
  // ....
};

Person.prototype = {
  sayHello: function() {
    // ...
  },
  getName: function() {
    // ....
  }
};

のように、prototype を新しいオブジェクトで置き換えてしまうコードが揚げられている場合がありますが、 この方法だと、そのクラスが他のクラスを継承している場合にプロトタイプチェーンが切れてしまって継承関係が失われてしまうので 子クラスの定義では使えません。コードの一貫性を保つために、常に

Person.prototype.sayHello = function() {
  /_..._/;
};
Person.prototype.getName = function() {
  /_..._/;
};

のように1つずつメソッドを追加するスタイルのほうが好ましいかと思います。 それと多くの場合問題になりませんが、prototype を新しいオブジェクトで置き換えてしまうと、 Person.prototype.constructor == Person の関係は壊れてしまうのでそこも若干マイナスポイントです。

ES6 のクラス

ECMAScript 6 (ES6) でようやく JavaScript にもクラスの構文が追加されています。数年後、ユーザが使っているブラウザの大半が ECMAScript 6 をサポートするようになれば(2015 年 10 月時点では最新の Firefox ではサポートされていない)、新規のプロジェクトや単なる趣味であればここに書かれている文法を直接使う必要はなくなると思います。 ただ下位互換性が非常に重要になる Closure Library のようなコアライブラリや既存のコード読む上で、あるいはES6 to ES5 converterの出力をデバッグする上で、今後少なくとも数年はもここに書かれている手段への理解は必要となるかと思います。 またES6 のクラスがシンタックスシュガーに過ぎない以上、ES6 のクラスの挙動をきちんと理解したければプロトタイプを使ってどのようにクラスが実現できるのかを理解しておくのは今後も重要だと思います。ES6 はあくまで既存の JavaScript の上に作られた言語であり、JavaScript がプロトタイプベースの言語であることは変わりません。

最終更新: 2019/12/1