Google流JavaScriptにおけるクラス定義の実現方法(ES6以前)
目次
- 2019年追記
- はじめに
- Google Closure 流のクラスの実現方法の概要
- クラスの宣言とコンストラクタの定義
- メンバ変数 (インスタンス変数)
- inherits の実際のコード
- 良くないクラス実現方法
- 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
のように呼び出すと前者の場合では this
は alice
を, 後者の場合では
bob
を指すので それぞれの実行で I'm Alice
と I'm Bob
が表示されます。 また下の例のように .
が複数存在する場合は this
はその関数を直接格納していたオブジェクト child
を参照します。
bob.child.sayHello(); // Hello, I'm Alice
なおクラスを実現する上ではあまり重要なことではないですが、 関数を単に
method();
という形で単体で実行した場合には this
は window
を指します。
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.age
は Person
に渡された引数 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
を知っている人は this
は self
に相当するものだと思うと分かりやすいかと思います。
インスタンス変数やメソッド呼び出しの際に 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
のコンストラクタ Person
の prototype
プロパティ Person.prototype
が alice のプロトタイプとなります。 つまり
alice
に存在しないプロパティがアクセスされた場合、JavaScript は
Person.prototype
から同名のプロパティを探してきます。 そのため、
alice.sayHello
は Person.prototype.sayHello
になります(プロトタイプチェーン)。 さらに JavaScript では this
は関数が呼び出された際にその関数を保持していたオブジェクトがセットされるので、
alice.sayHello();
という形で sayHello
を呼び出した際には this
は
alice
となります。
このように prototype
と this
の単体だと何のためにあるのか分からない奇妙な仕様がこのように。
上のメソッド定義の例をみると this
が prototype
を指すのではないか?心配になるかもしれませんが前節で述べたように
JavaScript の this
は関数が呼び出された際にその関数をプロパティ保持していたオブジェクトを指します。そのため、
alice.sayHello();
という形で sayHello
を呼び出した場合は this
は
alice
を指すことになるのです。
private, protected
JavaScript でクラスを実現する場合、メンバ変数やメソッドを private
や
protected
にすることはできません。 ただし名前規約で 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.prototype
に method
が見つからない場合は、さらにプロトタイプチェーンをたどって
Parent.prototype
から method
を探してきて呼び出します。
仕組み上、C++
のようにオーバーライドする関数に virtual
などの特殊な修飾子を付ける必要はありません。 また Java
のようにメソッドに final
をつけてオーバーライドを禁止することもできません。
子クラスで同名のメソッドを定義されてしまえば問答無用でオーバーライドされてしまいます。
これは大規模なプログラムでは問題になってしまいますが、純粋な JavaScript
では解決する手段がありません。
また親クラスのメソッドを明示的に呼び出すには、親クラスのコンストラクタの呼び出しの場合と同様に
call
を使用します。
Parent.prototype.sayHello(this);
多重継承
プロトタイプチェーンを利用している仕組み上、多重継承はできません。
abstract, interface
JavaScript には interface
や abstract
に相当する言語仕様は用意されていません。 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 がプロトタイプベースの言語であることは変わりません。