を実現するにはどうすれば良いのか、ということについてです💕。
オブジェクト
継承の前にオブジェクトの作り方を見ましょう。オブジェクトを作成するには、オブジェクトを作成するための関数であるコンストラクタを定義しなければなりません(他に、オブジェクトをリテラルとして記述する記法もありますが)。コンストラクタは次のように作成することができます(以下のコードの記述と実行はNode.jsのREPLを使っています)。
> function Currency(name, code) {
... this.name = name;
... this.code = code;
... this.toString = function() {
..... return this.name + " " + this.code;
..... }
... }
undefined
普通の関数と同じ書き方ですが、thisを使うことによって、この関数がnewを使ってコンストラクタとして呼び出された際に、実体化されるオブジェクトにプロパティが設定されるようにしています。つまり、thisを使うことによって、実体化されたオブジェクトが持つことになるプロパティを定義することができます。
このコンストラクタを使ってオブジェクトを実体化するには、次のようにnewを使ってコンストラクタを呼び出します。オブジェクトのプロパティが正しく呼び出せるかも確認しましょう。
> let yen = new Currency("yen", "JPY")
undefined
> yen
Currency { name: 'yen', code: 'JPY', toString: [Function] }
> yen.name
'yen'
> yen.code
'JPY'
> yen.toString()
'yen JPY'
プロトタイプ
JavaScriptのオブジェクトにはプロトタイプという面白い仕組みが備わっています。これは、オブジェクトを実体化したコンストラクタのprototypeプロパティに格納されている別のオブジェクトのプロパティを、あたかもオブジェクト自体のプロパティであるかのように呼び出すことができるという機能です。少々分かり辛いのでコードで見ましょう。
上のコードからの続きで、まずコンストラクタのprototypeプロパティの内容を見てみましょう(JavaScriptでは関数もオブジェクトの一種なので関数もプロパティを持つことができます。そして、全ての関数はprototypeプロパティを最初から持っています)。
> Currency.prototype
Currency {}
どうやら空のオブジェクトのようですね。コンストラクタのprototypeプロパティは最初は空のオブジェクトです。
(実は、厳密には空ではありません。constructorプロパティだけは最初から設定されています。constructorプロパティは文字通りコンストラクタの参照を保持します。
> Currency.prototype.constructor
[Function: Currency]
)
この状態でyenオブジェクトのmaiプロパティ、すなわち、yen.maiを参照すると何が得られるでしょうか。
> yen.mai
undefined
yenオブジェクトにmaiプロパティは存在しませんのでundefinedが返ってきます。
では、Currencyコンストラクタのprototypeプロパティに最初から設定されているオブジェクトに新しいプロパティを追加してみましょう。
> Currency.prototype.mai = 10000
10000
> Currency.prototype.hon = 1000000
1000000
> Currency.prototype
Currency { mai: 10000, hon: 1000000 }
(どうでも良いですが、為替の世界では1万通貨を1枚と言ったり、100万通貨を1本と言ったりします)。
この状態で再びyen.maiを参照すると何が得られるでしょうか。
> yen.mai
10000
yenオブジェクトにmaiプロパティは存在しませんが、yenオブジェクトを実体化したCurrencyコンストラクタのprototypeプロパティに設定されているオブジェクトにはmaiプロパティが存在しますので、このプロパティの値が返ってきます。
これがプロトタイプという機能です。
ここで返ってきている値はオブジェクト自体が持っているものではないことに注意してください。そうではなく、同じコンストラクタ(この場合はCurrencyコンストラクタ)で実体化された全てのオブジェクト間で共有されることになる値です(更に継承が入ってくると、コンストラクタの子コンストラクタで実体化されたオブジェクトとの間でも共有される可能性があります)。値自体は、コンストラクタのprototypeプロパティに格納されているオブジェクトが持っているものです。
> let doru = new Currency("doru", "USD")
undefined
> doru.mai
10000
> Currency.prototype.mai = 1000
1000
> yen.mai
1000
> doru.mai
1000
そのため、prototypeプロパティの内容を変えれば、それはオブジェクトの方にも反映されることになります。
では、逆に、オブジェクト自体にmaiプロパティが存在する(オブジェクト自体がmaiプロパティを持っている)場合はどうなるでしょうか。yen.maiに値を設定してみましょう。
> yen.mai = 100
100
> yen.mai
100
> Currency.prototype.mai
1000
> doru.mai
1000
yen.maiに値を設定すると、yenオブジェクト自体のmaiプロパティに値が設定され、yen.maiを参照するとその値が返ってきます。結果としてCurrency.prototype.maiはyenオブジェクトからは見えなくなります。しかし、依然としてCurrency.prototype.maiには元の値が設定されていますし、他のオブジェクト(ここではdoruオブジェクト)からは参照できます。
delete演算子でyenオブジェクト自体のmaiプロパティを削除すれば元の状態に戻ります。
> delete yen.mai
true
> yen.mai
1000
さて、ここまではprototypeプロパティに最初から設定されているオブジェクトに新しいプロパティを追加していましたが、prototypeプロパティ自体に別のオブジェクトを設定することもできます。
> Currency.prototype = {}
{}
> Currency.prototype.constructor
[Function: Object]
> Currency.prototype.constructor = Currency
[Function: Currency]
> Currency.prototype.constructor
[Function: Currency]
Currencyコンストラクタのprototypeプロパティに空のオブジェクトを設定してみました。ただ、これではconstructorプロパティが正しくなくなってしまいますので、constructorプロパティを自分で設定し直しました。
この状態でyen.maiやdoru.maiを参照すると何が得られるでしょうか。
> yen.mai
1000
> doru.mai
1000
> Currency.prototype.mai
undefined
Currency.prototype.maiはundefinedであるにも拘らず、yen.maiやdoru.maiは1000です。これは、yenオブジェクトやdoruオブジェクトが古いprototypeプロパティの内容を参照しているからです。
この状態でCurrencyコンストラクタを使って新しいオブジェクトを作成した場合、そのオブジェクトは新しいprototypeプロパティの内容を参照します。
> var yuuro = new Currency("yuuro", "EUR")
undefined
> yuuro.mai
undefined
このように、コンストラクタのprototypeプロパティ自体に別のオブジェクトを設定した場合は、それ以前にそのコンストラクタを使って作成したオブジェクトの参照まで置き換わる訳ではないので注意してください。
プロトタイプ鎖
オブジェクトのプロパティを取得しようとした際にオブジェクトを実体化したコンストラクタのprototypeプロパティに格納されているオブジェクトのプロパティも調べるというプロトタイプの動作は連鎖します。つまり、オブジェクトのプロパティを取得しようとしたときには、JavaScriptのエンジンは次のような流れで値を返そうとします。
- オブジェクト自体にプロパティが存在しないか調べ、存在する場合にはそのプロパティの値を返す。
- 存在しない場合にはオブジェクトを実体化したコンストラクタの
prototypeプロパティに格納されているオブジェクトにプロパティが存在しないか調べ、存在する場合にはそのプロパティの値を返す。 - 存在しない場合にはオブジェクトを実体化したコンストラクタの
prototypeプロパティに格納されているオブジェクトを実体化したコンストラクタのprototypeプロパティに格納されているオブジェクトにプロパティが存在しないか調べ、存在する場合にはそのプロパティの値を返す。 - ・・・
- ただし、
Object.prototypeに到達しても値が得られなかった場合には、undefinedを返す。
この連鎖は継承を実現するのに使えます。オブジェクトを実体化したコンストラクタのprototypeプロパティに格納されているオブジェクトを親と見做せば、プロパティの取得の動作は、プロパティがオブジェクト自体に定義されていない場合には親のプロパティを見に行く動作(親にもプロパティが定義されていなければその親・・・と続けていく動作)となるからです。
コードで見ましょう。
> function Currency(name, code) {
... this.name = name;
... this.code = code;
... }
undefined
> Currency.prototype.toString = function() {
... return this.name + " " + this.code;
... }
[Function]
> let yen = new Currency("yen", "JPY")
undefined
> yen.name
'yen'
> yen.code
'JPY'
> yen.toString()
'yen JPY'
ここまでは、toString関数をCurrency.prototypeに移動した点以外は最初のコードと同じです。ここからCryptoCurrencyコンストラクタをCurrencyコンストラクタの子として(すなわち、Currencyコンストラクタを継承するものとして)定義します。
> function CryptoCurrency(name, code, author) {
... this.name = name;
... this.code = code;
... this.author = author;
... }
undefined
> CryptoCurrency.prototype = new Currency()
Currency { name: undefined, code: undefined }
> CryptoCurrency.prototype.constructor = CryptoCurrency
[Function: CryptoCurrency]
> CryptoCurrency.prototype.base = Currency.prototype
Currency { toString: [Function] }
> CryptoCurrency.prototype.toString = function() {
... return this.base.toString.call(this) + " " + this.author;
... }
[Function]
> let bitcoin = new CryptoCurrency("bitcoin", "BTC", "Satoshi Nakamoto")
undefined
> bitcoin.name
'bitcoin'
> bitcoin.code
'BTC'
> bitcoin.author
'Satoshi Nakamoto'
> bitcoin.toString()
'bitcoin BTC Satoshi Nakamoto'
CryptoCurrency.prototypeにはnew Currency()を指定します。これだけではconstructorプロパティがおかしいので自分で設定します。それからbaseプロパティ(親のプロトタイプを参照するためのプロパティ)も定義しておくと便利かもしれません。
このようにして継承を実現するパターンを疑似クラス的継承パターン(pseudo-classical inheritance pattern)と言います。これはコンストラクタを疑似的なクラスとして利用しているからです。JavaScriptで継承を実現するパターンは他にも幾つかありますが、このパターンが最も基本的なものではないかと思います。
おまけ
最後に、幾つか便利な演算子や関数を紹介しておきましょう。
ひとつめ。instanceof演算子であるオブジェクトがあるコンストラクタの実体であるか調べることができます。親のコンストラクタに対してもtrueとなります。
> yen instanceof Currency
true
> yen instanceof CryptoCurrency
false
> bitcoin instanceof Currency
true
> bitcoin instanceof CryptoCurrency
true
ふたつめ。hasOwnProperty関数であるプロパティがオブジェクト自体が定義しているものであるかを調べることができます。
> yen.hasOwnProperty("name")
true
> yen.hasOwnProperty("code")
true
> yen.hasOwnProperty("toString")
false
> bitcoin.hasOwnProperty("name")
true
> bitcoin.hasOwnProperty("code")
true
> bitcoin.hasOwnProperty("author")
true
> bitcoin.hasOwnProperty("toString")
false
みっつめ。isPrototypeOf関数であるオブジェクトが別のオブジェクトを実体化したコンストラクタのprototypeプロパティに設定されているものであるかを調べることができます。親のコンストラクタのprototypeプロパティに設定されているものに対してもtrueとなります。
> Currency.prototype.isPrototypeOf(yen)
true
> CryptoCurrency.prototype.isPrototypeOf(yen)
false
> Currency.prototype.isPrototypeOf(bitcoin)
true
> CryptoCurrency.prototype.isPrototypeOf(bitcoin)
true