JavaScript (ES5水準) でのクラスと継承

(2017.7.30) 現代的な内容に更新。

ECMAScript 2015 (ES6) で 'class' キーワードが導入された。糖衣構文 syntactic sugar で、本質的に新しい機能が入ったわけではない。

5th edition 水準の Internet Explorer 11 (IE11) のサポート期間は, Windows 7用が2020年1月まで, Windows 10用が2025年10月まである。少なくとも Windows 7がリタイアする2020年までは、そのままは使えない。

Babel のような変換コンパイラを使う手もあるが, 当面の間は無理せず 5th edition のレベルの書き方でいくのがいい,か。

ES6でのクラス

まず、ES6での書き方を示す。2017年7月現在、Webブラウザでのサポート範囲を考えると、まだ、バリバリ使えない。

だいぶclassベースのオブジェクト指向プログラミング言語に近い書き方。

JavaScript
[RAW]
  1. "use strict";
  2. // class の内側に静的変数は置けない.
  3. var animalCount = 0;
  4. class Animal {
  5. // コンストラクタ。'constructor' で固定.
  6. constructor(kind) {
  7. this._kind = kind;
  8. animalCount += 1;
  9. }
  10. speak(cry) {
  11. // 'this' は省略できない
  12. alert( (this._name ? this._name : this._kind) + ' cries ' + cry );
  13. }
  14. // 静的メソッド (作為的な例. この中身ならメソッドにすべきだが.)
  15. static hasName(animal) {
  16. // Object.getPrototypeOf(animal) === Animal.prototype とは違う
  17. // 派生クラスのオブジェクトでも正しく判定
  18. if (!(animal instanceof Animal))
  19. throw new TypeError();
  20. return !!animal._name;
  21. }
  22. static count() {
  23. return animalCount;
  24. }
  25. // アクセサ. name=() が定義される.
  26. set name(name) {
  27. this._name = name + "-san";
  28. }
  29. }

構築子 (コンストラクタ) は constructor()で固定。静的メソッドも static で作れる。get または set でアクセサを作れる。

継承したサブクラスを作る。

JavaScript
[RAW]
  1. // 継承
  2. class Cat extends Animal {
  3. constructor(color) {
  4. if (color === undefined) // 実引数なしで呼び出せてしまう.
  5. throw new Error('color need');
  6. // 基底クラスのコンストラクタは明示的に呼ばなければならない。
  7. super('cat');
  8. this._color = color;
  9. }
  10. meow() {
  11. // 基底クラスのメソッドを呼び出す
  12. this.speak('みゃーー');
  13. // または super.speak('みゃーー');
  14. alert('color is ' + this._color );
  15. }
  16. }
  17. // Cat(); => TypeError: cannot be invoked without 'new'
  18. var myCat = new Cat('red');
  19. // 静的メソッドも継承される
  20. document.write( Cat.hasName(myCat) ); //=> false
  21. myCat.meow();
  22. myCat.name = "taro";
  23. document.write( Cat.hasName(myCat) ); //=> true
  24. myCat.meow();
  25. document.write( Animal.count() );

コンストラクタといっても、糖衣構文で実態はただの関数なので、super()で明示的に基底クラスのコンストラクタを呼び出さないといけない。

メソッドをオーバーライドした場合で、基底クラスのメソッドを呼び出したいときは, super.メソッド名() でよい。

ES5: コンストラクタ, メソッド定義

ここからは, 'class' キーワードを使わずに、ECMAScript 5 の範囲で、上のクラスを再現していく。

まずは基底クラス。

JavaScript
[RAW]
  1. "use strict";
  2. var animalCount = 0;
  3. // コンストラクタ
  4. function Animal(kind) {
  5. if (this === undefined) // 直接呼び出し対策. ES6の class では自動で入る.
  6. throw new TypeError();
  7. this._kind = kind;
  8. animalCount += 1;
  9. }
  10. // メソッドを定義する
  11. Animal.prototype = {
  12. constructor: Animal,
  13. speak: function(cry) {
  14. alert( (this._name ? this._name : this._kind) + ' cries ' + cry );
  15. }, // カンマが必要
  16. // アクセサ
  17. set name(name) {
  18. this._name = name + "-san";
  19. }
  20. };
  21. // 静的メソッド.
  22. // IE11でも動く.
  23. Animal.hasName = function(animal) {
  24. if (!(animal instanceof Animal))
  25. throw new TypeError();
  26. return !!animal._name;
  27. }
  28. Animal.count = function() {
  29. return animalCount;
  30. }
  31. // Animal(); => コンストラクタで TypeError になるようにしている
  32. var ani = new Animal('dog');
  33. document.write( Animal.hasName(ani) );
  34. ani.speak('bow-wow');
  35. ani.name = 'shiro';
  36. document.write( Animal.hasName(ani) );
  37. ani.speak('bow-wow');
  38. document.write( Animal.count() );

JavaScript では, コンストラクタはただの関数。コンストラクタの関数を new してオブジェクトを生成する。newせずに直接呼び出せてしまう。このときは thisundefined になるので、エラーにする。

メソッドは, コンストラクタの関数オブジェクトの prototype プロパティ値に設定する。コンストラクタを prototype.constructor に設定するお約束。

