を実現するにはどうすれば良いのか、ということについてです💕。
オブジェクト
継承の前にオブジェクトの作り方を見ましょう。オブジェクトを作成するには、オブジェクトを作成するための関数であるコンストラクタを定義しなければなりません(他に、オブジェクトをリテラルとして記述する記法もありますが)。コンストラクタは次のように作成することができます(以下のコードの記述と実行は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