こんにちは。
最近、オライリー・ジャパンの
「JavaScript パターン――優れたアプリケーションのための作法」という本を読んでいます。
この本は、JavaScript でのコーディングパターンや、 Javascript に限らず広義の意味での「パターン」を取り扱っている書籍です。
この本の中に、クラシカルな継承パターンというものがあります。
クラシカルな継承パターンとは、ざっくり言うと
JavaScript にクラスの概念は無いけれど、
長年クラスベースの言語を触ってきた人たちが js を触るときに馴染みやすいように、
クラスや継承のような機能を提供するパターン
だと僕は解釈しました。
このクラシカルな継承パターンを読んで、
CoffeeScript や TypeScript での class 記法や継承パターンは、js に変換するとどう表現されるのか
が気になったため、調査してみました。
当記事の目標は、
JavaScript における「クラス」の概念と継承のパターンが分かるようになる
ことです。
当記事では、CoffeeScript や TypeScript については、あまり深く触れませんのであらかじめご了承下さい。
当記事を読んでいる方ならご存知かと思いますが、
JavaScript はプロトタイプベースのオブジェクト指向言語であり、
クラスや継承という概念はありません。
ここが js の癖となる点の1つだと思います。
しかし、クラスや継承といった技法が実現不可能なわけではなく、
プロトタイプを上手く用いることでクラスのようなオブジェクトを作るも、それらの継承も可能です。
ただし、繰り返しになりますが
言語の概念として、クラスや継承が存在していないので、
あくまでそれらに似た振る舞いを再現できる、というだけです。
JavaScript パターンから引用すると、
「クラシカルな継承のパターン」の模範解答は、以下のようになります。
// 継承を行う関数
var inherit = (function() {
var F = function() {}
return function(C, P) {
F.prototype = P.prototype
C.prototype = new F()
C.uber = P.prototype
C.prototype.constructor = C
}
})()
// Personクラス(のようなオブジェクト(以下省略))
function Parent() {}
Parent.prototype.say = function() {
return this.name
}
// Childクラス
function Child(name) {
this.name = name
// 親のコンストラクタを拝借する
Parent.apply(this)
}
// 継承
inherit(Child, Parent)
// インスタンスを作成
var kid = new Child('Bob')
console.log(kid.say()) // 'Bob'
上記のように、
Child クラスは、say メソッドを持っていませんが、
Child クラスのインスタンスであるkid
は、say メソッドを Parent クラスのプロトタイプから利用できます。
明示的にクラスです! と宣言する文法がないため、文法は他の言語と大きく異なりますが、
これでおおよそクラスと継承の機構が再現出来ていると思います。
クロージャを利用すれば、プライベートメンバー・メソッドが定義可能です。
それはさておき、ここで重要なのが、継承のための関数inherit()
です。
var inherit = (function() {
var F = function() {}
// CとPのプロキシとなる関数F
return function(C, P) {
F.prototype = P.prototype
// Fのプロトタイプオブジェクトを親と共有する
C.prototype = new F()
// 子のプロトタイプオブジェクトは、Fのインスタンスを設定
C.uber = P.prototype
// スーパークラス(uberという名前にする)には親のプロトタイプを設定
C.prototype.constructor = C
// コンストラクタのポインタを再設定する
}
})()
この inherit()は、
ために、子と親をつなぐプロキシとなる関数 F()を作成します
この関数 F を利用することで、プロトタイプの共有を切りつつも、親のメンバーを共有することが可能になります。
※当記事は js でのコーディングパターンが主旨なので、
js のプロトタイプチェインについて詳しくは触れません。
プロトタイプ連鎖については、以下の記事が参考になるかと思います。
お待たせしました。
長い前置きを終えて、本題です。
CoffeeScript や Typescript などの言語では、
これらのややこしくて面倒なことを気にせずに、
class~extends という文だけで js でクラス(のようなオブジェクト)を使用出来ます。
CoffeeScript でクラスと継承を用いた例が、以下となります。
(CoffeeScript.org のClassesの説明から持ってきて一部改変)
class Parent
constructor: (@name) ->
move: (meters) ->
console.log @name + " moved #{meters}m."
class Child extends Parent
move: ->
console.log "slithering…"
super 5
child = new Child()
child.move()
ものすごくシンプルです。
だってclass
とかextends
って文法が使えるんですもん。
たったこれだけで、クラスの定義と継承ができます。
次は、TypeScript での例を見てみます。
比較しやすいように、CoffeeScript の例と同じものを TypeScript で書き直しました。
※TypeScript はまだ初心者なので、書き違いがあったらすみません。
TypeScript でのクラスの定義と継承の例は以下となります。
class Parent {
name: string
constructor(name: string) {
this.name = name
}
move(meters: number): void {
console.log(this.name + ' moved ' + meters + 'm.')
}
}
class Child extends Parent {
move(): void {
console.log('slithering… ')
super.move(5)
}
}
var child: Child = new Child()
child.move()
ややコード量が増えていますが、とても見やすいです。
TypeScript でも、class
とextends
を使用できます。
CoffeeScript や TypeScirpt では、prototype だの継承だのといったややこしい処理は、
js にコンパイルする際に、特定の表現と、継承するための汎用関数を吐き出して、それを利用しています。
次に、この特定の表現
と汎用関数
がどう実装されているのかを見ていきます。
まずは CoffeeScript バージョンを見ていきます。
var __hasProp = {}.hasOwnProperty,
__extends = function(child, parent) {
for (var key in parent) {
if (__hasProp.call(parent, key)) child[key] = parent[key]
}
function ctor() {
this.constructor = child
}
ctor.prototype = parent.prototype
child.prototype = new ctor()
child.__super__ = parent.prototype
return child
}
__hasProp
は、安全で確実にObject.hasOwnProperty()
を呼ぶためのショートカットです。
肝心の継承する関数が、__extends(child, parent)
です。
JavaScript パターンの聖杯バージョンに出てきた
inherit(Child, Parent)
と、順序は違うけれど似ていますね。
inherit()と異なる点は、for 文を用いて親のメンバーを子にコピーしている箇所です。
実はこのパターンも、JavaScript パターンで出てきます。
「プロパティのコピーによる継承」と題されており、
プロパティのコピーする関数extend()
を実装しています。
書籍のコード少し改造した例が以下となります。
function extend(parent, child) {
var hasProp = {}.hasOwnProperty,
p
child = child || {}
for (p in parent) {
if (hasProp.call(parent, p)) {
child[i] = parent[i]
}
}
return child
}
もうお分かりかと思いますが、Coffee 版に出てくる for 文と同じです。
このextend()と、inherit()を合体させた関数が、
CoffeeScript における継承用の関数__extends
となっています。
では、次に TypeScript バージョンを見てみます。
var __extends =
this.__extends ||
function(d, b) {
function __() {
this.constructor = d
}
__.prototype = b.prototype
d.prototype = new __()
}
TypeScript の方はややシンプルです。
ですが、やっていることはほぼ変わりません。
そしてさりげなく、__extends という関数が未定義の時のみ独自定義するといった気配りが入っています。
TypeScript で継承する関数__extends()
では、
JavaScript パターンでのinherit()
を少し簡略化したものとなっています。
また、クラスの定義や、super(親クラスへの参照)の表現も調査してみました。
CoffeeScript も TypeScript もほぼ同様の表現になっています。
下記は CoffeeScript の先程の例のクラス部分を js にコンパイルしたコードです。
var Parent = (function() {
function Parent(name) {
this.name = name
}
Parent.prototype.move = function(meters) {
console.log(this.name + ' moved ' + meters + 'm.')
}
return Parent
})()
var Child = (function(_super) {
__extends(Child, _super)
function Child() {
_super.apply(this, arguments)
}
Child.prototype.move = function() {
console.log('slithering… ')
_super.prototype.move.call(this, 5)
}
return Child
})(Parent)
まず、var クラス名 = (function() {})();
とクラスの定義を即時関数に包み、
その中でfunction クラス名()
と関数を再定義しています。
これにより、今後コンストラクタ関数(クラス名)を呼び出す際には、内側の関数が呼び出されます。
継承する子クラスでは、即時関数の引数に_super
を取っています。
この_super は、親クラスのコンストラクタ関数を指しています。
この親コンストラクタ関数を、継承する関数extends を通じて、 子クラスのsuper__オブジェクトに設定しています。
そして、superオブジェクトを経由して、
子クラスから親クラスのプロトタイプ上に存在するプロパティを利用します。
この書き方は TypeScript でもほぼ同様の表現になっており、
程度の違いがありますが、同様のパターンとみなすことができます。
js で、継承するためには、
ctor()
、TypeScript では__()
)を定義の3ステップが最低限必要となります。 ここに、上乗せとして
などが乗っかって来るようです。
また、クラスの表現方法は、
CoffeeScript、TypeScript 共にほぼ同じなので、 上記のような書き方をしておくのがベターだと思います。
なお、今回は
クラスのようなオブジェクトと継承の表現について書きましたが、
JavaScript には prototype を用いた柔軟な継承の表現が他にもあります。
今回の例はあくまでjs で”クラス”っぽいことがやりたい人向けの内容であることをご留意下さい。