アクセサは ES5から使える。

静的メソッドは, コンストラクタのプロパティとして設定する。

サンプル内で使っている instanceof で、オブジェクトがどのクラス (=コンストラクタ) から作られたか判定できる。挙動については後述。

継承

prototype プロパティ, setPrototypeOf() を使って, クラスの継承のようなものを作る。

基本的に、テストは IE11とそれ以外で行うようにする。

今回の解説のキモは、この class_inherits に集中している。

JavaScript
[RAW]
  1. // 継承を作る
  2. function class_inherits(subClass, superClass) {
  3. if (typeof superClass !== "function" && superClass !== null) {
  4. throw new TypeError("superClass must be null or a constructor");
  5. }
  6. // メソッド
  7. // prototype.constructor は, このサンプルではなくても問題ないが、コピーコン
  8. // ストラクタのため, いつも設定すべき.
  9. // See https://stackoverflow.com/questions/8453887/why-is-it-necessary-to-set-the-prototype-constructor
  10. // Object.create() の第2引数は, Object.defineProperties() の第2引数のアイテ
  11. // ムと同じ形.
  12. subClass.prototype = Object.create(superClass.prototype, {
  13. constructor: { value: subClass,
  14. enumerable: false,
  15. writable: true,
  16. configurable: true }
  17. });
  18. // 静的メソッド. 継承させないでもいいが、ES6に合わせて継承させる
  19. // Object.setPrototypeOf() は IE11でサポートされない。
  20. // See https://docs.microsoft.com/en-us/scripting/javascript/reference/object-setprototypeof-function-javascript
  21. if (Object.setPrototypeOf)
  22. Object.setPrototypeOf(subClass, superClass);
  23. else
  24. subClass.__proto__ = superClass;
  25. }

現代の標準的なやり方では Object.create(orig [, Properties]) を使う。この関数は,

  1. 空のオブジェクトを作る => new_obj とする
  2. new_obj の [[Prototype]] 内部プロパティに orig をセットする。orig.prototype ではない。
  3. 引数 Properties がある場合, new_obj のプロパティに追加する。
  4. 戻り値は new_obj

第一引数をコピーするのではなく, [[Prototype]] 内部プロパティ (= __proto__) に設定するのがミソ。このプロトタイプチェインによって、メソッドのオーバーライドや, instanceof による判定が実現できる。

非常に混乱しやすいのは、prototypeプロパティと [[Prototype]] 内部プロパティが別物というところ。前者は新しいオブジェクトのためのテンプレートであり、後者はオブジェクトのプロパティが存在しない場合の辿っていく先であり, __proto__ でアクセスできる。

上のスクリプトに話を戻して, constructorをサブクラスのものに差し替えるのを忘れずに。

静的メソッドの継承は, コンストラクタ関数オブジェクト自身がメソッド呼び出しの際のレシーバ (this) になるので、そのメソッドの辿り先は、スーパークラス関数オブジェクトになる。

では、これを利用して、派生クラスを作ってみる。

JavaScript
[RAW]
  1. /////////////////////////////
  2. // class Cat
  3. function Cat(color) {
  4. if (this === undefined) // 直接呼び出し対策. ES6の class では自動で入る.
  5. throw new TypeError();
  6. if (color === undefined) // 実引数なしで呼び出せてしまう.
  7. throw new Error('color need');
  8. // 基底クラスのコンストラクタを呼び出す
  9. Animal.call( this, "neko" );
  10. this._color = color;
  11. }
  12. // 基底-派生クラスの関係を作る
  13. class_inherits(Cat, Animal);
  14. // メソッドを追加
  15. Cat.prototype.meow = function() {
  16. // オーバーライドした場合に、基底クラスの同名メソッドを呼び出すときは
  17. // Animal.prototype.speak.call(this, 'みゃーー');
  18. this.speak('みゃーー');
  19. alert('color is ' + this._color );
  20. }
  21. // class Cat
  22. /////////////////////////////
  23. // Cat(); => コンストラクタで TypeError になるように.
  24. var myCat = new Cat('white');
  25. // 静的メソッドも継承される
  26. document.write( Cat.hasName(myCat) ); //=> false
  27. myCat.meow();
  28. myCat.name = "taro";
  29. document.write( Cat.hasName(myCat) ); //=> true
  30. myCat.meow();
  31. document.write( Cat.count() ); //=> 2

myCatオブジェクトは, 次のようなプロトタイプチェインを持つ;

JavaScript
[RAW]
  1. {
  2. _kind: "neko", _color: "white", _name: "taro-san",
  3. __proto__: {
  4. // このオブジェクトが Cat.prototype
  5. meow: function (),
  6. constructor: function Cat(color),
  7. __proto__: {
  8. // これが Animal.prototype
  9. constructor: function Animal(kind),
  10. speak: function (cry),
  11. set name: function name(name),
  12. __proto__: { 以下略 }
  13. }
  14. }
  15. }

これを実現するために, class_inherits() 内で, subClass.prototype.__proto__superClass.prototype を設定する必要があった。

obj instanceof Func は, プロトタイプチェインを順に辿って, Func.prototype と一致するかを調べていく。