現在ではどのブラウザや環境でも、とりあえず提供されてる最新の安定バージョン使っておけばclass構文や、それにより提供されるconstructor、extendsによるサブクラスの作成(継承)が実現できます。
しかしJSの上記構文はあくまでシンタックスシュガーとなっており、裏ではprototypeにより実現されています。
私自身の考えですが、裏がどう動いているかを理解することで、表をより深く理解できると考えていますので、本記事はES2015以降見かけなくなったprototypeにを用いたクラスと継承を考えます。
今現在この記事を参考にprototypeを用いたコードを書くのはオススメしません。
以前書いていたようなコードだとしても・class構文がシンタックスシュガーだとしても、prototypeを汚染しかねないコードなのであくまで参考程度に
前提として、現在よく使われるJavaScriptのclass構文と継承は以下のようになります。
//getter,setterの都合上プロパティにはアンダースコアをつけています。
//#をつけてプライベートプロパティにすることも可能です
class Position {
constructor(x, y) {
this._x = x;
this._y = y;
}
get position() {return {x:this._x, y:this._y};}
get x() {return this._x;}
get y() {return this._y;}
set position(x, y) = {
this._x = x;
this._y = y;
}
set x(x) {this._x = x;}
set y(y) {this._y = y;}
}
class Actor {
constructor(name, life) {
this._name = name;
this._life = life;
}
get name() {return this._name;}
get life() {return this._life;}
isAlive() {return (this._life > 0);}
damage(power) {this._life -= power;}
dead() {this._life = 0;}
}
class Weapon extends Actor {
constructor(name, life, power) {
super(name, life);
this._power = power;
}
get power() {return this._power;}
use() {this.damage(1);}
}
class Character extends Actor {
constructor(name, life, weapon, position) {
super(name, life);
this._weapon = weapon;
this._position = position;
}
get weapon() {return this._weapon;}
get position() {return this._position.position;}
attack() {return this._weapon.power;}
}
class Enemy extends Character {
constructor(name, life, weapon, position) {
super(name, life, weapon, position);
}
}
class Hero extends Character {
constructor(name, life, weapon, position, level) {
super(name, life, weapon, position);
this._level = level;
}
get level() {return this._level;}
attack() {return this._weapon.power + (this._level * 10);}
levelUp() {this._level++;}
}
Positionクラスと、Actorクラスがあり、Actorを継承するWeaponクラスとCharacterクラスがあります。さらにCharacterクラスを継承するEnemyクラスとHeroクラスが定義されています。
Actorを継承している全クラスは名前とライフを持っており、キャラクターはダメージを食らうと・武器は使用するとライフが減っていきます。
また、ヒーローのみレベルがあり、任意のタイミングでレベルアップできます。ゲームであれば通常経験値を元にレベルアップしますが、レベル毎に必須経験値が変わる処理とか色々書くの面倒くさいし本記事の内容に関わってこないので除外します。
ES2015以降であれば、このようなコードで記述できます。
裏ではprototypeにより実現されています。では上記コードをprototypeらしく書き直してみましょう。
もちろん,es2015以前は無かったのでletもconst使いません。
var Position = function(x, y) {
this._x = x;
this._y = y;
};
Position.prototype = {
getPosition: function() {return {x:this._x, y:this._y};},
getX: function() {return this._x;},
getY: function() {return this._y;},
setPosition: function(x, y) {
this._x = x;
this._y = y;
},
setX: function(x) {this._x = x;},
setY: function(y) {this._y = y;},
};
var Actor = function(name, life) {
this._name = name;
this._life = life;
};
Actor.prototype = {
getName: function() {return this._name;},
getLife: function() {return this._life;},
isAlice: function() {return (this._life > 0);},
damage: function(power) {this._life -= power;},
dead: function() {this._life = 0;},
};
var Weapon = function(name, life, power) {
Actor.call(this, name, life);
this._power = power;
};
Object.setPrototypeOf(Weapon.prototype, Actor.prototype);
Weapon.prototype = {
getPower: function() {return this._power;},
use: function() {this.damage(1);},
};
var Character = function(name, life, weapon, position) {
Actor.call(this, name, life);
this._weapon = weapon;
this._position = position;
};
Object.setPrototypeOf(Character.prototype, Actor.prototype);
Character.prototype = {
getWeapon: function() {return this._weapon;},
getPosition: function() {return this._position.getPosition();},
attack: function() {return this._weapon.power();},
};
var Enemy = function(name, life, weapon, position) {
Character.call(this, name, life, weapon, position);
};
Object.setPrototypeOf(Enemy.prototype, Character.prototype);
var Hero = function(name, life, weapon, position, level) {
Character.call(this, name, life, weapon, position);
this._level = level;
};
Object.setPrototypeOf(Hero.prototype, Character.prototype);
Hero.prototype = {
getLevel: function() {return this._level;},
attack: function() {return this._weapon.power() + (this._level * 10);},
levelUp: function() {this._level++;},
};
このようになります。
prototypeでは継承をSUPER_CLASS.call(this, arg1, arg2, ...);
と、Object.setPrototypeOf(SUB_CLASS.prototype, SUPER_CLASS.prototype);
の2行で行えます。
callメソッドはclass構文のconstructorで呼び出すsuper()
と同じようなものですが、setPrototypeOfメソッドは見慣れないものだと思います。
名前から推測できる通り、arg1のprototypeの中身をarg2のprototypeでセットします。
要はこういう事です。
arg1.prototype = arg2.prototype;
上の一行は簡単に書きすぎましたので、もう少し厳密に書くと以下のコードになります。こちらはMDNのsetPrototypeOfのポリフィルから引用しています。
引用元
if (!Object.setPrototypeOf) {
Object.prototype.setPrototypeOf = function(obj, proto) {
if(obj.__proto__) {
obj.__proto__ = proto;
return obj;
} else {
var Fn = function() {
for (var key in obj) {
Object.defineProperty(this, key, {
value: obj[key],
});
}
};
Fn.prototype = proto;
return new Fn();
}
}
}
setPrototypeOfが無かったらこうやって定義してね!というコードです。
このコードは、Object.create(null)のプロトタイプを返す場合が考慮されているので、あるクラスが他のクラスを継承したい、という要求を満たすコードは以下のように定義できます。
また、functionをクラスとして扱えていますが、要はFunctionなのでFunction.prototypeに継承する為のメソッドとして定義しています。
Function.prototype.inherit = function(superClass) {
var tempClass = function() {};
tempClass.prototype = superClass.prototype;
this.prototype = new tempClass();
this.prototype.constructor = this;
};
これにより、Object.setPrototypeOf(SUB_CLASS, SUPER_CLASS)
はSUB_CLASS.inherit(SUPER_CLASS);
と書き直す事ができます。
JavaScriptはthisの扱い方が大変面倒くさいので、どこで書かれたthisが何を指しているのか、function式とarrow function式でのthisの束縛とか、bindとか、本当に分かりにくいですがthisとprototypeを理解することで、そこらへんが隠されたモダンなJavaScriptの流儀や思想もわかるようになるのではないかと思います。